/src/karchive/src/karchive.cpp
Line | Count | Source (jump to first uncovered line) |
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 | 496k | : q(parent) |
54 | 496k | { |
55 | 496k | } |
56 | | |
57 | | ~KArchiveDirectoryPrivate() |
58 | 496k | { |
59 | 496k | qDeleteAll(entries); |
60 | 496k | } |
61 | | |
62 | | KArchiveDirectoryPrivate(const KArchiveDirectoryPrivate &) = delete; |
63 | | KArchiveDirectoryPrivate &operator=(const KArchiveDirectoryPrivate &) = delete; |
64 | | |
65 | | static KArchiveDirectoryPrivate *get(KArchiveDirectory *directory) |
66 | 524k | { |
67 | 524k | return directory->d; |
68 | 524k | } |
69 | | |
70 | | // Returns in containingDirectory the directory that actually contains the returned entry |
71 | | const KArchiveEntry *entry(const QString &_name, KArchiveDirectory **containingDirectory) const |
72 | 1.91M | { |
73 | 1.91M | *containingDirectory = q; |
74 | | |
75 | 1.91M | QString name = QDir::cleanPath(_name); |
76 | 1.91M | int pos = name.indexOf(QLatin1Char('/')); |
77 | 1.91M | if (pos == 0) { // absolute path (see also KArchive::findOrCreate) |
78 | 22.1k | if (name.length() > 1) { |
79 | 21.2k | name = name.mid(1); // remove leading slash |
80 | 21.2k | pos = name.indexOf(QLatin1Char('/')); // look again |
81 | 21.2k | } else { // "/" |
82 | 831 | return q; |
83 | 831 | } |
84 | 22.1k | } |
85 | | // trailing slash ? -> remove |
86 | 1.91M | if (pos != -1 && pos == name.length() - 1) { |
87 | 0 | name = name.left(pos); |
88 | 0 | pos = name.indexOf(QLatin1Char('/')); // look again |
89 | 0 | } |
90 | 1.91M | if (pos != -1) { |
91 | 1.48M | const QString left = name.left(pos); |
92 | 1.48M | const QString right = name.mid(pos + 1); |
93 | | |
94 | | // qCDebug(KArchiveLog) << "left=" << left << "right=" << right; |
95 | | |
96 | 1.48M | KArchiveEntry *e = entries.value(left); |
97 | 1.48M | if (!e || !e->isDirectory()) { |
98 | 332k | return nullptr; |
99 | 332k | } |
100 | 1.14M | *containingDirectory = static_cast<KArchiveDirectory *>(e); |
101 | 1.14M | return (*containingDirectory)->d->entry(right, containingDirectory); |
102 | 1.48M | } |
103 | | |
104 | 436k | return entries.value(name); |
105 | 1.91M | } |
106 | | |
107 | | KArchiveDirectory *q; |
108 | | QHash<QString, KArchiveEntry *> entries; |
109 | | }; |
110 | | |
111 | | //////////////////////////////////////////////////////////////////////// |
112 | | /////////////////////////// KArchive /////////////////////////////////// |
113 | | //////////////////////////////////////////////////////////////////////// |
114 | | |
115 | | KArchive::KArchive(const QString &fileName) |
116 | 0 | : d(new KArchivePrivate(this)) |
117 | 0 | { |
118 | 0 | if (fileName.isEmpty()) { |
119 | 0 | qCWarning(KArchiveLog) << "KArchive: No file name specified"; |
120 | 0 | } |
121 | 0 | d->fileName = fileName; |
122 | | // This constructor leaves the device set to 0. |
123 | | // This is for the use of QSaveFile, see open(). |
124 | 0 | } |
125 | | |
126 | | KArchive::KArchive(QIODevice *dev) |
127 | 156k | : d(new KArchivePrivate(this)) |
128 | 156k | { |
129 | 156k | if (!dev) { |
130 | 0 | qCWarning(KArchiveLog) << "KArchive: Null device specified"; |
131 | 0 | } |
132 | 156k | d->dev = dev; |
133 | 156k | } |
134 | | |
135 | | KArchive::~KArchive() |
136 | 156k | { |
137 | 156k | Q_ASSERT(!isOpen()); // the derived class destructor must have closed already |
138 | 156k | delete d; |
139 | 156k | } |
140 | | |
141 | | bool KArchive::open(QIODevice::OpenMode mode) |
142 | 156k | { |
143 | 156k | Q_ASSERT(mode != QIODevice::NotOpen); |
144 | | |
145 | 156k | if (isOpen()) { |
146 | 0 | close(); |
147 | 0 | } |
148 | | |
149 | 156k | if (!d->fileName.isEmpty()) { |
150 | 0 | Q_ASSERT(!d->dev); |
151 | 0 | if (!createDevice(mode)) { |
152 | 0 | return false; |
153 | 0 | } |
154 | 0 | } |
155 | | |
156 | 156k | if (!d->dev) { |
157 | 0 | setErrorString(tr("No filename or device was specified")); |
158 | 0 | return false; |
159 | 0 | } |
160 | | |
161 | 156k | if (!d->dev->isOpen() && !d->dev->open(mode)) { |
162 | 0 | setErrorString(tr("Could not open device in mode %1").arg(mode)); |
163 | 0 | return false; |
164 | 0 | } |
165 | | |
166 | 156k | d->mode = mode; |
167 | | |
168 | 156k | Q_ASSERT(!d->rootDir); |
169 | 156k | d->rootDir = nullptr; |
170 | | |
171 | 156k | return openArchive(mode); |
172 | 156k | } |
173 | | |
174 | | bool KArchive::createDevice(QIODevice::OpenMode mode) |
175 | 0 | { |
176 | 0 | switch (mode) { |
177 | 0 | case QIODevice::WriteOnly: |
178 | 0 | 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 | 0 | 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 | 0 | if (!d->saveFile->open(QIODevice::WriteOnly)) { |
189 | 0 | setErrorString(tr("QSaveFile creation for %1 failed: %2").arg(d->fileName, d->saveFile->errorString())); |
190 | |
|
191 | 0 | delete d->saveFile; |
192 | 0 | d->saveFile = nullptr; |
193 | 0 | return false; |
194 | 0 | } |
195 | 0 | d->dev = d->saveFile; |
196 | 0 | Q_ASSERT(d->dev); |
197 | 0 | } |
198 | 0 | break; |
199 | 0 | case QIODevice::ReadOnly: |
200 | 0 | case QIODevice::ReadWrite: |
201 | | // ReadWrite mode still uses QFile for now; we'd need to copy to the tempfile, in fact. |
202 | 0 | if (!d->fileName.isEmpty()) { |
203 | 0 | d->dev = new QFile(d->fileName); |
204 | 0 | d->deviceOwned = true; |
205 | 0 | } |
206 | 0 | break; // continued below |
207 | 0 | default: |
208 | 0 | setErrorString(tr("Unsupported mode %1").arg(d->mode)); |
209 | 0 | return false; |
210 | 0 | } |
211 | 0 | return true; |
212 | 0 | } |
213 | | |
214 | | bool KArchive::close() |
215 | 156k | { |
216 | 156k | if (!isOpen()) { |
217 | 0 | setErrorString(tr("Archive already closed")); |
218 | 0 | return false; // already closed (return false or true? arguable...) |
219 | 0 | } |
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 | 156k | bool closeSucceeded = true; |
225 | 156k | if (d->dev) { |
226 | 156k | closeSucceeded = closeArchive(); |
227 | 156k | if (d->mode == QIODevice::WriteOnly && !closeSucceeded) { |
228 | 0 | d->abortWriting(); |
229 | 0 | } |
230 | 156k | } |
231 | | |
232 | 156k | if (d->dev && d->dev != d->saveFile) { |
233 | 156k | d->dev->close(); |
234 | 156k | } |
235 | | |
236 | | // if d->saveFile is not null then it is equal to d->dev. |
237 | 156k | if (d->saveFile) { |
238 | 0 | closeSucceeded = d->saveFile->commit(); |
239 | 0 | delete d->saveFile; |
240 | 0 | d->saveFile = nullptr; |
241 | 0 | } |
242 | 156k | if (d->deviceOwned) { |
243 | 0 | delete d->dev; // we created it ourselves in open() |
244 | 0 | } |
245 | | |
246 | 156k | delete d->rootDir; |
247 | 156k | d->rootDir = nullptr; |
248 | 156k | d->mode = QIODevice::NotOpen; |
249 | 156k | d->dev = nullptr; |
250 | 156k | return closeSucceeded; |
251 | 156k | } |
252 | | |
253 | | QString KArchive::errorString() const |
254 | 0 | { |
255 | 0 | return d->errorStr; |
256 | 0 | } |
257 | | |
258 | | const KArchiveDirectory *KArchive::directory() const |
259 | 82.3k | { |
260 | | // rootDir isn't const so that parsing-on-demand is possible |
261 | 82.3k | return const_cast<KArchive *>(this)->rootDir(); |
262 | 82.3k | } |
263 | | |
264 | | bool KArchive::addLocalFile(const QString &fileName, const QString &destName) |
265 | 0 | { |
266 | 0 | QFileInfo fileInfo(fileName); |
267 | 0 | if (!fileInfo.isFile() && !fileInfo.isSymLink()) { |
268 | 0 | setErrorString(tr("%1 doesn't exist or is not a regular file.").arg(fileName)); |
269 | 0 | return false; |
270 | 0 | } |
271 | | |
272 | 0 | QT_STATBUF fi; |
273 | 0 | if (STAT_METHOD(QFile::encodeName(fileName).constData(), &fi) == -1) { |
274 | 0 | setErrorString(tr("Failed accessing the file %1 for adding to the archive. The error was: %2").arg(fileName).arg(QLatin1String{strerror(errno)})); |
275 | 0 | return false; |
276 | 0 | } |
277 | | |
278 | 0 | if (fileInfo.isSymLink()) { |
279 | 0 | 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 | 0 | #if defined(Q_OS_UNIX) && !defined(Q_OS_OS2EMX) |
283 | 0 | const QByteArray encodedFileName = QFile::encodeName(fileName); |
284 | 0 | QByteArray s; |
285 | 0 | #if defined(PATH_MAX) |
286 | 0 | 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 | 0 | int len = readlink(encodedFileName.data(), s.data(), s.size() - 1); |
295 | 0 | if (len >= 0) { |
296 | 0 | s[len] = '\0'; |
297 | 0 | symLinkTarget = QFile::decodeName(s.constData()); |
298 | 0 | } |
299 | 0 | #endif |
300 | 0 | if (symLinkTarget.isEmpty()) { // Mac or Windows |
301 | 0 | symLinkTarget = fileInfo.symLinkTarget(); |
302 | 0 | } |
303 | 0 | return writeSymLink(destName, |
304 | 0 | symLinkTarget, |
305 | 0 | fileInfo.owner(), |
306 | 0 | fileInfo.group(), |
307 | 0 | fi.st_mode, |
308 | 0 | fileInfo.lastRead(), |
309 | 0 | fileInfo.lastModified(), |
310 | 0 | fileInfo.birthTime()); |
311 | 0 | } /*end if*/ |
312 | | |
313 | 0 | 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 | 0 | QFile file(fileName); |
319 | 0 | if (!file.open(QIODevice::ReadOnly)) { |
320 | 0 | setErrorString(tr("Couldn't open file %1: %2").arg(fileName, file.errorString())); |
321 | 0 | return false; |
322 | 0 | } |
323 | | |
324 | 0 | if (!prepareWriting(destName, fileInfo.owner(), fileInfo.group(), size, fi.st_mode, fileInfo.lastRead(), fileInfo.lastModified(), fileInfo.birthTime())) { |
325 | | // qCWarning(KArchiveLog) << " prepareWriting" << destName << "failed"; |
326 | 0 | return false; |
327 | 0 | } |
328 | | |
329 | | // Read and write data in chunks to minimize memory usage |
330 | 0 | QByteArray array; |
331 | 0 | array.resize(int(qMin(qint64(1024 * 1024), size))); |
332 | 0 | qint64 n; |
333 | 0 | qint64 total = 0; |
334 | 0 | while ((n = file.read(array.data(), array.size())) > 0) { |
335 | 0 | if (!writeData(array.data(), n)) { |
336 | | // qCWarning(KArchiveLog) << "writeData failed"; |
337 | 0 | return false; |
338 | 0 | } |
339 | 0 | total += n; |
340 | 0 | } |
341 | 0 | Q_ASSERT(total == size); |
342 | |
|
343 | 0 | if (!finishWriting(size)) { |
344 | | // qCWarning(KArchiveLog) << "finishWriting failed"; |
345 | 0 | return false; |
346 | 0 | } |
347 | 0 | return true; |
348 | 0 | } |
349 | | |
350 | | bool KArchive::addLocalDirectory(const QString &path, const QString &destName) |
351 | 0 | { |
352 | 0 | QDir dir(path); |
353 | 0 | if (!dir.exists()) { |
354 | 0 | setErrorString(tr("Directory %1 does not exist").arg(path)); |
355 | 0 | return false; |
356 | 0 | } |
357 | 0 | dir.setFilter(dir.filter() | QDir::Hidden); |
358 | 0 | const QStringList files = dir.entryList(); |
359 | 0 | for (const QString &file : files) { |
360 | 0 | if (file != QLatin1String(".") && file != QLatin1String("..")) { |
361 | 0 | const QString fileName = path + QLatin1Char('/') + file; |
362 | | // qCDebug(KArchiveLog) << "storing " << fileName; |
363 | 0 | const QString dest = destName.isEmpty() ? file : (destName + QLatin1Char('/') + file); |
364 | 0 | QFileInfo fileInfo(fileName); |
365 | |
|
366 | 0 | if (fileInfo.isFile() || fileInfo.isSymLink()) { |
367 | 0 | addLocalFile(fileName, dest); |
368 | 0 | } else if (fileInfo.isDir()) { |
369 | | // Write directory, so that empty dirs are preserved (and permissions written out, etc.) |
370 | 0 | int perms = 0; |
371 | 0 | QT_STATBUF fi; |
372 | 0 | if (STAT_METHOD(QFile::encodeName(fileName).constData(), &fi) != -1) { |
373 | 0 | perms = fi.st_mode; |
374 | 0 | } |
375 | 0 | writeDir(dest, fileInfo.owner(), fileInfo.group(), perms, fileInfo.lastRead(), fileInfo.lastModified(), fileInfo.birthTime()); |
376 | | // Recurse |
377 | 0 | addLocalDirectory(fileName, dest); |
378 | 0 | } |
379 | | // We omit sockets |
380 | 0 | } |
381 | 0 | } |
382 | 0 | return true; |
383 | 0 | } |
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 | 0 | { |
394 | 0 | const qint64 size = data.size(); |
395 | 0 | if (!prepareWriting(name, user, group, size, perm, atime, mtime, ctime)) { |
396 | | // qCWarning(KArchiveLog) << "prepareWriting failed"; |
397 | 0 | return false; |
398 | 0 | } |
399 | | |
400 | | // Write data |
401 | | // Note: if data is null, don't call write, it would terminate the KCompressionDevice |
402 | 0 | if (data.constData() && size && !writeData(data.constData(), size)) { |
403 | | // qCWarning(KArchiveLog) << "writeData failed"; |
404 | 0 | return false; |
405 | 0 | } |
406 | | |
407 | 0 | if (!finishWriting(size)) { |
408 | | // qCWarning(KArchiveLog) << "finishWriting failed"; |
409 | 0 | return false; |
410 | 0 | } |
411 | 0 | return true; |
412 | 0 | } |
413 | | |
414 | | bool KArchive::writeData(const char *data, qint64 size) |
415 | 0 | { |
416 | 0 | return doWriteData(data, size); |
417 | 0 | } |
418 | | |
419 | | bool KArchive::writeData(QByteArrayView data) |
420 | 0 | { |
421 | 0 | return doWriteData(data.constData(), data.size()); |
422 | 0 | } |
423 | | |
424 | | bool KArchive::doWriteData(const char *data, qint64 size) |
425 | 0 | { |
426 | 0 | bool ok = device()->write(data, size) == size; |
427 | 0 | if (!ok) { |
428 | 0 | setErrorString(tr("Writing failed: %1").arg(device()->errorString())); |
429 | 0 | d->abortWriting(); |
430 | 0 | } |
431 | 0 | return ok; |
432 | 0 | } |
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 | 0 | { |
448 | 0 | return doWriteDir(name, user, group, perm | 040000, atime, mtime, ctime); |
449 | 0 | } |
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 | 0 | { |
460 | 0 | return doWriteSymLink(name, target, user, group, perm, atime, mtime, ctime); |
461 | 0 | } |
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 | 0 | { |
472 | 0 | bool ok = doPrepareWriting(name, user, group, size, perm, atime, mtime, ctime); |
473 | 0 | if (!ok) { |
474 | 0 | d->abortWriting(); |
475 | 0 | } |
476 | 0 | return ok; |
477 | 0 | } |
478 | | |
479 | | bool KArchive::finishWriting(qint64 size) |
480 | 0 | { |
481 | 0 | return doFinishWriting(size); |
482 | 0 | } |
483 | | |
484 | | void KArchive::setErrorString(const QString &errorStr) |
485 | 74.4k | { |
486 | 74.4k | d->errorStr = errorStr; |
487 | 74.4k | } |
488 | | |
489 | | static QString getCurrentUserName() |
490 | 89.0k | { |
491 | 89.0k | #if defined(Q_OS_UNIX) |
492 | 89.0k | struct passwd *pw = getpwuid(getuid()); |
493 | 89.0k | return pw ? QFile::decodeName(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 | 89.0k | } |
506 | | |
507 | | static QString getCurrentGroupName() |
508 | 89.0k | { |
509 | 89.0k | #if defined(Q_OS_UNIX) |
510 | 89.0k | struct group *grp = getgrgid(getgid()); |
511 | 89.0k | return grp ? QFile::decodeName(grp->gr_name) : QString::number(getgid()); |
512 | | #elif defined(Q_OS_WIN) |
513 | | return QString(); |
514 | | #else |
515 | | return QString(); |
516 | | #endif |
517 | 89.0k | } |
518 | | |
519 | | KArchiveDirectory *KArchive::rootDir() |
520 | 685k | { |
521 | 685k | if (!d->rootDir) { |
522 | | // qCDebug(KArchiveLog) << "Making root dir "; |
523 | 89.0k | QString username = ::getCurrentUserName(); |
524 | 89.0k | QString groupname = ::getCurrentGroupName(); |
525 | | |
526 | 89.0k | d->rootDir = new KArchiveDirectory(this, QStringLiteral("/"), int(0777 + S_IFDIR), QDateTime(), username, groupname, QString()); |
527 | 89.0k | } |
528 | 685k | return d->rootDir; |
529 | 685k | } |
530 | | |
531 | | KArchiveDirectory *KArchive::findOrCreate(const QString &path) |
532 | 172k | { |
533 | 172k | return d->findOrCreate(path, 0 /*recursionCounter*/); |
534 | 172k | } |
535 | | |
536 | | KArchiveDirectory *KArchivePrivate::findOrCreate(const QString &path, int recursionCounter) |
537 | 529k | { |
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 | 529k | if (recursionCounter > 2500) { |
543 | 1 | qCWarning(KArchiveLog) << "path recursion limit exceeded, bailing out"; |
544 | 1 | return nullptr; |
545 | 1 | } |
546 | | // qCDebug(KArchiveLog) << path; |
547 | 529k | if (path.isEmpty() || path == QLatin1String("/") || path == QLatin1String(".")) { // root dir => found |
548 | | // qCDebug(KArchiveLog) << "returning rootdir"; |
549 | 5.13k | return q->rootDir(); |
550 | 5.13k | } |
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 | 524k | KArchiveDirectory *existingEntryParentDirectory; |
559 | 524k | const KArchiveEntry *existingEntry = KArchiveDirectoryPrivate::get(q->rootDir())->entry(path, &existingEntryParentDirectory); |
560 | 524k | if (existingEntry) { |
561 | 130k | if (existingEntry->isDirectory()) |
562 | | // qCDebug(KArchiveLog) << "found it"; |
563 | 129k | { |
564 | 129k | const KArchiveDirectory *dir = static_cast<const KArchiveDirectory *>(existingEntry); |
565 | 129k | return const_cast<KArchiveDirectory *>(dir); |
566 | 129k | } else { |
567 | 1.59k | const KArchiveFile *file = static_cast<const KArchiveFile *>(existingEntry); |
568 | 1.59k | if (file->size() > 0) { |
569 | 51 | qCWarning(KArchiveLog) << path << "is normal file, but there are file paths in the archive assuming it is a directory, bailing out"; |
570 | 51 | return nullptr; |
571 | 51 | } |
572 | | |
573 | 1.54k | qCDebug(KArchiveLog) << path << " is an empty file, assuming it is actually a directory and replacing"; |
574 | 1.54k | KArchiveEntry *myEntry = const_cast<KArchiveEntry *>(existingEntry); |
575 | 1.54k | existingEntryParentDirectory->removeEntry(myEntry); |
576 | 1.54k | delete myEntry; |
577 | 1.54k | } |
578 | 130k | } |
579 | | |
580 | | // Otherwise go up and try again |
581 | 395k | int pos = path.lastIndexOf(QLatin1Char('/')); |
582 | 395k | KArchiveDirectory *parent; |
583 | 395k | QString dirname; |
584 | 395k | if (pos == -1) { // no more slash => create in root dir |
585 | 38.1k | parent = q->rootDir(); |
586 | 38.1k | dirname = path; |
587 | 357k | } else { |
588 | 357k | QString left = path.left(pos); |
589 | 357k | dirname = path.mid(pos + 1); |
590 | 357k | parent = findOrCreate(left, recursionCounter + 1); // recursive call... until we find an existing dir. |
591 | 357k | } |
592 | | |
593 | 395k | if (!parent) { |
594 | 3.96k | return nullptr; |
595 | 3.96k | } |
596 | | |
597 | | // qCDebug(KArchiveLog) << "found parent " << parent->name() << " adding " << dirname << " to ensure " << path; |
598 | | // Found -> add the missing piece |
599 | 391k | KArchiveDirectory *e = new KArchiveDirectory(q, dirname, rootDir->permissions(), rootDir->date(), rootDir->user(), rootDir->group(), QString()); |
600 | 391k | if (parent->addEntryV2(e)) { |
601 | 391k | return e; // now a directory to <path> exists |
602 | 391k | } else { |
603 | 0 | return nullptr; |
604 | 0 | } |
605 | 391k | } |
606 | | |
607 | | void KArchive::setDevice(QIODevice *dev) |
608 | 0 | { |
609 | 0 | if (d->deviceOwned) { |
610 | 0 | delete d->dev; |
611 | 0 | } |
612 | 0 | d->dev = dev; |
613 | 0 | d->deviceOwned = false; |
614 | 0 | } |
615 | | |
616 | | void KArchive::setRootDir(KArchiveDirectory *rootDir) |
617 | 12 | { |
618 | 12 | Q_ASSERT(!d->rootDir); // Call setRootDir only once during parsing please ;) |
619 | 12 | delete d->rootDir; // but if it happens, don't leak |
620 | 12 | d->rootDir = rootDir; |
621 | 12 | } |
622 | | |
623 | | QIODevice::OpenMode KArchive::mode() const |
624 | 39.2k | { |
625 | 39.2k | return d->mode; |
626 | 39.2k | } |
627 | | |
628 | | QIODevice *KArchive::device() const |
629 | 494k | { |
630 | 494k | return d->dev; |
631 | 494k | } |
632 | | |
633 | | bool KArchive::isOpen() const |
634 | 647k | { |
635 | 647k | return d->mode != QIODevice::NotOpen; |
636 | 647k | } |
637 | | |
638 | | QString KArchive::fileName() const |
639 | 98.0k | { |
640 | 98.0k | return d->fileName; |
641 | 98.0k | } |
642 | | |
643 | | void KArchivePrivate::abortWriting() |
644 | 0 | { |
645 | 0 | if (saveFile) { |
646 | 0 | saveFile->cancelWriting(); |
647 | 0 | delete saveFile; |
648 | 0 | saveFile = nullptr; |
649 | 0 | dev = nullptr; |
650 | 0 | } |
651 | 0 | } |
652 | | |
653 | | // this is a hacky wrapper to check if time_t value is invalid |
654 | | QDateTime KArchivePrivate::time_tToDateTime(uint time_t) |
655 | 191k | { |
656 | 191k | if (time_t == uint(-1)) { |
657 | 209 | return QDateTime(); |
658 | 209 | } |
659 | 191k | return QDateTime::fromSecsSinceEpoch(time_t); |
660 | 191k | } |
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 | 671k | : name(_name) |
677 | 671k | , date(_date) |
678 | 671k | , access(_access) |
679 | 671k | , user(_user) |
680 | 671k | , group(_group) |
681 | 671k | , symlink(_symlink) |
682 | 671k | , archive(_archive) |
683 | 671k | { |
684 | 671k | } |
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 | 671k | : d(new KArchiveEntryPrivate(t, name, access, date, user, group, symlink)) |
702 | 671k | { |
703 | 671k | } |
704 | | |
705 | | KArchiveEntry::~KArchiveEntry() |
706 | 671k | { |
707 | 671k | delete d; |
708 | 671k | } |
709 | | |
710 | | QDateTime KArchiveEntry::date() const |
711 | 428k | { |
712 | 428k | return d->date; |
713 | 428k | } |
714 | | |
715 | | QString KArchiveEntry::name() const |
716 | 1.30M | { |
717 | 1.30M | return d->name; |
718 | 1.30M | } |
719 | | |
720 | | mode_t KArchiveEntry::permissions() const |
721 | 391k | { |
722 | 391k | return d->access; |
723 | 391k | } |
724 | | |
725 | | QString KArchiveEntry::user() const |
726 | 437k | { |
727 | 437k | return d->user; |
728 | 437k | } |
729 | | |
730 | | QString KArchiveEntry::group() const |
731 | 437k | { |
732 | 437k | return d->group; |
733 | 437k | } |
734 | | |
735 | | QString KArchiveEntry::symLinkTarget() const |
736 | 0 | { |
737 | 0 | return d->symlink; |
738 | 0 | } |
739 | | |
740 | | bool KArchiveEntry::isFile() const |
741 | 206k | { |
742 | 206k | return false; |
743 | 206k | } |
744 | | |
745 | | bool KArchiveEntry::isDirectory() const |
746 | 5.81k | { |
747 | 5.81k | return false; |
748 | 5.81k | } |
749 | | |
750 | | KArchive *KArchiveEntry::archive() const |
751 | 37.0k | { |
752 | 37.0k | return d->archive; |
753 | 37.0k | } |
754 | | |
755 | | //////////////////////////////////////////////////////////////////////// |
756 | | /////////////////////// KArchiveFile /////////////////////////////////// |
757 | | //////////////////////////////////////////////////////////////////////// |
758 | | |
759 | | class KArchiveFilePrivate |
760 | | { |
761 | | public: |
762 | | KArchiveFilePrivate(qint64 _pos, qint64 _size) |
763 | 175k | : pos(_pos) |
764 | 175k | , size(_size) |
765 | 175k | { |
766 | 175k | } |
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 | 175k | : KArchiveEntry(t, name, access, date, user, group, symlink) |
781 | 175k | , d(new KArchiveFilePrivate(pos, size)) |
782 | 175k | { |
783 | 175k | } |
784 | | |
785 | | KArchiveFile::~KArchiveFile() |
786 | 175k | { |
787 | 175k | delete d; |
788 | 175k | } |
789 | | |
790 | | qint64 KArchiveFile::position() const |
791 | 1.00k | { |
792 | 1.00k | return d->pos; |
793 | 1.00k | } |
794 | | |
795 | | qint64 KArchiveFile::size() const |
796 | 38.5k | { |
797 | 38.5k | return d->size; |
798 | 38.5k | } |
799 | | |
800 | | void KArchiveFile::setSize(qint64 s) |
801 | 0 | { |
802 | 0 | d->size = s; |
803 | 0 | } |
804 | | |
805 | | QByteArray KArchiveFile::data() const |
806 | 35.8k | { |
807 | 35.8k | bool ok = archive()->device()->seek(d->pos); |
808 | 35.8k | if (!ok) { |
809 | | // qCWarning(KArchiveLog) << "Failed to sync to" << d->pos << "to read" << name(); |
810 | 89 | } |
811 | | |
812 | | // Read content |
813 | 35.8k | QByteArray arr; |
814 | 35.8k | if (d->size) { |
815 | 325 | arr = archive()->device()->read(d->size); |
816 | 325 | Q_ASSERT(arr.size() == d->size); |
817 | 325 | } |
818 | 35.8k | return arr; |
819 | 35.8k | } |
820 | | |
821 | | QIODevice *KArchiveFile::createDevice() const |
822 | 0 | { |
823 | 0 | return new KLimitedIODevice(archive()->device(), d->pos, d->size); |
824 | 0 | } |
825 | | |
826 | | bool KArchiveFile::isFile() const |
827 | 36.8k | { |
828 | 36.8k | return true; |
829 | 36.8k | } |
830 | | |
831 | | static QFileDevice::Permissions withExecutablePerms(QFileDevice::Permissions filePerms, mode_t perms) |
832 | 0 | { |
833 | 0 | if (perms & 01) { |
834 | 0 | filePerms |= QFileDevice::ExeOther; |
835 | 0 | } |
836 | |
|
837 | 0 | if (perms & 010) { |
838 | 0 | filePerms |= QFileDevice::ExeGroup; |
839 | 0 | } |
840 | |
|
841 | 0 | if (perms & 0100) { |
842 | 0 | filePerms |= QFileDevice::ExeOwner; |
843 | 0 | } |
844 | |
|
845 | 0 | return filePerms; |
846 | 0 | } |
847 | | |
848 | | bool KArchiveFile::copyTo(const QString &dest) const |
849 | 0 | { |
850 | 0 | QFile f(dest + QLatin1Char('/') + name()); |
851 | 0 | if (f.open(QIODevice::ReadWrite | QIODevice::Truncate)) { |
852 | 0 | QIODevice *inputDev = createDevice(); |
853 | 0 | if (!inputDev) { |
854 | 0 | f.remove(); |
855 | 0 | return false; |
856 | 0 | } |
857 | | |
858 | | // Read and write data in chunks to minimize memory usage |
859 | 0 | const qint64 chunkSize = 1024 * 1024; |
860 | 0 | qint64 remainingSize = d->size; |
861 | 0 | QByteArray array; |
862 | 0 | array.resize(int(qMin(chunkSize, remainingSize))); |
863 | |
|
864 | 0 | while (remainingSize > 0) { |
865 | 0 | const qint64 currentChunkSize = qMin(chunkSize, remainingSize); |
866 | 0 | const qint64 n = inputDev->read(array.data(), currentChunkSize); |
867 | | Q_UNUSED(n) // except in Q_ASSERT |
868 | 0 | Q_ASSERT(n == currentChunkSize); |
869 | 0 | f.write(array.data(), currentChunkSize); |
870 | 0 | remainingSize -= currentChunkSize; |
871 | 0 | } |
872 | 0 | f.setPermissions(withExecutablePerms(f.permissions(), permissions())); |
873 | 0 | f.close(); |
874 | |
|
875 | 0 | delete inputDev; |
876 | 0 | return true; |
877 | 0 | } |
878 | 0 | return false; |
879 | 0 | } |
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 | 496k | : KArchiveEntry(t, name, access, date, user, group, symlink) |
893 | 496k | , d(new KArchiveDirectoryPrivate(this)) |
894 | 496k | { |
895 | 496k | } |
896 | | |
897 | | KArchiveDirectory::~KArchiveDirectory() |
898 | 496k | { |
899 | 496k | delete d; |
900 | 496k | } |
901 | | |
902 | | QStringList KArchiveDirectory::entries() const |
903 | 289k | { |
904 | 289k | return d->entries.keys(); |
905 | 289k | } |
906 | | |
907 | | const KArchiveEntry *KArchiveDirectory::entry(const QString &_name) const |
908 | 245k | { |
909 | 245k | KArchiveDirectory *dummy; |
910 | 245k | return d->entry(_name, &dummy); |
911 | 245k | } |
912 | | |
913 | | const KArchiveFile *KArchiveDirectory::file(const QString &name) const |
914 | 0 | { |
915 | 0 | const KArchiveEntry *e = entry(name); |
916 | 0 | if (e && e->isFile()) { |
917 | 0 | return static_cast<const KArchiveFile *>(e); |
918 | 0 | } |
919 | 0 | return nullptr; |
920 | 0 | } |
921 | | |
922 | | void KArchiveDirectory::addEntry(KArchiveEntry *entry) |
923 | 187k | { |
924 | 187k | addEntryV2(entry); |
925 | 187k | } |
926 | | |
927 | | bool KArchiveDirectory::addEntryV2(KArchiveEntry *entry) |
928 | 579k | { |
929 | 579k | if (d->entries.value(entry->name())) { |
930 | 108k | qCWarning(KArchiveLog) << "directory " << name() << "has entry" << entry->name() << "already"; |
931 | 108k | delete entry; |
932 | 108k | return false; |
933 | 108k | } |
934 | 470k | d->entries.insert(entry->name(), entry); |
935 | 470k | return true; |
936 | 579k | } |
937 | | |
938 | | void KArchiveDirectory::removeEntry(KArchiveEntry *entry) |
939 | 1.54k | { |
940 | 1.54k | if (!entry) { |
941 | 0 | return; |
942 | 0 | } |
943 | | |
944 | 1.54k | QHash<QString, KArchiveEntry *>::Iterator it = d->entries.find(entry->name()); |
945 | | // nothing removed? |
946 | 1.54k | if (it == d->entries.end()) { |
947 | 0 | qCWarning(KArchiveLog) << "directory " << name() << "has no entry with name " << entry->name(); |
948 | 0 | return; |
949 | 0 | } |
950 | 1.54k | if (it.value() != entry) { |
951 | 0 | qCWarning(KArchiveLog) << "directory " << name() << "has another entry for name " << entry->name(); |
952 | 0 | return; |
953 | 0 | } |
954 | 1.54k | d->entries.erase(it); |
955 | 1.54k | } |
956 | | |
957 | | bool KArchiveDirectory::isDirectory() const |
958 | 1.48M | { |
959 | 1.48M | return true; |
960 | 1.48M | } |
961 | | |
962 | | static bool sortByPosition(const KArchiveFile *file1, const KArchiveFile *file2) |
963 | 0 | { |
964 | 0 | return file1->position() < file2->position(); |
965 | 0 | } |
966 | | |
967 | | bool KArchiveDirectory::copyTo(const QString &dest, bool recursiveCopy) const |
968 | 0 | { |
969 | 0 | QDir root; |
970 | 0 | const QString destDir(QDir(dest).absolutePath()); // get directory path without any "." or ".." |
971 | |
|
972 | 0 | QList<const KArchiveFile *> fileList; |
973 | 0 | QMap<qint64, QString> fileToDir; |
974 | | |
975 | | // placeholders for iterated items |
976 | 0 | QStack<const KArchiveDirectory *> dirStack; |
977 | 0 | QStack<QString> dirNameStack; |
978 | |
|
979 | 0 | dirStack.push(this); // init stack at current directory |
980 | 0 | dirNameStack.push(destDir); // ... with given path |
981 | 0 | do { |
982 | 0 | 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 | 0 | QString curDirName = dirNameStack.pop(); |
987 | 0 | if (!QDir(curDirName).absolutePath().startsWith(destDir)) { |
988 | 0 | qCWarning(KArchiveLog) << "Attempted export into folder" << curDirName << "which is outside of the extraction root folder" << destDir << "." |
989 | 0 | << "Changing export of contained files to extraction root folder."; |
990 | 0 | curDirName = destDir; |
991 | 0 | } |
992 | |
|
993 | 0 | if (!root.mkpath(curDirName)) { |
994 | 0 | return false; |
995 | 0 | } |
996 | | |
997 | 0 | const QStringList dirEntries = curDir->entries(); |
998 | 0 | for (QStringList::const_iterator it = dirEntries.begin(); it != dirEntries.end(); ++it) { |
999 | 0 | const KArchiveEntry *curEntry = curDir->entry(*it); |
1000 | 0 | if (!curEntry->symLinkTarget().isEmpty()) { |
1001 | 0 | 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 | 0 | QFile symLinkTarget(curEntry->symLinkTarget()); |
1009 | 0 | if (!symLinkTarget.link(linkName)) { |
1010 | | // qCDebug(KArchiveLog) << "symlink(" << curEntry->symLinkTarget() << ',' << linkName << ") failed:" << strerror(errno); |
1011 | 0 | } |
1012 | 0 | } else { |
1013 | 0 | if (curEntry->isFile()) { |
1014 | 0 | const KArchiveFile *curFile = dynamic_cast<const KArchiveFile *>(curEntry); |
1015 | 0 | if (curFile) { |
1016 | 0 | fileList.append(curFile); |
1017 | 0 | fileToDir.insert(curFile->position(), curDirName); |
1018 | 0 | } |
1019 | 0 | } |
1020 | |
|
1021 | 0 | if (curEntry->isDirectory() && recursiveCopy) { |
1022 | 0 | const KArchiveDirectory *ad = dynamic_cast<const KArchiveDirectory *>(curEntry); |
1023 | 0 | if (ad) { |
1024 | 0 | dirStack.push(ad); |
1025 | 0 | dirNameStack.push(curDirName + QLatin1Char('/') + curEntry->name()); |
1026 | 0 | } |
1027 | 0 | } |
1028 | 0 | } |
1029 | 0 | } |
1030 | 0 | } while (!dirStack.isEmpty()); |
1031 | | |
1032 | 0 | std::sort(fileList.begin(), fileList.end(), sortByPosition); // sort on d->pos, so we have a linear access |
1033 | |
|
1034 | 0 | for (QList<const KArchiveFile *>::const_iterator it = fileList.constBegin(), end = fileList.constEnd(); it != end; ++it) { |
1035 | 0 | const KArchiveFile *f = *it; |
1036 | 0 | qint64 pos = f->position(); |
1037 | 0 | if (!f->copyTo(fileToDir[pos])) { |
1038 | 0 | return false; |
1039 | 0 | } |
1040 | 0 | } |
1041 | 0 | return true; |
1042 | 0 | } |
1043 | | |
1044 | | void KArchive::virtual_hook(int, void *) |
1045 | 0 | { |
1046 | 0 | /*BASE::virtual_hook( id, data )*/; |
1047 | 0 | } |
1048 | | |
1049 | | void KArchiveEntry::virtual_hook(int, void *) |
1050 | 0 | { |
1051 | | /*BASE::virtual_hook( id, data );*/ |
1052 | 0 | } |
1053 | | |
1054 | | void KArchiveFile::virtual_hook(int id, void *data) |
1055 | 0 | { |
1056 | 0 | KArchiveEntry::virtual_hook(id, data); |
1057 | 0 | } |
1058 | | |
1059 | | void KArchiveDirectory::virtual_hook(int id, void *data) |
1060 | 0 | { |
1061 | 0 | KArchiveEntry::virtual_hook(id, data); |
1062 | 0 | } |