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 // We don't want to fail opening potentially malformed files, so void the return value
533 (void)rootDir()->addEntryV2(e);
534 }
535 } else {
536 // In some tar files we can find dir/./file => call cleanPath
537 QString path = QDir::cleanPath(name.left(pos));
538 // Ensure container directory exists, create otherwise
540 if (d) {
541 (void)d->addEntryV2(e);
542 } else {
543 delete e;
544 return false;
545 }
546 }
547 } else {
548 // qCDebug(KArchiveLog) << "Terminating. Read " << n << " bytes, first one is " << buffer[0];
549 d->tarEnd = dev->pos() - n; // Remember end of archive
550 ende = true;
551 }
552 } while (!ende);
553 return true;
554}
555
556/*
557 * Writes back the changes of the temporary file
558 * to the original file.
559 * Must only be called if in write mode, not in read mode
560 */
561bool KTar::KTarPrivate::writeBackTempFile(const QString &fileName)
562{
563 if (!tmpFile) {
564 return true;
565 }
566
567 // qCDebug(KArchiveLog) << "Write temporary file to compressed file" << fileName << mimetype;
568
569 bool forced = false;
570 /* clang-format off */
571 if (MimeType::application_gzip_old() == mimetype ||
572 QLatin1String(application_bzip) == mimetype ||
573 QLatin1String(application_lzma) == mimetype ||
574 QLatin1String(application_xz) == mimetype) {
575 /* clang-format on */
576 forced = true;
577 }
578
579 // #### TODO this should use QSaveFile to avoid problems on disk full
580 // (KArchive uses QSaveFile by default, but the temp-uncompressed-file trick
581 // circumvents that).
582
583 KCompressionDevice dev(fileName);
584 QFile *file = tmpFile;
585 if (!dev.open(QIODevice::WriteOnly)) {
586 file->close();
587 q->setErrorString(tr("Failed to write back temp file: %1").arg(dev.errorString()));
588 return false;
589 }
590 if (forced) {
591 dev.setOrigFileName(origFileName);
592 }
593 file->seek(0);
594 QByteArray buffer;
595 buffer.resize(8 * 1024);
596 qint64 len;
597 while (!file->atEnd()) {
598 len = file->read(buffer.data(), buffer.size());
599 if (dev.write(buffer.data(), len) != len) {
600 file->close();
601 dev.close();
602 q->setErrorString(tr("Failed to write back temp file: %1").arg(dev.errorString()));
603 return false;
604 }
605 }
606 file->close();
607 dev.close();
608
609 // qCDebug(KArchiveLog) << "Write temporary file to compressed file done.";
610 return true;
611}
612
614{
615 d->dirList.clear();
616
617 bool ok = true;
618
619 // If we are in readwrite mode and had created
620 // a temporary tar file, we have to write
621 // back the changes to the original file
622 if (d->tmpFile && (mode() & QIODevice::WriteOnly)) {
623 ok = d->writeBackTempFile(fileName());
624 delete d->tmpFile;
625 d->tmpFile = nullptr;
626 setDevice(nullptr);
627 }
628
629 return ok;
630}
631
632bool KTar::doFinishWriting(qint64 size)
633{
634 // Write alignment
635 int rest = size % 0x200;
637 d->tarEnd = device()->pos() + (rest ? 0x200 - rest : 0); // Record our new end of archive
638 }
639 if (rest) {
640 char buffer[0x201];
641 for (uint i = 0; i < 0x200; ++i) {
642 buffer[i] = 0;
643 }
644 qint64 nwritten = device()->write(buffer, 0x200 - rest);
645 const bool ok = nwritten == 0x200 - rest;
646
647 if (!ok) {
648 setErrorString(tr("Couldn't write alignment: %1").arg(device()->errorString()));
649 }
650
651 return ok;
652 }
653 return true;
654}
655
656/*** Some help from the tar sources
657struct posix_header
658{ byte offset
659 char name[100]; * 0 * 0x0
660 char mode[8]; * 100 * 0x64
661 char uid[8]; * 108 * 0x6c
662 char gid[8]; * 116 * 0x74
663 char size[12]; * 124 * 0x7c
664 char mtime[12]; * 136 * 0x88
665 char chksum[8]; * 148 * 0x94
666 char typeflag; * 156 * 0x9c
667 char linkname[100]; * 157 * 0x9d
668 char magic[6]; * 257 * 0x101
669 char version[2]; * 263 * 0x107
670 char uname[32]; * 265 * 0x109
671 char gname[32]; * 297 * 0x129
672 char devmajor[8]; * 329 * 0x149
673 char devminor[8]; * 337 * ...
674 char prefix[155]; * 345 *
675 * 500 *
676};
677*/
678
679void KTar::KTarPrivate::fillBuffer(char *buffer, const char *mode, qint64 size, const QDateTime &mtime, char typeflag, const char *uname, const char *gname)
680{
681 // mode (as in stpos())
682 assert(strlen(mode) == 6);
683 memcpy(buffer + 0x64, mode, 6);
684 buffer[0x6a] = ' ';
685 buffer[0x6b] = '\0';
686
687 // dummy uid
688 strcpy(buffer + 0x6c, " 765 "); // 501 in decimal
689 // dummy gid
690 strcpy(buffer + 0x74, " 144 "); // 100 in decimal
691
692 // size
693 QByteArray s = QByteArray::number(size, 8); // octal
694 s = s.rightJustified(11, '0');
695 memcpy(buffer + 0x7c, s.data(), 11);
696 buffer[0x87] = ' '; // space-terminate (no null after)
697
698 // modification time
699 const QDateTime modificationTime = mtime.isValid() ? mtime : QDateTime::currentDateTime();
700 s = QByteArray::number(static_cast<qulonglong>(modificationTime.toMSecsSinceEpoch() / 1000), 8); // octal
701 s = s.rightJustified(11, '0');
702 memcpy(buffer + 0x88, s.data(), 11);
703 buffer[0x93] = ' '; // space-terminate (no null after) -- well current tar writes a null byte
704
705 // spaces, replaced by the check sum later
706 buffer[0x94] = 0x20;
707 buffer[0x95] = 0x20;
708 buffer[0x96] = 0x20;
709 buffer[0x97] = 0x20;
710 buffer[0x98] = 0x20;
711 buffer[0x99] = 0x20;
712
713 /* From the tar sources :
714 Fill in the checksum field. It's formatted differently from the
715 other fields: it has [6] digits, a null, then a space -- rather than
716 digits, a space, then a null. */
717
718 buffer[0x9a] = '\0';
719 buffer[0x9b] = ' ';
720
721 // type flag (dir, file, link)
722 buffer[0x9c] = typeflag;
723
724 // magic + version
725 strcpy(buffer + 0x101, "ustar");
726 strcpy(buffer + 0x107, "00");
727
728 // user
729 strcpy(buffer + 0x109, uname);
730 // group
731 strcpy(buffer + 0x129, gname);
732
733 // Header check sum
734 int check = 32;
735 for (uint j = 0; j < 0x200; ++j) {
736 check += static_cast<unsigned char>(buffer[j]);
737 }
738 s = QByteArray::number(check, 8); // octal
739 s = s.rightJustified(6, '0');
740 memcpy(buffer + 0x94, s.constData(), 6);
741}
742
743bool KTar::KTarPrivate::writeLonglink(char *buffer, const QByteArray &name, char typeflag, const char *uname, const char *gname)
744{
745 strcpy(buffer, "././@LongLink");
746 qint64 namelen = name.length() + 1;
747 fillBuffer(buffer, " 0", namelen, QDateTime(), typeflag, uname, gname);
748
749 if (q->device()->write(buffer, 0x200) != 0x200) {
750 q->setErrorString(tr("Couldn't write long link: %1").arg(q->device()->errorString()));
751 return false;
752 }
753
754 qint64 offset = 0;
755 while (namelen > 0) {
756 int chunksize = qMin(namelen, 0x200LL);
757 memcpy(buffer, name.data() + offset, chunksize);
758 // write long name
759 if (q->device()->write(buffer, 0x200) != 0x200) {
760 q->setErrorString(tr("Couldn't write long link: %1").arg(q->device()->errorString()));
761 return false;
762 }
763 // not even needed to reclear the buffer, tar doesn't do it
764 namelen -= chunksize;
765 offset += 0x200;
766 } /*wend*/
767
768 return true;
769}
770
772 const QString &user,
773 const QString &group,
774 qint64 size,
775 mode_t perm,
776 const QDateTime & /*atime*/,
777 const QDateTime &mtime,
778 const QDateTime & /*ctime*/)
779{
780 if (!isOpen()) {
781 setErrorString(tr("Application error: TAR file must be open before being written into"));
782 qCWarning(KArchiveLog) << "doPrepareWriting failed: !isOpen()";
783 return false;
784 }
785
786 if (!(mode() & QIODevice::WriteOnly)) {
787 setErrorString(tr("Application error: attempted to write into non-writable 7-Zip file"));
788 qCWarning(KArchiveLog) << "doPrepareWriting failed: !(mode() & QIODevice::WriteOnly)";
789 return false;
790 }
791
792 const qint64 MAX_FILESIZE = 077777777777L; // the format we use only allows 11 octal digits for size
793 if (size > MAX_FILESIZE) {
794 setErrorString(tr("Application limitation: Can not add file larger than %1 bytes").arg(MAX_FILESIZE));
795 return false;
796 }
797
798 // In some tar files we can find dir/./file => call cleanPath
800
801 /*
802 // Create toplevel dirs
803 // Commented out by David since it's not necessary, and if anybody thinks it is,
804 // he needs to implement a findOrCreate equivalent in writeDir.
805 // But as KTar and the "tar" program both handle tar files without
806 // dir entries, there's really no need for that
807 QString tmp ( fileName );
808 int i = tmp.lastIndexOf( '/' );
809 if ( i != -1 )
810 {
811 QString d = tmp.left( i + 1 ); // contains trailing slash
812 if ( !m_dirList.contains( d ) )
813 {
814 tmp = tmp.mid( i + 1 );
815 writeDir( d, user, group ); // WARNING : this one doesn't create its toplevel dirs
816 }
817 }
818 */
819
820 char buffer[0x201] = {0};
821
823 device()->seek(d->tarEnd); // Go to end of archive as might have moved with a read
824 }
825
826 // provide converted stuff we need later on
827 const QByteArray encodedFileName = QFile::encodeName(fileName);
828 const QByteArray uname = user.toLocal8Bit();
829 const QByteArray gname = group.toLocal8Bit();
830
831 // If more than 100 bytes, we need to use the LongLink trick
832 if (encodedFileName.length() > 99 && !d->writeLonglink(buffer, encodedFileName, 'L', uname.constData(), gname.constData())) {
833 return false;
834 }
835
836 // Write (potentially truncated) name
837 strncpy(buffer, encodedFileName.constData(), 99);
838 buffer[99] = 0;
839 // zero out the rest (except for what gets filled anyways)
840 memset(buffer + 0x9d, 0, 0x200 - 0x9d);
841
842 QByteArray permstr = QByteArray::number(static_cast<unsigned int>(perm), 8);
843 permstr = permstr.rightJustified(6, '0');
844 d->fillBuffer(buffer, permstr.constData(), size, mtime, 0x30, uname.constData(), gname.constData());
845
846 // Write header
847 if (device()->write(buffer, 0x200) != 0x200) {
848 setErrorString(tr("Failed to write header: %1").arg(device()->errorString()));
849 return false;
850 } else {
851 return true;
852 }
853}
854
855bool KTar::doWriteDir(const QString &name,
856 const QString &user,
857 const QString &group,
858 mode_t perm,
859 const QDateTime & /*atime*/,
860 const QDateTime &mtime,
861 const QDateTime & /*ctime*/)
862{
863 if (!isOpen()) {
864 setErrorString(tr("Application error: TAR file must be open before being written into"));
865 qCWarning(KArchiveLog) << "doWriteDir failed: !isOpen()";
866 return false;
867 }
868
869 if (!(mode() & QIODevice::WriteOnly)) {
870 setErrorString(tr("Application error: attempted to write into non-writable TAR file"));
871 qCWarning(KArchiveLog) << "doWriteDir failed: !(mode() & QIODevice::WriteOnly)";
872 return false;
873 }
874
875 // In some tar files we can find dir/./ => call cleanPath
876 QString dirName(QDir::cleanPath(name));
877
878 // Need trailing '/'
879 if (!dirName.endsWith(QLatin1Char('/'))) {
880 dirName += QLatin1Char('/');
881 }
882
883 if (d->dirList.contains(dirName)) {
884 return true; // already there
885 }
886
887 char buffer[0x201] = {0};
888
890 device()->seek(d->tarEnd); // Go to end of archive as might have moved with a read
891 }
892
893 // provide converted stuff we need lateron
894 QByteArray encodedDirname = QFile::encodeName(dirName);
895 QByteArray uname = user.toLocal8Bit();
896 QByteArray gname = group.toLocal8Bit();
897
898 // If more than 100 bytes, we need to use the LongLink trick
899 if (encodedDirname.length() > 99 && !d->writeLonglink(buffer, encodedDirname, 'L', uname.constData(), gname.constData())) {
900 return false;
901 }
902
903 // Write (potentially truncated) name
904 strncpy(buffer, encodedDirname.constData(), 99);
905 buffer[99] = 0;
906 // zero out the rest (except for what gets filled anyways)
907 memset(buffer + 0x9d, 0, 0x200 - 0x9d);
908
909 QByteArray permstr = QByteArray::number(static_cast<unsigned int>(perm), 8);
910 permstr = permstr.rightJustified(6, ' ');
911 d->fillBuffer(buffer, permstr.constData(), 0, mtime, 0x35, uname.constData(), gname.constData());
912
913 // Write header
914 device()->write(buffer, 0x200);
916 d->tarEnd = device()->pos();
917 }
918
919 d->dirList.append(dirName); // contains trailing slash
920 return true; // TODO if wanted, better error control
921}
922
924 const QString &target,
925 const QString &user,
926 const QString &group,
927 mode_t perm,
928 const QDateTime & /*atime*/,
929 const QDateTime &mtime,
930 const QDateTime & /*ctime*/)
931{
932 if (!isOpen()) {
933 setErrorString(tr("Application error: TAR file must be open before being written into"));
934 qCWarning(KArchiveLog) << "doWriteSymLink failed: !isOpen()";
935 return false;
936 }
937
938 if (!(mode() & QIODevice::WriteOnly)) {
939 setErrorString(tr("Application error: attempted to write into non-writable TAR file"));
940 qCWarning(KArchiveLog) << "doWriteSymLink failed: !(mode() & QIODevice::WriteOnly)";
941 return false;
942 }
943
944 // In some tar files we can find dir/./file => call cleanPath
946
947 char buffer[0x201] = {0};
948
950 device()->seek(d->tarEnd); // Go to end of archive as might have moved with a read
951 }
952
953 // provide converted stuff we need lateron
954 QByteArray encodedFileName = QFile::encodeName(fileName);
955 QByteArray encodedTarget = QFile::encodeName(target);
956 QByteArray uname = user.toLocal8Bit();
957 QByteArray gname = group.toLocal8Bit();
958
959 // If more than 100 bytes, we need to use the LongLink trick
960 if (encodedTarget.length() > 99 && !d->writeLonglink(buffer, encodedTarget, 'K', uname.constData(), gname.constData())) {
961 return false;
962 }
963 if (encodedFileName.length() > 99 && !d->writeLonglink(buffer, encodedFileName, 'L', uname.constData(), gname.constData())) {
964 return false;
965 }
966
967 // Write (potentially truncated) name
968 strncpy(buffer, encodedFileName.constData(), 99);
969 buffer[99] = 0;
970 // Write (potentially truncated) symlink target
971 strncpy(buffer + 0x9d, encodedTarget.constData(), 99);
972 buffer[0x9d + 99] = 0;
973 // zero out the rest
974 memset(buffer + 0x9d + 100, 0, 0x200 - 100 - 0x9d);
975
976 QByteArray permstr = QByteArray::number(static_cast<unsigned int>(perm), 8);
977 permstr = permstr.rightJustified(6, ' ');
978 d->fillBuffer(buffer, permstr.constData(), 0, mtime, 0x32, uname.constData(), gname.constData());
979
980 // Write header
981 bool retval = device()->write(buffer, 0x200) == 0x200;
983 d->tarEnd = device()->pos();
984 }
985 return retval;
986}
987
988void KTar::virtual_hook(int id, void *data)
989{
990 KArchive::virtual_hook(id, data);
991}
Represents a directory entry in a KArchive.
bool addEntryV2(KArchiveEntry *)
Definition karchive.cpp:928
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:251
QIODevice * device() const
The underlying device.
Definition karchive.cpp:630
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:517
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:529
QIODevice::OpenMode mode() const
Returns the mode in which the archive was opened.
Definition karchive.cpp:625
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:640
void setRootDir(KArchiveDirectory *rootDir)
Derived classes call setRootDir from openArchive, to set the root directory after parsing an existing...
Definition karchive.cpp:618
bool isOpen() const
Checks whether the archive is open.
Definition karchive.cpp:635
void setDevice(QIODevice *dev)
Can be called by derived classes in order to set the underlying device.
Definition karchive.cpp:609
void setErrorString(const QString &errorStr)
Sets error description.
Definition karchive.cpp:482
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:632
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:923
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:613
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:855
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:771
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 Apr 4 2025 12:10:52 by doxygen 1.13.2 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.