1/* This file is part of the KDE libraries
2 SPDX-FileCopyrightText: 2000-2005 David Faure <faure@kde.org>
3 SPDX-FileCopyrightText: 2003 Leo Savernik <l.savernik@aon.at>
4
5 Moved from ktar.cpp by Roberto Teixeira <maragato@kde.org>
6
7 SPDX-License-Identifier: LGPL-2.0-or-later
8*/
9
10#include "karchive.h"
11#include "karchive_p.h"
12#include "klimitediodevice_p.h"
13#include "loggingcategory.h"
14
15#include <qplatformdefs.h> // QT_STATBUF, QT_LSTAT
16
17#include <QDebug>
18#include <QDir>
19#include <QFile>
20#include <QMap>
21#include <QStack>
22
23#include <cerrno>
24#include <stdio.h>
25#include <stdlib.h>
26
27#include <assert.h>
28
29#ifdef Q_OS_UNIX
30#include <grp.h>
31#include <limits.h> // PATH_MAX
32#include <pwd.h>
33#include <unistd.h>
34#endif
35#ifdef Q_OS_WIN
36#include <windows.h> // DWORD, GetUserNameW
37#endif // Q_OS_WIN
38
39#if defined(Q_OS_UNIX)
40#define STAT_METHOD QT_LSTAT
41#else
42#define STAT_METHOD QT_STAT
43#endif
44
45////////////////////////////////////////////////////////////////////////
46/////////////////// KArchiveDirectoryPrivate ///////////////////////////
47////////////////////////////////////////////////////////////////////////
48
49class KArchiveDirectoryPrivate
50{
51public:
52 KArchiveDirectoryPrivate(KArchiveDirectory *parent)
53 : q(parent)
54 {
55 }
56
57 ~KArchiveDirectoryPrivate()
58 {
59 qDeleteAll(c: entries);
60 }
61
62 KArchiveDirectoryPrivate(const KArchiveDirectoryPrivate &) = delete;
63 KArchiveDirectoryPrivate &operator=(const KArchiveDirectoryPrivate &) = delete;
64
65 static KArchiveDirectoryPrivate *get(KArchiveDirectory *directory)
66 {
67 return directory->d;
68 }
69
70 const KArchiveEntry *entry(const QString &_name) const
71 {
72 QString name = QDir::cleanPath(path: _name);
73
74 if (name.isEmpty()) {
75 return entries.value(key: name);
76 } else if (name == QLatin1String("/")) {
77 return q;
78 }
79
80 auto r = lookupPath(path: name);
81 return r.entry;
82 }
83
84 struct LookupResult {
85 KArchiveDirectory *parent = nullptr;
86 KArchiveEntry *entry = nullptr;
87 };
88
89 // \c path preconditions:
90 // - path is not empty
91 // - is cleaned (no "..", no double slash) - \sa QDir::cleanPath
92 // - does not end with a slash
93 const LookupResult lookupPath(QStringView path) const
94 {
95 auto findChild = [](KArchiveDirectory *dir, QStringView name) -> KArchiveEntry * {
96 if (dir->d->entries.empty()) {
97 return nullptr;
98 } else if (dir->d->entries.size() == 1) {
99 auto it = dir->d->entries.cbegin();
100 return (it.key() == name) ? it.value() : nullptr;
101 }
102 return dir->d->entries.value(key: name);
103 };
104
105 qsizetype startPos = 0;
106 if (path[0] == QLatin1Char('/')) {
107 startPos = 1;
108 }
109
110 auto endPos = path.indexOf(c: QLatin1Char('/'), from: startPos);
111 auto parent = static_cast<KArchiveDirectory *>(q);
112
113 while (endPos > 0) {
114 auto match = findChild(parent, path.sliced(pos: startPos, n: endPos - startPos));
115 if (match == nullptr) {
116 return {.parent: parent, .entry: nullptr};
117 } else if (!match->isDirectory()) {
118 return {.parent: parent, .entry: nullptr};
119 }
120 parent = static_cast<KArchiveDirectory *>(match);
121 startPos = endPos + 1;
122 endPos = path.indexOf(c: QLatin1Char('/'), from: startPos);
123 }
124 auto match = findChild(parent, path.sliced(pos: startPos));
125 return {.parent: parent, .entry: match};
126 }
127
128 KArchiveDirectory *q;
129 QHash<QString, KArchiveEntry *> entries;
130};
131
132////////////////////////////////////////////////////////////////////////
133/////////////////////////// KArchive ///////////////////////////////////
134////////////////////////////////////////////////////////////////////////
135
136KArchive::KArchive(const QString &fileName)
137 : d(new KArchivePrivate(this))
138{
139 if (fileName.isEmpty()) {
140 qCWarning(KArchiveLog) << "KArchive: No file name specified";
141 }
142 d->fileName = fileName;
143 // This constructor leaves the device set to 0.
144 // This is for the use of QSaveFile, see open().
145}
146
147KArchive::KArchive(QIODevice *dev)
148 : d(new KArchivePrivate(this))
149{
150 if (!dev) {
151 qCWarning(KArchiveLog) << "KArchive: Null device specified";
152 }
153 d->dev = dev;
154}
155
156KArchive::~KArchive()
157{
158 Q_ASSERT(!isOpen()); // the derived class destructor must have closed already
159 delete d;
160}
161
162bool KArchive::open(QIODevice::OpenMode mode)
163{
164 Q_ASSERT(mode != QIODevice::NotOpen);
165
166 if (isOpen()) {
167 close();
168 }
169
170 if (!d->fileName.isEmpty()) {
171 Q_ASSERT(!d->dev);
172 if (!createDevice(mode)) {
173 return false;
174 }
175 }
176
177 if (!d->dev) {
178 setErrorString(tr(sourceText: "No filename or device was specified"));
179 return false;
180 }
181
182 if (!d->dev->isOpen() && !d->dev->open(mode)) {
183 setErrorString(tr(sourceText: "Could not open device in mode %1").arg(a: static_cast<int>(mode)));
184 return false;
185 }
186
187 d->mode = mode;
188
189 Q_ASSERT(!d->rootDir);
190 d->rootDir = nullptr;
191
192 return openArchive(mode);
193}
194
195bool KArchive::createDevice(QIODevice::OpenMode mode)
196{
197 switch (mode) {
198 case QIODevice::WriteOnly:
199 if (!d->fileName.isEmpty()) {
200 // The use of QSaveFile can't be done in the ctor (no mode known yet)
201 // qCDebug(KArchiveLog) << "Writing to a file using QSaveFile";
202 d->saveFile = std::make_unique<QSaveFile>(args&: d->fileName);
203#ifdef Q_OS_ANDROID
204 // we cannot rename on to Android content: URLs
205 if (d->fileName.startsWith(QLatin1String("content://"))) {
206 d->saveFile->setDirectWriteFallback(true);
207 }
208#endif
209 if (!d->saveFile->open(flags: QIODevice::WriteOnly)) {
210 setErrorString(tr(sourceText: "QSaveFile creation for %1 failed: %2").arg(args&: d->fileName, args: d->saveFile->errorString()));
211
212 d->saveFile.reset();
213 return false;
214 }
215 d->dev = d->saveFile.get();
216 d->deviceOwned = false;
217 Q_ASSERT(d->dev);
218 }
219 break;
220 case QIODevice::ReadOnly:
221 case QIODevice::ReadWrite:
222 // ReadWrite mode still uses QFile for now; we'd need to copy to the tempfile, in fact.
223 if (!d->fileName.isEmpty()) {
224 d->dev = new QFile(d->fileName);
225 d->deviceOwned = true;
226 }
227 break; // continued below
228 default:
229 setErrorString(tr(sourceText: "Unsupported mode %1").arg(a: static_cast<int>(mode)));
230 return false;
231 }
232 return true;
233}
234
235bool KArchive::close()
236{
237 if (!isOpen()) {
238 setErrorString(tr(sourceText: "Archive already closed"));
239 return false; // already closed (return false or true? arguable...)
240 }
241
242 // moved by holger to allow kzip to write the zip central dir
243 // to the file in closeArchive()
244 // DF: added d->dev so that we skip closeArchive if saving aborted.
245 bool closeSucceeded = true;
246 if (d->dev) {
247 closeSucceeded = closeArchive();
248 if (d->mode == QIODevice::WriteOnly && !closeSucceeded) {
249 d->abortWriting();
250 }
251 }
252
253 if (d->dev && d->dev != d->saveFile.get()) {
254 d->dev->close();
255 }
256
257 // if d->saveFile is not null then it is equal to d->dev.
258 if (d->saveFile) {
259 closeSucceeded = d->saveFile->commit();
260 d->saveFile.reset();
261 } else if (d->deviceOwned) {
262 delete d->dev; // we created it ourselves in open()
263 }
264
265 delete d->rootDir;
266 d->rootDir = nullptr;
267 d->mode = QIODevice::NotOpen;
268 d->dev = nullptr;
269 return closeSucceeded;
270}
271
272QString KArchive::errorString() const
273{
274 return d->errorStr;
275}
276
277const KArchiveDirectory *KArchive::directory() const
278{
279 // rootDir isn't const so that parsing-on-demand is possible
280 return const_cast<KArchive *>(this)->rootDir();
281}
282
283bool KArchive::addLocalFile(const QString &fileName, const QString &destName)
284{
285 QFileInfo fileInfo(fileName);
286 if (!fileInfo.isFile() && !fileInfo.isSymLink()) {
287 setErrorString(tr(sourceText: "%1 doesn't exist or is not a regular file.").arg(a: fileName));
288 return false;
289 }
290
291 QT_STATBUF fi;
292 if (STAT_METHOD(file: QFile::encodeName(fileName).constData(), buf: &fi) == -1) {
293 setErrorString(tr(sourceText: "Failed accessing the file %1 for adding to the archive. The error was: %2").arg(a: fileName).arg(a: QLatin1String{strerror(errno)}));
294 return false;
295 }
296
297 if (fileInfo.isSymLink()) {
298 QString symLinkTarget;
299 // Do NOT use fileInfo.symLinkTarget() for unix symlinks!
300 // It returns the -full- path to the target, while we want the target string "as is".
301#if defined(Q_OS_UNIX) && !defined(Q_OS_OS2EMX)
302 const QByteArray encodedFileName = QFile::encodeName(fileName);
303 QByteArray s;
304#if defined(PATH_MAX)
305 s.resize(PATH_MAX + 1);
306#else
307 int path_max = pathconf(encodedFileName.data(), _PC_PATH_MAX);
308 if (path_max <= 0) {
309 path_max = 4096;
310 }
311 s.resize(path_max);
312#endif
313 int len = readlink(path: encodedFileName.data(), buf: s.data(), len: s.size() - 1);
314 if (len >= 0) {
315 s[len] = '\0';
316 symLinkTarget = QFile::decodeName(localFileName: s.constData());
317 }
318#endif
319 if (symLinkTarget.isEmpty()) { // Mac or Windows
320 symLinkTarget = fileInfo.symLinkTarget();
321 }
322 return writeSymLink(name: destName,
323 target: symLinkTarget,
324 user: fileInfo.owner(),
325 group: fileInfo.group(),
326 perm: fi.st_mode,
327 atime: fileInfo.lastRead(),
328 mtime: fileInfo.lastModified(),
329 ctime: fileInfo.birthTime());
330 } /*end if*/
331
332 qint64 size = fileInfo.size();
333
334 // the file must be opened before prepareWriting is called, otherwise
335 // if the opening fails, no content will follow the already written
336 // header and the tar file is incorrect
337 QFile file(fileName);
338 if (!file.open(flags: QIODevice::ReadOnly)) {
339 setErrorString(tr(sourceText: "Couldn't open file %1: %2").arg(args: fileName, args: file.errorString()));
340 return false;
341 }
342
343 if (!prepareWriting(name: destName, user: fileInfo.owner(), group: fileInfo.group(), size, perm: fi.st_mode, atime: fileInfo.lastRead(), mtime: fileInfo.lastModified(), ctime: fileInfo.birthTime())) {
344 // qCWarning(KArchiveLog) << " prepareWriting" << destName << "failed";
345 return false;
346 }
347
348 // Read and write data in chunks to minimize memory usage
349 QByteArray array;
350 array.resize(size: int(qMin(a: qint64(1024 * 1024), b: size)));
351 qint64 n;
352 qint64 total = 0;
353 while ((n = file.read(data: array.data(), maxlen: array.size())) > 0) {
354 if (!writeData(data: array.data(), size: n)) {
355 // qCWarning(KArchiveLog) << "writeData failed";
356 return false;
357 }
358 total += n;
359 }
360 Q_ASSERT(total == size);
361
362 if (!finishWriting(size)) {
363 // qCWarning(KArchiveLog) << "finishWriting failed";
364 return false;
365 }
366 return true;
367}
368
369bool KArchive::addLocalDirectory(const QString &path, const QString &destName)
370{
371 QDir dir(path);
372 if (!dir.exists()) {
373 setErrorString(tr(sourceText: "Directory %1 does not exist").arg(a: path));
374 return false;
375 }
376 dir.setFilter(dir.filter() | QDir::Hidden);
377 const QStringList files = dir.entryList();
378 for (const QString &file : files) {
379 if (file != QLatin1String(".") && file != QLatin1String("..")) {
380 const QString fileName = path + QLatin1Char('/') + file;
381 // qCDebug(KArchiveLog) << "storing " << fileName;
382 const QString dest = destName.isEmpty() ? file : (destName + QLatin1Char('/') + file);
383 QFileInfo fileInfo(fileName);
384
385 if (fileInfo.isFile() || fileInfo.isSymLink()) {
386 addLocalFile(fileName, destName: dest);
387 } else if (fileInfo.isDir()) {
388 // Write directory, so that empty dirs are preserved (and permissions written out, etc.)
389 int perms = 0;
390 QT_STATBUF fi;
391 if (STAT_METHOD(file: QFile::encodeName(fileName).constData(), buf: &fi) != -1) {
392 perms = fi.st_mode;
393 }
394 writeDir(name: dest, user: fileInfo.owner(), group: fileInfo.group(), perm: perms, atime: fileInfo.lastRead(), mtime: fileInfo.lastModified(), ctime: fileInfo.birthTime());
395 // Recurse
396 addLocalDirectory(path: fileName, destName: dest);
397 }
398 // We omit sockets
399 }
400 }
401 return true;
402}
403
404bool KArchive::writeFile(const QString &name,
405 QByteArrayView data,
406 mode_t perm,
407 const QString &user,
408 const QString &group,
409 const QDateTime &atime,
410 const QDateTime &mtime,
411 const QDateTime &ctime)
412{
413 const qint64 size = data.size();
414 if (!prepareWriting(name, user, group, size, perm, atime, mtime, ctime)) {
415 // qCWarning(KArchiveLog) << "prepareWriting failed";
416 return false;
417 }
418
419 // Write data
420 // Note: if data is null, don't call write, it would terminate the KCompressionDevice
421 if (data.constData() && size && !writeData(data: data.constData(), size)) {
422 // qCWarning(KArchiveLog) << "writeData failed";
423 return false;
424 }
425
426 if (!finishWriting(size)) {
427 // qCWarning(KArchiveLog) << "finishWriting failed";
428 return false;
429 }
430 return true;
431}
432
433bool KArchive::writeData(const char *data, qint64 size)
434{
435 return doWriteData(data, size);
436}
437
438bool KArchive::writeData(QByteArrayView data)
439{
440 return doWriteData(data: data.constData(), size: data.size());
441}
442
443bool KArchive::doWriteData(const char *data, qint64 size)
444{
445 bool ok = device()->write(data, len: size) == size;
446 if (!ok) {
447 setErrorString(tr(sourceText: "Writing failed: %1").arg(a: device()->errorString()));
448 d->abortWriting();
449 }
450 return ok;
451}
452
453// The writeDir -> doWriteDir pattern allows to avoid propagating the default
454// values into all virtual methods of subclasses, and it allows more extensibility:
455// if a new argument is needed, we can add a writeDir overload which stores the
456// additional argument in the d pointer, and doWriteDir reimplementations can fetch
457// it from there.
458
459bool KArchive::writeDir(const QString &name,
460 const QString &user,
461 const QString &group,
462 mode_t perm,
463 const QDateTime &atime,
464 const QDateTime &mtime,
465 const QDateTime &ctime)
466{
467 return doWriteDir(name, user, group, perm: perm | 040000, atime, mtime, ctime);
468}
469
470bool KArchive::writeSymLink(const QString &name,
471 const QString &target,
472 const QString &user,
473 const QString &group,
474 mode_t perm,
475 const QDateTime &atime,
476 const QDateTime &mtime,
477 const QDateTime &ctime)
478{
479 return doWriteSymLink(name, target, user, group, perm, atime, mtime, ctime);
480}
481
482bool KArchive::prepareWriting(const QString &name,
483 const QString &user,
484 const QString &group,
485 qint64 size,
486 mode_t perm,
487 const QDateTime &atime,
488 const QDateTime &mtime,
489 const QDateTime &ctime)
490{
491 bool ok = doPrepareWriting(name, user, group, size, perm, atime, mtime, ctime);
492 if (!ok) {
493 d->abortWriting();
494 }
495 return ok;
496}
497
498bool KArchive::finishWriting(qint64 size)
499{
500 return doFinishWriting(size);
501}
502
503void KArchive::setErrorString(const QString &errorStr)
504{
505 d->errorStr = errorStr;
506}
507
508static QString getCurrentUserName()
509{
510#if defined(Q_OS_UNIX)
511 struct passwd *pw = getpwuid(uid: getuid());
512 return pw ? QFile::decodeName(localFileName: pw->pw_name) : QString::number(getuid());
513#elif defined(Q_OS_WIN)
514 wchar_t buffer[255];
515 DWORD size = 255;
516 bool ok = GetUserNameW(buffer, &size);
517 if (!ok) {
518 return QString();
519 }
520 return QString::fromWCharArray(buffer);
521#else
522 return QString();
523#endif
524}
525
526static QString getCurrentGroupName()
527{
528#if defined(Q_OS_UNIX)
529 struct group *grp = getgrgid(gid: getgid());
530 return grp ? QFile::decodeName(localFileName: grp->gr_name) : QString::number(getgid());
531#elif defined(Q_OS_WIN)
532 return QString();
533#else
534 return QString();
535#endif
536}
537
538KArchiveDirectory *KArchive::rootDir()
539{
540 if (!d->rootDir) {
541 // qCDebug(KArchiveLog) << "Making root dir ";
542 QString username = ::getCurrentUserName();
543 QString groupname = ::getCurrentGroupName();
544
545 d->rootDir = new KArchiveDirectory(this, QStringLiteral("/"), int(0777 + S_IFDIR), QDateTime(), username, groupname, QString());
546 }
547 return d->rootDir;
548}
549
550KArchiveDirectory *KArchive::findOrCreate(const QString &path)
551{
552 auto cleanPath = QDir::cleanPath(path);
553 // There is hardly any practical path length limit on Linux, as PATH_MAX only limits the
554 // *relative* path name.
555 // An ultra deep recursion will make us crash due to not enough stack. Tests show that 1MB stack
556 // (default on Linux seems to be 8MB) gives us up to around 4000 recursions
557 if (auto len = cleanPath.size(); len > 2500 * 255) {
558 qCWarning(KArchiveLog) << "path length limit exceeded, bailing out";
559 return nullptr;
560 }
561 if (auto count = cleanPath.count(c: QLatin1Char('/')); count > 2500) {
562 qCWarning(KArchiveLog) << "path recursion limit exceeded, bailing out";
563 return nullptr;
564 }
565
566 if (cleanPath.isEmpty() || cleanPath == QLatin1String("/") || cleanPath == QLatin1String(".")) { // root dir => found
567 // qCDebug(KArchiveLog) << "returning rootdir";
568 return rootDir();
569 }
570 // Important note : for tar files containing absolute paths
571 // (i.e. beginning with "/"), this means the leading "/" will
572 // be removed (no KDirectory for it), which is exactly the way
573 // the "tar" program works (though it displays a warning about it)
574 // See also KArchiveDirectory::entry().
575 // qCWarning(KArchiveLog) << path << cleanPath;
576 if (cleanPath.startsWith(c: QLatin1Char('/'))) {
577 return d->findOrCreateDirectory(path: cleanPath.mid(position: 1));
578 }
579
580 return d->findOrCreateDirectory(path: cleanPath);
581}
582
583KArchiveDirectory *KArchivePrivate::findOrCreateDirectory(const QStringView path)
584{
585 // qCDebug(KArchiveLog) << path;
586 // Already created ? => found
587 auto rc = KArchiveDirectoryPrivate::get(directory: q->rootDir())->lookupPath(path);
588 if (rc.entry) {
589 if (rc.entry->isDirectory()) {
590 // qCDebug(KArchiveLog) << "found it";
591 return static_cast<KArchiveDirectory *>(rc.entry);
592 } else {
593 KArchiveFile *file = static_cast<KArchiveFile *>(rc.entry);
594 if (file->size() > 0) {
595 qCWarning(KArchiveLog) << path << "is normal file, but there are file paths in the archive assuming it is a directory, bailing out";
596 return nullptr;
597 }
598
599 qCDebug(KArchiveLog) << path << " is an empty file, assuming it is actually a directory and replacing";
600 if (rc.parent->removeEntryV2(rc.entry)) {
601 delete rc.entry;
602 } else {
603 qCDebug(KArchiveLog) << path << " is an empty file, but failed to remove it";
604 return nullptr;
605 }
606 }
607 }
608
609 // Otherwise go up and try again
610 int pos = path.lastIndexOf(c: QLatin1Char('/'));
611 KArchiveDirectory *parent;
612 QStringView dirname;
613 if (pos == -1) { // no more slash => create in root dir
614 parent = q->rootDir();
615 dirname = path;
616 } else {
617 QStringView left = path.left(n: pos);
618 dirname = path.mid(pos: pos + 1);
619 parent = findOrCreateDirectory(path: left); // recursive call... until we find an existing dir.
620 }
621
622 if (!parent) {
623 return nullptr;
624 }
625
626 // qCDebug(KArchiveLog) << "found parent " << parent->name() << " adding " << dirname << " to ensure " << path;
627 // Found -> add the missing piece
628 KArchiveDirectory *e = new KArchiveDirectory(q, dirname.toString(), rootDir->permissions(), rootDir->date(), rootDir->user(), rootDir->group(), QString());
629 if (parent->addEntryV2(e)) {
630 return e; // now a directory to <path> exists
631 } else {
632 return nullptr;
633 }
634}
635
636void KArchive::setDevice(QIODevice *dev)
637{
638 if (d->deviceOwned) {
639 delete d->dev;
640 }
641 d->dev = dev;
642 d->deviceOwned = false;
643}
644
645void KArchive::setRootDir(KArchiveDirectory *rootDir)
646{
647 Q_ASSERT(!d->rootDir); // Call setRootDir only once during parsing please ;)
648 delete d->rootDir; // but if it happens, don't leak
649 d->rootDir = rootDir;
650}
651
652QIODevice::OpenMode KArchive::mode() const
653{
654 return d->mode;
655}
656
657QIODevice *KArchive::device() const
658{
659 return d->dev;
660}
661
662bool KArchive::isOpen() const
663{
664 return d->mode != QIODevice::NotOpen;
665}
666
667QString KArchive::fileName() const
668{
669 return d->fileName;
670}
671
672void KArchivePrivate::abortWriting()
673{
674 if (saveFile) {
675 saveFile->cancelWriting();
676 saveFile.reset();
677 dev = nullptr;
678 }
679}
680
681// this is a hacky wrapper to check if time_t value is invalid
682QDateTime KArchivePrivate::time_tToDateTime(uint seconds)
683{
684 if (seconds == uint(-1)) {
685 return QDateTime();
686 }
687 return QDateTime::fromSecsSinceEpoch(secs: seconds);
688}
689
690////////////////////////////////////////////////////////////////////////
691/////////////////////// KArchiveEntry //////////////////////////////////
692////////////////////////////////////////////////////////////////////////
693
694class KArchiveEntryPrivate
695{
696public:
697 KArchiveEntryPrivate(KArchive *_archive,
698 const QString &_name,
699 int _access,
700 const QDateTime &_date,
701 const QString &_user,
702 const QString &_group,
703 const QString &_symlink)
704 : name(_name)
705 , date(_date)
706 , access(_access)
707 , user(_user)
708 , group(_group)
709 , symlink(_symlink)
710 , archive(_archive)
711 {
712 }
713 QString name;
714 QDateTime date;
715 mode_t access;
716 QString user;
717 QString group;
718 QString symlink;
719 KArchive *archive;
720};
721
722KArchiveEntry::KArchiveEntry(KArchive *t,
723 const QString &name,
724 int access,
725 const QDateTime &date,
726 const QString &user,
727 const QString &group,
728 const QString &symlink)
729 : d(new KArchiveEntryPrivate(t, name, access, date, user, group, symlink))
730{
731}
732
733KArchiveEntry::~KArchiveEntry()
734{
735 delete d;
736}
737
738QDateTime KArchiveEntry::date() const
739{
740 return d->date;
741}
742
743QString KArchiveEntry::name() const
744{
745 return d->name;
746}
747
748mode_t KArchiveEntry::permissions() const
749{
750 return d->access;
751}
752
753QString KArchiveEntry::user() const
754{
755 return d->user;
756}
757
758QString KArchiveEntry::group() const
759{
760 return d->group;
761}
762
763QString KArchiveEntry::symLinkTarget() const
764{
765 return d->symlink;
766}
767
768bool KArchiveEntry::isFile() const
769{
770 return false;
771}
772
773bool KArchiveEntry::isDirectory() const
774{
775 return false;
776}
777
778KArchive *KArchiveEntry::archive() const
779{
780 return d->archive;
781}
782
783////////////////////////////////////////////////////////////////////////
784/////////////////////// KArchiveFile ///////////////////////////////////
785////////////////////////////////////////////////////////////////////////
786
787class KArchiveFilePrivate
788{
789public:
790 KArchiveFilePrivate(qint64 _pos, qint64 _size)
791 : pos(_pos)
792 , size(_size)
793 {
794 }
795 qint64 pos;
796 qint64 size;
797};
798
799KArchiveFile::KArchiveFile(KArchive *t,
800 const QString &name,
801 int access,
802 const QDateTime &date,
803 const QString &user,
804 const QString &group,
805 const QString &symlink,
806 qint64 pos,
807 qint64 size)
808 : KArchiveEntry(t, name, access, date, user, group, symlink)
809 , d(new KArchiveFilePrivate(pos, size))
810{
811}
812
813KArchiveFile::~KArchiveFile()
814{
815 delete d;
816}
817
818qint64 KArchiveFile::position() const
819{
820 return d->pos;
821}
822
823qint64 KArchiveFile::size() const
824{
825 return d->size;
826}
827
828void KArchiveFile::setSize(qint64 s)
829{
830 d->size = s;
831}
832
833QByteArray KArchiveFile::data() const
834{
835 bool ok = archive()->device()->seek(pos: d->pos);
836 if (!ok) {
837 // qCWarning(KArchiveLog) << "Failed to sync to" << d->pos << "to read" << name();
838 }
839
840 // Read content
841 QByteArray arr;
842 if (d->size) {
843 arr = archive()->device()->read(maxlen: d->size);
844 if (arr.size() != d->size) {
845 qCWarning(KArchiveLog) << "KArchiveFile::data: Different size" << arr.size() << "than expected" << d->size << "in" << name();
846 }
847 }
848 return arr;
849}
850
851QIODevice *KArchiveFile::createDevice() const
852{
853 return new KLimitedIODevice(archive()->device(), d->pos, d->size);
854}
855
856bool KArchiveFile::isFile() const
857{
858 return true;
859}
860
861static QFileDevice::Permissions withExecutablePerms(QFileDevice::Permissions filePerms, mode_t perms)
862{
863 if (perms & 01) {
864 filePerms |= QFileDevice::ExeOther;
865 }
866
867 if (perms & 010) {
868 filePerms |= QFileDevice::ExeGroup;
869 }
870
871 if (perms & 0100) {
872 filePerms |= QFileDevice::ExeOwner;
873 }
874
875 return filePerms;
876}
877
878bool KArchiveFile::copyTo(const QString &dest) const
879{
880 QFile f(dest + QLatin1Char('/') + name());
881 if (f.open(flags: QIODevice::ReadWrite | QIODevice::Truncate)) {
882 QIODevice *inputDev = createDevice();
883 if (!inputDev) {
884 f.remove();
885 return false;
886 }
887
888 // Read and write data in chunks to minimize memory usage
889 const qint64 chunkSize = 1024 * 1024;
890 qint64 remainingSize = d->size;
891 QByteArray array;
892 array.resize(size: int(qMin(a: chunkSize, b: remainingSize)));
893
894 while (remainingSize > 0) {
895 const qint64 currentChunkSize = qMin(a: chunkSize, b: remainingSize);
896 const qint64 n = inputDev->read(data: array.data(), maxlen: currentChunkSize);
897 Q_UNUSED(n) // except in Q_ASSERT
898 Q_ASSERT(n == currentChunkSize);
899 f.write(data: array.data(), len: currentChunkSize);
900 remainingSize -= currentChunkSize;
901 }
902 f.setPermissions(withExecutablePerms(filePerms: f.permissions(), perms: permissions()));
903 f.close();
904
905 delete inputDev;
906 return true;
907 }
908 return false;
909}
910
911////////////////////////////////////////////////////////////////////////
912//////////////////////// KArchiveDirectory /////////////////////////////////
913////////////////////////////////////////////////////////////////////////
914
915KArchiveDirectory::KArchiveDirectory(KArchive *t,
916 const QString &name,
917 int access,
918 const QDateTime &date,
919 const QString &user,
920 const QString &group,
921 const QString &symlink)
922 : KArchiveEntry(t, name, access, date, user, group, symlink)
923 , d(new KArchiveDirectoryPrivate(this))
924{
925}
926
927KArchiveDirectory::~KArchiveDirectory()
928{
929 delete d;
930}
931
932QStringList KArchiveDirectory::entries() const
933{
934 return d->entries.keys();
935}
936
937const KArchiveEntry *KArchiveDirectory::entry(const QString &_name) const
938{
939 return d->entry(_name);
940}
941
942const KArchiveFile *KArchiveDirectory::file(const QString &name) const
943{
944 const KArchiveEntry *e = d->entry(name: name);
945 if (e && e->isFile()) {
946 return static_cast<const KArchiveFile *>(e);
947 }
948 return nullptr;
949}
950
951#if KARCHIVE_BUILD_DEPRECATED_SINCE(6, 13)
952void KArchiveDirectory::addEntry(KArchiveEntry *entry)
953{
954 (void)addEntryV2(entry);
955}
956#endif
957
958bool KArchiveDirectory::addEntryV2(KArchiveEntry *entry)
959{
960 if (d->entries.value(key: entry->name())) {
961 qCWarning(KArchiveLog) << "directory " << name() << "has entry" << entry->name() << "already";
962 delete entry;
963 return false;
964 }
965 d->entries.insert(key: entry->name(), value: entry);
966 return true;
967}
968
969#if KARCHIVE_BUILD_DEPRECATED_SINCE(6, 13)
970void KArchiveDirectory::removeEntry(KArchiveEntry *entry)
971{
972 (void)removeEntryV2(entry);
973}
974#endif
975
976bool KArchiveDirectory::removeEntryV2(KArchiveEntry *entry)
977{
978 if (!entry) {
979 return false;
980 }
981
982 QHash<QString, KArchiveEntry *>::Iterator it = d->entries.find(key: entry->name());
983 // nothing removed?
984 if (it == d->entries.end()) {
985 qCWarning(KArchiveLog) << "directory " << name() << "has no entry with name " << entry->name();
986 return false;
987 }
988 if (it.value() != entry) {
989 qCWarning(KArchiveLog) << "directory " << name() << "has another entry for name " << entry->name();
990 return false;
991 }
992 d->entries.erase(it);
993 return true;
994}
995
996bool KArchiveDirectory::isDirectory() const
997{
998 return true;
999}
1000
1001static bool sortByPosition(const KArchiveFile *file1, const KArchiveFile *file2)
1002{
1003 return file1->position() < file2->position();
1004}
1005
1006bool KArchiveDirectory::copyTo(const QString &dest, bool recursiveCopy) const
1007{
1008 QDir root;
1009 const QString destDir(QDir(dest).absolutePath()); // get directory path without any "." or ".."
1010
1011 QList<const KArchiveFile *> fileList;
1012 QMap<qint64, QString> fileToDir;
1013
1014 // placeholders for iterated items
1015 QStack<const KArchiveDirectory *> dirStack;
1016 QStack<QString> dirNameStack;
1017
1018 dirStack.push(t: this); // init stack at current directory
1019 dirNameStack.push(t: destDir); // ... with given path
1020 do {
1021 const KArchiveDirectory *curDir = dirStack.pop();
1022
1023 // extract only to specified folder if it is located within archive's extraction folder
1024 // otherwise put file under root position in extraction folder
1025 QString curDirName = dirNameStack.pop();
1026 if (!QDir(curDirName).absolutePath().startsWith(s: destDir)) {
1027 qCWarning(KArchiveLog) << "Attempted export into folder" << curDirName << "which is outside of the extraction root folder" << destDir << "."
1028 << "Changing export of contained files to extraction root folder.";
1029 curDirName = destDir;
1030 }
1031
1032 if (!root.mkpath(dirPath: curDirName)) {
1033 return false;
1034 }
1035
1036 for (const KArchiveEntry *curEntry : std::as_const(t&: curDir->d->entries)) {
1037 if (!curEntry->symLinkTarget().isEmpty()) {
1038 QString linkName = curDirName + QLatin1Char('/') + curEntry->name();
1039 // To create a valid link on Windows, linkName must have a .lnk file extension.
1040#ifdef Q_OS_WIN
1041 if (!linkName.endsWith(QLatin1String(".lnk"))) {
1042 linkName += QLatin1String(".lnk");
1043 }
1044#endif
1045 QFile symLinkTarget(curEntry->symLinkTarget());
1046 if (!symLinkTarget.link(newName: linkName)) {
1047 // qCDebug(KArchiveLog) << "symlink(" << curEntry->symLinkTarget() << ',' << linkName << ") failed:" << strerror(errno);
1048 }
1049 } else {
1050 if (curEntry->isFile()) {
1051 const KArchiveFile *curFile = dynamic_cast<const KArchiveFile *>(curEntry);
1052 if (curFile) {
1053 fileList.append(t: curFile);
1054 fileToDir.insert(key: curFile->position(), value: curDirName);
1055 }
1056 }
1057
1058 if (curEntry->isDirectory() && recursiveCopy) {
1059 const KArchiveDirectory *ad = dynamic_cast<const KArchiveDirectory *>(curEntry);
1060 if (ad) {
1061 dirStack.push(t: ad);
1062 dirNameStack.push(t: curDirName + QLatin1Char('/') + curEntry->name());
1063 }
1064 }
1065 }
1066 }
1067 } while (!dirStack.isEmpty());
1068
1069 std::sort(first: fileList.begin(), last: fileList.end(), comp: sortByPosition); // sort on d->pos, so we have a linear access
1070
1071 for (QList<const KArchiveFile *>::const_iterator it = fileList.constBegin(), end = fileList.constEnd(); it != end; ++it) {
1072 const KArchiveFile *f = *it;
1073 qint64 pos = f->position();
1074 if (!f->copyTo(dest: fileToDir[pos])) {
1075 return false;
1076 }
1077 }
1078 return true;
1079}
1080
1081void KArchive::virtual_hook(int, void *)
1082{
1083 /*BASE::virtual_hook( id, data )*/;
1084}
1085
1086void KArchiveEntry::virtual_hook(int, void *)
1087{
1088 /*BASE::virtual_hook( id, data );*/
1089}
1090
1091void KArchiveFile::virtual_hook(int id, void *data)
1092{
1093 KArchiveEntry::virtual_hook(id, data);
1094}
1095
1096void KArchiveDirectory::virtual_hook(int id, void *data)
1097{
1098 KArchiveEntry::virtual_hook(id, data);
1099}
1100

source code of karchive/src/karchive.cpp