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