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"