KArchive

ktar.cpp
1 /* This file is part of the KDE libraries
2  SPDX-FileCopyrightText: 2000 David Faure <[email protected]>
3  SPDX-FileCopyrightText: 2003 Leo Savernik <[email protected]>
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 {
36 QString application_gzip() { return QStringLiteral("application/gzip"); }
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:
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;
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 
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 
85  : KArchive(dev)
86  , d(new KTarPrivate(this))
87 {
88 }
89 
90 // Only called when a filename was given
92 {
93  if (d->mimetype.isEmpty()) {
94  // Find out mimetype manually
95 
96  QMimeDatabase db;
97  QMimeType mime;
99  // Give priority to file contents: if someone renames a .tar.bz2 to .tar.gz,
100  // we can still do the right thing here.
101  QFile f(fileName());
102  if (f.open(QIODevice::ReadOnly)) {
103  mime = db.mimeTypeForData(&f);
104  }
105  if (!mime.isValid()) {
106  // Unable to determine mimetype from contents, get it from file name
108  }
109  } else {
111  }
112 
113  // qCDebug(KArchiveLog) << mode << mime->name();
114 
115  if (mime.inherits(QStringLiteral("application/x-compressed-tar")) || mime.inherits(MimeType::application_gzip_old())) {
116  // gzipped tar file (with possibly invalid file name), ask for gzip filter
117  d->mimetype = MimeType::application_gzip_old();
118  } else if (mime.inherits(QStringLiteral("application/x-bzip-compressed-tar")) || mime.inherits(QStringLiteral("application/x-bzip2-compressed-tar"))
119  || mime.inherits(QStringLiteral("application/x-bzip2")) || mime.inherits(QString::fromLatin1(application_bzip))) {
120  // bzipped2 tar file (with possibly invalid file name), ask for bz2 filter
121  d->mimetype = QString::fromLatin1(application_bzip);
122  } else if (mime.inherits(QStringLiteral("application/x-lzma-compressed-tar")) || mime.inherits(QString::fromLatin1(application_lzma))) {
123  // lzma compressed tar file (with possibly invalid file name), ask for xz filter
124  d->mimetype = QString::fromLatin1(application_lzma);
125  } else if (mime.inherits(QStringLiteral("application/x-xz-compressed-tar")) || mime.inherits(QString::fromLatin1(application_xz))) {
126  // xz compressed tar file (with possibly invalid name), ask for xz filter
127  d->mimetype = QString::fromLatin1(application_xz);
128  } else if (mime.inherits(QStringLiteral("application/x-zstd-compressed-tar")) || mime.inherits(QString::fromLatin1(application_zstd))) {
129  // zstd compressed tar file (with possibly invalid name), ask for zstd filter
130  d->mimetype = QString::fromLatin1(application_zstd);
131  }
132  }
133 
134  if (d->mimetype == QLatin1String("application/x-tar")) {
136  } else if (mode == QIODevice::WriteOnly) {
138  return false;
139  }
140  if (!d->mimetype.isEmpty()) {
141  // Create a compression filter on top of the QSaveFile device that KArchive created.
142  // qCDebug(KArchiveLog) << "creating KCompressionDevice for" << d->mimetype;
144  d->compressionDevice = new KCompressionDevice(device(), false, type);
145  setDevice(d->compressionDevice);
146  }
147  return true;
148  } else {
149  // The compression filters are very slow with random access.
150  // So instead of applying the filter to the device,
151  // the file is completely extracted instead,
152  // and we work on the extracted tar file.
153  // This improves the extraction speed by the archive KIO worker supporting the tar protocol dramatically,
154  // if the archive file contains many files.
155  // This is because the archive KIO worker extracts one file after the other and normally
156  // has to walk through the decompression filter each time.
157  // Which is in fact nearly as slow as a complete decompression for each file.
158 
159  Q_ASSERT(!d->tmpFile);
160  d->tmpFile = new QTemporaryFile();
161  d->tmpFile->setFileTemplate(QDir::tempPath() + QLatin1Char('/') + QLatin1String("ktar-XXXXXX.tar"));
162  d->tmpFile->open();
163  // qCDebug(KArchiveLog) << "creating tempfile:" << d->tmpFile->fileName();
164 
165  setDevice(d->tmpFile);
166  return true;
167  }
168 }
169 
171 {
172  // mjarrett: Closes to prevent ~KArchive from aborting w/o device
173  if (isOpen()) {
174  close();
175  }
176 
177  delete d->tmpFile;
178  delete d->compressionDevice;
179  delete d;
180 }
181 
182 void 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 
191 qint64 KTar::KTarPrivate::readRawHeader(char *buffer)
192 {
193  // Read header
194  qint64 n = q->device()->read(buffer, 0x200);
195  // we need to test if there is a prefix value because the file name can be null
196  // and the prefix can have a value and in this case we don't reset n.
197  if (n == 0x200 && (buffer[0] != 0 || buffer[0x159] != 0)) {
198  // Make sure this is actually a tar header
199  if (strncmp(buffer + 257, "ustar", 5)) {
200  // The magic isn't there (broken/old tars), but maybe a correct checksum?
201 
202  int check = 0;
203  for (uint j = 0; j < 0x200; ++j) {
204  check += static_cast<unsigned char>(buffer[j]);
205  }
206 
207  // adjust checksum to count the checksum fields as blanks
208  for (uint j = 0; j < 8 /*size of the checksum field including the \0 and the space*/; j++) {
209  check -= static_cast<unsigned char>(buffer[148 + j]);
210  }
211  check += 8 * ' ';
212 
213  QByteArray s = QByteArray::number(check, 8); // octal
214 
215  // only compare those of the 6 checksum digits that mean something,
216  // because the other digits are filled with all sorts of different chars by different tars ...
217  // Some tars right-justify the checksum so it could start in one of three places - we have to check each.
218  if (strncmp(buffer + 148 + 6 - s.length(), s.data(), s.length()) //
219  && strncmp(buffer + 148 + 7 - s.length(), s.data(), s.length()) //
220  && strncmp(buffer + 148 + 8 - s.length(), s.data(), s.length())) {
221  /*qCWarning(KArchiveLog) << "KTar: invalid TAR file. Header is:" << QByteArray( buffer+257, 5 )
222  << "instead of ustar. Reading from wrong pos in file?"
223  << "checksum=" << QByteArray( buffer + 148 + 6 - s.length(), s.length() );*/
224  return -1;
225  }
226  } /*end if*/
227  } else {
228  // reset to 0 if 0x200 because logical end of archive has been reached
229  if (n == 0x200) {
230  n = 0;
231  }
232  } /*end if*/
233  return n;
234 }
235 
236 bool KTar::KTarPrivate::readLonglink(char *buffer, QByteArray &longlink)
237 {
238  qint64 n = 0;
239  // qCDebug(KArchiveLog) << "reading longlink from pos " << q->device()->pos();
240  QIODevice *dev = q->device();
241  // read size of longlink from size field in header
242  // size is in bytes including the trailing null (which we ignore)
243  qint64 size = QByteArray(buffer + 0x7c, 12).trimmed().toLongLong(nullptr, 8 /*octal*/);
244 
245  size--; // ignore trailing null
246  if (size > std::numeric_limits<int>::max() - 32) { // QByteArray can't really be INT_MAX big, it's max size is something between INT_MAX - 32 and INT_MAX
247  // depending the platform so just be safe
248  qCWarning(KArchiveLog) << "Failed to allocate memory for longlink of size" << size;
249  return false;
250  }
251  if (size < 0) {
252  qCWarning(KArchiveLog) << "Invalid longlink size" << size;
253  return false;
254  }
255  longlink.resize(size);
256  qint64 offset = 0;
257  while (size > 0) {
258  int chunksize = qMin(size, 0x200LL);
259  n = dev->read(longlink.data() + offset, chunksize);
260  if (n == -1) {
261  return false;
262  }
263  size -= chunksize;
264  offset += 0x200;
265  } /*wend*/
266  // jump over the rest
267  const int skip = 0x200 - (n % 0x200);
268  if (skip <= 0x200) {
269  if (dev->read(buffer, skip) != skip) {
270  return false;
271  }
272  }
273  longlink.truncate(qstrlen(longlink.constData()));
274  return true;
275 }
276 
277 qint64 KTar::KTarPrivate::readHeader(char *buffer, QString &name, QString &symlink)
278 {
279  name.truncate(0);
280  symlink.truncate(0);
281  while (true) {
282  qint64 n = readRawHeader(buffer);
283  if (n != 0x200) {
284  return n;
285  }
286 
287  // is it a longlink?
288  if (strcmp(buffer, "././@LongLink") == 0) {
289  char typeflag = buffer[0x9c];
290  QByteArray longlink;
291  if (readLonglink(buffer, longlink)) {
292  switch (typeflag) {
293  case 'L':
294  name = QFile::decodeName(longlink.constData());
295  break;
296  case 'K':
297  symlink = QFile::decodeName(longlink.constData());
298  break;
299  } /*end switch*/
300  }
301  } else {
302  break;
303  } /*end if*/
304  } /*wend*/
305 
306  // if not result of longlink, read names directly from the header
307  if (name.isEmpty())
308  // there are names that are exactly 100 bytes long
309  // and neither longlink nor \0 terminated (bug:101472)
310  {
311  name = QFile::decodeName(QByteArray(buffer, qstrnlen(buffer, 100)));
312  }
313  if (symlink.isEmpty()) {
314  char *symlinkBuffer = buffer + 0x9d /*?*/;
315  symlink = QFile::decodeName(QByteArray(symlinkBuffer, qstrnlen(symlinkBuffer, 100)));
316  }
317 
318  return 0x200;
319 }
320 
321 /*
322  * If we have created a temporary file, we have
323  * to decompress the original file now and write
324  * the contents to the temporary file.
325  */
326 bool KTar::KTarPrivate::fillTempFile(const QString &fileName)
327 {
328  if (!tmpFile) {
329  return true;
330  }
331 
332  // qCDebug(KArchiveLog) << "filling tmpFile of mimetype" << mimetype;
333 
335  KCompressionDevice filterDev(fileName, compressionType);
336 
337  QFile *file = tmpFile;
338  Q_ASSERT(file->isOpen());
339  Q_ASSERT(file->openMode() & QIODevice::WriteOnly);
340  file->seek(0);
341  QByteArray buffer;
342  buffer.resize(8 * 1024);
343  if (!filterDev.open(QIODevice::ReadOnly)) {
344  q->setErrorString(tr("File %1 does not exist").arg(fileName));
345  return false;
346  }
347  qint64 len = -1;
348  while (!filterDev.atEnd() && len != 0) {
349  len = filterDev.read(buffer.data(), buffer.size());
350  if (len < 0) { // corrupted archive
351  q->setErrorString(tr("Archive %1 is corrupt").arg(fileName));
352  return false;
353  }
354  if (file->write(buffer.data(), len) != len) { // disk full
355  q->setErrorString(tr("Disk full"));
356  return false;
357  }
358  }
359  filterDev.close();
360 
361  file->flush();
362  file->seek(0);
363  Q_ASSERT(file->isOpen());
364  Q_ASSERT(file->openMode() & QIODevice::ReadOnly);
365 
366  // qCDebug(KArchiveLog) << "filling tmpFile finished.";
367  return true;
368 }
369 
371 {
372  if (!(mode & QIODevice::ReadOnly)) {
373  return true;
374  }
375 
376  if (!d->fillTempFile(fileName())) {
377  return false;
378  }
379 
380  // We'll use the permission and user/group of d->rootDir
381  // for any directory we emulate (see findOrCreate)
382  // struct stat buf;
383  // stat( fileName(), &buf );
384 
385  d->dirList.clear();
386  QIODevice *dev = device();
387 
388  if (!dev) {
389  setErrorString(tr("Could not get underlying device"));
390  qCWarning(KArchiveLog) << "Could not get underlying device";
391  return false;
392  }
393 
394  // read dir information
395  char buffer[0x200];
396  bool ende = false;
397  do {
398  QString name;
399  QString symlink;
400 
401  // Read header
402  qint64 n = d->readHeader(buffer, name, symlink);
403  if (n < 0) {
404  setErrorString(tr("Could not read tar header"));
405  return false;
406  }
407  if (n == 0x200) {
408  bool isdir = false;
409 
410  if (name.isEmpty()) {
411  continue;
412  }
413  if (name.endsWith(QLatin1Char('/'))) {
414  isdir = true;
415  name.truncate(name.length() - 1);
416  }
417 
418  QByteArray prefix = QByteArray(buffer + 0x159, 155);
419  if (prefix[0] != '\0') {
420  name = (QString::fromLatin1(prefix.constData()) + QLatin1Char('/') + name);
421  }
422 
423  int pos = name.lastIndexOf(QLatin1Char('/'));
424  QString nm = (pos == -1) ? name : name.mid(pos + 1);
425 
426  // read access
427  buffer[0x6b] = 0;
428  char *dummy;
429  const char *p = buffer + 0x64;
430  while (*p == ' ') {
431  ++p;
432  }
433  int access = strtol(p, &dummy, 8);
434 
435  // read user and group
436  const int maxUserGroupLength = 32;
437  const char *userStart = buffer + 0x109;
438  const int userLen = qstrnlen(userStart, maxUserGroupLength);
439  const QString user = QString::fromLocal8Bit(userStart, userLen);
440  const char *groupStart = buffer + 0x129;
441  const int groupLen = qstrnlen(groupStart, maxUserGroupLength);
442  const QString group = QString::fromLocal8Bit(groupStart, groupLen);
443 
444  // read time
445  buffer[0x93] = 0;
446  p = buffer + 0x88;
447  while (*p == ' ') {
448  ++p;
449  }
450  uint time = strtol(p, &dummy, 8);
451 
452  // read type flag
453  char typeflag = buffer[0x9c];
454  // '0' for files, '1' hard link, '2' symlink, '5' for directory
455  // (and 'L' for longlink fileNames, 'K' for longlink symlink targets)
456  // 'D' for GNU tar extension DUMPDIR, 'x' for Extended header referring
457  // to the next file in the archive and 'g' for Global extended header
458 
459  if (typeflag == '5') {
460  isdir = true;
461  }
462 
463  bool isDumpDir = false;
464  if (typeflag == 'D') {
465  isdir = false;
466  isDumpDir = true;
467  }
468  // qCDebug(KArchiveLog) << nm << "isdir=" << isdir << "pos=" << dev->pos() << "typeflag=" << typeflag << " islink=" << ( typeflag == '1' || typeflag
469  // == '2' );
470 
471  if (typeflag == 'x' || typeflag == 'g') { // pax extended header, or pax global extended header
472  // Skip it for now. TODO: implement reading of extended header, as per https://pubs.opengroup.org/onlinepubs/9699919799/utilities/pax.html
473  (void)dev->read(buffer, 0x200);
474  continue;
475  }
476 
477  if (isdir) {
478  access |= S_IFDIR; // broken tar files...
479  }
480 
481  KArchiveEntry *e;
482  if (isdir) {
483  // qCDebug(KArchiveLog) << "directory" << nm;
484  e = new KArchiveDirectory(this, nm, access, KArchivePrivate::time_tToDateTime(time), user, group, symlink);
485  } else {
486  // read size
487  QByteArray sizeBuffer(buffer + 0x7c, 12);
488  qint64 size = sizeBuffer.trimmed().toLongLong(nullptr, 8 /*octal*/);
489  // 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), 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), 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(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(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(name.left(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  */
556 bool 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(QIODevice::WriteOnly)) {
581  file->close();
582  q->setErrorString(tr("Failed to write back temp file: %1").arg(dev.errorString()));
583  return false;
584  }
585  if (forced) {
586  dev.setOrigFileName(origFileName);
587  }
588  file->seek(0);
589  QByteArray buffer;
590  buffer.resize(8 * 1024);
591  qint64 len;
592  while (!file->atEnd()) {
593  len = file->read(buffer.data(), buffer.size());
594  dev.write(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 
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());
614  delete d->tmpFile;
615  d->tmpFile = nullptr;
616  setDevice(nullptr);
617  }
618 
619  return ok;
620 }
621 
622 bool KTar::doFinishWriting(qint64 size)
623 {
624  // Write alignment
625  int rest = size % 0x200;
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(buffer, 0x200 - rest);
635  const bool ok = nwritten == 0x200 - rest;
636 
637  if (!ok) {
638  setErrorString(tr("Couldn't write alignment: %1").arg(device()->errorString()));
639  }
640 
641  return ok;
642  }
643  return true;
644 }
645 
646 /*** Some help from the tar sources
647 struct 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 
669 void 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(buffer + 0x64, mode, 6);
674  buffer[0x6a] = ' ';
675  buffer[0x6b] = '\0';
676 
677  // dummy uid
678  strcpy(buffer + 0x6c, " 765 "); // 501 in decimal
679  // dummy gid
680  strcpy(buffer + 0x74, " 144 "); // 100 in decimal
681 
682  // size
683  QByteArray s = QByteArray::number(size, 8); // octal
684  s = s.rightJustified(11, '0');
685  memcpy(buffer + 0x7c, s.data(), 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), 8); // octal
691  s = s.rightJustified(11, '0');
692  memcpy(buffer + 0x88, s.data(), 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(buffer + 0x101, "ustar");
716  strcpy(buffer + 0x107, "00");
717 
718  // user
719  strcpy(buffer + 0x109, uname);
720  // group
721  strcpy(buffer + 0x129, 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, 8); // octal
729  s = s.rightJustified(6, '0');
730  memcpy(buffer + 0x94, s.constData(), 6);
731 }
732 
733 void KTar::KTarPrivate::writeLonglink(char *buffer, const QByteArray &name, char typeflag, const char *uname, const char *gname)
734 {
735  strcpy(buffer, "././@LongLink");
736  qint64 namelen = name.length() + 1;
737  fillBuffer(buffer, " 0", namelen, QDateTime(), typeflag, uname, gname);
738  q->device()->write(buffer, 0x200); // TODO error checking
739  qint64 offset = 0;
740  while (namelen > 0) {
741  int chunksize = qMin(namelen, 0x200LL);
742  memcpy(buffer, name.data() + offset, chunksize);
743  // write long name
744  q->device()->write(buffer, 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 
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("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("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("Application limitation: Can not add file larger than %1 bytes").arg(MAX_FILESIZE));
775  return false;
776  }
777 
778  // In some tar files we can find dir/./file => call cleanPath
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 
803  device()->seek(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, encodedFileName, 'L', uname.constData(), gname.constData());
814  }
815 
816  // Write (potentially truncated) name
817  strncpy(buffer, encodedFileName.constData(), 99);
818  buffer[99] = 0;
819  // zero out the rest (except for what gets filled anyways)
820  memset(buffer + 0x9d, 0, 0x200 - 0x9d);
821 
822  QByteArray permstr = QByteArray::number(static_cast<unsigned int>(perm), 8);
823  permstr = permstr.rightJustified(6, '0');
824  d->fillBuffer(buffer, permstr.constData(), size, mtime, 0x30, uname.constData(), gname.constData());
825 
826  // Write header
827  if (device()->write(buffer, 0x200) != 0x200) {
828  setErrorString(tr("Failed to write header: %1").arg(device()->errorString()));
829  return false;
830  } else {
831  return true;
832  }
833 }
834 
835 bool 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("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("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(name));
857 
858  // Need trailing '/'
859  if (!dirName.endsWith(QLatin1Char('/'))) {
860  dirName += QLatin1Char('/');
861  }
862 
863  if (d->dirList.contains(dirName)) {
864  return true; // already there
865  }
866 
867  char buffer[0x201] = {0};
868 
870  device()->seek(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(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, encodedDirname, 'L', uname.constData(), gname.constData());
881  }
882 
883  // Write (potentially truncated) name
884  strncpy(buffer, encodedDirname.constData(), 99);
885  buffer[99] = 0;
886  // zero out the rest (except for what gets filled anyways)
887  memset(buffer + 0x9d, 0, 0x200 - 0x9d);
888 
889  QByteArray permstr = QByteArray::number(static_cast<unsigned int>(perm), 8);
890  permstr = permstr.rightJustified(6, ' ');
891  d->fillBuffer(buffer, permstr.constData(), 0, mtime, 0x35, uname.constData(), gname.constData());
892 
893  // Write header
894  device()->write(buffer, 0x200);
896  d->tarEnd = device()->pos();
897  }
898 
899  d->dirList.append(dirName); // contains trailing slash
900  return true; // TODO if wanted, better error control
901 }
902 
903 bool 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("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("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
926 
927  char buffer[0x201] = {0};
928 
930  device()->seek(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(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, encodedTarget, 'K', uname.constData(), gname.constData());
942  }
943  if (encodedFileName.length() > 99) {
944  d->writeLonglink(buffer, encodedFileName, 'L', uname.constData(), gname.constData());
945  }
946 
947  // Write (potentially truncated) name
948  strncpy(buffer, encodedFileName.constData(), 99);
949  buffer[99] = 0;
950  // Write (potentially truncated) symlink target
951  strncpy(buffer + 0x9d, encodedTarget.constData(), 99);
952  buffer[0x9d + 99] = 0;
953  // zero out the rest
954  memset(buffer + 0x9d + 100, 0, 0x200 - 100 - 0x9d);
955 
956  QByteArray permstr = QByteArray::number(static_cast<unsigned int>(perm), 8);
957  permstr = permstr.rightJustified(6, ' ');
958  d->fillBuffer(buffer, permstr.constData(), 0, mtime, 0x32, uname.constData(), gname.constData());
959 
960  // Write header
961  bool retval = device()->write(buffer, 0x200) == 0x200;
963  d->tarEnd = device()->pos();
964  }
965  return retval;
966 }
967 
968 void KTar::virtual_hook(int id, void *data)
969 {
970  KArchive::virtual_hook(id, data);
971 }
bool inherits(const QString &mimeTypeName) const const
bool endsWith(const QString &s, Qt::CaseSensitivity cs) const const
QString errorString() const const
void truncate(int position)
bool openArchive(QIODevice::OpenMode mode) override
Opens the archive for reading.
Definition: ktar.cpp:370
typedef OpenMode
virtual bool open(QIODevice::OpenMode mode) override
QDateTime currentDateTime()
void setRootDir(KArchiveDirectory *rootDir)
Derived classes call setRootDir from openArchive, to set the root directory after parsing an existing...
Definition: karchive.cpp:606
QByteArray encodeName(const QString &fileName)
~KTar() override
If the tar ball is still opened, then it will be closed automatically by the destructor.
Definition: ktar.cpp:170
QByteArray number(int n, int base)
virtual bool open(QIODevice::OpenMode mode)
QIODevice * device() const
The underlying device.
Definition: karchive.cpp:618
bool closeArchive() override
Closes the archive.
Definition: ktar.cpp:603
QByteArray trimmed() const const
virtual bool seek(qint64 pos)
int lastIndexOf(QChar ch, int from, Qt::CaseSensitivity cs) const const
bool exists() const const
virtual KArchiveDirectory * rootDir()
Retrieves or create the root directory.
Definition: karchive.cpp:509
KIOCORE_EXPORT MimetypeJob * mimetype(const QUrl &url, JobFlags flags=DefaultFlags)
QString tempPath()
QMimeType mimeTypeForData(const QByteArray &data) const const
qlonglong toLongLong(bool *ok, int base) const const
virtual bool close()
Closes the archive.
Definition: karchive.cpp:214
KTar(const QString &filename, const QString &mimetype=QString())
Creates an instance that operates on the given filename using the compression filter associated to gi...
Definition: ktar.cpp:69
A directory in an archive.
Base class for the archive-file's directory structure.
Definition: karchiveentry.h:35
QString fromLocal8Bit(const char *str, int size)
static CompressionType compressionTypeForMimeType(const QString &mimetype)
Returns the compression type for the given MIME type, if possible.
virtual qint64 pos() const const
QString fileName() const
The name of the archive file, as passed to the constructor that takes a fileName, or an empty string ...
Definition: karchive.cpp:628
bool isOpen() const const
bool isEmpty() const const
int length() const const
qint64 toMSecsSinceEpoch() const const
bool doWriteDir(const QString &name, const QString &user, const QString &group, mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override
Reimplemented from KArchive.
Definition: ktar.cpp:835
virtual bool seek(qint64 pos) override
bool isValid() const const
virtual void close() override
QIODevice::OpenMode openMode() const const
virtual bool atEnd() const const override
bool doPrepareWriting(const QString &name, const QString &user, const QString &group, qint64 size, mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override
Reimplemented from KArchive.
Definition: ktar.cpp:751
generic class for reading/writing archives
Definition: karchive.h:39
QByteArray rightJustified(int width, char fill, bool truncate) const const
bool doFinishWriting(qint64 size) override
Reimplemented from KArchive.
Definition: ktar.cpp:622
bool isOpen() const
Checks whether the archive is open.
Definition: karchive.cpp:623
QIODevice::OpenMode mode() const
Returns the mode in which the archive was opened.
Definition: karchive.cpp:613
QString cleanPath(const QString &path)
qint64 read(char *data, qint64 maxSize)
bool flush()
void resize(int size)
void addEntry(KArchiveEntry *)
Definition: karchive.cpp:912
const char * constData() const const
QString left(int n) const const
QString fromLatin1(const char *str, int size)
QString name(StandardShortcut id)
virtual bool createDevice(QIODevice::OpenMode mode)
Can be reimplemented in order to change the creation of the device (when using the fileName construct...
Definition: karchive.cpp:174
bool isValid() const const
void setErrorString(const QString &errorStr)
Sets error description.
Definition: karchive.cpp:474
A file in an archive.
Definition: karchivefile.h:24
QMimeType mimeTypeForFile(const QString &fileName, QMimeDatabase::MatchMode mode) const const
bool doWriteSymLink(const QString &name, const QString &target, const QString &user, const QString &group, mode_t perm, const QDateTime &atime, const QDateTime &mtime, const QDateTime &ctime) override
Reimplemented from KArchive.
Definition: ktar.cpp:903
int size() const const
QChar * data()
int length() const const
QByteArray toLocal8Bit() const const
virtual void close()
void truncate(int pos)
KIOCORE_EXPORT SimpleJob * symlink(const QString &target, const QUrl &dest, JobFlags flags=DefaultFlags)
QString errorString() const
Returns a description of the last error.
Definition: karchive.cpp:253
QString mid(int position, int n) const const
KArchiveDirectory * findOrCreate(const QString &path)
Ensures that path exists, create otherwise.
Definition: karchive.cpp:521
void setDevice(QIODevice *dev)
Can be called by derived classes in order to set the underlying device.
Definition: karchive.cpp:597
void setOrigFileName(const QByteArray &fileName)
Special function for setting the "original file name" in the gzip header, when writing a tar....
Definition: ktar.cpp:182
Definition: ktar.h:22
char * data()
int access(const QString &path, int mode)
qint64 write(const char *data, qint64 maxSize)
QString decodeName(const QByteArray &localFileName)
bool createDevice(QIODevice::OpenMode mode) override
Can be reimplemented in order to change the creation of the device (when using the fileName construct...
Definition: ktar.cpp:91
This file is part of the KDE documentation.
Documentation copyright © 1996-2023 The KDE developers.
Generated on Tue Nov 28 2023 03:53:59 by doxygen 1.8.17 written by Dimitri van Heesch, © 1997-2006

KDE's Doxygen guidelines are available online.