KArchive

ktar.cpp
1/* This file is part of the KDE libraries
2 SPDX-FileCopyrightText: 2000 David Faure <faure@kde.org>
3 SPDX-FileCopyrightText: 2003 Leo Savernik <l.savernik@aon.at>
4
5 SPDX-License-Identifier: LGPL-2.0-or-later
6*/
7
8#include "ktar.h"
9#include "karchive_p.h"
10#include "kcompressiondevice.h"
11#include "kfilterbase.h"
12#include "loggingcategory.h"
13
14#include <QDebug>
15#include <QDir>
16#include <QFile>
17#include <QMimeDatabase>
18#include <QTemporaryFile>
19
20#include <assert.h>
21#include <stdlib.h> // strtol
22
23////////////////////////////////////////////////////////////////////////
24/////////////////////////// KTar ///////////////////////////////////
25////////////////////////////////////////////////////////////////////////
26
27// Mime types of known filters
28static const char application_bzip[] = "application/x-bzip";
29static const char application_lzma[] = "application/x-lzma";
30static const char application_xz[] = "application/x-xz";
31static const char application_zstd[] = "application/zstd";
32
33/* clang-format off */
34namespace MimeType
35{
36QString application_gzip() { return QStringLiteral("application/gzip"); }
37QString application_gzip_old() { return QStringLiteral("application/x-gzip"); }
38}
39/* clang-format on */
40
41class Q_DECL_HIDDEN KTar::KTarPrivate
42{
43public:
44 KTarPrivate(KTar *parent)
45 : q(parent)
46 , tarEnd(0)
47 , tmpFile(nullptr)
48 , compressionDevice(nullptr)
49 {
50 }
51
52 KTar *q;
53 QStringList dirList;
54 qint64 tarEnd;
55 QTemporaryFile *tmpFile;
56 QString mimetype;
57 QByteArray origFileName;
58 KCompressionDevice *compressionDevice;
59
60 bool fillTempFile(const QString &fileName);
61 bool writeBackTempFile(const QString &fileName);
62 void fillBuffer(char *buffer, const char *mode, qint64 size, const QDateTime &mtime, char typeflag, const char *uname, const char *gname);
63 [[nodiscard]] bool writeLonglink(char *buffer, const QByteArray &name, char typeflag, const char *uname, const char *gname);
64 qint64 readRawHeader(char *buffer);
65 bool readLonglink(char *buffer, QByteArray &longlink);
66 qint64 readHeader(char *buffer, QString &name, QString &symlink);
67};
68
69KTar::KTar(const QString &fileName, const QString &_mimetype)
71 , d(new KTarPrivate(this))
72{
73 // shared-mime-info < 1.1 does not know about application/gzip.
74 // While Qt has optionally a copy of shared-mime-info (1.10 for 5.15.0),
75 // it uses the system one if it exists.
76 // Once shared-mime-info 1.1 is required or can be assumed on all targeted
77 // platforms (right now RHEL/CentOS 6 as target of appimage-based apps
78 // bundling also karchive does not meet this requirement)
79 // switch to use the new application/gzip id instead when interacting with QMimeDatabase
80 // For now: map new name to legacy name and use that
81 d->mimetype = (_mimetype == MimeType::application_gzip()) ? MimeType::application_gzip_old() : _mimetype;
82}
83
85 : KArchive(dev)
86 , d(new KTarPrivate(this))
87{
88}
89
90// Only called when a filename was given
92{
93 if (d->mimetype.isEmpty()) {
94 // Find out mimetype manually
95
97 QMimeType mime;
99 // Give priority to file contents: if someone renames a .tar.bz2 to .tar.gz,
100 // we can still do the right thing here.
101 QFile f(fileName());
102 if (f.open(QIODevice::ReadOnly)) {
103 mime = db.mimeTypeForData(&f);
104 }
105 if (!mime.isValid()) {
106 // Unable to determine mimetype from contents, get it from file name
108 }
109 } else {
111 }
112
113 // qCDebug(KArchiveLog) << mode << mime->name();
114
115 if (mime.inherits(QStringLiteral("application/x-compressed-tar")) || mime.inherits(MimeType::application_gzip_old())) {
116 // gzipped tar file (with possibly invalid file name), ask for gzip filter
117 d->mimetype = MimeType::application_gzip_old();
118 } else if (mime.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || mime.inherits(QStringLiteral("application/x-bzip2-compressed-tar"))
119 || mime.inherits(QStringLiteral("application/x-bzip2")) || mime.inherits(QString::fromLatin1(application_bzip))) {
120 // bzipped2 tar file (with possibly invalid file name), ask for bz2 filter
121 d->mimetype = QString::fromLatin1(application_bzip);
122 } else if (mime.inherits(QStringLiteral("application/x-lzma-compressed-tar")) || mime.inherits(QString::fromLatin1(application_lzma))) {
123 // lzma compressed tar file (with possibly invalid file name), ask for xz filter
124 d->mimetype = QString::fromLatin1(application_lzma);
125 } else if (mime.inherits(QStringLiteral("application/x-xz-compressed-tar")) || mime.inherits(QString::fromLatin1(application_xz))) {
126 // xz compressed tar file (with possibly invalid name), ask for xz filter
127 d->mimetype = QString::fromLatin1(application_xz);
128 } else if (mime.inherits(QStringLiteral("application/x-zstd-compressed-tar")) || mime.inherits(QString::fromLatin1(application_zstd))) {
129 // zstd compressed tar file (with possibly invalid name), ask for zstd filter
130 d->mimetype = QString::fromLatin1(application_zstd);
131 }
132 }
133
134 if (d->mimetype == QLatin1String("application/x-tar")) {
136 } else if (mode == QIODevice::WriteOnly) {
138 return false;
139 }
140 if (!d->mimetype.isEmpty()) {
141 // Create a compression filter on top of the QSaveFile device that KArchive created.
142 // qCDebug(KArchiveLog) << "creating KCompressionDevice for" << d->mimetype;
144 d->compressionDevice = new KCompressionDevice(device(), false, type);
145 setDevice(d->compressionDevice);
146 }
147 return true;
148 } else {
149 // The compression filters are very slow with random access.
150 // So instead of applying the filter to the device,
151 // the file is completely extracted instead,
152 // and we work on the extracted tar file.
153 // This improves the extraction speed by the archive KIO worker supporting the tar protocol dramatically,
154 // if the archive file contains many files.
155 // This is because the archive KIO worker extracts one file after the other and normally
156 // has to walk through the decompression filter each time.
157 // Which is in fact nearly as slow as a complete decompression for each file.
158
159 Q_ASSERT(!d->tmpFile);
160 d->tmpFile = new QTemporaryFile();
161 d->tmpFile->setFileTemplate(QDir::tempPath() + QLatin1Char('/') + QLatin1String("ktar-XXXXXX.tar"));
162 d->tmpFile->open();
163 // qCDebug(KArchiveLog) << "creating tempfile:" << d->tmpFile->fileName();
164
165 setDevice(d->tmpFile);
166 return true;
167 }
168}
169
171{
172 // mjarrett: Closes to prevent ~KArchive from aborting w/o device
173 if (isOpen()) {
174 close();
175 }
176
177 delete d->tmpFile;
178 delete d->compressionDevice;
179 delete d;
180}
181
183{
184 if (!isOpen() || !(mode() & QIODevice::WriteOnly)) {
185 // qCWarning(KArchiveLog) << "KTar::setOrigFileName: File must be opened for writing first.\n";
186 return;
187 }
188 d->origFileName = fileName;
189}
190
191qint64 KTar::KTarPrivate::readRawHeader(char *buffer)
192{
193 // Read header
194 qint64 n = q->device()->read(buffer, 0x200);
195 // we need to test if there is a prefix value because the file name can be null
196 // and the prefix can have a value and in this case we don't reset n.
197 if (n == 0x200 && (buffer[0] != 0 || buffer[0x159] != 0)) {
198 // Make sure this is actually a tar header
199 if (strncmp(buffer + 257, "ustar", 5)) {
200 // The magic isn't there (broken/old tars), but maybe a correct checksum?
201
202 int check = 0;
203 for (uint j = 0; j < 0x200; ++j) {
204 check += static_cast<unsigned char>(buffer[j]);
205 }
206
207 // adjust checksum to count the checksum fields as blanks
208 for (uint j = 0; j < 8 /*size of the checksum field including the \0 and the space*/; j++) {
209 check -= static_cast<unsigned char>(buffer[148 + j]);
210 }
211 check += 8 * ' ';
212
213 QByteArray s = QByteArray::number(check, 8); // octal
214
215 // only compare those of the 6 checksum digits that mean something,
216 // because the other digits are filled with all sorts of different chars by different tars ...
217 // Some tars right-justify the checksum so it could start in one of three places - we have to check each.
218 if (strncmp(buffer + 148 + 6 - s.length(), s.data(), s.length()) //
219 && strncmp(buffer + 148 + 7 - s.length(), s.data(), s.length()) //
220 && strncmp(buffer + 148 + 8 - s.length(), s.data(), s.length())) {
221 /*qCWarning(KArchiveLog) << "KTar: invalid TAR file. Header is:" << QByteArray( buffer+257, 5 )
222 << "instead of ustar. Reading from wrong pos in file?"
223 << "checksum=" << QByteArray( buffer + 148 + 6 - s.length(), s.length() );*/
224 return -1;
225 }
226 } /*end if*/
227 } else {
228 // reset to 0 if 0x200 because logical end of archive has been reached
229 if (n == 0x200) {
230 n = 0;
231 }
232 } /*end if*/
233 return n;
234}
235
236bool KTar::KTarPrivate::readLonglink(char *buffer, QByteArray &longlink)
237{
238 qint64 n = 0;
239 // qCDebug(KArchiveLog) << "reading longlink from pos " << q->device()->pos();
240 QIODevice *dev = q->device();
241 // read size of longlink from size field in header
242 // size is in bytes including the trailing null (which we ignore)
243 qint64 size = QByteArray(buffer + 0x7c, 12).trimmed().toLongLong(nullptr, 8 /*octal*/);
244
245 size--; // ignore trailing null
246 if (size > std::numeric_limits<int>::max() - 32) { // QByteArray can't really be INT_MAX big, it's max size is something between INT_MAX - 32 and INT_MAX
247 // depending the platform so just be safe
248 qCWarning(KArchiveLog) << "Failed to allocate memory for longlink of size" << size;
249 return false;
250 }
251 if (size < 0) {
252 qCWarning(KArchiveLog) << "Invalid longlink size" << size;
253 return false;
254 }
255 longlink.resize(size);
256 qint64 offset = 0;
257 while (size > 0) {
258 int chunksize = qMin(size, 0x200LL);
259 n = dev->read(longlink.data() + offset, chunksize);
260 if (n == -1) {
261 return false;
262 }
263 size -= chunksize;
264 offset += 0x200;
265 } /*wend*/
266 // jump over the rest
267 const int skip = 0x200 - (n % 0x200);
268 if (skip <= 0x200) {
269 if (dev->read(buffer, skip) != skip) {
270 return false;
271 }
272 }
273 longlink.truncate(qstrlen(longlink.constData()));
274 return true;
275}
276
277qint64 KTar::KTarPrivate::readHeader(char *buffer, QString &name, QString &symlink)
278{
279 name.truncate(0);
280 symlink.truncate(0);
281 while (true) {
282 qint64 n = readRawHeader(buffer);
283 if (n != 0x200) {
284 return n;
285 }
286
287 // is it a longlink?
288 if (strcmp(buffer, "././@LongLink") == 0) {
289 char typeflag = buffer[0x9c];
290 QByteArray longlink;
291 if (readLonglink(buffer, longlink)) {
292 switch (typeflag) {
293 case 'L':
294 name = QFile::decodeName(longlink.constData());
295 break;
296 case 'K':
297 symlink = QFile::decodeName(longlink.constData());
298 break;
299 } /*end switch*/
300 }
301 } else {
302 break;
303 } /*end if*/
304 } /*wend*/
305
306 // if not result of longlink, read names directly from the header
307 if (name.isEmpty())
308 // there are names that are exactly 100 bytes long
309 // and neither longlink nor \0 terminated (bug:101472)
310 {
311 name = QFile::decodeName(QByteArray(buffer, qstrnlen(buffer, 100)));
312 }
313 if (symlink.isEmpty()) {
314 char *symlinkBuffer = buffer + 0x9d /*?*/;
315 symlink = QFile::decodeName(QByteArray(symlinkBuffer, qstrnlen(symlinkBuffer, 100)));
316 }
317
318 return 0x200;
319}
320
321/*
322 * If we have created a temporary file, we have
323 * to decompress the original file now and write
324 * the contents to the temporary file.
325 */
326bool KTar::KTarPrivate::fillTempFile(const QString &fileName)
327{
328 if (!tmpFile) {
329 return true;
330 }
331
332 // qCDebug(KArchiveLog) << "filling tmpFile of mimetype" << mimetype;
333
335 KCompressionDevice filterDev(fileName, compressionType);
336
337 QFile *file = tmpFile;
338 Q_ASSERT(file->isOpen());
339 Q_ASSERT(file->openMode() & QIODevice::WriteOnly);
340 file->seek(0);
341 QByteArray buffer;
342 buffer.resize(8 * 1024);
343 if (!filterDev.open(QIODevice::ReadOnly)) {
344 q->setErrorString(tr("File %1 does not exist").arg(fileName));
345 return false;
346 }
347 qint64 len = -1;
348 while (!filterDev.atEnd() && len != 0) {
349 len = filterDev.read(buffer.data(), buffer.size());
350 if (len < 0) { // corrupted archive
351 q->setErrorString(tr("Archive %1 is corrupt").arg(fileName));
352 return false;
353 }
354 if (file->write(buffer.data(), len) != len) { // disk full
355 q->setErrorString(tr("Disk full"));
356 return false;
357 }
358 }
359 filterDev.close();
360
361 file->flush();
362 file->seek(0);
363 Q_ASSERT(file->isOpen());
364 Q_ASSERT(file->openMode() & QIODevice::ReadOnly);
365
366 // qCDebug(KArchiveLog) << "filling tmpFile finished.";
367 return true;
368}
369
371{
372 if (!(mode & QIODevice::ReadOnly)) {
373 return true;
374 }
375
376 if (!d->fillTempFile(fileName())) {
377 return false;
378 }
379
380 // We'll use the permission and user/group of d->rootDir
381 // for any directory we emulate (see findOrCreate)
382 // struct stat buf;
383 // stat( fileName(), &buf );
384
385 d->dirList.clear();
386 QIODevice *dev = device();
387
388 if (!dev) {
389 setErrorString(tr("Could not get underlying device"));
390 qCWarning(KArchiveLog) << "Could not get underlying device";
391 return false;
392 }
393
394 // read dir information
395 char buffer[0x200];
396 bool ende = false;
397 do {
398 QString name;
399 QString symlink;
400
401 // Read header
402 qint64 n = d->readHeader(buffer, name, symlink);
403 if (n < 0) {
404 setErrorString(tr("Could not read tar header"));
405 return false;
406 }
407 if (n == 0x200) {
408 bool isdir = false;
409
410 if (name.isEmpty()) {
411 continue;
412 }
413 if (name.endsWith(QLatin1Char('/'))) {
414 isdir = true;
415 name.truncate(name.length() - 1);
416 }
417
418 QByteArray prefix = QByteArray(buffer + 0x159, 155);
419 if (prefix[0] != '\0') {
420 name = (QString::fromLatin1(prefix.constData()) + QLatin1Char('/') + name);
421 }
422
423 int pos = name.lastIndexOf(QLatin1Char('/'));
424 QString nm = (pos == -1) ? name : name.mid(pos + 1);
425
426 // read access
427 buffer[0x6b] = 0;
428 char *dummy;
429 const char *p = buffer + 0x64;
430 while (*p == ' ') {
431 ++p;
432 }
433 int access = strtol(p, &dummy, 8);
434
435 // read user and group
436 const int maxUserGroupLength = 32;
437 const char *userStart = buffer + 0x109;
438 const int userLen = qstrnlen(userStart, maxUserGroupLength);
439 const QString user = QString::fromLocal8Bit(userStart, userLen);
440 const char *groupStart = buffer + 0x129;
441 const int groupLen = qstrnlen(groupStart, maxUserGroupLength);
442 const QString group = QString::fromLocal8Bit(groupStart, groupLen);
443
444 // read time
445 buffer[0x93] = 0;
446 p = buffer + 0x88;
447 while (*p == ' ') {
448 ++p;
449 }
450 uint time = strtol(p, &dummy, 8);
451
452 // read type flag
453 char typeflag = buffer[0x9c];
454 // '0' for files, '1' hard link, '2' symlink, '5' for directory
455 // (and 'L' for longlink fileNames, 'K' for longlink symlink targets)
456 // 'D' for GNU tar extension DUMPDIR, 'x' for Extended header referring
457 // to the next file in the archive and 'g' for Global extended header
458
459 if (typeflag == '5') {
460 isdir = true;
461 }
462
463 bool isDumpDir = false;
464 if (typeflag == 'D') {
465 isdir = false;
466 isDumpDir = true;
467 }
468 // qCDebug(KArchiveLog) << nm << "isdir=" << isdir << "pos=" << dev->pos() << "typeflag=" << typeflag << " islink=" << ( typeflag == '1' || typeflag
469 // == '2' );
470
471 if (typeflag == 'x' || typeflag == 'g') { // pax extended header, or pax global extended header
472 // Skip it for now. TODO: implement reading of extended header, as per https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html
473 (void)dev->read(buffer, 0x200);
474 continue;
475 }
476
477 if (isdir) {
478 access |= S_IFDIR; // broken tar files...
479 }
480
481 KArchiveEntry *e;
482 if (isdir) {
483 // qCDebug(KArchiveLog) << "directory" << nm;
484 e = new KArchiveDirectory(this, nm, access, KArchivePrivate::time_tToDateTime(time), user, group, symlink);
485 } else {
486 // read size
487 QByteArray sizeBuffer(buffer + 0x7c, 12);
488 qint64 size = sizeBuffer.trimmed().toLongLong(nullptr, 8 /*octal*/);
489 if (size < 0) {
490 qWarning() << "Tar file has negative size, resetting to 0";
491 size = 0;
492 }
493 // qCDebug(KArchiveLog) << "sizeBuffer='" << sizeBuffer << "' -> size=" << size;
494
495 // for isDumpDir we will skip the additional info about that dirs contents
496 if (isDumpDir) {
497 // qCDebug(KArchiveLog) << nm << "isDumpDir";
498 e = new KArchiveDirectory(this, nm, access, KArchivePrivate::time_tToDateTime(time), user, group, symlink);
499 } else {
500 // Let's hack around hard links. Our classes don't support that, so make them symlinks
501 if (typeflag == '1') {
502 // qCDebug(KArchiveLog) << "Hard link, setting size to 0 instead of" << size;
503 size = 0; // no contents
504 }
505
506 // qCDebug(KArchiveLog) << "file" << nm << "size=" << size;
507 e = new KArchiveFile(this, nm, access, KArchivePrivate::time_tToDateTime(time), user, group, symlink, dev->pos(), size);
508 }
509
510 // Skip contents + align bytes
511 qint64 rest = size % 0x200;
512 qint64 skip = size + (rest ? 0x200 - rest : 0);
513 // qCDebug(KArchiveLog) << "pos()=" << dev->pos() << "rest=" << rest << "skipping" << skip;
514 if (!dev->seek(dev->pos() + skip)) {
515 // qCWarning(KArchiveLog) << "skipping" << skip << "failed";
516 }
517 }
518
519 if (pos == -1) {
520 if (nm == QLatin1String(".")) { // special case
521 if (isdir) {
522 if (KArchivePrivate::hasRootDir(this)) {
523 qWarning() << "Broken tar file has two root dir entries";
524 delete e;
525 } else {
526 setRootDir(static_cast<KArchiveDirectory *>(e));
527 }
528 } else {
529 delete e;
530 }
531 } else {
532 rootDir()->addEntry(e);
533 }
534 } else {
535 // In some tar files we can find dir/./file => call cleanPath
536 QString path = QDir::cleanPath(name.left(pos));
537 // Ensure container directory exists, create otherwise
539 if (d) {
540 d->addEntry(e);
541 } else {
542 delete e;
543 return false;
544 }
545 }
546 } else {
547 // qCDebug(KArchiveLog) << "Terminating. Read " << n << " bytes, first one is " << buffer[0];
548 d->tarEnd = dev->pos() - n; // Remember end of archive
549 ende = true;
550 }
551 } while (!ende);
552 return true;
553}
554
555/*
556 * Writes back the changes of the temporary file
557 * to the original file.
558 * Must only be called if in write mode, not in read mode
559 */
560bool KTar::KTarPrivate::writeBackTempFile(const QString &fileName)
561{
562 if (!tmpFile) {
563 return true;
564 }
565
566 // qCDebug(KArchiveLog) << "Write temporary file to compressed file" << fileName << mimetype;
567
568 bool forced = false;
569 /* clang-format off */
570 if (MimeType::application_gzip_old() == mimetype ||
571 QLatin1String(application_bzip) == mimetype ||
572 QLatin1String(application_lzma) == mimetype ||
573 QLatin1String(application_xz) == mimetype) {
574 /* clang-format on */
575 forced = true;
576 }
577
578 // #### TODO this should use QSaveFile to avoid problems on disk full
579 // (KArchive uses QSaveFile by default, but the temp-uncompressed-file trick
580 // circumvents that).
581
582 KCompressionDevice dev(fileName);
583 QFile *file = tmpFile;
584 if (!dev.open(QIODevice::WriteOnly)) {
585 file->close();
586 q->setErrorString(tr("Failed to write back temp file: %1").arg(dev.errorString()));
587 return false;
588 }
589 if (forced) {
590 dev.setOrigFileName(origFileName);
591 }
592 file->seek(0);
593 QByteArray buffer;
594 buffer.resize(8 * 1024);
595 qint64 len;
596 while (!file->atEnd()) {
597 len = file->read(buffer.data(), buffer.size());
598 if (dev.write(buffer.data(), len) != len) {
599 file->close();
600 dev.close();
601 q->setErrorString(tr("Failed to write back temp file: %1").arg(dev.errorString()));
602 return false;
603 }
604 }
605 file->close();
606 dev.close();
607
608 // qCDebug(KArchiveLog) << "Write temporary file to compressed file done.";
609 return true;
610}
611
613{
614 d->dirList.clear();
615
616 bool ok = true;
617
618 // If we are in readwrite mode and had created
619 // a temporary tar file, we have to write
620 // back the changes to the original file
621 if (d->tmpFile && (mode() & QIODevice::WriteOnly)) {
622 ok = d->writeBackTempFile(fileName());
623 delete d->tmpFile;
624 d->tmpFile = nullptr;
625 setDevice(nullptr);
626 }
627
628 return ok;
629}
630
631bool KTar::doFinishWriting(qint64 size)
632{
633 // Write alignment
634 int rest = size % 0x200;
636 d->tarEnd = device()->pos() + (rest ? 0x200 - rest : 0); // Record our new end of archive
637 }
638 if (rest) {
639 char buffer[0x201];
640 for (uint i = 0; i < 0x200; ++i) {
641 buffer[i] = 0;
642 }
643 qint64 nwritten = device()->write(buffer, 0x200 - rest);
644 const bool ok = nwritten == 0x200 - rest;
645
646 if (!ok) {
647 setErrorString(tr("Couldn't write alignment: %1").arg(device()->errorString()));
648 }
649
650 return ok;
651 }
652 return true;
653}
654
655/*** Some help from the tar sources
656struct posix_header
657{ byte offset
658 char name[100]; * 0 * 0x0
659 char mode[8]; * 100 * 0x64
660 char uid[8]; * 108 * 0x6c
661 char gid[8]; * 116 * 0x74
662 char size[12]; * 124 * 0x7c
663 char mtime[12]; * 136 * 0x88
664 char chksum[8]; * 148 * 0x94
665 char typeflag; * 156 * 0x9c
666 char linkname[100]; * 157 * 0x9d
667 char magic[6]; * 257 * 0x101
668 char version[2]; * 263 * 0x107
669 char uname[32]; * 265 * 0x109
670 char gname[32]; * 297 * 0x129
671 char devmajor[8]; * 329 * 0x149
672 char devminor[8]; * 337 * ...
673 char prefix[155]; * 345 *
674 * 500 *
675};
676*/
677
678void KTar::KTarPrivate::fillBuffer(char *buffer, const char *mode, qint64 size, const QDateTime &mtime, char typeflag, const char *uname, const char *gname)
679{
680 // mode (as in stpos())
681 assert(strlen(mode) == 6);
682 memcpy(buffer + 0x64, mode, 6);
683 buffer[0x6a] = ' ';
684 buffer[0x6b] = '\0';
685
686 // dummy uid
687 strcpy(buffer + 0x6c, " 765 "); // 501 in decimal
688 // dummy gid
689 strcpy(buffer + 0x74, " 144 "); // 100 in decimal
690
691 // size
692 QByteArray s = QByteArray::number(size, 8); // octal
693 s = s.rightJustified(11, '0');
694 memcpy(buffer + 0x7c, s.data(), 11);
695 buffer[0x87] = ' '; // space-terminate (no null after)
696
697 // modification time
698 const QDateTime modificationTime = mtime.isValid() ? mtime : QDateTime::currentDateTime();
699 s = QByteArray::number(static_cast<qulonglong>(modificationTime.toMSecsSinceEpoch() / 1000), 8); // octal
700 s = s.rightJustified(11, '0');
701 memcpy(buffer + 0x88, s.data(), 11);
702 buffer[0x93] = ' '; // space-terminate (no null after) -- well current tar writes a null byte
703
704 // spaces, replaced by the check sum later
705 buffer[0x94] = 0x20;
706 buffer[0x95] = 0x20;
707 buffer[0x96] = 0x20;
708 buffer[0x97] = 0x20;
709 buffer[0x98] = 0x20;
710 buffer[0x99] = 0x20;
711
712 /* From the tar sources :
713 Fill in the checksum field. It's formatted differently from the
714 other fields: it has [6] digits, a null, then a space -- rather than
715 digits, a space, then a null. */
716
717 buffer[0x9a] = '\0';
718 buffer[0x9b] = ' ';
719
720 // type flag (dir, file, link)
721 buffer[0x9c] = typeflag;
722
723 // magic + version
724 strcpy(buffer + 0x101, "ustar");
725 strcpy(buffer + 0x107, "00");
726
727 // user
728 strcpy(buffer + 0x109, uname);
729 // group
730 strcpy(buffer + 0x129, gname);
731
732 // Header check sum
733 int check = 32;
734 for (uint j = 0; j < 0x200; ++j) {
735 check += static_cast<unsigned char>(buffer[j]);
736 }
737 s = QByteArray::number(check, 8); // octal
738 s = s.rightJustified(6, '0');
739 memcpy(buffer + 0x94, s.constData(), 6);
740}
741
742bool KTar::KTarPrivate::writeLonglink(char *buffer, const QByteArray &name, char typeflag, const char *uname, const char *gname)
743{
744 strcpy(buffer, "././@LongLink");
745 qint64 namelen = name.length() + 1;
746 fillBuffer(buffer, " 0", namelen, QDateTime(), typeflag, uname, gname);
747
748 if (q->device()->write(buffer, 0x200) != 0x200) {
749 q->setErrorString(tr("Couldn't write long link: %1").arg(q->device()->errorString()));
750 return false;
751 }
752
753 qint64 offset = 0;
754 while (namelen > 0) {
755 int chunksize = qMin(namelen, 0x200LL);
756 memcpy(buffer, name.data() + offset, chunksize);
757 // write long name
758 if (q->device()->write(buffer, 0x200) != 0x200) {
759 q->setErrorString(tr("Couldn't write long link: %1").arg(q->device()->errorString()));
760 return false;
761 }
762 // not even needed to reclear the buffer, tar doesn't do it
763 namelen -= chunksize;
764 offset += 0x200;
765 } /*wend*/
766
767 return true;
768}
769
771 const QString &user,
772 const QString &group,
773 qint64 size,
774 mode_t perm,
775 const QDateTime & /*atime*/,
776 const QDateTime &mtime,
777 const QDateTime & /*ctime*/)
778{
779 if (!isOpen()) {
780 setErrorString(tr("Application error: TAR file must be open before being written into"));
781 qCWarning(KArchiveLog) << "doPrepareWriting failed: !isOpen()";
782 return false;
783 }
784
785 if (!(mode() & QIODevice::WriteOnly)) {
786 setErrorString(tr("Application error: attempted to write into non-writable 7-Zip file"));
787 qCWarning(KArchiveLog) << "doPrepareWriting failed: !(mode() & QIODevice::WriteOnly)";
788 return false;
789 }
790
791 const qint64 MAX_FILESIZE = 077777777777L; // the format we use only allows 11 octal digits for size
792 if (size > MAX_FILESIZE) {
793 setErrorString(tr("Application limitation: Can not add file larger than %1 bytes").arg(MAX_FILESIZE));
794 return false;
795 }
796
797 // In some tar files we can find dir/./file => call cleanPath
799
800 /*
801 // Create toplevel dirs
802 // Commented out by David since it's not necessary, and if anybody thinks it is,
803 // he needs to implement a findOrCreate equivalent in writeDir.
804 // But as KTar and the "tar" program both handle tar files without
805 // dir entries, there's really no need for that
806 QString tmp ( fileName );
807 int i = tmp.lastIndexOf( '/' );
808 if ( i != -1 )
809 {
810 QString d = tmp.left( i + 1 ); // contains trailing slash
811 if ( !m_dirList.contains( d ) )
812 {
813 tmp = tmp.mid( i + 1 );
814 writeDir( d, user, group ); // WARNING : this one doesn't create its toplevel dirs
815 }
816 }
817 */
818
819 char buffer[0x201] = {0};
820
822 device()->seek(d->tarEnd); // Go to end of archive as might have moved with a read
823 }
824
825 // provide converted stuff we need later on
826 const QByteArray encodedFileName = QFile::encodeName(fileName);
827 const QByteArray uname = user.toLocal8Bit();
828 const QByteArray gname = group.toLocal8Bit();
829
830 // If more than 100 bytes, we need to use the LongLink trick
831 if (encodedFileName.length() > 99 && !d->writeLonglink(buffer, encodedFileName, 'L', uname.constData(), gname.constData())) {
832 return false;
833 }
834
835 // Write (potentially truncated) name
836 strncpy(buffer, encodedFileName.constData(), 99);
837 buffer[99] = 0;
838 // zero out the rest (except for what gets filled anyways)
839 memset(buffer + 0x9d, 0, 0x200 - 0x9d);
840
841 QByteArray permstr = QByteArray::number(static_cast<unsigned int>(perm), 8);
842 permstr = permstr.rightJustified(6, '0');
843 d->fillBuffer(buffer, permstr.constData(), size, mtime, 0x30, uname.constData(), gname.constData());
844
845 // Write header
846 if (device()->write(buffer, 0x200) != 0x200) {
847 setErrorString(tr("Failed to write header: %1").arg(device()->errorString()));
848 return false;
849 } else {
850 return true;
851 }
852}
853
854bool KTar::doWriteDir(const QString &name,
855 const QString &user,
856 const QString &group,
857 mode_t perm,
858 const QDateTime & /*atime*/,
859 const QDateTime &mtime,
860 const QDateTime & /*ctime*/)
861{
862 if (!isOpen()) {
863 setErrorString(tr("Application error: TAR file must be open before being written into"));
864 qCWarning(KArchiveLog) << "doWriteDir failed: !isOpen()";
865 return false;
866 }
867
868 if (!(mode() & QIODevice::WriteOnly)) {
869 setErrorString(tr("Application error: attempted to write into non-writable TAR file"));
870 qCWarning(KArchiveLog) << "doWriteDir failed: !(mode() & QIODevice::WriteOnly)";
871 return false;
872 }
873
874 // In some tar files we can find dir/./ => call cleanPath
875 QString dirName(QDir::cleanPath(name));
876
877 // Need trailing '/'
878 if (!dirName.endsWith(QLatin1Char('/'))) {
879 dirName += QLatin1Char('/');
880 }
881
882 if (d->dirList.contains(dirName)) {
883 return true; // already there
884 }
885
886 char buffer[0x201] = {0};
887
889 device()->seek(d->tarEnd); // Go to end of archive as might have moved with a read
890 }
891
892 // provide converted stuff we need lateron
893 QByteArray encodedDirname = QFile::encodeName(dirName);
894 QByteArray uname = user.toLocal8Bit();
895 QByteArray gname = group.toLocal8Bit();
896
897 // If more than 100 bytes, we need to use the LongLink trick
898 if (encodedDirname.length() > 99 && !d->writeLonglink(buffer, encodedDirname, 'L', uname.constData(), gname.constData())) {
899 return false;
900 }
901
902 // Write (potentially truncated) name
903 strncpy(buffer, encodedDirname.constData(), 99);
904 buffer[99] = 0;
905 // zero out the rest (except for what gets filled anyways)
906 memset(buffer + 0x9d, 0, 0x200 - 0x9d);
907
908 QByteArray permstr = QByteArray::number(static_cast<unsigned int>(perm), 8);
909 permstr = permstr.rightJustified(6, ' ');
910 d->fillBuffer(buffer, permstr.constData(), 0, mtime, 0x35, uname.constData(), gname.constData());
911
912 // Write header
913 device()->write(buffer, 0x200);
915 d->tarEnd = device()->pos();
916 }
917
918 d->dirList.append(dirName); // contains trailing slash
919 return true; // TODO if wanted, better error control
920}
921
923 const QString &target,
924 const QString &user,
925 const QString &group,
926 mode_t perm,
927 const QDateTime & /*atime*/,
928 const QDateTime &mtime,
929 const QDateTime & /*ctime*/)
930{
931 if (!isOpen()) {
932 setErrorString(tr("Application error: TAR file must be open before being written into"));
933 qCWarning(KArchiveLog) << "doWriteSymLink failed: !isOpen()";
934 return false;
935 }
936
937 if (!(mode() & QIODevice::WriteOnly)) {
938 setErrorString(tr("Application error: attempted to write into non-writable TAR file"));
939 qCWarning(KArchiveLog) << "doWriteSymLink failed: !(mode() & QIODevice::WriteOnly)";
940 return false;
941 }
942
943 // In some tar files we can find dir/./file => call cleanPath
945
946 char buffer[0x201] = {0};
947
949 device()->seek(d->tarEnd); // Go to end of archive as might have moved with a read
950 }
951
952 // provide converted stuff we need lateron
953 QByteArray encodedFileName = QFile::encodeName(fileName);
954 QByteArray encodedTarget = QFile::encodeName(target);
955 QByteArray uname = user.toLocal8Bit();
956 QByteArray gname = group.toLocal8Bit();
957
958 // If more than 100 bytes, we need to use the LongLink trick
959 if (encodedTarget.length() > 99 && !d->writeLonglink(buffer, encodedTarget, 'K', uname.constData(), gname.constData())) {
960 return false;
961 }
962 if (encodedFileName.length() > 99 && !d->writeLonglink(buffer, encodedFileName, 'L', uname.constData(), gname.constData())) {
963 return false;
964 }
965
966 // Write (potentially truncated) name
967 strncpy(buffer, encodedFileName.constData(), 99);
968 buffer[99] = 0;
969 // Write (potentially truncated) symlink target
970 strncpy(buffer + 0x9d, encodedTarget.constData(), 99);
971 buffer[0x9d + 99] = 0;
972 // zero out the rest
973 memset(buffer + 0x9d + 100, 0, 0x200 - 100 - 0x9d);
974
975 QByteArray permstr = QByteArray::number(static_cast<unsigned int>(perm), 8);
976 permstr = permstr.rightJustified(6, ' ');
977 d->fillBuffer(buffer, permstr.constData(), 0, mtime, 0x32, uname.constData(), gname.constData());
978
979 // Write header
980 bool retval = device()->write(buffer, 0x200) == 0x200;
982 d->tarEnd = device()->pos();
983 }
984 return retval;
985}
986
987void KTar::virtual_hook(int id, void *data)
988{
989 KArchive::virtual_hook(id, data);
990}
Represents a directory entry in a KArchive.
void addEntry(KArchiveEntry *)
Definition karchive.cpp:922
A base class for entries in an KArchive.
Represents a file entry in a KArchive.
QString errorString() const
Returns a description of the last error.
Definition karchive.cpp:253
QIODevice * device() const
The underlying device.
Definition karchive.cpp:628
virtual bool createDevice(QIODevice::OpenMode mode)
Can be reimplemented in order to change the creation of the device (when using the fileName construct...
Definition karchive.cpp:174
virtual bool close()
Closes the archive.
Definition karchive.cpp:214
virtual KArchiveDirectory * rootDir()
Retrieves or create the root directory.
Definition karchive.cpp:519
KArchive(const QString &fileName)
Base constructor (protected since this is a pure virtual class).
Definition karchive.cpp:115
KArchiveDirectory * findOrCreate(const QString &path)
Ensures that path exists, create otherwise.
Definition karchive.cpp:531
QIODevice::OpenMode mode() const
Returns the mode in which the archive was opened.
Definition karchive.cpp:623
QString fileName() const
The name of the archive file, as passed to the constructor that takes a fileName, or an empty string ...
Definition karchive.cpp:638
void setRootDir(KArchiveDirectory *rootDir)
Derived classes call setRootDir from openArchive, to set the root directory after parsing an existing...
Definition karchive.cpp:616
bool isOpen() const
Checks whether the archive is open.
Definition karchive.cpp:633
void setDevice(QIODevice *dev)
Can be called by derived classes in order to set the underlying device.
Definition karchive.cpp:607
void setErrorString(const QString &errorStr)
Sets error description.
Definition karchive.cpp:484
A class for reading and writing compressed data onto a device (e.g.
static CompressionType compressionTypeForMimeType(const QString &mimetype)
Returns the compression type for the given MIME type, if possible.
A class for reading / writing (optionally compressed) tar archives.
Definition ktar.h:23
bool doFinishWriting(qint64 size) override
Reimplemented from KArchive.
Definition ktar.cpp:631
bool doWriteSymLink(const QString &name, const QString &target, const QString &user, const QString &group, mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override
Reimplemented from KArchive.
Definition ktar.cpp:922
KTar(const QString &filename, const QString &mimetype=QString())
Creates an instance that operates on the given filename using the compression filter associated to gi...
Definition ktar.cpp:69
~KTar() override
If the tar ball is still opened, then it will be closed automatically by the destructor.
Definition ktar.cpp:170
bool createDevice(QIODevice::OpenMode mode) override
Can be reimplemented in order to change the creation of the device (when using the fileName construct...
Definition ktar.cpp:91
void setOrigFileName(const QByteArray &fileName)
Special function for setting the "original file name" in the gzip header, when writing a tar....
Definition ktar.cpp:182
bool openArchive(QIODevice::OpenMode mode) override
Opens the archive for reading.
Definition ktar.cpp:370
bool closeArchive() override
Closes the archive.
Definition ktar.cpp:612
bool doWriteDir(const QString &name, const QString &user, const QString &group, mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override
Reimplemented from KArchive.
Definition ktar.cpp:854
bool doPrepareWriting(const QString &name, const QString &user, const QString &group, qint64 size, mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override
Reimplemented from KArchive.
Definition ktar.cpp:770
KIOCORE_EXPORT SimpleJob * symlink(const QString &target, const QUrl &dest, JobFlags flags=DefaultFlags)
KIOCORE_EXPORT MimetypeJob * mimetype(const QUrl &url, JobFlags flags=DefaultFlags)
QString name(StandardAction id)
const char * constData() const const
char * data()
qsizetype length() const const
QByteArray number(double n, char format, int precision)
void resize(qsizetype newSize, char c)
QByteArray rightJustified(qsizetype width, char fill, bool truncate) const const
qsizetype size() const const
qlonglong toLongLong(bool *ok, int base) const const
QByteArray trimmed() const const
void truncate(qsizetype pos)
QDateTime currentDateTime()
bool isValid() const const
qint64 toMSecsSinceEpoch() const const
QString cleanPath(const QString &path)
QString tempPath()
QString decodeName(const QByteArray &localFileName)
QByteArray encodeName(const QString &fileName)
bool exists(const QString &fileName)
bool open(FILE *fh, OpenMode mode, FileHandleFlags handleFlags)
virtual bool atEnd() const const override
virtual void close() override
bool flush()
virtual bool seek(qint64 pos) override
virtual void close()
QString errorString() const const
bool isOpen() const const
virtual bool open(QIODeviceBase::OpenMode mode)
QIODeviceBase::OpenMode openMode() const const
virtual qint64 pos() const const
QByteArray read(qint64 maxSize)
virtual bool seek(qint64 pos)
qint64 write(const QByteArray &data)
typedef OpenMode
QMimeType mimeTypeForData(QIODevice *device) const const
QMimeType mimeTypeForFile(const QFileInfo &fileInfo, MatchMode mode) const const
bool inherits(const QString &mimeTypeName) const const
bool isValid() const const
QChar * data()
bool endsWith(QChar c, Qt::CaseSensitivity cs) const const
QString fromLatin1(QByteArrayView str)
QString fromLocal8Bit(QByteArrayView str)
bool isEmpty() const const
qsizetype lastIndexOf(QChar ch, Qt::CaseSensitivity cs) const const
QString left(qsizetype n) const const
qsizetype length() const const
QString mid(qsizetype position, qsizetype n) const const
QByteArray toLocal8Bit() const const
void truncate(qsizetype position)
This file is part of the KDE documentation.
Documentation copyright © 1996-2025 The KDE developers.
Generated on Fri Feb 28 2025 11:49:28 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.