File indexing completed on 2024-03-24 03:55:19
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"