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

KDE's Doxygen guidelines are available online.