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