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

source code of karchive/src/karchive.cpp