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