File indexing completed on 2025-02-16 13:00:36
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"