KArchive

kcompressiondevice.cpp
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
35class KCompressionDevicePrivate
36{
37public:
38 KCompressionDevicePrivate(KCompressionDevice *qq)
39 : bNeedHeader(true)
40 , bSkipHeaders(false)
41 , bOpenedUnderlyingDevice(false)
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;
57 KFilterBase *filter;
59 QFileDevice::FileError errorCode;
60 qint64 deviceReadPos;
62};
63
64void 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
76static 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)) {
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
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")) {
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
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;
180#if HAVE_ZSTD_SUPPORT
181 return new KZstdFilter;
182#else
183 return nullptr;
184#endif
185 }
186 return nullptr;
187}
188
189KCompressionDevice::KCompressionDevice(QIODevice *inputDevice, bool autoDeleteInputDevice, CompressionType type)
190 : d(new KCompressionDevicePrivate(this))
191{
192 assert(inputDevice);
193 d->filter = filterForCompressionType(type);
194 if (d->filter) {
195 d->type = type;
196 d->filter->setDevice(inputDevice, autoDeleteInputDevice);
197 }
198}
199
201 : d(new KCompressionDevicePrivate(this))
202{
203 QFile *f = new QFile(fileName);
204 d->filter = filterForCompressionType(type);
205 if (d->filter) {
206 d->type = type;
207 d->filter->setDevice(f, true);
208 } else {
209 delete f;
210 }
211}
212
214 : KCompressionDevice(fileName, findCompressionByFileName(fileName))
215{
216}
217
219{
220 if (isOpen()) {
221 close();
222 }
223 delete d->filter;
224 delete d;
225}
226
231
233{
234 if (isOpen()) {
235 // qCWarning(KArchiveLog) << "KCompressionDevice::open: device is already open";
236 return true; // QFile returns false, but well, the device -is- open...
237 }
238 if (!d->filter) {
239 return false;
240 }
241 d->bOpenedUnderlyingDevice = false;
242 // qCDebug(KArchiveLog) << mode;
243 if (mode == QIODevice::ReadOnly) {
244 d->buffer.resize(0);
245 } else {
246 d->buffer.resize(BUFFER_SIZE);
247 d->filter->setOutBuffer(d->buffer.data(), d->buffer.size());
248 }
249 if (!d->filter->device()->isOpen()) {
250 if (!d->filter->device()->open(mode)) {
251 // qCWarning(KArchiveLog) << "KCompressionDevice::open: Couldn't open underlying device";
252 d->propagateErrorCode();
253 return false;
254 }
255 d->bOpenedUnderlyingDevice = true;
256 }
257 d->bNeedHeader = !d->bSkipHeaders;
258 d->filter->setFilterFlags(d->bSkipHeaders ? KFilterBase::NoHeaders : KFilterBase::WithHeaders);
259 if (!d->filter->init(mode & ~QIODevice::Truncate)) {
260 return false;
261 }
262 d->result = KFilterBase::Ok;
263 setOpenMode(mode);
264 return true;
265}
266
268{
269 if (!isOpen()) {
270 return;
271 }
272 if (d->filter->mode() == QIODevice::WriteOnly && d->errorCode == QFileDevice::NoError) {
273 write(nullptr, 0); // finish writing
274 }
275 // qCDebug(KArchiveLog) << "Calling terminate().";
276
277 if (!d->filter->terminate()) {
278 // qCWarning(KArchiveLog) << "KCompressionDevice::close: terminate returned an error";
279 d->errorCode = QFileDevice::UnspecifiedError;
280 }
281 if (d->bOpenedUnderlyingDevice) {
282 QIODevice *dev = d->filter->device();
283 dev->close();
284 d->propagateErrorCode();
285 }
287}
288
290{
291 return d->errorCode;
292}
293
295{
296 if (d->deviceReadPos == pos) {
297 return QIODevice::seek(pos);
298 }
299
300 // qCDebug(KArchiveLog) << "seek(" << pos << ") called, current pos=" << QIODevice::pos();
301
302 Q_ASSERT(d->filter->mode() == QIODevice::ReadOnly);
303
304 if (pos == 0) {
305 if (!QIODevice::seek(pos)) {
306 return false;
307 }
308
309 // We can forget about the cached data
310 d->bNeedHeader = !d->bSkipHeaders;
311 d->result = KFilterBase::Ok;
312 d->filter->setInBuffer(nullptr, 0);
313 d->filter->reset();
314 d->deviceReadPos = 0;
315 return d->filter->device()->reset();
316 }
317
318 qint64 bytesToRead;
319 if (d->deviceReadPos < pos) { // we can start from here
320 bytesToRead = pos - d->deviceReadPos;
321 // Since we're going to do a read() below
322 // we need to reset the internal QIODevice pos to the real position we are
323 // so that after read() we are indeed pointing to the pos seek
324 // asked us to be in
325 if (!QIODevice::seek(d->deviceReadPos)) {
326 return false;
327 }
328 } else {
329 // we have to start from 0 ! Ugly and slow, but better than the previous
330 // solution (KTarGz was allocating everything into memory)
331 if (!seek(0)) { // recursive
332 return false;
333 }
334 bytesToRead = pos;
335 }
336
337 // qCDebug(KArchiveLog) << "reading " << bytesToRead << " dummy bytes";
338 QByteArray dummy(qMin(bytesToRead, qint64(SEEK_BUFFER_SIZE)), 0);
339 while (bytesToRead > 0) {
340 const qint64 bytesToReadThisTime = qMin(bytesToRead, qint64(dummy.size()));
341 const bool result = (read(dummy.data(), bytesToReadThisTime) == bytesToReadThisTime);
342 if (!result) {
343 return false;
344 }
345 bytesToRead -= bytesToReadThisTime;
346 }
347 return true;
348}
349
350bool KCompressionDevice::atEnd() const
351{
352 return (d->type == KCompressionDevice::None || d->result == KFilterBase::End) //
353 && QIODevice::atEnd() // take QIODevice's internal buffer into account
354 && d->filter->device()->atEnd();
355}
356
357qint64 KCompressionDevice::readData(char *data, qint64 maxlen)
358{
359 Q_ASSERT(d->filter->mode() == QIODevice::ReadOnly);
360 // qCDebug(KArchiveLog) << "maxlen=" << maxlen;
361 KFilterBase *filter = d->filter;
362
363 uint dataReceived = 0;
364
365 // We came to the end of the stream
366 if (d->result == KFilterBase::End) {
367 return dataReceived;
368 }
369
370 // If we had an error, return -1.
371 if (d->result != KFilterBase::Ok) {
372 return -1;
373 }
374
375 qint64 availOut = maxlen;
376 filter->setOutBuffer(data, maxlen);
377
378 while (dataReceived < maxlen) {
379 if (filter->inBufferEmpty()) {
380 // Not sure about the best size to set there.
381 // For sure, it should be bigger than the header size (see comment in readHeader)
382 d->buffer.resize(BUFFER_SIZE);
383 // Request data from underlying device
384 int size = filter->device()->read(d->buffer.data(), d->buffer.size());
385 // qCDebug(KArchiveLog) << "got" << size << "bytes from device";
386 if (size) {
387 filter->setInBuffer(d->buffer.data(), size);
388 } else {
389 // Not enough data available in underlying device for now
390 break;
391 }
392 }
393 if (d->bNeedHeader) {
394 (void)filter->readHeader();
395 d->bNeedHeader = false;
396 }
397
398 d->result = filter->uncompress();
399
400 if (d->result == KFilterBase::Error) {
401 // qCWarning(KArchiveLog) << "KCompressionDevice: Error when uncompressing data";
402 break;
403 }
404
405 // We got that much data since the last time we went here
406 uint outReceived = availOut - filter->outBufferAvailable();
407 // qCDebug(KArchiveLog) << "avail_out = " << filter->outBufferAvailable() << " result=" << d->result << " outReceived=" << outReceived;
408 if (availOut < uint(filter->outBufferAvailable())) {
409 // qCWarning(KArchiveLog) << " last availOut " << availOut << " smaller than new avail_out=" << filter->outBufferAvailable() << " !";
410 }
411
412 dataReceived += outReceived;
413 data += outReceived;
414 availOut = maxlen - dataReceived;
415 if (d->result == KFilterBase::End) {
416 // We're actually at the end, no more data to check
417 if (filter->device()->atEnd()) {
418 break;
419 }
420
421 // Still not done, re-init and try again
422 filter->init(filter->mode());
423 }
424 filter->setOutBuffer(data, availOut);
425 }
426
427 d->deviceReadPos += dataReceived;
428 return dataReceived;
429}
430
431qint64 KCompressionDevice::writeData(const char *data /*0 to finish*/, qint64 len)
432{
433 KFilterBase *filter = d->filter;
434 Q_ASSERT(filter->mode() == QIODevice::WriteOnly);
435 // If we had an error, return 0.
436 if (d->result != KFilterBase::Ok) {
437 return 0;
438 }
439
440 bool finish = (data == nullptr);
441 if (!finish) {
442 filter->setInBuffer(data, len);
443 if (d->bNeedHeader) {
444 (void)filter->writeHeader(d->origFileName);
445 d->bNeedHeader = false;
446 }
447 }
448
449 uint dataWritten = 0;
450 uint availIn = len;
451 while (dataWritten < len || finish) {
452 d->result = filter->compress(finish);
453
454 if (d->result == KFilterBase::Error) {
455 // qCWarning(KArchiveLog) << "KCompressionDevice: Error when compressing data";
456 // What to do ?
457 break;
458 }
459
460 // Wrote everything ?
461 if (filter->inBufferEmpty() || (d->result == KFilterBase::End)) {
462 // We got that much data since the last time we went here
463 uint wrote = availIn - filter->inBufferAvailable();
464
465 // qCDebug(KArchiveLog) << " Wrote everything for now. avail_in=" << filter->inBufferAvailable() << "result=" << d->result << "wrote=" << wrote;
466
467 // Move on in the input buffer
468 data += wrote;
469 dataWritten += wrote;
470
471 availIn = len - dataWritten;
472 // qCDebug(KArchiveLog) << " availIn=" << availIn << "dataWritten=" << dataWritten << "pos=" << pos();
473 if (availIn > 0) {
474 filter->setInBuffer(data, availIn);
475 }
476 }
477
478 if (filter->outBufferFull() || (d->result == KFilterBase::End) || finish) {
479 // qCDebug(KArchiveLog) << " writing to underlying. avail_out=" << filter->outBufferAvailable();
480 int towrite = d->buffer.size() - filter->outBufferAvailable();
481 if (towrite > 0) {
482 // Write compressed data to underlying device
483 int size = filter->device()->write(d->buffer.data(), towrite);
484 if (size != towrite) {
485 // qCWarning(KArchiveLog) << "KCompressionDevice::write. Could only write " << size << " out of " << towrite << " bytes";
486 d->errorCode = QFileDevice::WriteError;
487 setErrorString(tr("Could not write. Partition full?"));
488 return 0; // indicate an error
489 }
490 // qCDebug(KArchiveLog) << " wrote " << size << " bytes";
491 }
492 if (d->result == KFilterBase::End) {
493 Q_ASSERT(finish); // hopefully we don't get end before finishing
494 break;
495 }
496 d->buffer.resize(BUFFER_SIZE);
497 filter->setOutBuffer(d->buffer.data(), d->buffer.size());
498 }
499 }
500
501 return dataWritten;
502}
503
505{
506 d->origFileName = fileName;
507}
508
510{
511 d->bSkipHeaders = true;
512}
513
514KFilterBase *KCompressionDevice::filterBase()
515{
516 return d->filter;
517}
518
519#include "moc_kcompressiondevice.cpp"
A class for reading and writing compressed data onto a device (e.g.
void setOrigFileName(const QByteArray &fileName)
For writing gzip compressed files only: set the name of the original file, to be used in the gzip hea...
KCompressionDevice(QIODevice *inputDevice, bool autoDeleteInputDevice, CompressionType type)
Constructs a KCompressionDevice for a given CompressionType (e.g.
static CompressionType compressionTypeForMimeType(const QString &mimetype)
Returns the compression type for the given MIME type, if possible.
static KFilterBase * filterForCompressionType(CompressionType type)
Call this to create the appropriate filter for the CompressionType named type.
QFileDevice::FileError error() const
Returns the error code from the last failing operation.
bool seek(qint64) override
That one can be quite slow, when going back.
void close() override
Close after reading or writing.
CompressionType compressionType() const
The compression actually used by this device.
~KCompressionDevice() override
Destructs the KCompressionDevice.
void setSkipHeaders()
Call this let this device skip the gzip headers when reading/writing.
bool open(QIODevice::OpenMode mode) override
Open for reading or writing.
This is the base class for compression filters such as gzip and bzip2.
Definition kfilterbase.h:27
virtual bool terminate()
virtual void setInBuffer(const char *data, uint size)=0
virtual bool init(int mode)=0
virtual void setOutBuffer(char *data, uint maxlen)=0
void setFilterFlags(FilterFlags flags)
virtual void reset()
QIODevice * device()
Returns the device on which the filter will work.
void setDevice(QIODevice *dev, bool autodelete=false)
Sets the device on which the filter will work.
virtual int mode() const =0
Internal class used by KCompressionDevice.
Definition kgzipfilter.h:20
Internal class used by KCompressionDevice.
Definition knonefilter.h:23
char * data()
void resize(qsizetype newSize, char c)
qsizetype size() const const
virtual bool atEnd() const const
virtual void close()
QString errorString() const const
bool isOpen() const const
virtual bool open(QIODeviceBase::OpenMode mode)
virtual qint64 pos() const const
QByteArray read(qint64 maxSize)
virtual bool reset()
virtual bool seek(qint64 pos)
void setErrorString(const QString &str)
void setOpenMode(QIODeviceBase::OpenMode openMode)
virtual qint64 size() const const
qint64 write(const QByteArray &data)
QMimeType mimeTypeForName(const QString &nameOrAlias) const const
bool inherits(const QString &mimeTypeName) const const
bool isValid() const const
QString tr(const char *sourceText, const char *disambiguation, int n)
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
CaseInsensitive
QFuture< void > filter(QThreadPool *pool, Sequence &sequence, KeepFunctor &&filterFunction)
This file is part of the KDE documentation.
Documentation copyright © 1996-2024 The KDE developers.
Generated on Fri Dec 6 2024 12:05:35 by doxygen 1.12.0 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.