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
21class Q_DECL_HIDDEN KGzipFilter::Private
22{
23public:
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 headerWritten;
39 bool footerWritten;
40 bool compressed;
41 int mode;
42 ulong crc;
43 bool isInitialized;
44};
45
46KGzipFilter::KGzipFilter()
47 : d(new Private)
48{
49}
50
51KGzipFilter::~KGzipFilter()
52{
53 delete d;
54}
55
56bool 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
69bool 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
103int KGzipFilter::mode() const
104{
105 return d->mode;
106}
107
108bool 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
127void 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
146bool KGzipFilter::readHeader()
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
191bool KGzipFilter::writeHeader(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 headerSize = 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
218void KGzipFilter::writeFooter()
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
234void KGzipFilter::setOutBuffer(char *data, uint maxlen)
235{
236 d->zStream.avail_out = maxlen;
237 d->zStream.next_out = reinterpret_cast<Bytef *>(data);
238}
239void 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}
247int KGzipFilter::inBufferAvailable() const
248{
249 return d->zStream.avail_in;
250}
251int KGzipFilter::outBufferAvailable() const
252{
253 return d->zStream.avail_out;
254}
255
256KGzipFilter::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
273KGzipFilter::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
335KGzipFilter::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

source code of karchive/src/kgzipfilter.cpp