1 /**
2  * Compress/decompress data using the zlib library.
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.utils.zlib;
15 
16 import etc.c.zlib;
17 import std.conv;
18 import std.exception;
19 
20 import ae.sys.data;
21 
22 class ZlibException : Exception
23 {
24 	this(int err, z_stream* zs)
25 	{
26 		if (zs.msg)
27 			super(to!string(zs.msg));
28 		else
29 		{
30 			string msg;
31 			switch (err)
32 			{
33 				case Z_STREAM_END:      msg = "stream end"; break;
34 				case Z_NEED_DICT:       msg = "need dict"; break;
35 				case Z_ERRNO:           msg = "errno"; break;
36 				case Z_STREAM_ERROR:    msg = "stream error"; break;
37 				case Z_DATA_ERROR:      msg = "data error"; break;
38 				case Z_MEM_ERROR:       msg = "mem error"; break;
39 				case Z_BUF_ERROR:       msg = "buf error"; break;
40 				case Z_VERSION_ERROR:   msg = "version error"; break;
41 				default:                msg = "unknown error"; break;
42 			}
43 			super(msg);
44 		}
45 	}
46 
47 	this(string msg) { super(msg); }
48 }
49 
50 enum ZlibMode { normal, raw, gzipOnly, gzipAuto }
51 
52 struct ZlibOptions
53 {
54 	int deflateLevel = Z_DEFAULT_COMPRESSION;
55 	int windowBits = 15; /// 8..15 - actual windowBits, without additional meaning
56 	ZlibMode mode;
57 
58 	invariant()
59 	{
60 		assert(deflateLevel == Z_DEFAULT_COMPRESSION || (deflateLevel >= 0 && deflateLevel <= 9));
61 		assert(windowBits >= 8 && windowBits <= 15);
62 	}
63 
64 private:
65 	@property
66 	int zwindowBits()
67 	{
68 		final switch (mode)
69 		{
70 		case ZlibMode.normal:
71 			return windowBits;
72 		case ZlibMode.raw:
73 			return -windowBits;
74 		case ZlibMode.gzipOnly:
75 			return 16+windowBits;
76 		case ZlibMode.gzipAuto:
77 			return 32+windowBits;
78 		}
79 	}
80 }
81 
82 struct ZlibProcess(bool COMPRESSING)
83 {
84 	void init(ZlibOptions options = ZlibOptions.init)
85 	{
86 		static if (COMPRESSING)
87 			//zenforce(deflateInit(&zs, options.deflateLevel));
88 			zenforce(deflateInit2(&zs, options.deflateLevel, Z_DEFLATED, options.zwindowBits, 8, Z_DEFAULT_STRATEGY));
89 		else
90 			//zenforce(inflateInit(&zs));
91 			zenforce(inflateInit2(&zs, options.zwindowBits));
92 	}
93 
94 	void processChunk(Data chunk)
95 	{
96 		if (!chunk.length)
97 			return;
98 
99 		assert(zs.avail_in == 0);
100 		zs.next_in  = cast(ubyte*) chunk.ptr;
101 		zs.avail_in = to!uint(chunk.length);
102 
103 		do
104 		{
105 			if (zs.avail_out == 0)
106 				allocChunk(adjustSize(zs.avail_in));
107 
108 			assert(zs.avail_in  && zs.next_in );
109 			assert(zs.avail_out && zs.next_out);
110 			if (zend(processFunc(&zs, Z_NO_FLUSH)))
111 				enforce(zs.avail_in==0, new ZlibException("Trailing data"));
112 		} while (zs.avail_in);
113 	}
114 
115 	Data[] flush()
116 	{
117 		if (zs.avail_out == 0)
118 			allocChunk(adjustSize(zs.avail_in));
119 
120 		while (!zend(processFunc(&zs, Z_FINISH)))
121 			allocChunk(zs.avail_out*2+1);
122 
123 		saveChunk();
124 		return outputChunks;
125 	}
126 
127 	static Data[] process(Data[] input, ZlibOptions options = ZlibOptions.init)
128 	{
129 		typeof(this) zp;
130 		zp.init(options);
131 		foreach (ref chunk; input)
132 			zp.processChunk(chunk);
133 		return zp.flush();
134 	}
135 
136 	static Data process(Data input, ZlibOptions options = ZlibOptions.init)
137 	{
138 		return process([input], options).joinData();
139 	}
140 
141 	~this()
142 	{
143 		zenforce(endFunc(&zs));
144 	}
145 
146 private:
147 	z_stream zs;
148 	Data currentChunk;
149 	Data[] outputChunks;
150 
151 	static if (COMPRESSING)
152 	{
153 		alias deflate processFunc;
154 		alias deflateEnd endFunc;
155 
156 		size_t adjustSize(size_t sz) { return sz / 4 + 1; }
157 	}
158 	else
159 	{
160 		alias inflate processFunc;
161 		alias inflateEnd endFunc;
162 
163 		size_t adjustSize(size_t sz) { return sz * 4 + 1; }
164 	}
165 
166 	void zenforce(int ret)
167 	{
168 		if (ret != Z_OK)
169 			throw new ZlibException(ret, &zs);
170 	}
171 
172 	bool zend(int ret)
173 	{
174 		if (ret == Z_STREAM_END)
175 			return true;
176 		zenforce(ret);
177 		return false;
178 	}
179 
180 	void saveChunk()
181 	{
182 		if (zs.next_out && zs.next_out != currentChunk.ptr)
183 		{
184 			outputChunks ~= currentChunk[0..zs.next_out-cast(ubyte*)currentChunk.ptr];
185 			currentChunk = Data();
186 		}
187 		zs.next_out = null;
188 	}
189 
190 	void allocChunk(size_t sz)
191 	{
192 		saveChunk();
193 		currentChunk = Data(sz);
194 		currentChunk.length = currentChunk.capacity;
195 		zs.next_out  = cast(ubyte*)currentChunk.mptr;
196 		zs.avail_out = to!uint(currentChunk.length);
197 	}
198 }
199 
200 alias ZlibProcess!true  ZlibDeflater;
201 alias ZlibProcess!false ZlibInflater;
202 
203 alias ZlibDeflater.process compress;
204 alias ZlibInflater.process uncompress;
205 
206 Data compress(Data input, int level)
207 {
208 	return compress(input, ZlibOptions(level));
209 }
210 
211 unittest
212 {
213 	void testRoundtrip(ubyte[] src)
214 	{
215 		ubyte[] def = cast(ubyte[])  compress(Data(src)).toHeap;
216 		ubyte[] res = cast(ubyte[])uncompress(Data(def)).toHeap;
217 		assert(res == src);
218 	}
219 
220 	testRoundtrip(cast(ubyte[])
221 "the quick brown fox jumps over the lazy dog\r
222 the quick brown fox jumps over the lazy dog\r
223 ");
224 	testRoundtrip([0]);
225 	testRoundtrip(null);
226 }