1 | /* This file is part of the KDE libraries |
2 | SPDX-FileCopyrightText: 2000-2005 David Faure <faure@kde.org> |
3 | |
4 | SPDX-License-Identifier: LGPL-2.0-or-later |
5 | */ |
6 | |
7 | #include "kgzipfilter.h" |
8 | #include "loggingcategory.h" |
9 | |
10 | #include <QDebug> |
11 | #include <QIODevice> |
12 | |
13 | #include <time.h> |
14 | #include <zlib.h> |
15 | |
16 | /* gzip flag byte */ |
17 | #define ORIG_NAME 0x08 /* bit 3 set: original file name present */ |
18 | |
19 | // #define DEBUG_GZIP |
20 | |
21 | class Q_DECL_HIDDEN KGzipFilter::Private |
22 | { |
23 | public: |
24 | Private() |
25 | : headerWritten(false) |
26 | , footerWritten(false) |
27 | , compressed(false) |
28 | , mode(0) |
29 | , crc(0) |
30 | , isInitialized(false) |
31 | { |
32 | zStream.zalloc = static_cast<alloc_func>(nullptr); |
33 | zStream.zfree = static_cast<free_func>(nullptr); |
34 | zStream.opaque = static_cast<voidpf>(nullptr); |
35 | } |
36 | |
37 | z_stream zStream; |
38 | bool ; |
39 | bool ; |
40 | bool compressed; |
41 | int mode; |
42 | ulong crc; |
43 | bool isInitialized; |
44 | }; |
45 | |
46 | KGzipFilter::KGzipFilter() |
47 | : d(new Private) |
48 | { |
49 | } |
50 | |
51 | KGzipFilter::~KGzipFilter() |
52 | { |
53 | delete d; |
54 | } |
55 | |
56 | bool KGzipFilter::init(int mode) |
57 | { |
58 | switch (filterFlags()) { |
59 | case NoHeaders: |
60 | return init(mode, flag: RawDeflate); |
61 | case WithHeaders: |
62 | return init(mode, flag: GZipHeader); |
63 | case ZlibHeaders: |
64 | return init(mode, flag: ZlibHeader); |
65 | } |
66 | return false; |
67 | } |
68 | |
69 | bool KGzipFilter::init(int mode, Flag flag) |
70 | { |
71 | if (d->isInitialized) { |
72 | terminate(); |
73 | } |
74 | d->zStream.next_in = Z_NULL; |
75 | d->zStream.avail_in = 0; |
76 | if (mode == QIODevice::ReadOnly) { |
77 | const int windowBits = (flag == RawDeflate) ? -MAX_WBITS /*no zlib header*/ |
78 | : (flag == GZipHeader) ? MAX_WBITS + 32 /* auto-detect and eat gzip header */ |
79 | : MAX_WBITS /*zlib header*/; |
80 | const int result = inflateInit2(&d->zStream, windowBits); |
81 | if (result != Z_OK) { |
82 | // qCDebug(KArchiveLog) << "inflateInit2 returned " << result; |
83 | return false; |
84 | } |
85 | } else if (mode == QIODevice::WriteOnly) { |
86 | int result = deflateInit2(&d->zStream, Z_DEFAULT_COMPRESSION, Z_DEFLATED, -MAX_WBITS, 8, Z_DEFAULT_STRATEGY); // same here |
87 | if (result != Z_OK) { |
88 | // qCDebug(KArchiveLog) << "deflateInit returned " << result; |
89 | return false; |
90 | } |
91 | } else { |
92 | // qCWarning(KArchiveLog) << "KGzipFilter: Unsupported mode " << mode << ". Only QIODevice::ReadOnly and QIODevice::WriteOnly supported"; |
93 | return false; |
94 | } |
95 | d->mode = mode; |
96 | d->compressed = true; |
97 | d->headerWritten = false; |
98 | d->footerWritten = false; |
99 | d->isInitialized = true; |
100 | return true; |
101 | } |
102 | |
103 | int KGzipFilter::mode() const |
104 | { |
105 | return d->mode; |
106 | } |
107 | |
108 | bool KGzipFilter::terminate() |
109 | { |
110 | if (d->mode == QIODevice::ReadOnly) { |
111 | int result = inflateEnd(strm: &d->zStream); |
112 | if (result != Z_OK) { |
113 | // qCDebug(KArchiveLog) << "inflateEnd returned " << result; |
114 | return false; |
115 | } |
116 | } else if (d->mode == QIODevice::WriteOnly) { |
117 | int result = deflateEnd(strm: &d->zStream); |
118 | if (result != Z_OK) { |
119 | // qCDebug(KArchiveLog) << "deflateEnd returned " << result; |
120 | return false; |
121 | } |
122 | } |
123 | d->isInitialized = false; |
124 | return true; |
125 | } |
126 | |
127 | void KGzipFilter::reset() |
128 | { |
129 | if (d->mode == QIODevice::ReadOnly) { |
130 | int result = inflateReset(strm: &d->zStream); |
131 | if (result != Z_OK) { |
132 | // qCDebug(KArchiveLog) << "inflateReset returned " << result; |
133 | // TODO return false |
134 | } |
135 | } else if (d->mode == QIODevice::WriteOnly) { |
136 | int result = deflateReset(strm: &d->zStream); |
137 | if (result != Z_OK) { |
138 | // qCDebug(KArchiveLog) << "deflateReset returned " << result; |
139 | // TODO return false |
140 | } |
141 | d->headerWritten = false; |
142 | d->footerWritten = false; |
143 | } |
144 | } |
145 | |
146 | bool KGzipFilter::() |
147 | { |
148 | // We now rely on zlib to read the full header (see the MAX_WBITS + 32 in init). |
149 | // We just use this method to check if the data is actually compressed. |
150 | |
151 | #ifdef DEBUG_GZIP |
152 | qCDebug(KArchiveLog) << "avail=" << d->zStream.avail_in; |
153 | #endif |
154 | // Assume not compressed until we see a gzip header |
155 | d->compressed = false; |
156 | const Bytef *p = d->zStream.next_in; |
157 | int i = d->zStream.avail_in; |
158 | if ((i -= 10) < 0) { |
159 | return false; // Need at least 10 bytes |
160 | } |
161 | #ifdef DEBUG_GZIP |
162 | qCDebug(KArchiveLog) << "first byte is " << QString::number(*p, 16); |
163 | #endif |
164 | if (*p++ != 0x1f) { |
165 | return false; // GZip magic |
166 | } |
167 | #ifdef DEBUG_GZIP |
168 | qCDebug(KArchiveLog) << "second byte is " << QString::number(*p, 16); |
169 | #endif |
170 | if (*p++ != 0x8b) { |
171 | return false; |
172 | } |
173 | |
174 | d->compressed = true; |
175 | #ifdef DEBUG_GZIP |
176 | qCDebug(KArchiveLog) << "header OK" ; |
177 | #endif |
178 | return true; |
179 | } |
180 | |
181 | /* Output a 16 bit value, lsb first */ |
182 | #define put_short(w) \ |
183 | *p++ = uchar((w)&0xff); \ |
184 | *p++ = uchar(ushort(w) >> 8); |
185 | |
186 | /* Output a 32 bit value to the bit stream, lsb first */ |
187 | #define put_long(n) \ |
188 | put_short((n)&0xffff); \ |
189 | put_short((ulong(n)) >> 16); |
190 | |
191 | bool KGzipFilter::(const QByteArray &fileName) |
192 | { |
193 | Bytef *p = d->zStream.next_out; |
194 | int i = d->zStream.avail_out; |
195 | *p++ = 0x1f; |
196 | *p++ = 0x8b; |
197 | *p++ = Z_DEFLATED; |
198 | *p++ = ORIG_NAME; |
199 | put_long(time(nullptr)); // Modification time (in unix format) |
200 | *p++ = 0; // Extra flags (2=max compress, 4=fastest compress) |
201 | *p++ = 3; // Unix |
202 | |
203 | uint len = fileName.length(); |
204 | for (uint j = 0; j < len; ++j) { |
205 | *p++ = fileName[j]; |
206 | } |
207 | *p++ = 0; |
208 | int = p - d->zStream.next_out; |
209 | i -= headerSize; |
210 | Q_ASSERT(i > 0); |
211 | d->crc = crc32(crc: 0L, buf: nullptr, len: 0); |
212 | d->zStream.next_out = p; |
213 | d->zStream.avail_out = i; |
214 | d->headerWritten = true; |
215 | return true; |
216 | } |
217 | |
218 | void KGzipFilter::() |
219 | { |
220 | Q_ASSERT(d->headerWritten); |
221 | Q_ASSERT(!d->footerWritten); |
222 | Bytef *p = d->zStream.next_out; |
223 | int i = d->zStream.avail_out; |
224 | // qCDebug(KArchiveLog) << "avail_out=" << i << "writing CRC=" << QString::number(d->crc, 16) << "at p=" << p; |
225 | put_long(d->crc); |
226 | // qCDebug(KArchiveLog) << "writing totalin=" << d->zStream.total_in << "at p=" << p; |
227 | put_long(d->zStream.total_in); |
228 | i -= p - d->zStream.next_out; |
229 | d->zStream.next_out = p; |
230 | d->zStream.avail_out = i; |
231 | d->footerWritten = true; |
232 | } |
233 | |
234 | void KGzipFilter::setOutBuffer(char *data, uint maxlen) |
235 | { |
236 | d->zStream.avail_out = maxlen; |
237 | d->zStream.next_out = reinterpret_cast<Bytef *>(data); |
238 | } |
239 | void KGzipFilter::setInBuffer(const char *data, uint size) |
240 | { |
241 | #ifdef DEBUG_GZIP |
242 | qCDebug(KArchiveLog) << "avail_in=" << size; |
243 | #endif |
244 | d->zStream.avail_in = size; |
245 | d->zStream.next_in = reinterpret_cast<Bytef *>(const_cast<char *>(data)); |
246 | } |
247 | int KGzipFilter::inBufferAvailable() const |
248 | { |
249 | return d->zStream.avail_in; |
250 | } |
251 | int KGzipFilter::outBufferAvailable() const |
252 | { |
253 | return d->zStream.avail_out; |
254 | } |
255 | |
256 | KGzipFilter::Result KGzipFilter::uncompress_noop() |
257 | { |
258 | // I'm not sure that we really need support for that (uncompressed streams), |
259 | // but why not, it can't hurt to have it. One case I can think of is someone |
260 | // naming a tar file "blah.tar.gz" :-) |
261 | if (d->zStream.avail_in > 0) { |
262 | int n = (d->zStream.avail_in < d->zStream.avail_out) ? d->zStream.avail_in : d->zStream.avail_out; |
263 | memcpy(dest: d->zStream.next_out, src: d->zStream.next_in, n: n); |
264 | d->zStream.avail_out -= n; |
265 | d->zStream.next_in += n; |
266 | d->zStream.avail_in -= n; |
267 | return KFilterBase::Ok; |
268 | } else { |
269 | return KFilterBase::End; |
270 | } |
271 | } |
272 | |
273 | KGzipFilter::Result KGzipFilter::uncompress() |
274 | { |
275 | #ifndef NDEBUG |
276 | if (d->mode == 0) { |
277 | // qCWarning(KArchiveLog) << "mode==0; KGzipFilter::init was not called!"; |
278 | return KFilterBase::Error; |
279 | } else if (d->mode == QIODevice::WriteOnly) { |
280 | // qCWarning(KArchiveLog) << "uncompress called but the filter was opened for writing!"; |
281 | return KFilterBase::Error; |
282 | } |
283 | Q_ASSERT(d->mode == QIODevice::ReadOnly); |
284 | #endif |
285 | |
286 | if (!d->compressed) { |
287 | return uncompress_noop(); |
288 | } |
289 | |
290 | #ifdef DEBUG_GZIP |
291 | qCDebug(KArchiveLog) << "Calling inflate with avail_in=" << inBufferAvailable() << " avail_out=" << outBufferAvailable(); |
292 | qCDebug(KArchiveLog) << " next_in=" << d->zStream.next_in; |
293 | #endif |
294 | |
295 | while (d->zStream.avail_in > 0) { |
296 | int result = inflate(strm: &d->zStream, Z_SYNC_FLUSH); |
297 | |
298 | #ifdef DEBUG_GZIP |
299 | qCDebug(KArchiveLog) << " -> inflate returned " << result; |
300 | qCDebug(KArchiveLog) << " now avail_in=" << inBufferAvailable() << " avail_out=" << outBufferAvailable(); |
301 | qCDebug(KArchiveLog) << " next_in=" << d->zStream.next_in; |
302 | #endif |
303 | |
304 | if (result == Z_OK) { |
305 | return KFilterBase::Ok; |
306 | } |
307 | |
308 | // We can't handle any other results |
309 | if (result != Z_STREAM_END) { |
310 | return KFilterBase::Error; |
311 | } |
312 | |
313 | // It really was the end |
314 | if (d->zStream.avail_in == 0) { |
315 | return KFilterBase::End; |
316 | } |
317 | |
318 | // Store before resetting |
319 | Bytef *data = d->zStream.next_in; // This is increased appropriately by zlib beforehand |
320 | uInt size = d->zStream.avail_in; |
321 | |
322 | // Reset the stream, if that fails we assume we're at the end |
323 | if (!init(mode: d->mode)) { |
324 | return KFilterBase::End; |
325 | } |
326 | |
327 | // Reset the data to where we left off |
328 | d->zStream.next_in = data; |
329 | d->zStream.avail_in = size; |
330 | } |
331 | |
332 | return KFilterBase::End; |
333 | } |
334 | |
335 | KGzipFilter::Result KGzipFilter::compress(bool finish) |
336 | { |
337 | Q_ASSERT(d->compressed); |
338 | Q_ASSERT(d->mode == QIODevice::WriteOnly); |
339 | |
340 | const Bytef *p = d->zStream.next_in; |
341 | ulong len = d->zStream.avail_in; |
342 | #ifdef DEBUG_GZIP |
343 | qCDebug(KArchiveLog) << " calling deflate with avail_in=" << inBufferAvailable() << " avail_out=" << outBufferAvailable(); |
344 | #endif |
345 | const int result = deflate(strm: &d->zStream, flush: finish ? Z_FINISH : Z_NO_FLUSH); |
346 | if (result != Z_OK && result != Z_STREAM_END) { |
347 | // qCDebug(KArchiveLog) << " deflate returned " << result; |
348 | } |
349 | if (d->headerWritten) { |
350 | // qCDebug(KArchiveLog) << "Computing CRC for the next " << len - d->zStream.avail_in << " bytes"; |
351 | d->crc = crc32(crc: d->crc, buf: p, len: len - d->zStream.avail_in); |
352 | } |
353 | KGzipFilter::Result callerResult = result == Z_OK ? KFilterBase::Ok : (Z_STREAM_END ? KFilterBase::End : KFilterBase::Error); |
354 | |
355 | if (result == Z_STREAM_END && d->headerWritten && !d->footerWritten) { |
356 | if (d->zStream.avail_out >= 8 /*footer size*/) { |
357 | // qCDebug(KArchiveLog) << "finished, write footer"; |
358 | writeFooter(); |
359 | } else { |
360 | // No room to write the footer (#157706/#188415), we'll have to do it on the next pass. |
361 | // qCDebug(KArchiveLog) << "finished, but no room for footer yet"; |
362 | callerResult = KFilterBase::Ok; |
363 | } |
364 | } |
365 | return callerResult; |
366 | } |
367 | |