Coverage Report

Created: 2026-07-30 07:17

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