1 /* This file is part of the KDE libraries
2    SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
3    SPDX-FileCopyrightText: 2011 Mario Bensi <mbensi@ipsquad.net>
4 
5    SPDX-License-Identifier: LGPL-2.0-or-later
6 */
7 
8 #include "kcompressiondevice.h"
9 #include "kcompressiondevice_p.h"
10 #include "kfilterbase.h"
11 #include "loggingcategory.h"
12 #include "kgzipfilter.h"
13 #include "knonefilter.h"
14 
15 #include "config-compression.h"
16 
17 #if HAVE_BZIP2_SUPPORT
18 #include "kbzip2filter.h"
19 #endif
20 #if HAVE_XZ_SUPPORT
21 #include "kxzfilter.h"
22 #endif
23 #if HAVE_ZSTD_SUPPORT
24 #include "kzstdfilter.h"
25 #endif
26 
27 #include <QDebug>
28 #include <QFile>
29 #include <QMimeDatabase>
30 
31 #include <assert.h>
32 #include <stdio.h> // for EOF
33 #include <stdlib.h>
34 
35 class KCompressionDevicePrivate
36 {
37 public:
KCompressionDevicePrivate(KCompressionDevice * qq)38     KCompressionDevicePrivate(KCompressionDevice *qq)
39         : bNeedHeader(true)
40         , bSkipHeaders(false)
41         , bOpenedUnderlyingDevice(false)
42         , type(KCompressionDevice::None)
43         , errorCode(QFileDevice::NoError)
44         , deviceReadPos(0)
45         , q(qq)
46     {
47     }
48 
49     void propagateErrorCode();
50 
51     bool bNeedHeader;
52     bool bSkipHeaders;
53     bool bOpenedUnderlyingDevice;
54     QByteArray buffer; // Used as 'input buffer' when reading, as 'output buffer' when writing
55     QByteArray origFileName;
56     KFilterBase::Result result;
57     KFilterBase *filter;
58     KCompressionDevice::CompressionType type;
59     QFileDevice::FileError errorCode;
60     qint64 deviceReadPos;
61     KCompressionDevice *q;
62 };
63 
propagateErrorCode()64 void KCompressionDevicePrivate::propagateErrorCode()
65 {
66     QIODevice *dev = filter->device();
67     if (QFileDevice *fileDev = qobject_cast<QFileDevice *>(dev)) {
68         if (fileDev->error() != QFileDevice::NoError) {
69             errorCode = fileDev->error();
70             q->setErrorString(dev->errorString());
71         }
72     }
73     // ... we have no generic way to propagate errors from other kinds of iodevices. Sucks, heh? :(
74 }
75 
findCompressionByFileName(const QString & fileName)76 static KCompressionDevice::CompressionType findCompressionByFileName(const QString &fileName)
77 {
78     if (fileName.endsWith(QLatin1String(".gz"), Qt::CaseInsensitive)) {
79         return KCompressionDevice::GZip;
80     }
81 #if HAVE_BZIP2_SUPPORT
82     if (fileName.endsWith(QLatin1String(".bz2"), Qt::CaseInsensitive)) {
83         return KCompressionDevice::BZip2;
84     }
85 #endif
86 #if HAVE_XZ_SUPPORT
87     if (fileName.endsWith(QLatin1String(".lzma"), Qt::CaseInsensitive) || fileName.endsWith(QLatin1String(".xz"), Qt::CaseInsensitive)) {
88         return KCompressionDevice::Xz;
89     }
90 #endif
91 #if HAVE_ZSTD_SUPPORT
92     if (fileName.endsWith(QLatin1String(".zst"), Qt::CaseInsensitive)) {
93         return KCompressionDevice::Zstd;
94     }
95 #endif
96     else {
97         // not a warning, since this is called often with other MIME types (see #88574)...
98         // maybe we can avoid that though?
99         // qCDebug(KArchiveLog) << "findCompressionByFileName : no compression found for " << fileName;
100     }
101 
102     return KCompressionDevice::None;
103 }
104 
compressionTypeForMimeType(const QString & mimeType)105 KCompressionDevice::CompressionType KCompressionDevice::compressionTypeForMimeType(const QString &mimeType)
106 {
107     if (mimeType == QLatin1String("application/gzip") //
108         || mimeType == QLatin1String("application/x-gzip") // legacy name, kept for compatibility
109     ) {
110         return KCompressionDevice::GZip;
111     }
112 #if HAVE_BZIP2_SUPPORT
113     if (mimeType == QLatin1String("application/x-bzip") //
114         || mimeType == QLatin1String("application/x-bzip2") // old name, kept for compatibility
115     ) {
116         return KCompressionDevice::BZip2;
117     }
118 #endif
119 #if HAVE_XZ_SUPPORT
120     if (mimeType == QLatin1String("application/x-lzma") // legacy name, still used
121         || mimeType == QLatin1String("application/x-xz") // current naming
122     ) {
123         return KCompressionDevice::Xz;
124     }
125 #endif
126 #if HAVE_ZSTD_SUPPORT
127     if (mimeType == QLatin1String("application/zstd")) {
128         return KCompressionDevice::Zstd;
129     }
130 #endif
131     QMimeDatabase db;
132     const QMimeType mime = db.mimeTypeForName(mimeType);
133     if (mime.isValid()) {
134         // use legacy MIME type for now, see comment in impl. of KTar(const QString &, const QString &_mimetype)
135         if (mime.inherits(QStringLiteral("application/x-gzip"))) {
136             return KCompressionDevice::GZip;
137         }
138 #if HAVE_BZIP2_SUPPORT
139         if (mime.inherits(QStringLiteral("application/x-bzip"))) {
140             return KCompressionDevice::BZip2;
141         }
142 #endif
143 #if HAVE_XZ_SUPPORT
144         if (mime.inherits(QStringLiteral("application/x-lzma"))) {
145             return KCompressionDevice::Xz;
146         }
147 
148         if (mime.inherits(QStringLiteral("application/x-xz"))) {
149             return KCompressionDevice::Xz;
150         }
151 #endif
152     }
153 
154     // not a warning, since this is called often with other MIME types (see #88574)...
155     // maybe we can avoid that though?
156     // qCDebug(KArchiveLog) << "no compression found for" << mimeType;
157     return KCompressionDevice::None;
158 }
159 
filterForCompressionType(KCompressionDevice::CompressionType type)160 KFilterBase *KCompressionDevice::filterForCompressionType(KCompressionDevice::CompressionType type)
161 {
162     switch (type) {
163     case KCompressionDevice::GZip:
164         return new KGzipFilter;
165     case KCompressionDevice::BZip2:
166 #if HAVE_BZIP2_SUPPORT
167         return new KBzip2Filter;
168 #else
169         return nullptr;
170 #endif
171     case KCompressionDevice::Xz:
172 #if HAVE_XZ_SUPPORT
173         return new KXzFilter;
174 #else
175         return nullptr;
176 #endif
177     case KCompressionDevice::None:
178         return new KNoneFilter;
179 #if HAVE_ZSTD_SUPPORT
180     case KCompressionDevice::Zstd:
181         return new KZstdFilter;
182 #endif
183     }
184     return nullptr;
185 }
186 
KCompressionDevice(QIODevice * inputDevice,bool autoDeleteInputDevice,CompressionType type)187 KCompressionDevice::KCompressionDevice(QIODevice *inputDevice, bool autoDeleteInputDevice, CompressionType type)
188     : d(new KCompressionDevicePrivate(this))
189 {
190     assert(inputDevice);
191     d->filter = filterForCompressionType(type);
192     if (d->filter) {
193         d->type = type;
194         d->filter->setDevice(inputDevice, autoDeleteInputDevice);
195     }
196 }
197 
KCompressionDevice(const QString & fileName,CompressionType type)198 KCompressionDevice::KCompressionDevice(const QString &fileName, CompressionType type)
199     : d(new KCompressionDevicePrivate(this))
200 {
201     QFile *f = new QFile(fileName);
202     d->filter = filterForCompressionType(type);
203     if (d->filter) {
204         d->type = type;
205         d->filter->setDevice(f, true);
206     } else {
207         delete f;
208     }
209 }
210 
KCompressionDevice(const QString & fileName)211 KCompressionDevice::KCompressionDevice(const QString &fileName)
212     : KCompressionDevice(fileName, findCompressionByFileName(fileName))
213 {
214 }
215 
~KCompressionDevice()216 KCompressionDevice::~KCompressionDevice()
217 {
218     if (isOpen()) {
219         close();
220     }
221     delete d->filter;
222     delete d;
223 }
224 
compressionType() const225 KCompressionDevice::CompressionType KCompressionDevice::compressionType() const
226 {
227     return d->type;
228 }
229 
open(QIODevice::OpenMode mode)230 bool KCompressionDevice::open(QIODevice::OpenMode mode)
231 {
232     if (isOpen()) {
233         // qCWarning(KArchiveLog) << "KCompressionDevice::open: device is already open";
234         return true; // QFile returns false, but well, the device -is- open...
235     }
236     if (!d->filter) {
237         return false;
238     }
239     d->bOpenedUnderlyingDevice = false;
240     // qCDebug(KArchiveLog) << mode;
241     if (mode == QIODevice::ReadOnly) {
242         d->buffer.resize(0);
243     } else {
244         d->buffer.resize(BUFFER_SIZE);
245         d->filter->setOutBuffer(d->buffer.data(), d->buffer.size());
246     }
247     if (!d->filter->device()->isOpen()) {
248         if (!d->filter->device()->open(mode)) {
249             // qCWarning(KArchiveLog) << "KCompressionDevice::open: Couldn't open underlying device";
250             d->propagateErrorCode();
251             return false;
252         }
253         d->bOpenedUnderlyingDevice = true;
254     }
255     d->bNeedHeader = !d->bSkipHeaders;
256     d->filter->setFilterFlags(d->bSkipHeaders ? KFilterBase::NoHeaders : KFilterBase::WithHeaders);
257     if (!d->filter->init(mode)) {
258         return false;
259     }
260     d->result = KFilterBase::Ok;
261     setOpenMode(mode);
262     return true;
263 }
264 
close()265 void KCompressionDevice::close()
266 {
267     if (!isOpen()) {
268         return;
269     }
270     if (d->filter->mode() == QIODevice::WriteOnly && d->errorCode == QFileDevice::NoError) {
271         write(nullptr, 0); // finish writing
272     }
273     // qCDebug(KArchiveLog) << "Calling terminate().";
274 
275     if (!d->filter->terminate()) {
276         // qCWarning(KArchiveLog) << "KCompressionDevice::close: terminate returned an error";
277         d->errorCode = QFileDevice::UnspecifiedError;
278     }
279     if (d->bOpenedUnderlyingDevice) {
280         QIODevice *dev = d->filter->device();
281         dev->close();
282         d->propagateErrorCode();
283     }
284     setOpenMode(QIODevice::NotOpen);
285 }
286 
error() const287 QFileDevice::FileError KCompressionDevice::error() const
288 {
289     return d->errorCode;
290 }
291 
seek(qint64 pos)292 bool KCompressionDevice::seek(qint64 pos)
293 {
294     if (d->deviceReadPos == pos) {
295         return QIODevice::seek(pos);
296     }
297 
298     // qCDebug(KArchiveLog) << "seek(" << pos << ") called, current pos=" << QIODevice::pos();
299 
300     Q_ASSERT(d->filter->mode() == QIODevice::ReadOnly);
301 
302     if (pos == 0) {
303         if (!QIODevice::seek(pos)) {
304             return false;
305         }
306 
307         // We can forget about the cached data
308         d->bNeedHeader = !d->bSkipHeaders;
309         d->result = KFilterBase::Ok;
310         d->filter->setInBuffer(nullptr, 0);
311         d->filter->reset();
312         d->deviceReadPos = 0;
313         return d->filter->device()->reset();
314     }
315 
316     qint64 bytesToRead;
317     if (d->deviceReadPos < pos) { // we can start from here
318         bytesToRead = pos - d->deviceReadPos;
319         // Since we're going to do a read() below
320         // we need to reset the internal QIODevice pos to the real position we are
321         // so that after read() we are indeed pointing to the pos seek
322         // asked us to be in
323         if (!QIODevice::seek(d->deviceReadPos)) {
324             return false;
325         }
326     } else {
327         // we have to start from 0 ! Ugly and slow, but better than the previous
328         // solution (KTarGz was allocating everything into memory)
329         if (!seek(0)) { // recursive
330             return false;
331         }
332         bytesToRead = pos;
333     }
334 
335     // qCDebug(KArchiveLog) << "reading " << bytesToRead << " dummy bytes";
336     QByteArray dummy(qMin(bytesToRead, qint64(SEEK_BUFFER_SIZE)), 0);
337     while (bytesToRead > 0) {
338         const qint64 bytesToReadThisTime = qMin(bytesToRead, qint64(dummy.size()));
339         const bool result = (read(dummy.data(), bytesToReadThisTime) == bytesToReadThisTime);
340         if (!result) {
341             return false;
342         }
343         bytesToRead -= bytesToReadThisTime;
344     }
345     return true;
346 }
347 
atEnd() const348 bool KCompressionDevice::atEnd() const
349 {
350     return (d->type == KCompressionDevice::None || d->result == KFilterBase::End) //
351         && QIODevice::atEnd() // take QIODevice's internal buffer into account
352         && d->filter->device()->atEnd();
353 }
354 
readData(char * data,qint64 maxlen)355 qint64 KCompressionDevice::readData(char *data, qint64 maxlen)
356 {
357     Q_ASSERT(d->filter->mode() == QIODevice::ReadOnly);
358     // qCDebug(KArchiveLog) << "maxlen=" << maxlen;
359     KFilterBase *filter = d->filter;
360 
361     uint dataReceived = 0;
362 
363     // We came to the end of the stream
364     if (d->result == KFilterBase::End) {
365         return dataReceived;
366     }
367 
368     // If we had an error, return -1.
369     if (d->result != KFilterBase::Ok) {
370         return -1;
371     }
372 
373     qint64 availOut = maxlen;
374     filter->setOutBuffer(data, maxlen);
375 
376     while (dataReceived < maxlen) {
377         if (filter->inBufferEmpty()) {
378             // Not sure about the best size to set there.
379             // For sure, it should be bigger than the header size (see comment in readHeader)
380             d->buffer.resize(BUFFER_SIZE);
381             // Request data from underlying device
382             int size = filter->device()->read(d->buffer.data(), d->buffer.size());
383             // qCDebug(KArchiveLog) << "got" << size << "bytes from device";
384             if (size) {
385                 filter->setInBuffer(d->buffer.data(), size);
386             } else {
387                 // Not enough data available in underlying device for now
388                 break;
389             }
390         }
391         if (d->bNeedHeader) {
392             (void)filter->readHeader();
393             d->bNeedHeader = false;
394         }
395 
396         d->result = filter->uncompress();
397 
398         if (d->result == KFilterBase::Error) {
399             // qCWarning(KArchiveLog) << "KCompressionDevice: Error when uncompressing data";
400             break;
401         }
402 
403         // We got that much data since the last time we went here
404         uint outReceived = availOut - filter->outBufferAvailable();
405         // qCDebug(KArchiveLog) << "avail_out = " << filter->outBufferAvailable() << " result=" << d->result << " outReceived=" << outReceived;
406         if (availOut < uint(filter->outBufferAvailable())) {
407             // qCWarning(KArchiveLog) << " last availOut " << availOut << " smaller than new avail_out=" << filter->outBufferAvailable() << " !";
408         }
409 
410         dataReceived += outReceived;
411         data += outReceived;
412         availOut = maxlen - dataReceived;
413         if (d->result == KFilterBase::End) {
414             // We're actually at the end, no more data to check
415             if (filter->device()->atEnd()) {
416                 break;
417             }
418 
419             // Still not done, re-init and try again
420             filter->init(filter->mode());
421         }
422         filter->setOutBuffer(data, availOut);
423     }
424 
425     d->deviceReadPos += dataReceived;
426     return dataReceived;
427 }
428 
writeData(const char * data,qint64 len)429 qint64 KCompressionDevice::writeData(const char *data /*0 to finish*/, qint64 len)
430 {
431     KFilterBase *filter = d->filter;
432     Q_ASSERT(filter->mode() == QIODevice::WriteOnly);
433     // If we had an error, return 0.
434     if (d->result != KFilterBase::Ok) {
435         return 0;
436     }
437 
438     bool finish = (data == nullptr);
439     if (!finish) {
440         filter->setInBuffer(data, len);
441         if (d->bNeedHeader) {
442             (void)filter->writeHeader(d->origFileName);
443             d->bNeedHeader = false;
444         }
445     }
446 
447     uint dataWritten = 0;
448     uint availIn = len;
449     while (dataWritten < len || finish) {
450         d->result = filter->compress(finish);
451 
452         if (d->result == KFilterBase::Error) {
453             // qCWarning(KArchiveLog) << "KCompressionDevice: Error when compressing data";
454             // What to do ?
455             break;
456         }
457 
458         // Wrote everything ?
459         if (filter->inBufferEmpty() || (d->result == KFilterBase::End)) {
460             // We got that much data since the last time we went here
461             uint wrote = availIn - filter->inBufferAvailable();
462 
463             // qCDebug(KArchiveLog) << " Wrote everything for now. avail_in=" << filter->inBufferAvailable() << "result=" << d->result << "wrote=" << wrote;
464 
465             // Move on in the input buffer
466             data += wrote;
467             dataWritten += wrote;
468 
469             availIn = len - dataWritten;
470             // qCDebug(KArchiveLog) << " availIn=" << availIn << "dataWritten=" << dataWritten << "pos=" << pos();
471             if (availIn > 0) {
472                 filter->setInBuffer(data, availIn);
473             }
474         }
475 
476         if (filter->outBufferFull() || (d->result == KFilterBase::End) || finish) {
477             // qCDebug(KArchiveLog) << " writing to underlying. avail_out=" << filter->outBufferAvailable();
478             int towrite = d->buffer.size() - filter->outBufferAvailable();
479             if (towrite > 0) {
480                 // Write compressed data to underlying device
481                 int size = filter->device()->write(d->buffer.data(), towrite);
482                 if (size != towrite) {
483                     // qCWarning(KArchiveLog) << "KCompressionDevice::write. Could only write " << size << " out of " << towrite << " bytes";
484                     d->errorCode = QFileDevice::WriteError;
485                     setErrorString(tr("Could not write. Partition full?"));
486                     return 0; // indicate an error
487                 }
488                 // qCDebug(KArchiveLog) << " wrote " << size << " bytes";
489             }
490             if (d->result == KFilterBase::End) {
491                 Q_ASSERT(finish); // hopefully we don't get end before finishing
492                 break;
493             }
494             d->buffer.resize(BUFFER_SIZE);
495             filter->setOutBuffer(d->buffer.data(), d->buffer.size());
496         }
497     }
498 
499     return dataWritten;
500 }
501 
setOrigFileName(const QByteArray & fileName)502 void KCompressionDevice::setOrigFileName(const QByteArray &fileName)
503 {
504     d->origFileName = fileName;
505 }
506 
setSkipHeaders()507 void KCompressionDevice::setSkipHeaders()
508 {
509     d->bSkipHeaders = true;
510 }
511 
filterBase()512 KFilterBase *KCompressionDevice::filterBase()
513 {
514     return d->filter;
515 }
516