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)
42 , type(KCompressionDevice::None)
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;
56 KFilterBase::Result result;
57 KFilterBase *filter;
58 KCompressionDevice::CompressionType type;
59 QFileDevice::FileError errorCode;
60 qint64 deviceReadPos;
61 KCompressionDevice *q;
62};
63
64void KCompressionDevicePrivate::propagateErrorCode()
65{
66 QIODevice *dev = filter->device();
67 if (QFileDevice *fileDev = qobject_cast<QFileDevice *>(object: 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(s: QLatin1String(".gz"), cs: Qt::CaseInsensitive)) {
79 return KCompressionDevice::GZip;
80 }
81#if HAVE_BZIP2_SUPPORT
82 if (fileName.endsWith(s: QLatin1String(".bz2"), cs: Qt::CaseInsensitive)) {
83 return KCompressionDevice::BZip2;
84 }
85#endif
86#if HAVE_XZ_SUPPORT
87 if (fileName.endsWith(s: QLatin1String(".lzma"), cs: Qt::CaseInsensitive) || fileName.endsWith(s: QLatin1String(".xz"), cs: Qt::CaseInsensitive)) {
88 return KCompressionDevice::Xz;
89 }
90#endif
91#if HAVE_ZSTD_SUPPORT
92 if (fileName.endsWith(s: QLatin1String(".zst"), cs: Qt::CaseInsensitive)) {
93 return KCompressionDevice::Zstd;
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
105KCompressionDevice::CompressionType KCompressionDevice::compressionTypeForMimeType(const QString &mimeType)
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")) {
128 return KCompressionDevice::Zstd;
129 }
130#endif
131 QMimeDatabase db;
132 const QMimeType mime = db.mimeTypeForName(nameOrAlias: 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
160KFilterBase *KCompressionDevice::filterForCompressionType(KCompressionDevice::CompressionType type)
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;
179 case KCompressionDevice::Zstd:
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(dev: inputDevice, autodelete: autoDeleteInputDevice);
197 }
198}
199
200KCompressionDevice::KCompressionDevice(const QString &fileName, CompressionType type)
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(dev: f, autodelete: true);
208 } else {
209 delete f;
210 }
211}
212
213KCompressionDevice::KCompressionDevice(const QString &fileName)
214 : KCompressionDevice(fileName, findCompressionByFileName(fileName))
215{
216}
217
218KCompressionDevice::~KCompressionDevice()
219{
220 if (isOpen()) {
221 close();
222 }
223 delete d->filter;
224 delete d;
225}
226
227KCompressionDevice::CompressionType KCompressionDevice::compressionType() const
228{
229 return d->type;
230}
231
232bool KCompressionDevice::open(QIODevice::OpenMode mode)
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(size: 0);
245 } else {
246 d->buffer.resize(BUFFER_SIZE);
247 d->filter->setOutBuffer(data: d->buffer.data(), maxlen: 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)) {
260 return false;
261 }
262 d->result = KFilterBase::Ok;
263 setOpenMode(mode);
264 return true;
265}
266
267void KCompressionDevice::close()
268{
269 if (!isOpen()) {
270 return;
271 }
272 if (d->filter->mode() == QIODevice::WriteOnly && d->errorCode == QFileDevice::NoError) {
273 write(data: nullptr, len: 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 }
286 setOpenMode(QIODevice::NotOpen);
287}
288
289QFileDevice::FileError KCompressionDevice::error() const
290{
291 return d->errorCode;
292}
293
294bool KCompressionDevice::seek(qint64 pos)
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(data: nullptr, size: 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(pos: 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(pos: 0)) { // recursive
332 return false;
333 }
334 bytesToRead = pos;
335 }
336
337 // qCDebug(KArchiveLog) << "reading " << bytesToRead << " dummy bytes";
338 QByteArray dummy(qMin(a: bytesToRead, b: qint64(SEEK_BUFFER_SIZE)), 0);
339 while (bytesToRead > 0) {
340 const qint64 bytesToReadThisTime = qMin(a: bytesToRead, b: qint64(dummy.size()));
341 const bool result = (read(data: dummy.data(), maxlen: 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(data: d->buffer.data(), maxlen: d->buffer.size());
385 // qCDebug(KArchiveLog) << "got" << size << "bytes from device";
386 if (size) {
387 filter->setInBuffer(data: 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(mode: filter->mode());
423 }
424 filter->setOutBuffer(data, maxlen: 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, size: len);
443 if (d->bNeedHeader) {
444 (void)filter->writeHeader(filename: 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, size: 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(data: d->buffer.data(), len: 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(s: "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(data: d->buffer.data(), maxlen: d->buffer.size());
498 }
499 }
500
501 return dataWritten;
502}
503
504void KCompressionDevice::setOrigFileName(const QByteArray &fileName)
505{
506 d->origFileName = fileName;
507}
508
509void KCompressionDevice::setSkipHeaders()
510{
511 d->bSkipHeaders = true;
512}
513
514KFilterBase *KCompressionDevice::filterBase()
515{
516 return d->filter;
517}
518
519#include "moc_kcompressiondevice.cpp"
520

source code of karchive/src/kcompressiondevice.cpp