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