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

source code of karchive/src/karchive.cpp