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