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