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 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
69KTar::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
84KTar::KTar(QIODevice *dev)
85 : KArchive(dev)
86 , d(new KTarPrivate(this))
87{
88}
89
90// Only called when a filename was given
91bool 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: 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(flags: QIODevice::ReadOnly)) {
103 mime = db.mimeTypeForData(device: &f);
104 }
105 if (!mime.isValid()) {
106 // Unable to determine mimetype from contents, get it from file name
107 mime = db.mimeTypeForFile(fileName: fileName(), mode: QMimeDatabase::MatchExtension);
108 }
109 } else {
110 mime = db.mimeTypeForFile(fileName: fileName(), mode: QMimeDatabase::MatchExtension);
111 }
112
113 // qCDebug(KArchiveLog) << mode << mime->name();
114
115 if (mime.inherits(QStringLiteral("application/x-compressed-tar")) || mime.inherits(mimeTypeName: 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(mimeTypeName: QString::fromLatin1(ba: application_bzip))) {
120 // bzipped2 tar file (with possibly invalid file name), ask for bz2 filter
121 d->mimetype = QString::fromLatin1(ba: application_bzip);
122 } else if (mime.inherits(QStringLiteral("application/x-lzma-compressed-tar")) || mime.inherits(mimeTypeName: QString::fromLatin1(ba: application_lzma))) {
123 // lzma compressed tar file (with possibly invalid file name), ask for xz filter
124 d->mimetype = QString::fromLatin1(ba: application_lzma);
125 } else if (mime.inherits(QStringLiteral("application/x-xz-compressed-tar")) || mime.inherits(mimeTypeName: QString::fromLatin1(ba: application_xz))) {
126 // xz compressed tar file (with possibly invalid name), ask for xz filter
127 d->mimetype = QString::fromLatin1(ba: application_xz);
128 } else if (mime.inherits(QStringLiteral("application/x-zstd-compressed-tar")) || mime.inherits(mimeTypeName: QString::fromLatin1(ba: application_zstd))) {
129 // zstd compressed tar file (with possibly invalid name), ask for zstd filter
130 d->mimetype = QString::fromLatin1(ba: application_zstd);
131 }
132 }
133
134 if (d->mimetype == QLatin1String("application/x-tar")) {
135 return KArchive::createDevice(mode);
136 } else if (mode == QIODevice::WriteOnly) {
137 if (!KArchive::createDevice(mode)) {
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;
143 KCompressionDevice::CompressionType type = KCompressionDevice::compressionTypeForMimeType(mimetype: 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
170KTar::~KTar()
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
182void KTar::setOrigFileName(const QByteArray &fileName)
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(data: buffer, maxlen: 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(s1: buffer + 257, s2: "ustar", n: 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, base: 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(s1: buffer + 148 + 6 - s.length(), s2: s.data(), n: s.length()) //
219 && strncmp(s1: buffer + 148 + 7 - s.length(), s2: s.data(), n: s.length()) //
220 && strncmp(s1: buffer + 148 + 8 - s.length(), s2: s.data(), n: 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(ok: nullptr, base: 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(a: size, b: 0x200LL);
259 n = dev->read(data: longlink.data() + offset, maxlen: 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(data: buffer, maxlen: skip) != skip) {
270 return false;
271 }
272 }
273 longlink.truncate(pos: qstrlen(str: longlink.constData()));
274 return true;
275}
276
277qint64 KTar::KTarPrivate::readHeader(char *buffer, QString &name, QString &symlink)
278{
279 name.truncate(pos: 0);
280 symlink.truncate(pos: 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(s1: buffer, s2: "././@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(localFileName: longlink.constData());
295 break;
296 case 'K':
297 symlink = QFile::decodeName(localFileName: 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(localFileName: QByteArray(buffer, qstrnlen(str: buffer, maxlen: 100)));
312 }
313 if (symlink.isEmpty()) {
314 char *symlinkBuffer = buffer + 0x9d /*?*/;
315 symlink = QFile::decodeName(localFileName: QByteArray(symlinkBuffer, qstrnlen(str: symlinkBuffer, maxlen: 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
334 KCompressionDevice::CompressionType compressionType = KCompressionDevice::compressionTypeForMimeType(mimetype);
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(offset: 0);
341 QByteArray buffer;
342 buffer.resize(size: 8 * 1024);
343 if (!filterDev.open(mode: QIODevice::ReadOnly)) {
344 q->setErrorString(tr(sourceText: "File %1 does not exist").arg(a: fileName));
345 return false;
346 }
347 qint64 len = -1;
348 while (!filterDev.atEnd() && len != 0) {
349 len = filterDev.read(data: buffer.data(), maxlen: buffer.size());
350 if (len < 0) { // corrupted archive
351 q->setErrorString(tr(sourceText: "Archive %1 is corrupt").arg(a: fileName));
352 return false;
353 }
354 if (file->write(data: buffer.data(), len) != len) { // disk full
355 q->setErrorString(tr(sourceText: "Disk full"));
356 return false;
357 }
358 }
359 filterDev.close();
360
361 file->flush();
362 file->seek(offset: 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
370bool KTar::openArchive(QIODevice::OpenMode mode)
371{
372 if (!(mode & QIODevice::ReadOnly)) {
373 return true;
374 }
375
376 if (!d->fillTempFile(fileName: 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(sourceText: "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(sourceText: "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(c: QLatin1Char('/'))) {
414 isdir = true;
415 name.truncate(pos: name.length() - 1);
416 }
417
418 QByteArray prefix = QByteArray(buffer + 0x159, 155);
419 if (prefix[0] != '\0') {
420 name = (QString::fromLatin1(ba: prefix.constData()) + QLatin1Char('/') + name);
421 }
422
423 int pos = name.lastIndexOf(c: QLatin1Char('/'));
424 QString nm = (pos == -1) ? name : name.mid(position: 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(nptr: p, endptr: &dummy, base: 8);
434
435 // read user and group
436 const int maxUserGroupLength = 32;
437 const char *userStart = buffer + 0x109;
438 const int userLen = qstrnlen(str: userStart, maxlen: maxUserGroupLength);
439 const QString user = QString::fromLocal8Bit(str: userStart, size: userLen);
440 const char *groupStart = buffer + 0x129;
441 const int groupLen = qstrnlen(str: groupStart, maxlen: maxUserGroupLength);
442 const QString group = QString::fromLocal8Bit(str: groupStart, size: groupLen);
443
444 // read time
445 buffer[0x93] = 0;
446 p = buffer + 0x88;
447 while (*p == ' ') {
448 ++p;
449 }
450 uint time = strtol(nptr: p, endptr: &dummy, base: 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(data: buffer, maxlen: 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_t: time), user, group, symlink);
485 } else {
486 // read size
487 QByteArray sizeBuffer(buffer + 0x7c, 12);
488 qint64 size = sizeBuffer.trimmed().toLongLong(ok: nullptr, base: 8 /*octal*/);
489 // qCDebug(KArchiveLog) << "sizeBuffer='" << sizeBuffer << "' -> size=" << size;
490
491 // for isDumpDir we will skip the additional info about that dirs contents
492 if (isDumpDir) {
493 // qCDebug(KArchiveLog) << nm << "isDumpDir";
494 e = new KArchiveDirectory(this, nm, access, KArchivePrivate::time_tToDateTime(time_t: time), user, group, symlink);
495 } else {
496 // Let's hack around hard links. Our classes don't support that, so make them symlinks
497 if (typeflag == '1') {
498 // qCDebug(KArchiveLog) << "Hard link, setting size to 0 instead of" << size;
499 size = 0; // no contents
500 }
501
502 // qCDebug(KArchiveLog) << "file" << nm << "size=" << size;
503 e = new KArchiveFile(this, nm, access, KArchivePrivate::time_tToDateTime(time_t: time), user, group, symlink, dev->pos(), size);
504 }
505
506 // Skip contents + align bytes
507 qint64 rest = size % 0x200;
508 qint64 skip = size + (rest ? 0x200 - rest : 0);
509 // qCDebug(KArchiveLog) << "pos()=" << dev->pos() << "rest=" << rest << "skipping" << skip;
510 if (!dev->seek(pos: dev->pos() + skip)) {
511 // qCWarning(KArchiveLog) << "skipping" << skip << "failed";
512 }
513 }
514
515 if (pos == -1) {
516 if (nm == QLatin1String(".")) { // special case
517 if (isdir) {
518 if (KArchivePrivate::hasRootDir(archive: this)) {
519 qWarning() << "Broken tar file has two root dir entries";
520 delete e;
521 } else {
522 setRootDir(static_cast<KArchiveDirectory *>(e));
523 }
524 } else {
525 delete e;
526 }
527 } else {
528 rootDir()->addEntry(e);
529 }
530 } else {
531 // In some tar files we can find dir/./file => call cleanPath
532 QString path = QDir::cleanPath(path: name.left(n: pos));
533 // Ensure container directory exists, create otherwise
534 KArchiveDirectory *d = findOrCreate(path);
535 if (d) {
536 d->addEntry(e);
537 } else {
538 delete e;
539 return false;
540 }
541 }
542 } else {
543 // qCDebug(KArchiveLog) << "Terminating. Read " << n << " bytes, first one is " << buffer[0];
544 d->tarEnd = dev->pos() - n; // Remember end of archive
545 ende = true;
546 }
547 } while (!ende);
548 return true;
549}
550
551/*
552 * Writes back the changes of the temporary file
553 * to the original file.
554 * Must only be called if in write mode, not in read mode
555 */
556bool KTar::KTarPrivate::writeBackTempFile(const QString &fileName)
557{
558 if (!tmpFile) {
559 return true;
560 }
561
562 // qCDebug(KArchiveLog) << "Write temporary file to compressed file" << fileName << mimetype;
563
564 bool forced = false;
565 /* clang-format off */
566 if (MimeType::application_gzip_old() == mimetype ||
567 QLatin1String(application_bzip) == mimetype ||
568 QLatin1String(application_lzma) == mimetype ||
569 QLatin1String(application_xz) == mimetype) {
570 /* clang-format on */
571 forced = true;
572 }
573
574 // #### TODO this should use QSaveFile to avoid problems on disk full
575 // (KArchive uses QSaveFile by default, but the temp-uncompressed-file trick
576 // circumvents that).
577
578 KCompressionDevice dev(fileName);
579 QFile *file = tmpFile;
580 if (!dev.open(mode: QIODevice::WriteOnly)) {
581 file->close();
582 q->setErrorString(tr(sourceText: "Failed to write back temp file: %1").arg(a: dev.errorString()));
583 return false;
584 }
585 if (forced) {
586 dev.setOrigFileName(origFileName);
587 }
588 file->seek(offset: 0);
589 QByteArray buffer;
590 buffer.resize(size: 8 * 1024);
591 qint64 len;
592 while (!file->atEnd()) {
593 len = file->read(data: buffer.data(), maxlen: buffer.size());
594 dev.write(data: buffer.data(), len); // TODO error checking
595 }
596 file->close();
597 dev.close();
598
599 // qCDebug(KArchiveLog) << "Write temporary file to compressed file done.";
600 return true;
601}
602
603bool KTar::closeArchive()
604{
605 d->dirList.clear();
606
607 bool ok = true;
608
609 // If we are in readwrite mode and had created
610 // a temporary tar file, we have to write
611 // back the changes to the original file
612 if (d->tmpFile && (mode() & QIODevice::WriteOnly)) {
613 ok = d->writeBackTempFile(fileName: fileName());
614 delete d->tmpFile;
615 d->tmpFile = nullptr;
616 setDevice(nullptr);
617 }
618
619 return ok;
620}
621
622bool KTar::doFinishWriting(qint64 size)
623{
624 // Write alignment
625 int rest = size % 0x200;
626 if ((mode() & QIODevice::ReadWrite) == QIODevice::ReadWrite) {
627 d->tarEnd = device()->pos() + (rest ? 0x200 - rest : 0); // Record our new end of archive
628 }
629 if (rest) {
630 char buffer[0x201];
631 for (uint i = 0; i < 0x200; ++i) {
632 buffer[i] = 0;
633 }
634 qint64 nwritten = device()->write(data: buffer, len: 0x200 - rest);
635 const bool ok = nwritten == 0x200 - rest;
636
637 if (!ok) {
638 setErrorString(tr(sourceText: "Couldn't write alignment: %1").arg(a: device()->errorString()));
639 }
640
641 return ok;
642 }
643 return true;
644}
645
646/*** Some help from the tar sources
647struct posix_header
648{ byte offset
649 char name[100]; * 0 * 0x0
650 char mode[8]; * 100 * 0x64
651 char uid[8]; * 108 * 0x6c
652 char gid[8]; * 116 * 0x74
653 char size[12]; * 124 * 0x7c
654 char mtime[12]; * 136 * 0x88
655 char chksum[8]; * 148 * 0x94
656 char typeflag; * 156 * 0x9c
657 char linkname[100]; * 157 * 0x9d
658 char magic[6]; * 257 * 0x101
659 char version[2]; * 263 * 0x107
660 char uname[32]; * 265 * 0x109
661 char gname[32]; * 297 * 0x129
662 char devmajor[8]; * 329 * 0x149
663 char devminor[8]; * 337 * ...
664 char prefix[155]; * 345 *
665 * 500 *
666};
667*/
668
669void KTar::KTarPrivate::fillBuffer(char *buffer, const char *mode, qint64 size, const QDateTime &mtime, char typeflag, const char *uname, const char *gname)
670{
671 // mode (as in stpos())
672 assert(strlen(mode) == 6);
673 memcpy(dest: buffer + 0x64, src: mode, n: 6);
674 buffer[0x6a] = ' ';
675 buffer[0x6b] = '\0';
676
677 // dummy uid
678 strcpy(dest: buffer + 0x6c, src: " 765 "); // 501 in decimal
679 // dummy gid
680 strcpy(dest: buffer + 0x74, src: " 144 "); // 100 in decimal
681
682 // size
683 QByteArray s = QByteArray::number(size, base: 8); // octal
684 s = s.rightJustified(width: 11, fill: '0');
685 memcpy(dest: buffer + 0x7c, src: s.data(), n: 11);
686 buffer[0x87] = ' '; // space-terminate (no null after)
687
688 // modification time
689 const QDateTime modificationTime = mtime.isValid() ? mtime : QDateTime::currentDateTime();
690 s = QByteArray::number(static_cast<qulonglong>(modificationTime.toMSecsSinceEpoch() / 1000), base: 8); // octal
691 s = s.rightJustified(width: 11, fill: '0');
692 memcpy(dest: buffer + 0x88, src: s.data(), n: 11);
693 buffer[0x93] = ' '; // space-terminate (no null after) -- well current tar writes a null byte
694
695 // spaces, replaced by the check sum later
696 buffer[0x94] = 0x20;
697 buffer[0x95] = 0x20;
698 buffer[0x96] = 0x20;
699 buffer[0x97] = 0x20;
700 buffer[0x98] = 0x20;
701 buffer[0x99] = 0x20;
702
703 /* From the tar sources :
704 Fill in the checksum field. It's formatted differently from the
705 other fields: it has [6] digits, a null, then a space -- rather than
706 digits, a space, then a null. */
707
708 buffer[0x9a] = '\0';
709 buffer[0x9b] = ' ';
710
711 // type flag (dir, file, link)
712 buffer[0x9c] = typeflag;
713
714 // magic + version
715 strcpy(dest: buffer + 0x101, src: "ustar");
716 strcpy(dest: buffer + 0x107, src: "00");
717
718 // user
719 strcpy(dest: buffer + 0x109, src: uname);
720 // group
721 strcpy(dest: buffer + 0x129, src: gname);
722
723 // Header check sum
724 int check = 32;
725 for (uint j = 0; j < 0x200; ++j) {
726 check += static_cast<unsigned char>(buffer[j]);
727 }
728 s = QByteArray::number(check, base: 8); // octal
729 s = s.rightJustified(width: 6, fill: '0');
730 memcpy(dest: buffer + 0x94, src: s.constData(), n: 6);
731}
732
733void KTar::KTarPrivate::writeLonglink(char *buffer, const QByteArray &name, char typeflag, const char *uname, const char *gname)
734{
735 strcpy(dest: buffer, src: "././@LongLink");
736 qint64 namelen = name.length() + 1;
737 fillBuffer(buffer, mode: " 0", size: namelen, mtime: QDateTime(), typeflag, uname, gname);
738 q->device()->write(data: buffer, len: 0x200); // TODO error checking
739 qint64 offset = 0;
740 while (namelen > 0) {
741 int chunksize = qMin(a: namelen, b: 0x200LL);
742 memcpy(dest: buffer, src: name.data() + offset, n: chunksize);
743 // write long name
744 q->device()->write(data: buffer, len: 0x200); // TODO error checking
745 // not even needed to reclear the buffer, tar doesn't do it
746 namelen -= chunksize;
747 offset += 0x200;
748 } /*wend*/
749}
750
751bool KTar::doPrepareWriting(const QString &name,
752 const QString &user,
753 const QString &group,
754 qint64 size,
755 mode_t perm,
756 const QDateTime & /*atime*/,
757 const QDateTime &mtime,
758 const QDateTime & /*ctime*/)
759{
760 if (!isOpen()) {
761 setErrorString(tr(sourceText: "Application error: TAR file must be open before being written into"));
762 qCWarning(KArchiveLog) << "doPrepareWriting failed: !isOpen()";
763 return false;
764 }
765
766 if (!(mode() & QIODevice::WriteOnly)) {
767 setErrorString(tr(sourceText: "Application error: attempted to write into non-writable 7-Zip file"));
768 qCWarning(KArchiveLog) << "doPrepareWriting failed: !(mode() & QIODevice::WriteOnly)";
769 return false;
770 }
771
772 const qint64 MAX_FILESIZE = 077777777777L; // the format we use only allows 11 octal digits for size
773 if (size > MAX_FILESIZE) {
774 setErrorString(tr(sourceText: "Application limitation: Can not add file larger than %1 bytes").arg(a: MAX_FILESIZE));
775 return false;
776 }
777
778 // In some tar files we can find dir/./file => call cleanPath
779 QString fileName(QDir::cleanPath(path: name));
780
781 /*
782 // Create toplevel dirs
783 // Commented out by David since it's not necessary, and if anybody thinks it is,
784 // he needs to implement a findOrCreate equivalent in writeDir.
785 // But as KTar and the "tar" program both handle tar files without
786 // dir entries, there's really no need for that
787 QString tmp ( fileName );
788 int i = tmp.lastIndexOf( '/' );
789 if ( i != -1 )
790 {
791 QString d = tmp.left( i + 1 ); // contains trailing slash
792 if ( !m_dirList.contains( d ) )
793 {
794 tmp = tmp.mid( i + 1 );
795 writeDir( d, user, group ); // WARNING : this one doesn't create its toplevel dirs
796 }
797 }
798 */
799
800 char buffer[0x201] = {0};
801
802 if ((mode() & QIODevice::ReadWrite) == QIODevice::ReadWrite) {
803 device()->seek(pos: d->tarEnd); // Go to end of archive as might have moved with a read
804 }
805
806 // provide converted stuff we need later on
807 const QByteArray encodedFileName = QFile::encodeName(fileName);
808 const QByteArray uname = user.toLocal8Bit();
809 const QByteArray gname = group.toLocal8Bit();
810
811 // If more than 100 bytes, we need to use the LongLink trick
812 if (encodedFileName.length() > 99) {
813 d->writeLonglink(buffer, name: encodedFileName, typeflag: 'L', uname: uname.constData(), gname: gname.constData());
814 }
815
816 // Write (potentially truncated) name
817 strncpy(dest: buffer, src: encodedFileName.constData(), n: 99);
818 buffer[99] = 0;
819 // zero out the rest (except for what gets filled anyways)
820 memset(s: buffer + 0x9d, c: 0, n: 0x200 - 0x9d);
821
822 QByteArray permstr = QByteArray::number(static_cast<unsigned int>(perm), base: 8);
823 permstr = permstr.rightJustified(width: 6, fill: '0');
824 d->fillBuffer(buffer, mode: permstr.constData(), size, mtime, typeflag: 0x30, uname: uname.constData(), gname: gname.constData());
825
826 // Write header
827 if (device()->write(data: buffer, len: 0x200) != 0x200) {
828 setErrorString(tr(sourceText: "Failed to write header: %1").arg(a: device()->errorString()));
829 return false;
830 } else {
831 return true;
832 }
833}
834
835bool KTar::doWriteDir(const QString &name,
836 const QString &user,
837 const QString &group,
838 mode_t perm,
839 const QDateTime & /*atime*/,
840 const QDateTime &mtime,
841 const QDateTime & /*ctime*/)
842{
843 if (!isOpen()) {
844 setErrorString(tr(sourceText: "Application error: TAR file must be open before being written into"));
845 qCWarning(KArchiveLog) << "doWriteDir failed: !isOpen()";
846 return false;
847 }
848
849 if (!(mode() & QIODevice::WriteOnly)) {
850 setErrorString(tr(sourceText: "Application error: attempted to write into non-writable TAR file"));
851 qCWarning(KArchiveLog) << "doWriteDir failed: !(mode() & QIODevice::WriteOnly)";
852 return false;
853 }
854
855 // In some tar files we can find dir/./ => call cleanPath
856 QString dirName(QDir::cleanPath(path: name));
857
858 // Need trailing '/'
859 if (!dirName.endsWith(c: QLatin1Char('/'))) {
860 dirName += QLatin1Char('/');
861 }
862
863 if (d->dirList.contains(str: dirName)) {
864 return true; // already there
865 }
866
867 char buffer[0x201] = {0};
868
869 if ((mode() & QIODevice::ReadWrite) == QIODevice::ReadWrite) {
870 device()->seek(pos: d->tarEnd); // Go to end of archive as might have moved with a read
871 }
872
873 // provide converted stuff we need lateron
874 QByteArray encodedDirname = QFile::encodeName(fileName: dirName);
875 QByteArray uname = user.toLocal8Bit();
876 QByteArray gname = group.toLocal8Bit();
877
878 // If more than 100 bytes, we need to use the LongLink trick
879 if (encodedDirname.length() > 99) {
880 d->writeLonglink(buffer, name: encodedDirname, typeflag: 'L', uname: uname.constData(), gname: gname.constData());
881 }
882
883 // Write (potentially truncated) name
884 strncpy(dest: buffer, src: encodedDirname.constData(), n: 99);
885 buffer[99] = 0;
886 // zero out the rest (except for what gets filled anyways)
887 memset(s: buffer + 0x9d, c: 0, n: 0x200 - 0x9d);
888
889 QByteArray permstr = QByteArray::number(static_cast<unsigned int>(perm), base: 8);
890 permstr = permstr.rightJustified(width: 6, fill: ' ');
891 d->fillBuffer(buffer, mode: permstr.constData(), size: 0, mtime, typeflag: 0x35, uname: uname.constData(), gname: gname.constData());
892
893 // Write header
894 device()->write(data: buffer, len: 0x200);
895 if ((mode() & QIODevice::ReadWrite) == QIODevice::ReadWrite) {
896 d->tarEnd = device()->pos();
897 }
898
899 d->dirList.append(t: dirName); // contains trailing slash
900 return true; // TODO if wanted, better error control
901}
902
903bool KTar::doWriteSymLink(const QString &name,
904 const QString &target,
905 const QString &user,
906 const QString &group,
907 mode_t perm,
908 const QDateTime & /*atime*/,
909 const QDateTime &mtime,
910 const QDateTime & /*ctime*/)
911{
912 if (!isOpen()) {
913 setErrorString(tr(sourceText: "Application error: TAR file must be open before being written into"));
914 qCWarning(KArchiveLog) << "doWriteSymLink failed: !isOpen()";
915 return false;
916 }
917
918 if (!(mode() & QIODevice::WriteOnly)) {
919 setErrorString(tr(sourceText: "Application error: attempted to write into non-writable TAR file"));
920 qCWarning(KArchiveLog) << "doWriteSymLink failed: !(mode() & QIODevice::WriteOnly)";
921 return false;
922 }
923
924 // In some tar files we can find dir/./file => call cleanPath
925 QString fileName(QDir::cleanPath(path: name));
926
927 char buffer[0x201] = {0};
928
929 if ((mode() & QIODevice::ReadWrite) == QIODevice::ReadWrite) {
930 device()->seek(pos: d->tarEnd); // Go to end of archive as might have moved with a read
931 }
932
933 // provide converted stuff we need lateron
934 QByteArray encodedFileName = QFile::encodeName(fileName);
935 QByteArray encodedTarget = QFile::encodeName(fileName: target);
936 QByteArray uname = user.toLocal8Bit();
937 QByteArray gname = group.toLocal8Bit();
938
939 // If more than 100 bytes, we need to use the LongLink trick
940 if (encodedTarget.length() > 99) {
941 d->writeLonglink(buffer, name: encodedTarget, typeflag: 'K', uname: uname.constData(), gname: gname.constData());
942 }
943 if (encodedFileName.length() > 99) {
944 d->writeLonglink(buffer, name: encodedFileName, typeflag: 'L', uname: uname.constData(), gname: gname.constData());
945 }
946
947 // Write (potentially truncated) name
948 strncpy(dest: buffer, src: encodedFileName.constData(), n: 99);
949 buffer[99] = 0;
950 // Write (potentially truncated) symlink target
951 strncpy(dest: buffer + 0x9d, src: encodedTarget.constData(), n: 99);
952 buffer[0x9d + 99] = 0;
953 // zero out the rest
954 memset(s: buffer + 0x9d + 100, c: 0, n: 0x200 - 100 - 0x9d);
955
956 QByteArray permstr = QByteArray::number(static_cast<unsigned int>(perm), base: 8);
957 permstr = permstr.rightJustified(width: 6, fill: ' ');
958 d->fillBuffer(buffer, mode: permstr.constData(), size: 0, mtime, typeflag: 0x32, uname: uname.constData(), gname: gname.constData());
959
960 // Write header
961 bool retval = device()->write(data: buffer, len: 0x200) == 0x200;
962 if ((mode() & QIODevice::ReadWrite) == QIODevice::ReadWrite) {
963 d->tarEnd = device()->pos();
964 }
965 return retval;
966}
967
968void KTar::virtual_hook(int id, void *data)
969{
970 KArchive::virtual_hook(id, data);
971}
972

source code of karchive/src/ktar.cpp