File indexing completed on 2024-04-28 11:33:46

0001 /* This file is part of the KDE libraries
0002    SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
0003    SPDX-FileCopyrightText: 2011 Mario Bensi <mbensi@ipsquad.net>
0004 
0005    SPDX-License-Identifier: LGPL-2.0-or-later
0006 */
0007 
0008 #include "kcompressiondevice.h"
0009 #include "kcompressiondevice_p.h"
0010 #include "kfilterbase.h"
0011 #include "loggingcategory.h"
0012 #include "kgzipfilter.h"
0013 #include "knonefilter.h"
0014 
0015 #include "config-compression.h"
0016 
0017 #if HAVE_BZIP2_SUPPORT
0018 #include "kbzip2filter.h"
0019 #endif
0020 #if HAVE_XZ_SUPPORT
0021 #include "kxzfilter.h"
0022 #endif
0023 #if HAVE_ZSTD_SUPPORT
0024 #include "kzstdfilter.h"
0025 #endif
0026 
0027 #include <QDebug>
0028 #include <QFile>
0029 #include <QMimeDatabase>
0030 
0031 #include <assert.h>
0032 #include <stdio.h> // for EOF
0033 #include <stdlib.h>
0034 
0035 class KCompressionDevicePrivate
0036 {
0037 public:
0038     KCompressionDevicePrivate(KCompressionDevice *qq)
0039         : bNeedHeader(true)
0040         , bSkipHeaders(false)
0041         , bOpenedUnderlyingDevice(false)
0042         , type(KCompressionDevice::None)
0043         , errorCode(QFileDevice::NoError)
0044         , deviceReadPos(0)
0045         , q(qq)
0046     {
0047     }
0048 
0049     void propagateErrorCode();
0050 
0051     bool bNeedHeader;
0052     bool bSkipHeaders;
0053     bool bOpenedUnderlyingDevice;
0054     QByteArray buffer; // Used as 'input buffer' when reading, as 'output buffer' when writing
0055     QByteArray origFileName;
0056     KFilterBase::Result result;
0057     KFilterBase *filter;
0058     KCompressionDevice::CompressionType type;
0059     QFileDevice::FileError errorCode;
0060     qint64 deviceReadPos;
0061     KCompressionDevice *q;
0062 };
0063 
0064 void KCompressionDevicePrivate::propagateErrorCode()
0065 {
0066     QIODevice *dev = filter->device();
0067     if (QFileDevice *fileDev = qobject_cast<QFileDevice *>(dev)) {
0068         if (fileDev->error() != QFileDevice::NoError) {
0069             errorCode = fileDev->error();
0070             q->setErrorString(dev->errorString());
0071         }
0072     }
0073     // ... we have no generic way to propagate errors from other kinds of iodevices. Sucks, heh? :(
0074 }
0075 
0076 static KCompressionDevice::CompressionType findCompressionByFileName(const QString &fileName)
0077 {
0078     if (fileName.endsWith(QLatin1String(".gz"), Qt::CaseInsensitive)) {
0079         return KCompressionDevice::GZip;
0080     }
0081 #if HAVE_BZIP2_SUPPORT
0082     if (fileName.endsWith(QLatin1String(".bz2"), Qt::CaseInsensitive)) {
0083         return KCompressionDevice::BZip2;
0084     }
0085 #endif
0086 #if HAVE_XZ_SUPPORT
0087     if (fileName.endsWith(QLatin1String(".lzma"), Qt::CaseInsensitive) || fileName.endsWith(QLatin1String(".xz"), Qt::CaseInsensitive)) {
0088         return KCompressionDevice::Xz;
0089     }
0090 #endif
0091 #if HAVE_ZSTD_SUPPORT
0092     if (fileName.endsWith(QLatin1String(".zst"), Qt::CaseInsensitive)) {
0093         return KCompressionDevice::Zstd;
0094     }
0095 #endif
0096     else {
0097         // not a warning, since this is called often with other MIME types (see #88574)...
0098         // maybe we can avoid that though?
0099         // qCDebug(KArchiveLog) << "findCompressionByFileName : no compression found for " << fileName;
0100     }
0101 
0102     return KCompressionDevice::None;
0103 }
0104 
0105 KCompressionDevice::CompressionType KCompressionDevice::compressionTypeForMimeType(const QString &mimeType)
0106 {
0107     if (mimeType == QLatin1String("application/gzip") //
0108         || mimeType == QLatin1String("application/x-gzip") // legacy name, kept for compatibility
0109     ) {
0110         return KCompressionDevice::GZip;
0111     }
0112 #if HAVE_BZIP2_SUPPORT
0113     if (mimeType == QLatin1String("application/x-bzip") //
0114         || mimeType == QLatin1String("application/x-bzip2") // old name, kept for compatibility
0115     ) {
0116         return KCompressionDevice::BZip2;
0117     }
0118 #endif
0119 #if HAVE_XZ_SUPPORT
0120     if (mimeType == QLatin1String("application/x-lzma") // legacy name, still used
0121         || mimeType == QLatin1String("application/x-xz") // current naming
0122     ) {
0123         return KCompressionDevice::Xz;
0124     }
0125 #endif
0126 #if HAVE_ZSTD_SUPPORT
0127     if (mimeType == QLatin1String("application/zstd")) {
0128         return KCompressionDevice::Zstd;
0129     }
0130 #endif
0131     QMimeDatabase db;
0132     const QMimeType mime = db.mimeTypeForName(mimeType);
0133     if (mime.isValid()) {
0134         // use legacy MIME type for now, see comment in impl. of KTar(const QString &, const QString &_mimetype)
0135         if (mime.inherits(QStringLiteral("application/x-gzip"))) {
0136             return KCompressionDevice::GZip;
0137         }
0138 #if HAVE_BZIP2_SUPPORT
0139         if (mime.inherits(QStringLiteral("application/x-bzip"))) {
0140             return KCompressionDevice::BZip2;
0141         }
0142 #endif
0143 #if HAVE_XZ_SUPPORT
0144         if (mime.inherits(QStringLiteral("application/x-lzma"))) {
0145             return KCompressionDevice::Xz;
0146         }
0147 
0148         if (mime.inherits(QStringLiteral("application/x-xz"))) {
0149             return KCompressionDevice::Xz;
0150         }
0151 #endif
0152     }
0153 
0154     // not a warning, since this is called often with other MIME types (see #88574)...
0155     // maybe we can avoid that though?
0156     // qCDebug(KArchiveLog) << "no compression found for" << mimeType;
0157     return KCompressionDevice::None;
0158 }
0159 
0160 KFilterBase *KCompressionDevice::filterForCompressionType(KCompressionDevice::CompressionType type)
0161 {
0162     switch (type) {
0163     case KCompressionDevice::GZip:
0164         return new KGzipFilter;
0165     case KCompressionDevice::BZip2:
0166 #if HAVE_BZIP2_SUPPORT
0167         return new KBzip2Filter;
0168 #else
0169         return nullptr;
0170 #endif
0171     case KCompressionDevice::Xz:
0172 #if HAVE_XZ_SUPPORT
0173         return new KXzFilter;
0174 #else
0175         return nullptr;
0176 #endif
0177     case KCompressionDevice::None:
0178         return new KNoneFilter;
0179 #if HAVE_ZSTD_SUPPORT
0180     case KCompressionDevice::Zstd:
0181         return new KZstdFilter;
0182 #endif
0183     }
0184     return nullptr;
0185 }
0186 
0187 KCompressionDevice::KCompressionDevice(QIODevice *inputDevice, bool autoDeleteInputDevice, CompressionType type)
0188     : d(new KCompressionDevicePrivate(this))
0189 {
0190     assert(inputDevice);
0191     d->filter = filterForCompressionType(type);
0192     if (d->filter) {
0193         d->type = type;
0194         d->filter->setDevice(inputDevice, autoDeleteInputDevice);
0195     }
0196 }
0197 
0198 KCompressionDevice::KCompressionDevice(const QString &fileName, CompressionType type)
0199     : d(new KCompressionDevicePrivate(this))
0200 {
0201     QFile *f = new QFile(fileName);
0202     d->filter = filterForCompressionType(type);
0203     if (d->filter) {
0204         d->type = type;
0205         d->filter->setDevice(f, true);
0206     } else {
0207         delete f;
0208     }
0209 }
0210 
0211 KCompressionDevice::KCompressionDevice(const QString &fileName)
0212     : KCompressionDevice(fileName, findCompressionByFileName(fileName))
0213 {
0214 }
0215 
0216 KCompressionDevice::~KCompressionDevice()
0217 {
0218     if (isOpen()) {
0219         close();
0220     }
0221     delete d->filter;
0222     delete d;
0223 }
0224 
0225 KCompressionDevice::CompressionType KCompressionDevice::compressionType() const
0226 {
0227     return d->type;
0228 }
0229 
0230 bool KCompressionDevice::open(QIODevice::OpenMode mode)
0231 {
0232     if (isOpen()) {
0233         // qCWarning(KArchiveLog) << "KCompressionDevice::open: device is already open";
0234         return true; // QFile returns false, but well, the device -is- open...
0235     }
0236     if (!d->filter) {
0237         return false;
0238     }
0239     d->bOpenedUnderlyingDevice = false;
0240     // qCDebug(KArchiveLog) << mode;
0241     if (mode == QIODevice::ReadOnly) {
0242         d->buffer.resize(0);
0243     } else {
0244         d->buffer.resize(BUFFER_SIZE);
0245         d->filter->setOutBuffer(d->buffer.data(), d->buffer.size());
0246     }
0247     if (!d->filter->device()->isOpen()) {
0248         if (!d->filter->device()->open(mode)) {
0249             // qCWarning(KArchiveLog) << "KCompressionDevice::open: Couldn't open underlying device";
0250             d->propagateErrorCode();
0251             return false;
0252         }
0253         d->bOpenedUnderlyingDevice = true;
0254     }
0255     d->bNeedHeader = !d->bSkipHeaders;
0256     d->filter->setFilterFlags(d->bSkipHeaders ? KFilterBase::NoHeaders : KFilterBase::WithHeaders);
0257     if (!d->filter->init(mode)) {
0258         return false;
0259     }
0260     d->result = KFilterBase::Ok;
0261     setOpenMode(mode);
0262     return true;
0263 }
0264 
0265 void KCompressionDevice::close()
0266 {
0267     if (!isOpen()) {
0268         return;
0269     }
0270     if (d->filter->mode() == QIODevice::WriteOnly && d->errorCode == QFileDevice::NoError) {
0271         write(nullptr, 0); // finish writing
0272     }
0273     // qCDebug(KArchiveLog) << "Calling terminate().";
0274 
0275     if (!d->filter->terminate()) {
0276         // qCWarning(KArchiveLog) << "KCompressionDevice::close: terminate returned an error";
0277         d->errorCode = QFileDevice::UnspecifiedError;
0278     }
0279     if (d->bOpenedUnderlyingDevice) {
0280         QIODevice *dev = d->filter->device();
0281         dev->close();
0282         d->propagateErrorCode();
0283     }
0284     setOpenMode(QIODevice::NotOpen);
0285 }
0286 
0287 QFileDevice::FileError KCompressionDevice::error() const
0288 {
0289     return d->errorCode;
0290 }
0291 
0292 bool KCompressionDevice::seek(qint64 pos)
0293 {
0294     if (d->deviceReadPos == pos) {
0295         return QIODevice::seek(pos);
0296     }
0297 
0298     // qCDebug(KArchiveLog) << "seek(" << pos << ") called, current pos=" << QIODevice::pos();
0299 
0300     Q_ASSERT(d->filter->mode() == QIODevice::ReadOnly);
0301 
0302     if (pos == 0) {
0303         if (!QIODevice::seek(pos)) {
0304             return false;
0305         }
0306 
0307         // We can forget about the cached data
0308         d->bNeedHeader = !d->bSkipHeaders;
0309         d->result = KFilterBase::Ok;
0310         d->filter->setInBuffer(nullptr, 0);
0311         d->filter->reset();
0312         d->deviceReadPos = 0;
0313         return d->filter->device()->reset();
0314     }
0315 
0316     qint64 bytesToRead;
0317     if (d->deviceReadPos < pos) { // we can start from here
0318         bytesToRead = pos - d->deviceReadPos;
0319         // Since we're going to do a read() below
0320         // we need to reset the internal QIODevice pos to the real position we are
0321         // so that after read() we are indeed pointing to the pos seek
0322         // asked us to be in
0323         if (!QIODevice::seek(d->deviceReadPos)) {
0324             return false;
0325         }
0326     } else {
0327         // we have to start from 0 ! Ugly and slow, but better than the previous
0328         // solution (KTarGz was allocating everything into memory)
0329         if (!seek(0)) { // recursive
0330             return false;
0331         }
0332         bytesToRead = pos;
0333     }
0334 
0335     // qCDebug(KArchiveLog) << "reading " << bytesToRead << " dummy bytes";
0336     QByteArray dummy(qMin(bytesToRead, qint64(SEEK_BUFFER_SIZE)), 0);
0337     while (bytesToRead > 0) {
0338         const qint64 bytesToReadThisTime = qMin(bytesToRead, qint64(dummy.size()));
0339         const bool result = (read(dummy.data(), bytesToReadThisTime) == bytesToReadThisTime);
0340         if (!result) {
0341             return false;
0342         }
0343         bytesToRead -= bytesToReadThisTime;
0344     }
0345     return true;
0346 }
0347 
0348 bool KCompressionDevice::atEnd() const
0349 {
0350     return (d->type == KCompressionDevice::None || d->result == KFilterBase::End) //
0351         && QIODevice::atEnd() // take QIODevice's internal buffer into account
0352         && d->filter->device()->atEnd();
0353 }
0354 
0355 qint64 KCompressionDevice::readData(char *data, qint64 maxlen)
0356 {
0357     Q_ASSERT(d->filter->mode() == QIODevice::ReadOnly);
0358     // qCDebug(KArchiveLog) << "maxlen=" << maxlen;
0359     KFilterBase *filter = d->filter;
0360 
0361     uint dataReceived = 0;
0362 
0363     // We came to the end of the stream
0364     if (d->result == KFilterBase::End) {
0365         return dataReceived;
0366     }
0367 
0368     // If we had an error, return -1.
0369     if (d->result != KFilterBase::Ok) {
0370         return -1;
0371     }
0372 
0373     qint64 availOut = maxlen;
0374     filter->setOutBuffer(data, maxlen);
0375 
0376     while (dataReceived < maxlen) {
0377         if (filter->inBufferEmpty()) {
0378             // Not sure about the best size to set there.
0379             // For sure, it should be bigger than the header size (see comment in readHeader)
0380             d->buffer.resize(BUFFER_SIZE);
0381             // Request data from underlying device
0382             int size = filter->device()->read(d->buffer.data(), d->buffer.size());
0383             // qCDebug(KArchiveLog) << "got" << size << "bytes from device";
0384             if (size) {
0385                 filter->setInBuffer(d->buffer.data(), size);
0386             } else {
0387                 // Not enough data available in underlying device for now
0388                 break;
0389             }
0390         }
0391         if (d->bNeedHeader) {
0392             (void)filter->readHeader();
0393             d->bNeedHeader = false;
0394         }
0395 
0396         d->result = filter->uncompress();
0397 
0398         if (d->result == KFilterBase::Error) {
0399             // qCWarning(KArchiveLog) << "KCompressionDevice: Error when uncompressing data";
0400             break;
0401         }
0402 
0403         // We got that much data since the last time we went here
0404         uint outReceived = availOut - filter->outBufferAvailable();
0405         // qCDebug(KArchiveLog) << "avail_out = " << filter->outBufferAvailable() << " result=" << d->result << " outReceived=" << outReceived;
0406         if (availOut < uint(filter->outBufferAvailable())) {
0407             // qCWarning(KArchiveLog) << " last availOut " << availOut << " smaller than new avail_out=" << filter->outBufferAvailable() << " !";
0408         }
0409 
0410         dataReceived += outReceived;
0411         data += outReceived;
0412         availOut = maxlen - dataReceived;
0413         if (d->result == KFilterBase::End) {
0414             // We're actually at the end, no more data to check
0415             if (filter->device()->atEnd()) {
0416                 break;
0417             }
0418 
0419             // Still not done, re-init and try again
0420             filter->init(filter->mode());
0421         }
0422         filter->setOutBuffer(data, availOut);
0423     }
0424 
0425     d->deviceReadPos += dataReceived;
0426     return dataReceived;
0427 }
0428 
0429 qint64 KCompressionDevice::writeData(const char *data /*0 to finish*/, qint64 len)
0430 {
0431     KFilterBase *filter = d->filter;
0432     Q_ASSERT(filter->mode() == QIODevice::WriteOnly);
0433     // If we had an error, return 0.
0434     if (d->result != KFilterBase::Ok) {
0435         return 0;
0436     }
0437 
0438     bool finish = (data == nullptr);
0439     if (!finish) {
0440         filter->setInBuffer(data, len);
0441         if (d->bNeedHeader) {
0442             (void)filter->writeHeader(d->origFileName);
0443             d->bNeedHeader = false;
0444         }
0445     }
0446 
0447     uint dataWritten = 0;
0448     uint availIn = len;
0449     while (dataWritten < len || finish) {
0450         d->result = filter->compress(finish);
0451 
0452         if (d->result == KFilterBase::Error) {
0453             // qCWarning(KArchiveLog) << "KCompressionDevice: Error when compressing data";
0454             // What to do ?
0455             break;
0456         }
0457 
0458         // Wrote everything ?
0459         if (filter->inBufferEmpty() || (d->result == KFilterBase::End)) {
0460             // We got that much data since the last time we went here
0461             uint wrote = availIn - filter->inBufferAvailable();
0462 
0463             // qCDebug(KArchiveLog) << " Wrote everything for now. avail_in=" << filter->inBufferAvailable() << "result=" << d->result << "wrote=" << wrote;
0464 
0465             // Move on in the input buffer
0466             data += wrote;
0467             dataWritten += wrote;
0468 
0469             availIn = len - dataWritten;
0470             // qCDebug(KArchiveLog) << " availIn=" << availIn << "dataWritten=" << dataWritten << "pos=" << pos();
0471             if (availIn > 0) {
0472                 filter->setInBuffer(data, availIn);
0473             }
0474         }
0475 
0476         if (filter->outBufferFull() || (d->result == KFilterBase::End) || finish) {
0477             // qCDebug(KArchiveLog) << " writing to underlying. avail_out=" << filter->outBufferAvailable();
0478             int towrite = d->buffer.size() - filter->outBufferAvailable();
0479             if (towrite > 0) {
0480                 // Write compressed data to underlying device
0481                 int size = filter->device()->write(d->buffer.data(), towrite);
0482                 if (size != towrite) {
0483                     // qCWarning(KArchiveLog) << "KCompressionDevice::write. Could only write " << size << " out of " << towrite << " bytes";
0484                     d->errorCode = QFileDevice::WriteError;
0485                     setErrorString(tr("Could not write. Partition full?"));
0486                     return 0; // indicate an error
0487                 }
0488                 // qCDebug(KArchiveLog) << " wrote " << size << " bytes";
0489             }
0490             if (d->result == KFilterBase::End) {
0491                 Q_ASSERT(finish); // hopefully we don't get end before finishing
0492                 break;
0493             }
0494             d->buffer.resize(BUFFER_SIZE);
0495             filter->setOutBuffer(d->buffer.data(), d->buffer.size());
0496         }
0497     }
0498 
0499     return dataWritten;
0500 }
0501 
0502 void KCompressionDevice::setOrigFileName(const QByteArray &fileName)
0503 {
0504     d->origFileName = fileName;
0505 }
0506 
0507 void KCompressionDevice::setSkipHeaders()
0508 {
0509     d->bSkipHeaders = true;
0510 }
0511 
0512 KFilterBase *KCompressionDevice::filterBase()
0513 {
0514     return d->filter;
0515 }
0516 
0517 #include "moc_kcompressiondevice.cpp"