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

source code of karchive/src/kgzipfilter.cpp