File indexing completed on 2024-05-12 05:50:22

0001 /*
0002     SPDX-FileCopyrightText: 2007 Henrique Pinto <henrique.pinto@kdemail.net>
0003     SPDX-FileCopyrightText: 2008-2009 Harald Hvaal <haraldhv@stud.ntnu.no>
0004     SPDX-FileCopyrightText: 2010 Raphael Kubo da Costa <rakuco@FreeBSD.org>
0005     SPDX-FileCopyrightText: 2016 Vladyslav Batyrenko <mvlabat@gmail.com>
0006 
0007     SPDX-License-Identifier: BSD-2-Clause
0008 */
0009 
0010 #include "readwritelibarchiveplugin.h"
0011 #include "ark_debug.h"
0012 
0013 #include <KLocalizedString>
0014 #include <KPluginFactory>
0015 
0016 #include <QDirIterator>
0017 #include <QThread>
0018 
0019 #include <archive_entry.h>
0020 
0021 K_PLUGIN_CLASS_WITH_JSON(ReadWriteLibarchivePlugin, "kerfuffle_libarchive.json")
0022 
0023 ReadWriteLibarchivePlugin::ReadWriteLibarchivePlugin(QObject *parent, const QVariantList &args)
0024     : LibarchivePlugin(parent, args)
0025 {
0026     qCDebug(ARK) << "Loaded libarchive read-write plugin";
0027 }
0028 
0029 ReadWriteLibarchivePlugin::~ReadWriteLibarchivePlugin()
0030 {
0031 }
0032 
0033 bool ReadWriteLibarchivePlugin::addFiles(const QVector<Archive::Entry *> &files,
0034                                          const Archive::Entry *destination,
0035                                          const CompressionOptions &options,
0036                                          uint numberOfEntriesToAdd)
0037 {
0038     qCDebug(ARK) << "Adding" << files.size() << "entries with CompressionOptions" << options;
0039 
0040     const bool creatingNewFile = !QFileInfo::exists(filename());
0041     const uint totalCount = m_numberOfEntries + numberOfEntriesToAdd;
0042 
0043     m_writtenFiles.clear();
0044 
0045     if (!creatingNewFile && !initializeReader()) {
0046         return false;
0047     }
0048 
0049     if (!initializeWriter(creatingNewFile, options)) {
0050         return false;
0051     }
0052 
0053     // First write the new files.
0054     qCDebug(ARK) << "Writing new entries";
0055     uint addedEntries = 0;
0056     // Recreate destination directory structure.
0057     const QString destinationPath = (destination == nullptr) ? QString() : destination->fullPath();
0058 
0059     for (Archive::Entry *selectedFile : files) {
0060         if (QThread::currentThread()->isInterruptionRequested()) {
0061             break;
0062         }
0063 
0064         if (!writeFile(selectedFile->fullPath(), destinationPath)) {
0065             finish(false);
0066             return false;
0067         }
0068         addedEntries++;
0069         Q_EMIT progress(float(addedEntries) / float(totalCount));
0070 
0071         // For directories, write all subfiles/folders.
0072         const QString &fullPath = selectedFile->fullPath();
0073         if (QFileInfo(fullPath).isDir()) {
0074             QDirIterator it(fullPath, QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories);
0075 
0076             while (!QThread::currentThread()->isInterruptionRequested() && it.hasNext()) {
0077                 QString path = it.next();
0078 
0079                 if ((it.fileName() == QLatin1String("..")) || (it.fileName() == QLatin1Char('.'))) {
0080                     continue;
0081                 }
0082 
0083                 const bool isRealDir = it.fileInfo().isDir() && !it.fileInfo().isSymLink();
0084 
0085                 if (isRealDir) {
0086                     path.append(QLatin1Char('/'));
0087                 }
0088 
0089                 if (!writeFile(path, destinationPath)) {
0090                     finish(false);
0091                     return false;
0092                 }
0093                 addedEntries++;
0094                 Q_EMIT progress(float(addedEntries) / float(totalCount));
0095             }
0096         }
0097     }
0098     qCDebug(ARK) << "Added" << addedEntries << "new entries to archive";
0099 
0100     bool isSuccessful = true;
0101     // If we have old archive entries.
0102     if (!creatingNewFile) {
0103         qCDebug(ARK) << "Copying any old entries";
0104         m_filesPaths = m_writtenFiles;
0105         isSuccessful = processOldEntries(addedEntries, Add, totalCount);
0106         if (isSuccessful) {
0107             qCDebug(ARK) << "Added" << addedEntries << "old entries to archive";
0108         } else {
0109             qCDebug(ARK) << "Adding entries failed";
0110         }
0111     }
0112 
0113     finish(isSuccessful);
0114     return isSuccessful;
0115 }
0116 
0117 bool ReadWriteLibarchivePlugin::moveFiles(const QVector<Archive::Entry *> &files, Archive::Entry *destination, const CompressionOptions &options)
0118 {
0119     Q_UNUSED(options);
0120 
0121     qCDebug(ARK) << "Moving" << files.size() << "entries";
0122 
0123     if (!initializeReader()) {
0124         return false;
0125     }
0126 
0127     if (!initializeWriter()) {
0128         return false;
0129     }
0130 
0131     // Copy old elements from previous archive to new archive.
0132     uint movedEntries = 0;
0133     m_filesPaths = entryFullPaths(files);
0134     m_entriesWithoutChildren = entriesWithoutChildren(files).count();
0135     m_destination = destination;
0136     const bool isSuccessful = processOldEntries(movedEntries, Move, m_numberOfEntries);
0137     if (isSuccessful) {
0138         qCDebug(ARK) << "Moved" << movedEntries << "entries within archive";
0139     } else {
0140         qCDebug(ARK) << "Moving entries failed";
0141     }
0142 
0143     finish(isSuccessful);
0144     return isSuccessful;
0145 }
0146 
0147 bool ReadWriteLibarchivePlugin::copyFiles(const QVector<Archive::Entry *> &files, Archive::Entry *destination, const CompressionOptions &options)
0148 {
0149     Q_UNUSED(options);
0150 
0151     qCDebug(ARK) << "Copying" << files.size() << "entries";
0152 
0153     if (!initializeReader()) {
0154         return false;
0155     }
0156 
0157     if (!initializeWriter()) {
0158         return false;
0159     }
0160 
0161     // Copy old elements from previous archive to new archive.
0162     uint copiedEntries = 0;
0163     m_filesPaths = entryFullPaths(files);
0164     m_destination = destination;
0165     const bool isSuccessful = processOldEntries(copiedEntries, Copy, m_numberOfEntries);
0166     if (isSuccessful) {
0167         qCDebug(ARK) << "Copied" << copiedEntries << "entries within archive";
0168     } else {
0169         qCDebug(ARK) << "Copying entries failed";
0170     }
0171 
0172     finish(isSuccessful);
0173     return isSuccessful;
0174 }
0175 
0176 bool ReadWriteLibarchivePlugin::deleteFiles(const QVector<Archive::Entry *> &files)
0177 {
0178     qCDebug(ARK) << "Deleting" << files.size() << "entries";
0179 
0180     if (!initializeReader()) {
0181         return false;
0182     }
0183 
0184     if (!initializeWriter()) {
0185         return false;
0186     }
0187 
0188     // Copy old elements from previous archive to new archive.
0189     uint deletedEntries = 0;
0190     m_filesPaths = entryFullPaths(files);
0191     const bool isSuccessful = processOldEntries(deletedEntries, Delete, m_numberOfEntries);
0192     if (isSuccessful) {
0193         qCDebug(ARK) << "Removed" << deletedEntries << "entries from archive";
0194     } else {
0195         qCDebug(ARK) << "Removing entries failed";
0196     }
0197 
0198     finish(isSuccessful);
0199     return isSuccessful;
0200 }
0201 
0202 bool ReadWriteLibarchivePlugin::initializeWriter(const bool creatingNewFile, const CompressionOptions &options)
0203 {
0204     m_tempFile.setFileName(filename());
0205     if (!m_tempFile.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) {
0206         Q_EMIT error(i18nc("@info", "Failed to create a temporary file for writing data."));
0207         return false;
0208     }
0209 
0210     m_archiveWriter.reset(archive_write_new());
0211     if (!(m_archiveWriter.data())) {
0212         Q_EMIT error(i18n("The archive writer could not be initialized."));
0213         return false;
0214     }
0215 
0216     // pax_restricted is the libarchive default, let's go with that.
0217     archive_write_set_format_pax_restricted(m_archiveWriter.data());
0218 
0219     if (creatingNewFile) {
0220         if (!initializeNewFileWriterFilters(options)) {
0221             return false;
0222         }
0223     } else {
0224         if (!initializeWriterFilters()) {
0225             return false;
0226         }
0227     }
0228 
0229     if (archive_write_open_fd(m_archiveWriter.data(), m_tempFile.handle()) != ARCHIVE_OK) {
0230         Q_EMIT error(i18nc("@info", "Could not open the archive for writing entries."));
0231         return false;
0232     }
0233 
0234     return true;
0235 }
0236 
0237 bool ReadWriteLibarchivePlugin::initializeWriterFilters()
0238 {
0239     int ret;
0240     bool requiresExecutable = false;
0241     switch (archive_filter_code(m_archiveReader.data(), 0)) {
0242     case ARCHIVE_FILTER_GZIP:
0243         ret = archive_write_add_filter_gzip(m_archiveWriter.data());
0244         break;
0245     case ARCHIVE_FILTER_BZIP2:
0246         ret = archive_write_add_filter_bzip2(m_archiveWriter.data());
0247         break;
0248     case ARCHIVE_FILTER_XZ:
0249         ret = archive_write_add_filter_xz(m_archiveWriter.data());
0250         break;
0251     case ARCHIVE_FILTER_LZMA:
0252         ret = archive_write_add_filter_lzma(m_archiveWriter.data());
0253         break;
0254     case ARCHIVE_FILTER_COMPRESS:
0255         ret = archive_write_add_filter_compress(m_archiveWriter.data());
0256         break;
0257     case ARCHIVE_FILTER_LZIP:
0258         ret = archive_write_add_filter_lzip(m_archiveWriter.data());
0259         break;
0260     case ARCHIVE_FILTER_LZOP:
0261         ret = archive_write_add_filter_lzop(m_archiveWriter.data());
0262         break;
0263     case ARCHIVE_FILTER_LRZIP:
0264         ret = archive_write_add_filter_lrzip(m_archiveWriter.data());
0265         requiresExecutable = true;
0266         break;
0267     case ARCHIVE_FILTER_LZ4:
0268         ret = archive_write_add_filter_lz4(m_archiveWriter.data());
0269         break;
0270     case ARCHIVE_FILTER_ZSTD:
0271         ret = archive_write_add_filter_zstd(m_archiveWriter.data());
0272         break;
0273     case ARCHIVE_FILTER_NONE:
0274         ret = archive_write_add_filter_none(m_archiveWriter.data());
0275         break;
0276     default:
0277         Q_EMIT error(i18n("The compression type '%1' is not supported by Ark.", QLatin1String(archive_filter_name(m_archiveReader.data(), 0))));
0278         return false;
0279     }
0280 
0281     // Libarchive emits a warning for lrzip due to using external executable.
0282     if ((requiresExecutable && ret != ARCHIVE_WARN) || (!requiresExecutable && ret != ARCHIVE_OK)) {
0283         qCWarning(ARK) << "Failed to set compression method:" << archive_error_string(m_archiveWriter.data());
0284         Q_EMIT error(i18nc("@info", "Could not set the compression method."));
0285         return false;
0286     }
0287 
0288     return true;
0289 }
0290 
0291 bool ReadWriteLibarchivePlugin::initializeNewFileWriterFilters(const CompressionOptions &options)
0292 {
0293     int ret;
0294     bool requiresExecutable = false;
0295     const auto threads = std::to_string(static_cast<unsigned>(std::thread::hardware_concurrency() * 0.8));
0296 
0297     if (filename().right(2).toUpper() == QLatin1String("GZ")) {
0298         qCDebug(ARK) << "Detected gzip compression for new file";
0299         ret = archive_write_add_filter_gzip(m_archiveWriter.data());
0300     } else if (filename().right(3).toUpper() == QLatin1String("BZ2")) {
0301         qCDebug(ARK) << "Detected bzip2 compression for new file";
0302         ret = archive_write_add_filter_bzip2(m_archiveWriter.data());
0303     } else if (filename().right(2).toUpper() == QLatin1String("XZ")) {
0304         qCDebug(ARK) << "Detected xz compression for new file";
0305         ret = archive_write_add_filter_xz(m_archiveWriter.data());
0306 
0307         // Set number of threads.
0308         ret = archive_write_set_filter_option(m_archiveWriter.data(), "xz", "threads", threads.c_str());
0309         if (ret != ARCHIVE_OK) {
0310             qCWarning(ARK) << "Failed to set number of threads, fallback to single thread mode" << archive_error_string(m_archiveWriter.data());
0311         }
0312     } else if (filename().right(4).toUpper() == QLatin1String("LZMA")) {
0313         qCDebug(ARK) << "Detected lzma compression for new file";
0314         ret = archive_write_add_filter_lzma(m_archiveWriter.data());
0315     } else if (filename().right(2).toUpper() == QLatin1String(".Z")) {
0316         qCDebug(ARK) << "Detected compress (.Z) compression for new file";
0317         ret = archive_write_add_filter_compress(m_archiveWriter.data());
0318     } else if (filename().right(2).toUpper() == QLatin1String("LZ")) {
0319         qCDebug(ARK) << "Detected lzip compression for new file";
0320         ret = archive_write_add_filter_lzip(m_archiveWriter.data());
0321     } else if (filename().right(3).toUpper() == QLatin1String("LZO")) {
0322         qCDebug(ARK) << "Detected lzop compression for new file";
0323         ret = archive_write_add_filter_lzop(m_archiveWriter.data());
0324     } else if (filename().right(3).toUpper() == QLatin1String("LRZ")) {
0325         qCDebug(ARK) << "Detected lrzip compression for new file";
0326         ret = archive_write_add_filter_lrzip(m_archiveWriter.data());
0327         requiresExecutable = true;
0328     } else if (filename().right(3).toUpper() == QLatin1String("LZ4")) {
0329         qCDebug(ARK) << "Detected lz4 compression for new file";
0330         ret = archive_write_add_filter_lz4(m_archiveWriter.data());
0331     } else if (filename().right(3).toUpper() == QLatin1String("ZST")) {
0332         qCDebug(ARK) << "Detected zstd compression for new file";
0333         ret = archive_write_add_filter_zstd(m_archiveWriter.data());
0334 
0335         // Set number of threads.
0336         ret = archive_write_set_filter_option(m_archiveWriter.data(), "zstd", "threads", threads.c_str());
0337         if (ret != ARCHIVE_OK) {
0338             qCWarning(ARK) << "Failed to set number of threads, fallback to single thread mode" << archive_error_string(m_archiveWriter.data());
0339         }
0340     } else if (filename().right(3).toUpper() == QLatin1String("TAR")) {
0341         qCDebug(ARK) << "Detected no compression for new file (pure tar)";
0342         ret = archive_write_add_filter_none(m_archiveWriter.data());
0343     } else {
0344         qCDebug(ARK) << "Falling back to gzip";
0345         ret = archive_write_add_filter_gzip(m_archiveWriter.data());
0346     }
0347 
0348     // Libarchive emits a warning for lrzip due to using external executable.
0349     if ((requiresExecutable && ret != ARCHIVE_WARN) || (!requiresExecutable && ret != ARCHIVE_OK)) {
0350         qCWarning(ARK) << "Failed to set compression method:" << archive_error_string(m_archiveWriter.data());
0351         Q_EMIT error(i18nc("@info", "Could not set the compression method."));
0352         return false;
0353     }
0354 
0355     // Set compression level if passed in CompressionOptions.
0356     if (options.isCompressionLevelSet()) {
0357         qCDebug(ARK) << "Using compression level:" << options.compressionLevel();
0358         ret = archive_write_set_filter_option(m_archiveWriter.data(),
0359                                               nullptr,
0360                                               "compression-level",
0361                                               QString::number(options.compressionLevel()).toUtf8().constData());
0362         if (ret != ARCHIVE_OK) {
0363             qCWarning(ARK) << "Failed to set compression level" << archive_error_string(m_archiveWriter.data());
0364             Q_EMIT error(i18nc("@info", "Could not set the compression level."));
0365             return false;
0366         }
0367     }
0368 
0369     return true;
0370 }
0371 
0372 void ReadWriteLibarchivePlugin::finish(const bool isSuccessful)
0373 {
0374     if (!isSuccessful || QThread::currentThread()->isInterruptionRequested()) {
0375         archive_write_fail(m_archiveWriter.data());
0376         m_tempFile.cancelWriting();
0377     } else {
0378         // archive_write_close() needs to be called before calling QSaveFile::commit(),
0379         // otherwise the latter will close() the file descriptor m_archiveWriter is still working on.
0380         // TODO: We need to abstract this code better so that we only deal with one
0381         // object that manages both QSaveFile and ArchiveWriter.
0382         archive_write_close(m_archiveWriter.data());
0383         m_tempFile.commit();
0384     }
0385 }
0386 
0387 bool ReadWriteLibarchivePlugin::processOldEntries(uint &entriesCounter, OperationMode mode, uint totalCount)
0388 {
0389     const uint newEntries = entriesCounter;
0390     entriesCounter = 0;
0391     uint iteratedEntries = 0;
0392 
0393     // Create a map that contains old path as key and new path as value.
0394     QMap<QString, QString> pathMap;
0395     if (mode == Move || mode == Copy) {
0396         m_filesPaths.sort();
0397         QStringList resultList = entryPathsFromDestination(m_filesPaths, m_destination, m_entriesWithoutChildren);
0398         const int listSize = m_filesPaths.count();
0399         Q_ASSERT(listSize == resultList.count());
0400         for (int i = 0; i < listSize; ++i) {
0401             pathMap.insert(m_filesPaths.at(i), resultList.at(i));
0402         }
0403     }
0404 
0405     struct archive_entry *entry;
0406     while (!QThread::currentThread()->isInterruptionRequested() && archive_read_next_header(m_archiveReader.data(), &entry) == ARCHIVE_OK) {
0407         const QString file = QFile::decodeName(archive_entry_pathname(entry));
0408 
0409         if (mode == Move || mode == Copy) {
0410             const QString newPathname = pathMap.value(file);
0411             if (!newPathname.isEmpty()) {
0412                 if (mode == Copy) {
0413                     // Write the old entry.
0414                     if (!writeEntry(entry)) {
0415                         return false;
0416                     }
0417                 } else {
0418                     Q_EMIT entryRemoved(file);
0419                 }
0420 
0421                 entriesCounter++;
0422                 iteratedEntries--;
0423 
0424                 // Change entry path.
0425                 archive_entry_set_pathname(entry, newPathname.toUtf8().constData());
0426                 emitEntryFromArchiveEntry(entry);
0427             }
0428         } else if (m_filesPaths.contains(file)) {
0429             archive_read_data_skip(m_archiveReader.data());
0430             switch (mode) {
0431             case Delete:
0432                 entriesCounter++;
0433                 Q_EMIT entryRemoved(file);
0434                 Q_EMIT progress(float(newEntries + entriesCounter + iteratedEntries) / float(totalCount));
0435                 break;
0436 
0437             case Add:
0438                 qCDebug(ARK) << file << "is already present in the new archive, skipping.";
0439                 // When overwriting entries, we need to decrement the counter manually,
0440                 // because entry was emitted.
0441                 m_numberOfEntries--;
0442                 break;
0443 
0444             default:
0445                 qCDebug(ARK) << "Mode" << mode << "is not considered for processing old libarchive entries";
0446                 Q_ASSERT(false);
0447             }
0448             continue;
0449         }
0450 
0451         // Write old entries.
0452         if (writeEntry(entry)) {
0453             if (mode == Add) {
0454                 entriesCounter++;
0455             } else if (mode == Move || mode == Copy) {
0456                 iteratedEntries++;
0457             } else if (mode == Delete) {
0458                 iteratedEntries++;
0459             }
0460         } else {
0461             return false;
0462         }
0463         Q_EMIT progress(float(newEntries + entriesCounter + iteratedEntries) / float(totalCount));
0464     }
0465 
0466     return !QThread::currentThread()->isInterruptionRequested();
0467 }
0468 
0469 bool ReadWriteLibarchivePlugin::writeEntry(struct archive_entry *entry)
0470 {
0471     const int returnCode = archive_write_header(m_archiveWriter.data(), entry);
0472     switch (returnCode) {
0473     case ARCHIVE_OK:
0474         // If the whole archive is extracted and the total filesize is
0475         // available, we use partial progress.
0476         copyData(QLatin1String(archive_entry_pathname(entry)), m_archiveReader.data(), m_archiveWriter.data(), false);
0477         break;
0478     case ARCHIVE_FAILED:
0479     case ARCHIVE_FATAL:
0480         qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(m_archiveWriter.data());
0481         Q_EMIT error(i18nc("@info", "Could not compress entry, operation aborted."));
0482         return false;
0483     default:
0484         qCDebug(ARK) << "archive_writer_header() has returned" << returnCode << "which will be ignored.";
0485         break;
0486     }
0487 
0488     return true;
0489 }
0490 
0491 // TODO: if we merge this with copyData(), we can pass more data
0492 //       such as an fd to archive_read_disk_entry_from_file()
0493 bool ReadWriteLibarchivePlugin::writeFile(const QString &relativeName, const QString &destination)
0494 {
0495     const QString absoluteFilename = QFileInfo(relativeName).absoluteFilePath();
0496     const QString destinationFilename = destination + relativeName;
0497 
0498     struct stat st;
0499 #ifndef Q_OS_WIN
0500     // #253059: Even if we use archive_read_disk_entry_from_file,
0501     //          libarchive may have been compiled without HAVE_LSTAT,
0502     //          or something may have caused it to follow symlinks, in
0503     //          which case stat() will be called. To avoid this, we
0504     //          call lstat() ourselves.
0505 
0506     lstat(QFile::encodeName(absoluteFilename).constData(), &st); // krazy:exclude=syscalls
0507 #endif
0508 
0509     struct archive_entry *entry = archive_entry_new();
0510     archive_entry_set_pathname(entry, QFile::encodeName(destinationFilename).constData());
0511     archive_entry_copy_sourcepath(entry, QFile::encodeName(absoluteFilename).constData());
0512     archive_read_disk_entry_from_file(m_archiveReadDisk.data(), entry, -1, &st);
0513 
0514     const auto returnCode = archive_write_header(m_archiveWriter.data(), entry);
0515     if (returnCode == ARCHIVE_OK) {
0516         // If the whole archive is extracted and the total filesize is
0517         // available, we use partial progress.
0518         copyData(absoluteFilename, m_archiveWriter.data(), false);
0519     } else {
0520         qCCritical(ARK) << "Writing header failed with error code " << returnCode;
0521         qCCritical(ARK) << "Error while writing..." << archive_error_string(m_archiveWriter.data()) << "(error no =" << archive_errno(m_archiveWriter.data())
0522                         << ')';
0523 
0524         Q_EMIT error(i18nc("@info Error in a message box", "Could not compress entry."));
0525 
0526         archive_entry_free(entry);
0527 
0528         return false;
0529     }
0530 
0531     if (QThread::currentThread()->isInterruptionRequested()) {
0532         archive_entry_free(entry);
0533         return false;
0534     }
0535 
0536     m_writtenFiles.push_back(destinationFilename);
0537 
0538     emitEntryFromArchiveEntry(entry);
0539 
0540     archive_entry_free(entry);
0541 
0542     return true;
0543 }
0544 
0545 #include "moc_readwritelibarchiveplugin.cpp"
0546 #include "readwritelibarchiveplugin.moc"