1 /*
2 SPDX-FileCopyrightText: 2007 Henrique Pinto <henrique.pinto@kdemail.net>
3 SPDX-FileCopyrightText: 2008-2009 Harald Hvaal <haraldhv@stud.ntnu.no>
4 SPDX-FileCopyrightText: 2010 Raphael Kubo da Costa <rakuco@FreeBSD.org>
5 SPDX-FileCopyrightText: 2016 Vladyslav Batyrenko <mvlabat@gmail.com>
6
7 SPDX-License-Identifier: BSD-2-Clause
8 */
9
10 #include "libarchiveplugin.h"
11 #include "ark_debug.h"
12 #include "queries.h"
13
14 #include <KLocalizedString>
15
16 #include <QThread>
17 #include <QFileInfo>
18 #include <QDir>
19
20 #include <archive_entry.h>
21
LibarchivePlugin(QObject * parent,const QVariantList & args)22 LibarchivePlugin::LibarchivePlugin(QObject *parent, const QVariantList &args)
23 : ReadWriteArchiveInterface(parent, args)
24 , m_archiveReadDisk(archive_read_disk_new())
25 , m_cachedArchiveEntryCount(0)
26 , m_emitNoEntries(false)
27 , m_extractedFilesSize(0)
28 {
29 qCDebug(ARK) << "Initializing libarchive plugin";
30 archive_read_disk_set_standard_lookup(m_archiveReadDisk.data());
31
32 connect(this, &ReadOnlyArchiveInterface::error, this, &LibarchivePlugin::slotRestoreWorkingDir);
33 connect(this, &ReadOnlyArchiveInterface::cancelled, this, &LibarchivePlugin::slotRestoreWorkingDir);
34 }
35
~LibarchivePlugin()36 LibarchivePlugin::~LibarchivePlugin()
37 {
38 for (const auto e : std::as_const(m_emittedEntries)) {
39 // Entries might be passed to pending slots, so we just schedule their deletion.
40 e->deleteLater();
41 }
42 }
43
list()44 bool LibarchivePlugin::list()
45 {
46 qCDebug(ARK) << "Listing archive contents";
47
48 if (!initializeReader()) {
49 return false;
50 }
51
52 qCDebug(ARK) << "Detected compression filter:" << archive_filter_name(m_archiveReader.data(), 0);
53 QString compMethod = convertCompressionName(QString::fromUtf8(archive_filter_name(m_archiveReader.data(), 0)));
54 if (!compMethod.isEmpty()) {
55 Q_EMIT compressionMethodFound(compMethod);
56 }
57
58 m_cachedArchiveEntryCount = 0;
59 m_extractedFilesSize = 0;
60 m_numberOfEntries = 0;
61 auto compressedArchiveSize = QFileInfo(filename()).size();
62
63 struct archive_entry *aentry;
64 int result = ARCHIVE_RETRY;
65
66 bool firstEntry = true;
67 while (!QThread::currentThread()->isInterruptionRequested() && (result = archive_read_next_header(m_archiveReader.data(), &aentry)) == ARCHIVE_OK) {
68
69 if (firstEntry) {
70 qCDebug(ARK) << "Detected format for first entry:" << archive_format_name(m_archiveReader.data());
71 firstEntry = false;
72 }
73
74 if (!m_emitNoEntries) {
75 emitEntryFromArchiveEntry(aentry);
76 }
77
78 m_extractedFilesSize += (qlonglong)archive_entry_size(aentry);
79
80 Q_EMIT progress(float(archive_filter_bytes(m_archiveReader.data(), -1))/float(compressedArchiveSize));
81
82 m_cachedArchiveEntryCount++;
83
84 // Skip the entry data.
85 int readSkipResult = archive_read_data_skip(m_archiveReader.data());
86 if (readSkipResult != ARCHIVE_OK) {
87 qCCritical(ARK) << "Error while skipping data for entry:"
88 << QString::fromWCharArray(archive_entry_pathname_w(aentry))
89 << readSkipResult
90 << QLatin1String(archive_error_string(m_archiveReader.data()));
91 if (!emitCorruptArchive()) {
92 return false;
93 }
94 }
95 }
96
97 if (QThread::currentThread()->isInterruptionRequested()) {
98 return false;
99 }
100
101 if (result != ARCHIVE_EOF) {
102 qCCritical(ARK) << "Error while reading archive:"
103 << result
104 << QLatin1String(archive_error_string(m_archiveReader.data()));
105 if (!emitCorruptArchive()) {
106 return false;
107 }
108 }
109
110 return archive_read_close(m_archiveReader.data()) == ARCHIVE_OK;
111 }
112
emitCorruptArchive()113 bool LibarchivePlugin::emitCorruptArchive()
114 {
115 Kerfuffle::LoadCorruptQuery query(filename());
116 Q_EMIT userQuery(&query);
117 query.waitForResponse();
118 if (!query.responseYes()) {
119 Q_EMIT cancelled();
120 archive_read_close(m_archiveReader.data());
121 return false;
122 } else {
123 Q_EMIT progress(1.0);
124 return true;
125 }
126 }
127
addFiles(const QVector<Archive::Entry * > & files,const Archive::Entry * destination,const CompressionOptions & options,uint numberOfEntriesToAdd)128 bool LibarchivePlugin::addFiles(const QVector<Archive::Entry*> &files, const Archive::Entry *destination, const CompressionOptions &options, uint numberOfEntriesToAdd)
129 {
130 Q_UNUSED(files)
131 Q_UNUSED(destination)
132 Q_UNUSED(options)
133 Q_UNUSED(numberOfEntriesToAdd)
134 return false;
135 }
136
moveFiles(const QVector<Archive::Entry * > & files,Archive::Entry * destination,const CompressionOptions & options)137 bool LibarchivePlugin::moveFiles(const QVector<Archive::Entry*> &files, Archive::Entry *destination, const CompressionOptions &options)
138 {
139 Q_UNUSED(files)
140 Q_UNUSED(destination)
141 Q_UNUSED(options)
142 return false;
143 }
144
copyFiles(const QVector<Archive::Entry * > & files,Archive::Entry * destination,const CompressionOptions & options)145 bool LibarchivePlugin::copyFiles(const QVector<Archive::Entry*> &files, Archive::Entry *destination, const CompressionOptions &options)
146 {
147 Q_UNUSED(files)
148 Q_UNUSED(destination)
149 Q_UNUSED(options)
150 return false;
151 }
152
deleteFiles(const QVector<Archive::Entry * > & files)153 bool LibarchivePlugin::deleteFiles(const QVector<Archive::Entry*> &files)
154 {
155 Q_UNUSED(files)
156 return false;
157 }
158
addComment(const QString & comment)159 bool LibarchivePlugin::addComment(const QString &comment)
160 {
161 Q_UNUSED(comment)
162 return false;
163 }
164
testArchive()165 bool LibarchivePlugin::testArchive()
166 {
167 return false;
168 }
169
hasBatchExtractionProgress() const170 bool LibarchivePlugin::hasBatchExtractionProgress() const
171 {
172 return true;
173 }
174
doKill()175 bool LibarchivePlugin::doKill()
176 {
177 return false;
178 }
179
extractFiles(const QVector<Archive::Entry * > & files,const QString & destinationDirectory,const ExtractionOptions & options)180 bool LibarchivePlugin::extractFiles(const QVector<Archive::Entry*> &files, const QString &destinationDirectory, const ExtractionOptions &options)
181 {
182 if (!initializeReader()) {
183 return false;
184 }
185
186 ArchiveWrite writer(archive_write_disk_new());
187 if (!writer.data()) {
188 return false;
189 }
190
191 archive_write_disk_set_options(writer.data(), extractionFlags());
192
193 int totalEntriesCount = 0;
194 const bool extractAll = files.isEmpty();
195 if (extractAll) {
196 if (!m_cachedArchiveEntryCount) {
197 Q_EMIT progress(0);
198 //TODO: once information progress has been implemented, send
199 //feedback here that the archive is being read
200 qCDebug(ARK) << "For getting progress information, the archive will be listed once";
201 m_emitNoEntries = true;
202 list();
203 m_emitNoEntries = false;
204 }
205 totalEntriesCount = m_cachedArchiveEntryCount;
206 } else {
207 totalEntriesCount = files.size();
208 }
209
210 qCDebug(ARK) << "Going to extract" << totalEntriesCount << "entries";
211
212 qCDebug(ARK) << "Changing current directory to " << destinationDirectory;
213 m_oldWorkingDir = QDir::currentPath();
214 QDir::setCurrent(destinationDirectory);
215
216 // Initialize variables.
217 const bool preservePaths = options.preservePaths();
218 const bool removeRootNode = options.isDragAndDropEnabled();
219 bool overwriteAll = false; // Whether to overwrite all files
220 bool skipAll = false; // Whether to skip all files
221 bool dontPromptErrors = false; // Whether to prompt for errors
222 m_currentExtractedFilesSize = 0;
223 int extractedEntriesCount = 0;
224 int progressEntryCount = 0;
225 struct archive_entry *entry;
226 QString fileBeingRenamed;
227 // To avoid traversing the entire archive when extracting a limited set of
228 // entries, we maintain a list of remaining entries and stop when it's empty.
229 const QStringList fullPaths = entryFullPaths(files);
230 QStringList remainingFiles = entryFullPaths(files);
231
232 // Iterate through all entries in archive.
233 while (!QThread::currentThread()->isInterruptionRequested() && (archive_read_next_header(m_archiveReader.data(), &entry) == ARCHIVE_OK)) {
234
235 if (!extractAll && remainingFiles.isEmpty()) {
236 break;
237 }
238
239 fileBeingRenamed.clear();
240 int index = -1;
241
242 // Retry with renamed entry, fire an overwrite query again
243 // if the new entry also exists.
244 retry:
245 const bool entryIsDir = S_ISDIR(archive_entry_mode(entry));
246 // Skip directories if not preserving paths.
247 if (!preservePaths && entryIsDir) {
248 archive_read_data_skip(m_archiveReader.data());
249 continue;
250 }
251
252 // entryName is the name inside the archive, full path
253 QString entryName = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry)));
254
255 // Some archive types e.g. AppImage prepend all entries with "./" so remove this part.
256 if (entryName.startsWith(QLatin1String("./"))) {
257 entryName.remove(0, 2);
258 }
259
260 // Static libraries (*.a) contain the two entries "/" and "//".
261 // We just skip these to allow extracting this archive type.
262 if (entryName == QLatin1String("/") || entryName == QLatin1String("//")) {
263 archive_read_data_skip(m_archiveReader.data());
264 continue;
265 }
266
267 // For now we just can't handle absolute filenames in a tar archive.
268 // TODO: find out what to do here!!
269 if (entryName.startsWith(QLatin1Char( '/' ))) {
270 Q_EMIT error(i18n("This archive contains archive entries with absolute paths, "
271 "which are not supported by Ark."));
272 return false;
273 }
274
275 // Should the entry be extracted?
276 if (extractAll ||
277 remainingFiles.contains(entryName) ||
278 entryName == fileBeingRenamed) {
279
280 // Find the index of entry.
281 if (entryName != fileBeingRenamed) {
282 index = fullPaths.indexOf(entryName);
283 }
284 if (!extractAll && index == -1) {
285 // If entry is not found in files, skip entry.
286 continue;
287 }
288
289 // entryFI is the fileinfo pointing to where the file will be
290 // written from the archive.
291 QFileInfo entryFI(entryName);
292 //qCDebug(ARK) << "setting path to " << archive_entry_pathname( entry );
293
294 const QString fileWithoutPath(entryFI.fileName());
295 // If we DON'T preserve paths, we cut the path and set the entryFI
296 // fileinfo to the one without the path.
297 if (!preservePaths) {
298 // Empty filenames (ie dirs) should have been skipped already,
299 // so asserting.
300 Q_ASSERT(!fileWithoutPath.isEmpty());
301 archive_entry_copy_pathname(entry, QFile::encodeName(fileWithoutPath).constData());
302 entryFI = QFileInfo(fileWithoutPath);
303
304 // OR, if the file has a rootNode attached, remove it from file path.
305 } else if (!extractAll && removeRootNode && entryName != fileBeingRenamed) {
306 const QString &rootNode = files.at(index)->rootNode;
307 if (!rootNode.isEmpty()) {
308 const QString truncatedFilename(entryName.remove(entryName.indexOf(rootNode), rootNode.size()));
309 archive_entry_copy_pathname(entry, QFile::encodeName(truncatedFilename).constData());
310 entryFI = QFileInfo(truncatedFilename);
311 }
312 }
313
314 // Check if the file about to be written already exists.
315 if (!entryIsDir && entryFI.exists()) {
316 if (skipAll) {
317 archive_read_data_skip(m_archiveReader.data());
318 archive_entry_clear(entry);
319 continue;
320 } else if (!overwriteAll && !skipAll) {
321 Kerfuffle::OverwriteQuery query(entryName);
322 Q_EMIT userQuery(&query);
323 query.waitForResponse();
324
325 if (query.responseCancelled()) {
326 Q_EMIT cancelled();
327 archive_read_data_skip(m_archiveReader.data());
328 archive_entry_clear(entry);
329 break;
330 } else if (query.responseSkip()) {
331 archive_read_data_skip(m_archiveReader.data());
332 archive_entry_clear(entry);
333 continue;
334 } else if (query.responseAutoSkip()) {
335 archive_read_data_skip(m_archiveReader.data());
336 archive_entry_clear(entry);
337 skipAll = true;
338 continue;
339 } else if (query.responseRename()) {
340 const QString newName(query.newFilename());
341 fileBeingRenamed = newName;
342 archive_entry_copy_pathname(entry, QFile::encodeName(newName).constData());
343 goto retry;
344 } else if (query.responseOverwriteAll()) {
345 overwriteAll = true;
346 }
347 }
348 }
349
350 // If there is an already existing directory.
351 if (entryIsDir && entryFI.exists()) {
352 if (entryFI.isWritable()) {
353 qCWarning(ARK) << "Warning, existing, but writable dir";
354 } else {
355 qCWarning(ARK) << "Warning, existing, but non-writable dir. skipping";
356 archive_entry_clear(entry);
357 archive_read_data_skip(m_archiveReader.data());
358 continue;
359 }
360 }
361
362 // Write the entry header and check return value.
363 const int returnCode = archive_write_header(writer.data(), entry);
364 switch (returnCode) {
365 case ARCHIVE_OK:
366 // If the whole archive is extracted and the total filesize is
367 // available, we use partial progress.
368 copyData(entryName, m_archiveReader.data(), writer.data(), (extractAll && m_extractedFilesSize));
369 break;
370
371 case ARCHIVE_FAILED:
372 qCCritical(ARK) << "archive_write_header() has returned" << returnCode
373 << "with errno" << archive_errno(writer.data());
374
375 // If they user previously decided to ignore future errors,
376 // don't bother prompting again.
377 if (!dontPromptErrors) {
378 // Ask the user if he wants to continue extraction despite an error for this entry.
379 Kerfuffle::ContinueExtractionQuery query(QLatin1String(archive_error_string(writer.data())),
380 entryName);
381 Q_EMIT userQuery(&query);
382 query.waitForResponse();
383
384 if (query.responseCancelled()) {
385 Q_EMIT cancelled();
386 return false;
387 }
388 dontPromptErrors = query.dontAskAgain();
389 }
390 break;
391
392 case ARCHIVE_FATAL:
393 qCCritical(ARK) << "archive_write_header() has returned" << returnCode
394 << "with errno" << archive_errno(writer.data());
395 Q_EMIT error(i18nc("@info", "Fatal error, extraction aborted."));
396 return false;
397 default:
398 qCDebug(ARK) << "archive_write_header() returned" << returnCode
399 << "which will be ignored.";
400 break;
401 }
402
403 // If we only partially extract the archive and the number of
404 // archive entries is available we use a simple progress based on
405 // number of items extracted.
406 if (!extractAll && m_cachedArchiveEntryCount) {
407 ++progressEntryCount;
408 Q_EMIT progress(float(progressEntryCount) / totalEntriesCount);
409 }
410
411 extractedEntriesCount++;
412 remainingFiles.removeOne(entryName);
413 } else {
414 // Archive entry not among selected files, skip it.
415 archive_read_data_skip(m_archiveReader.data());
416 }
417 }
418
419 qCDebug(ARK) << "Extracted" << extractedEntriesCount << "entries";
420 slotRestoreWorkingDir();
421 return archive_read_close(m_archiveReader.data()) == ARCHIVE_OK;
422 }
423
initializeReader()424 bool LibarchivePlugin::initializeReader()
425 {
426 m_archiveReader.reset(archive_read_new());
427
428 if (!(m_archiveReader.data())) {
429 Q_EMIT error(i18n("The archive reader could not be initialized."));
430 return false;
431 }
432
433 if (archive_read_support_filter_all(m_archiveReader.data()) != ARCHIVE_OK) {
434 return false;
435 }
436
437 if (archive_read_support_format_all(m_archiveReader.data()) != ARCHIVE_OK) {
438 return false;
439 }
440
441 if (archive_read_open_filename(m_archiveReader.data(), QFile::encodeName(filename()).constData(), 10240) != ARCHIVE_OK) {
442 qCWarning(ARK) << "Could not open the archive:" << archive_error_string(m_archiveReader.data());
443 Q_EMIT error(i18nc("@info", "Archive corrupted or insufficient permissions."));
444 return false;
445 }
446
447 return true;
448 }
449
emitEntryFromArchiveEntry(struct archive_entry * aentry)450 void LibarchivePlugin::emitEntryFromArchiveEntry(struct archive_entry *aentry)
451 {
452 auto e = new Archive::Entry();
453
454 #ifdef Q_OS_WIN
455 e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromUtf16((ushort*)archive_entry_pathname_w(aentry))));
456 #else
457 e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromWCharArray(archive_entry_pathname_w(aentry))));
458 #endif
459
460 const QString owner = QString::fromLatin1(archive_entry_uname(aentry));
461 if (!owner.isEmpty()) {
462 e->setProperty("owner", owner);
463 } else {
464 e->setProperty("owner", static_cast<qlonglong>(archive_entry_uid(aentry)));
465 }
466
467 const QString group = QString::fromLatin1(archive_entry_gname(aentry));
468 if (!group.isEmpty()) {
469 e->setProperty("group", group);
470 } else {
471 e->setProperty("group", static_cast<qlonglong>(archive_entry_gid(aentry)));
472 }
473
474 const mode_t mode = archive_entry_mode(aentry);
475 if (mode != 0) {
476 e->setProperty("permissions", QString::number(mode, 8));
477 }
478 e->setProperty("isExecutable", mode & (S_IXUSR | S_IXGRP | S_IXOTH));
479
480 e->compressedSizeIsSet = false;
481 e->setProperty("size", (qlonglong)archive_entry_size(aentry));
482 e->setProperty("isDirectory", S_ISDIR(archive_entry_mode(aentry)));
483
484 if (archive_entry_symlink(aentry)) {
485 e->setProperty("link", QLatin1String( archive_entry_symlink(aentry) ));
486 }
487
488 auto time = static_cast<uint>(archive_entry_mtime(aentry));
489 e->setProperty("timestamp", QDateTime::fromSecsSinceEpoch(time));
490
491 Q_EMIT entry(e);
492 m_emittedEntries << e;
493 }
494
extractionFlags() const495 int LibarchivePlugin::extractionFlags() const
496 {
497 return ARCHIVE_EXTRACT_TIME
498 | ARCHIVE_EXTRACT_SECURE_NODOTDOT
499 | ARCHIVE_EXTRACT_SECURE_SYMLINKS;
500 }
501
copyData(const QString & filename,struct archive * dest,bool partialprogress)502 void LibarchivePlugin::copyData(const QString& filename, struct archive *dest, bool partialprogress)
503 {
504 char buff[10240];
505 QFile file(filename);
506
507 if (!file.open(QIODevice::ReadOnly)) {
508 return;
509 }
510
511 auto readBytes = file.read(buff, sizeof(buff));
512 while (readBytes > 0 && !QThread::currentThread()->isInterruptionRequested()) {
513 archive_write_data(dest, buff, static_cast<size_t>(readBytes));
514 if (archive_errno(dest) != ARCHIVE_OK) {
515 qCCritical(ARK) << "Error while writing" << filename << ":" << archive_error_string(dest)
516 << "(error no =" << archive_errno(dest) << ')';
517 return;
518 }
519
520 if (partialprogress) {
521 m_currentExtractedFilesSize += readBytes;
522 Q_EMIT progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize);
523 }
524
525 readBytes = file.read(buff, sizeof(buff));
526 }
527
528 file.close();
529 }
530
copyData(const QString & filename,struct archive * source,struct archive * dest,bool partialprogress)531 void LibarchivePlugin::copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress)
532 {
533 char buff[10240];
534
535 auto readBytes = archive_read_data(source, buff, sizeof(buff));
536 while (readBytes > 0 && !QThread::currentThread()->isInterruptionRequested()) {
537 archive_write_data(dest, buff, static_cast<size_t>(readBytes));
538 if (archive_errno(dest) != ARCHIVE_OK) {
539 qCCritical(ARK) << "Error while extracting" << filename << ":" << archive_error_string(dest)
540 << "(error no =" << archive_errno(dest) << ')';
541 return;
542 }
543
544 if (partialprogress) {
545 m_currentExtractedFilesSize += readBytes;
546 Q_EMIT progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize);
547 }
548
549 readBytes = archive_read_data(source, buff, sizeof(buff));
550 }
551 }
552
slotRestoreWorkingDir()553 void LibarchivePlugin::slotRestoreWorkingDir()
554 {
555 if (m_oldWorkingDir.isEmpty()) {
556 return;
557 }
558
559 if (!QDir::setCurrent(m_oldWorkingDir)) {
560 qCWarning(ARK) << "Failed to restore old working directory:" << m_oldWorkingDir;
561 } else {
562 m_oldWorkingDir.clear();
563 }
564 }
565
convertCompressionName(const QString & method)566 QString LibarchivePlugin::convertCompressionName(const QString &method)
567 {
568 if (method == QLatin1String("gzip")) {
569 return QStringLiteral("GZip");
570 } else if (method == QLatin1String("bzip2")) {
571 return QStringLiteral("BZip2");
572 } else if (method == QLatin1String("xz")) {
573 return QStringLiteral("XZ");
574 } else if (method == QLatin1String("compress (.Z)")) {
575 return QStringLiteral("Compress");
576 } else if (method == QLatin1String("lrzip")) {
577 return QStringLiteral("LRZip");
578 } else if (method == QLatin1String("lzip")) {
579 return QStringLiteral("LZip");
580 } else if (method == QLatin1String("lz4")) {
581 return QStringLiteral("LZ4");
582 } else if (method == QLatin1String("lzop")) {
583 return QStringLiteral("lzop");
584 } else if (method == QLatin1String("lzma")) {
585 return QStringLiteral("LZMA");
586 } else if (method == QLatin1String("zstd")) {
587 return QStringLiteral("Zstandard");
588 }
589 return QString();
590 }
591
592