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 | |
49 | class KArchiveDirectoryPrivate |
50 | { |
51 | public: |
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 | |
115 | KArchive::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 | |
126 | KArchive::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 | |
135 | KArchive::~KArchive() |
136 | { |
137 | Q_ASSERT(!isOpen()); // the derived class destructor must have closed already |
138 | delete d; |
139 | } |
140 | |
141 | bool 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 | |
174 | bool 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 | |
214 | bool 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 | |
251 | QString KArchive::errorString() const |
252 | { |
253 | return d->errorStr; |
254 | } |
255 | |
256 | const 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 | |
262 | bool 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 | |
348 | bool 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 | |
383 | bool 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 | |
412 | bool KArchive::writeData(const char *data, qint64 size) |
413 | { |
414 | return doWriteData(data, size); |
415 | } |
416 | |
417 | bool KArchive::writeData(QByteArrayView data) |
418 | { |
419 | return doWriteData(data: data.constData(), size: data.size()); |
420 | } |
421 | |
422 | bool 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 | |
438 | bool 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 | |
449 | bool 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 | |
461 | bool 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 | |
477 | bool KArchive::finishWriting(qint64 size) |
478 | { |
479 | return doFinishWriting(size); |
480 | } |
481 | |
482 | void KArchive::setErrorString(const QString &errorStr) |
483 | { |
484 | d->errorStr = errorStr; |
485 | } |
486 | |
487 | static 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 | |
505 | static 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 | |
517 | KArchiveDirectory *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 | |
529 | KArchiveDirectory *KArchive::findOrCreate(const QString &path) |
530 | { |
531 | return d->findOrCreate(path, recursionCounter: 0 /*recursionCounter*/); |
532 | } |
533 | |
534 | KArchiveDirectory *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 | |
609 | void KArchive::setDevice(QIODevice *dev) |
610 | { |
611 | if (d->deviceOwned) { |
612 | delete d->dev; |
613 | } |
614 | d->dev = dev; |
615 | d->deviceOwned = false; |
616 | } |
617 | |
618 | void 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 | |
625 | QIODevice::OpenMode KArchive::mode() const |
626 | { |
627 | return d->mode; |
628 | } |
629 | |
630 | QIODevice *KArchive::device() const |
631 | { |
632 | return d->dev; |
633 | } |
634 | |
635 | bool KArchive::isOpen() const |
636 | { |
637 | return d->mode != QIODevice::NotOpen; |
638 | } |
639 | |
640 | QString KArchive::fileName() const |
641 | { |
642 | return d->fileName; |
643 | } |
644 | |
645 | void 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 |
655 | QDateTime 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 | |
667 | class KArchiveEntryPrivate |
668 | { |
669 | public: |
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 | |
695 | KArchiveEntry::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 | |
706 | KArchiveEntry::~KArchiveEntry() |
707 | { |
708 | delete d; |
709 | } |
710 | |
711 | QDateTime KArchiveEntry::date() const |
712 | { |
713 | return d->date; |
714 | } |
715 | |
716 | QString KArchiveEntry::name() const |
717 | { |
718 | return d->name; |
719 | } |
720 | |
721 | mode_t KArchiveEntry::permissions() const |
722 | { |
723 | return d->access; |
724 | } |
725 | |
726 | QString KArchiveEntry::user() const |
727 | { |
728 | return d->user; |
729 | } |
730 | |
731 | QString KArchiveEntry::group() const |
732 | { |
733 | return d->group; |
734 | } |
735 | |
736 | QString KArchiveEntry::symLinkTarget() const |
737 | { |
738 | return d->symlink; |
739 | } |
740 | |
741 | bool KArchiveEntry::isFile() const |
742 | { |
743 | return false; |
744 | } |
745 | |
746 | bool KArchiveEntry::isDirectory() const |
747 | { |
748 | return false; |
749 | } |
750 | |
751 | KArchive *KArchiveEntry::archive() const |
752 | { |
753 | return d->archive; |
754 | } |
755 | |
756 | //////////////////////////////////////////////////////////////////////// |
757 | /////////////////////// KArchiveFile /////////////////////////////////// |
758 | //////////////////////////////////////////////////////////////////////// |
759 | |
760 | class KArchiveFilePrivate |
761 | { |
762 | public: |
763 | KArchiveFilePrivate(qint64 _pos, qint64 _size) |
764 | : pos(_pos) |
765 | , size(_size) |
766 | { |
767 | } |
768 | qint64 pos; |
769 | qint64 size; |
770 | }; |
771 | |
772 | KArchiveFile::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 | |
786 | KArchiveFile::~KArchiveFile() |
787 | { |
788 | delete d; |
789 | } |
790 | |
791 | qint64 KArchiveFile::position() const |
792 | { |
793 | return d->pos; |
794 | } |
795 | |
796 | qint64 KArchiveFile::size() const |
797 | { |
798 | return d->size; |
799 | } |
800 | |
801 | void KArchiveFile::setSize(qint64 s) |
802 | { |
803 | d->size = s; |
804 | } |
805 | |
806 | QByteArray 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 | |
824 | QIODevice *KArchiveFile::createDevice() const |
825 | { |
826 | return new KLimitedIODevice(archive()->device(), d->pos, d->size); |
827 | } |
828 | |
829 | bool KArchiveFile::isFile() const |
830 | { |
831 | return true; |
832 | } |
833 | |
834 | static 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 | |
851 | bool 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 | |
888 | KArchiveDirectory::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 | |
900 | KArchiveDirectory::~KArchiveDirectory() |
901 | { |
902 | delete d; |
903 | } |
904 | |
905 | QStringList KArchiveDirectory::entries() const |
906 | { |
907 | return d->entries.keys(); |
908 | } |
909 | |
910 | const KArchiveEntry *KArchiveDirectory::entry(const QString &_name) const |
911 | { |
912 | KArchiveDirectory *dummy; |
913 | return d->entry(_name, containingDirectory: &dummy); |
914 | } |
915 | |
916 | const 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) |
926 | void KArchiveDirectory::addEntry(KArchiveEntry *entry) |
927 | { |
928 | (void)addEntryV2(entry); |
929 | } |
930 | #endif |
931 | |
932 | bool 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) |
944 | void KArchiveDirectory::removeEntry(KArchiveEntry *entry) |
945 | { |
946 | (void)removeEntryV2(entry); |
947 | } |
948 | #endif |
949 | |
950 | bool 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 | |
970 | bool KArchiveDirectory::isDirectory() const |
971 | { |
972 | return true; |
973 | } |
974 | |
975 | static bool sortByPosition(const KArchiveFile *file1, const KArchiveFile *file2) |
976 | { |
977 | return file1->position() < file2->position(); |
978 | } |
979 | |
980 | bool 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 | |
1057 | void KArchive::virtual_hook(int, void *) |
1058 | { |
1059 | /*BASE::virtual_hook( id, data )*/; |
1060 | } |
1061 | |
1062 | void KArchiveEntry::virtual_hook(int, void *) |
1063 | { |
1064 | /*BASE::virtual_hook( id, data );*/ |
1065 | } |
1066 | |
1067 | void KArchiveFile::virtual_hook(int id, void *data) |
1068 | { |
1069 | KArchiveEntry::virtual_hook(id, data); |
1070 | } |
1071 | |
1072 | void KArchiveDirectory::virtual_hook(int id, void *data) |
1073 | { |
1074 | KArchiveEntry::virtual_hook(id, data); |
1075 | } |
1076 | |