File indexing completed on 2024-04-28 07:40:57

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     case KCompressionDevice::Zstd:
0180 #if HAVE_ZSTD_SUPPORT
0181         return new KZstdFilter;
0182 #else
0183         return nullptr;
0184 #endif
0185     }
0186     return nullptr;
0187 }
0188 
0189 KCompressionDevice::KCompressionDevice(QIODevice *inputDevice, bool autoDeleteInputDevice, CompressionType type)
0190     : d(new KCompressionDevicePrivate(this))
0191 {
0192     assert(inputDevice);
0193     d->filter = filterForCompressionType(type);
0194     if (d->filter) {
0195         d->type = type;
0196         d->filter->setDevice(inputDevice, autoDeleteInputDevice);
0197     }
0198 }
0199 
0200 KCompressionDevice::KCompressionDevice(const QString &fileName, CompressionType type)
0201     : d(new KCompressionDevicePrivate(this))
0202 {
0203     QFile *f = new QFile(fileName);
0204     d->filter = filterForCompressionType(type);
0205     if (d->filter) {
0206         d->type = type;
0207         d->filter->setDevice(f, true);
0208     } else {
0209         delete f;
0210     }
0211 }
0212 
0213 KCompressionDevice::KCompressionDevice(const QString &fileName)
0214     : KCompressionDevice(fileName, findCompressionByFileName(fileName))
0215 {
0216 }
0217 
0218 KCompressionDevice::~KCompressionDevice()
0219 {
0220     if (isOpen()) {
0221         close();
0222     }
0223     delete d->filter;
0224     delete d;
0225 }
0226 
0227 KCompressionDevice::CompressionType KCompressionDevice::compressionType() const
0228 {
0229     return d->type;
0230 }
0231 
0232 bool KCompressionDevice::open(QIODevice::OpenMode mode)
0233 {
0234     if (isOpen()) {
0235         // qCWarning(KArchiveLog) << "KCompressionDevice::open: device is already open";
0236         return true; // QFile returns false, but well, the device -is- open...
0237     }
0238     if (!d->filter) {
0239         return false;
0240     }
0241     d->bOpenedUnderlyingDevice = false;
0242     // qCDebug(KArchiveLog) << mode;
0243     if (mode == QIODevice::ReadOnly) {
0244         d->buffer.resize(0);
0245     } else {
0246         d->buffer.resize(BUFFER_SIZE);
0247         d->filter->setOutBuffer(d->buffer.data(), d->buffer.size());
0248     }
0249     if (!d->filter->device()->isOpen()) {
0250         if (!d->filter->device()->open(mode)) {
0251             // qCWarning(KArchiveLog) << "KCompressionDevice::open: Couldn't open underlying device";
0252             d->propagateErrorCode();
0253             return false;
0254         }
0255         d->bOpenedUnderlyingDevice = true;
0256     }
0257     d->bNeedHeader = !d->bSkipHeaders;
0258     d->filter->setFilterFlags(d->bSkipHeaders ? KFilterBase::NoHeaders : KFilterBase::WithHeaders);
0259     if (!d->filter->init(mode)) {
0260         return false;
0261     }
0262     d->result = KFilterBase::Ok;
0263     setOpenMode(mode);
0264     return true;
0265 }
0266 
0267 void KCompressionDevice::close()
0268 {
0269     if (!isOpen()) {
0270         return;
0271     }
0272     if (d->filter->mode() == QIODevice::WriteOnly && d->errorCode == QFileDevice::NoError) {
0273         write(nullptr, 0); // finish writing
0274     }
0275     // qCDebug(KArchiveLog) << "Calling terminate().";
0276 
0277     if (!d->filter->terminate()) {
0278         // qCWarning(KArchiveLog) << "KCompressionDevice::close: terminate returned an error";
0279         d->errorCode = QFileDevice::UnspecifiedError;
0280     }
0281     if (d->bOpenedUnderlyingDevice) {
0282         QIODevice *dev = d->filter->device();
0283         dev->close();
0284         d->propagateErrorCode();
0285     }
0286     setOpenMode(QIODevice::NotOpen);
0287 }
0288 
0289 QFileDevice::FileError KCompressionDevice::error() const
0290 {
0291     return d->errorCode;
0292 }
0293 
0294 bool KCompressionDevice::seek(qint64 pos)
0295 {
0296     if (d->deviceReadPos == pos) {
0297         return QIODevice::seek(pos);
0298     }
0299 
0300     // qCDebug(KArchiveLog) << "seek(" << pos << ") called, current pos=" << QIODevice::pos();
0301 
0302     Q_ASSERT(d->filter->mode() == QIODevice::ReadOnly);
0303 
0304     if (pos == 0) {
0305         if (!QIODevice::seek(pos)) {
0306             return false;
0307         }
0308 
0309         // We can forget about the cached data
0310         d->bNeedHeader = !d->bSkipHeaders;
0311         d->result = KFilterBase::Ok;
0312         d->filter->setInBuffer(nullptr, 0);
0313         d->filter->reset();
0314         d->deviceReadPos = 0;
0315         return d->filter->device()->reset();
0316     }
0317 
0318     qint64 bytesToRead;
0319     if (d->deviceReadPos < pos) { // we can start from here
0320         bytesToRead = pos - d->deviceReadPos;
0321         // Since we're going to do a read() below
0322         // we need to reset the internal QIODevice pos to the real position we are
0323         // so that after read() we are indeed pointing to the pos seek
0324         // asked us to be in
0325         if (!QIODevice::seek(d->deviceReadPos)) {
0326             return false;
0327         }
0328     } else {
0329         // we have to start from 0 ! Ugly and slow, but better than the previous
0330         // solution (KTarGz was allocating everything into memory)
0331         if (!seek(0)) { // recursive
0332             return false;
0333         }
0334         bytesToRead = pos;
0335     }
0336 
0337     // qCDebug(KArchiveLog) << "reading " << bytesToRead << " dummy bytes";
0338     QByteArray dummy(qMin(bytesToRead, qint64(SEEK_BUFFER_SIZE)), 0);
0339     while (bytesToRead > 0) {
0340         const qint64 bytesToReadThisTime = qMin(bytesToRead, qint64(dummy.size()));
0341         const bool result = (read(dummy.data(), bytesToReadThisTime) == bytesToReadThisTime);
0342         if (!result) {
0343             return false;
0344         }
0345         bytesToRead -= bytesToReadThisTime;
0346     }
0347     return true;
0348 }
0349 
0350 bool KCompressionDevice::atEnd() const
0351 {
0352     return (d->type == KCompressionDevice::None || d->result == KFilterBase::End) //
0353         && QIODevice::atEnd() // take QIODevice's internal buffer into account
0354         && d->filter->device()->atEnd();
0355 }
0356 
0357 qint64 KCompressionDevice::readData(char *data, qint64 maxlen)
0358 {
0359     Q_ASSERT(d->filter->mode() == QIODevice::ReadOnly);
0360     // qCDebug(KArchiveLog) << "maxlen=" << maxlen;
0361     KFilterBase *filter = d->filter;
0362 
0363     uint dataReceived = 0;
0364 
0365     // We came to the end of the stream
0366     if (d->result == KFilterBase::End) {
0367         return dataReceived;
0368     }
0369 
0370     // If we had an error, return -1.
0371     if (d->result != KFilterBase::Ok) {
0372         return -1;
0373     }
0374 
0375     qint64 availOut = maxlen;
0376     filter->setOutBuffer(data, maxlen);
0377 
0378     while (dataReceived < maxlen) {
0379         if (filter->inBufferEmpty()) {
0380             // Not sure about the best size to set there.
0381             // For sure, it should be bigger than the header size (see comment in readHeader)
0382             d->buffer.resize(BUFFER_SIZE);
0383             // Request data from underlying device
0384             int size = filter->device()->read(d->buffer.data(), d->buffer.size());
0385             // qCDebug(KArchiveLog) << "got" << size << "bytes from device";
0386             if (size) {
0387                 filter->setInBuffer(d->buffer.data(), size);
0388             } else {
0389                 // Not enough data available in underlying device for now
0390                 break;
0391             }
0392         }
0393         if (d->bNeedHeader) {
0394             (void)filter->readHeader();
0395             d->bNeedHeader = false;
0396         }
0397 
0398         d->result = filter->uncompress();
0399 
0400         if (d->result == KFilterBase::Error) {
0401             // qCWarning(KArchiveLog) << "KCompressionDevice: Error when uncompressing data";
0402             break;
0403         }
0404 
0405         // We got that much data since the last time we went here
0406         uint outReceived = availOut - filter->outBufferAvailable();
0407         // qCDebug(KArchiveLog) << "avail_out = " << filter->outBufferAvailable() << " result=" << d->result << " outReceived=" << outReceived;
0408         if (availOut < uint(filter->outBufferAvailable())) {
0409             // qCWarning(KArchiveLog) << " last availOut " << availOut << " smaller than new avail_out=" << filter->outBufferAvailable() << " !";
0410         }
0411 
0412         dataReceived += outReceived;
0413         data += outReceived;
0414         availOut = maxlen - dataReceived;
0415         if (d->result == KFilterBase::End) {
0416             // We're actually at the end, no more data to check
0417             if (filter->device()->atEnd()) {
0418                 break;
0419             }
0420 
0421             // Still not done, re-init and try again
0422             filter->init(filter->mode());
0423         }
0424         filter->setOutBuffer(data, availOut);
0425     }
0426 
0427     d->deviceReadPos += dataReceived;
0428     return dataReceived;
0429 }
0430 
0431 qint64 KCompressionDevice::writeData(const char *data /*0 to finish*/, qint64 len)
0432 {
0433     KFilterBase *filter = d->filter;
0434     Q_ASSERT(filter->mode() == QIODevice::WriteOnly);
0435     // If we had an error, return 0.
0436     if (d->result != KFilterBase::Ok) {
0437         return 0;
0438     }
0439 
0440     bool finish = (data == nullptr);
0441     if (!finish) {
0442         filter->setInBuffer(data, len);
0443         if (d->bNeedHeader) {
0444             (void)filter->writeHeader(d->origFileName);
0445             d->bNeedHeader = false;
0446         }
0447     }
0448 
0449     uint dataWritten = 0;
0450     uint availIn = len;
0451     while (dataWritten < len || finish) {
0452         d->result = filter->compress(finish);
0453 
0454         if (d->result == KFilterBase::Error) {
0455             // qCWarning(KArchiveLog) << "KCompressionDevice: Error when compressing data";
0456             // What to do ?
0457             break;
0458         }
0459 
0460         // Wrote everything ?
0461         if (filter->inBufferEmpty() || (d->result == KFilterBase::End)) {
0462             // We got that much data since the last time we went here
0463             uint wrote = availIn - filter->inBufferAvailable();
0464 
0465             // qCDebug(KArchiveLog) << " Wrote everything for now. avail_in=" << filter->inBufferAvailable() << "result=" << d->result << "wrote=" << wrote;
0466 
0467             // Move on in the input buffer
0468             data += wrote;
0469             dataWritten += wrote;
0470 
0471             availIn = len - dataWritten;
0472             // qCDebug(KArchiveLog) << " availIn=" << availIn << "dataWritten=" << dataWritten << "pos=" << pos();
0473             if (availIn > 0) {
0474                 filter->setInBuffer(data, availIn);
0475             }
0476         }
0477 
0478         if (filter->outBufferFull() || (d->result == KFilterBase::End) || finish) {
0479             // qCDebug(KArchiveLog) << " writing to underlying. avail_out=" << filter->outBufferAvailable();
0480             int towrite = d->buffer.size() - filter->outBufferAvailable();
0481             if (towrite > 0) {
0482                 // Write compressed data to underlying device
0483                 int size = filter->device()->write(d->buffer.data(), towrite);
0484                 if (size != towrite) {
0485                     // qCWarning(KArchiveLog) << "KCompressionDevice::write. Could only write " << size << " out of " << towrite << " bytes";
0486                     d->errorCode = QFileDevice::WriteError;
0487                     setErrorString(tr("Could not write. Partition full?"));
0488                     return 0; // indicate an error
0489                 }
0490                 // qCDebug(KArchiveLog) << " wrote " << size << " bytes";
0491             }
0492             if (d->result == KFilterBase::End) {
0493                 Q_ASSERT(finish); // hopefully we don't get end before finishing
0494                 break;
0495             }
0496             d->buffer.resize(BUFFER_SIZE);
0497             filter->setOutBuffer(d->buffer.data(), d->buffer.size());
0498         }
0499     }
0500 
0501     return dataWritten;
0502 }
0503 
0504 void KCompressionDevice::setOrigFileName(const QByteArray &fileName)
0505 {
0506     d->origFileName = fileName;
0507 }
0508 
0509 void KCompressionDevice::setSkipHeaders()
0510 {
0511     d->bSkipHeaders = true;
0512 }
0513 
0514 KFilterBase *KCompressionDevice::filterBase()
0515 {
0516     return d->filter;
0517 }
0518 
0519 #include "moc_kcompressiondevice.cpp"