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