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

source code of karchive/src/ktar.cpp