File indexing completed on 2024-05-05 05:50:40
0001 /* 0002 SPDX-FileCopyrightText: 2009 Harald Hvaal <haraldhv@stud.ntnu.no> 0003 SPDX-FileCopyrightText: 2009-2011 Raphael Kubo da Costa <rakuco@FreeBSD.org> 0004 SPDX-FileCopyrightText: 2016 Vladyslav Batyrenko <mvlabat@gmail.com> 0005 0006 SPDX-License-Identifier: BSD-2-Clause 0007 */ 0008 0009 #include "cliinterface.h" 0010 #include "ark_debug.h" 0011 #include "queries.h" 0012 0013 #ifdef Q_OS_WIN 0014 #include <KProcess> 0015 #else 0016 #include <KPtyDevice> 0017 #include <KPtyProcess> 0018 #endif 0019 0020 #include <KLocalizedString> 0021 0022 #include <QCoreApplication> 0023 #include <QDir> 0024 #include <QDirIterator> 0025 #include <QFile> 0026 #include <QMimeDatabase> 0027 #include <QStandardPaths> 0028 #include <QTemporaryDir> 0029 #include <QTemporaryFile> 0030 #include <QThread> 0031 #include <QUrl> 0032 0033 namespace Kerfuffle 0034 { 0035 CliInterface::CliInterface(QObject *parent, const QVariantList &args) 0036 : ReadWriteArchiveInterface(parent, args) 0037 { 0038 // because this interface uses the event loop 0039 setWaitForFinishedSignal(true); 0040 0041 if (QMetaType::type("QProcess::ExitStatus") == 0) { 0042 qRegisterMetaType<QProcess::ExitStatus>("QProcess::ExitStatus"); 0043 } 0044 m_cliProps = new CliProperties(this, m_metaData, mimetype()); 0045 } 0046 0047 CliInterface::~CliInterface() 0048 { 0049 Q_ASSERT(!m_process); 0050 } 0051 0052 void CliInterface::setListEmptyLines(bool emptyLines) 0053 { 0054 m_listEmptyLines = emptyLines; 0055 } 0056 0057 int CliInterface::copyRequiredSignals() const 0058 { 0059 return 2; 0060 } 0061 0062 bool CliInterface::list() 0063 { 0064 resetParsing(); 0065 m_operationMode = List; 0066 m_numberOfEntries = 0; 0067 0068 // To compute progress. 0069 m_archiveSizeOnDisk = static_cast<qulonglong>(QFileInfo(filename()).size()); 0070 connect(this, &ReadOnlyArchiveInterface::entry, this, &CliInterface::onEntry); 0071 0072 return runProcess(m_cliProps->property("listProgram").toString(), m_cliProps->listArgs(filename(), password())); 0073 } 0074 0075 bool CliInterface::extractFiles(const QVector<Archive::Entry *> &files, const QString &destinationDirectory, const ExtractionOptions &options) 0076 { 0077 qCDebug(ARK) << "destination directory:" << destinationDirectory; 0078 0079 m_operationMode = Extract; 0080 m_extractionOptions = options; 0081 m_extractedFiles = files; 0082 m_extractDestDir = destinationDirectory; 0083 0084 if (!m_cliProps->property("passwordSwitch").toStringList().isEmpty() && options.encryptedArchiveHint() && password().isEmpty()) { 0085 qCDebug(ARK) << "Password hint enabled, querying user"; 0086 if (!passwordQuery()) { 0087 return false; 0088 } 0089 } 0090 0091 QUrl destDir = QUrl(destinationDirectory); 0092 m_oldWorkingDirExtraction = QDir::currentPath(); 0093 QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); 0094 0095 const bool useTmpExtractDir = options.isDragAndDropEnabled() || options.alwaysUseTempDir(); 0096 0097 if (useTmpExtractDir) { 0098 // Create an hidden temp folder in the current directory. 0099 m_extractTempDir.reset(new QTemporaryDir(QStringLiteral(".%1-").arg(QCoreApplication::applicationName()))); 0100 0101 qCDebug(ARK) << "Using temporary extraction dir:" << m_extractTempDir->path(); 0102 if (!m_extractTempDir->isValid()) { 0103 qCDebug(ARK) << "Creation of temporary directory failed."; 0104 Q_EMIT finished(false); 0105 return false; 0106 } 0107 destDir = QUrl(m_extractTempDir->path()); 0108 QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); 0109 } 0110 0111 return runProcess(m_cliProps->property("extractProgram").toString(), 0112 m_cliProps->extractArgs(filename(), extractFilesList(files), options.preservePaths(), password())); 0113 } 0114 0115 bool CliInterface::addFiles(const QVector<Archive::Entry *> &files, 0116 const Archive::Entry *destination, 0117 const CompressionOptions &options, 0118 uint numberOfEntriesToAdd) 0119 { 0120 Q_UNUSED(numberOfEntriesToAdd) 0121 0122 m_operationMode = Add; 0123 0124 QVector<Archive::Entry *> filesToPass = QVector<Archive::Entry *>(); 0125 // If destination path is specified, we have recreate its structure inside the temp directory 0126 // and then place symlinks of targeted files there. 0127 const QString destinationPath = (destination == nullptr) ? QString() : destination->fullPath(); 0128 0129 qCDebug(ARK) << "Adding" << files.count() << "file(s) to destination:" << destinationPath; 0130 0131 if (!destinationPath.isEmpty()) { 0132 m_extractTempDir.reset(new QTemporaryDir()); 0133 const QString absoluteDestinationPath = m_extractTempDir->path() + QLatin1Char('/') + destinationPath; 0134 0135 QDir qDir; 0136 qDir.mkpath(absoluteDestinationPath); 0137 0138 QObject *preservedParent = nullptr; 0139 for (Archive::Entry *file : files) { 0140 // The entries may have parent. We have to save and apply it to our new entry in order to prevent memory 0141 // leaks. 0142 if (preservedParent == nullptr) { 0143 preservedParent = file->parent(); 0144 } 0145 0146 const QString filePath = QDir::currentPath() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); 0147 const QString newFilePath = absoluteDestinationPath + file->fullPath(NoTrailingSlash); 0148 if (QFile::link(filePath, newFilePath)) { 0149 qCDebug(ARK) << "Symlink's created:" << filePath << newFilePath; 0150 } else { 0151 qCDebug(ARK) << "Can't create symlink" << filePath << newFilePath; 0152 Q_EMIT finished(false); 0153 return false; 0154 } 0155 } 0156 0157 qCDebug(ARK) << "Changing working dir again to " << m_extractTempDir->path(); 0158 QDir::setCurrent(m_extractTempDir->path()); 0159 0160 filesToPass.push_back(new Archive::Entry(preservedParent, destinationPath.split(QLatin1Char('/'), Qt::SkipEmptyParts).at(0))); 0161 } else { 0162 filesToPass = files; 0163 } 0164 0165 if (!m_cliProps->property("passwordSwitch").toString().isEmpty() && options.encryptedArchiveHint() && password().isEmpty()) { 0166 qCDebug(ARK) << "Password hint enabled, querying user"; 0167 if (!passwordQuery()) { 0168 return false; 0169 } 0170 } 0171 0172 return runProcess(m_cliProps->property("addProgram").toString(), 0173 m_cliProps->addArgs(filename(), 0174 entryFullPaths(filesToPass, NoTrailingSlash), 0175 password(), 0176 isHeaderEncryptionEnabled(), 0177 options.compressionLevel(), 0178 options.compressionMethod(), 0179 options.encryptionMethod(), 0180 options.volumeSize())); 0181 } 0182 0183 bool CliInterface::moveFiles(const QVector<Archive::Entry *> &files, Archive::Entry *destination, const CompressionOptions &options) 0184 { 0185 Q_UNUSED(options); 0186 0187 m_operationMode = Move; 0188 0189 m_removedFiles = files; 0190 QVector<Archive::Entry *> withoutChildren = entriesWithoutChildren(files); 0191 setNewMovedFiles(files, destination, withoutChildren.count()); 0192 0193 return runProcess(m_cliProps->property("moveProgram").toString(), m_cliProps->moveArgs(filename(), withoutChildren, destination, password())); 0194 } 0195 0196 bool CliInterface::copyFiles(const QVector<Archive::Entry *> &files, Archive::Entry *destination, const CompressionOptions &options) 0197 { 0198 m_oldWorkingDir = QDir::currentPath(); 0199 m_tempWorkingDir.reset(new QTemporaryDir()); 0200 m_tempAddDir.reset(new QTemporaryDir()); 0201 QDir::setCurrent(m_tempWorkingDir->path()); 0202 m_passedFiles = files; 0203 m_passedDestination = destination; 0204 m_passedOptions = options; 0205 m_numberOfEntries = 0; 0206 0207 m_subOperation = Extract; 0208 connect(this, &CliInterface::finished, this, &CliInterface::continueCopying); 0209 0210 return extractFiles(files, QDir::currentPath(), ExtractionOptions()); 0211 } 0212 0213 bool CliInterface::deleteFiles(const QVector<Archive::Entry *> &files) 0214 { 0215 m_operationMode = Delete; 0216 0217 m_removedFiles = files; 0218 0219 return runProcess(m_cliProps->property("deleteProgram").toString(), m_cliProps->deleteArgs(filename(), files, password())); 0220 } 0221 0222 bool CliInterface::testArchive() 0223 { 0224 resetParsing(); 0225 m_operationMode = Test; 0226 0227 return runProcess(m_cliProps->property("testProgram").toString(), m_cliProps->testArgs(filename(), password())); 0228 } 0229 0230 bool CliInterface::runProcess(const QString &programName, const QStringList &arguments) 0231 { 0232 Q_ASSERT(!m_process); 0233 0234 QString programPath = QStandardPaths::findExecutable(programName); 0235 if (programPath.isEmpty()) { 0236 Q_EMIT error(xi18nc("@info", "Failed to locate program <filename>%1</filename> on disk.", programName)); 0237 Q_EMIT finished(false); 0238 return false; 0239 } 0240 0241 qCDebug(ARK) << "Executing" << programPath << arguments << "within directory" << QDir::currentPath(); 0242 0243 #ifdef Q_OS_WIN 0244 m_process = new KProcess; 0245 #else 0246 m_process = new KPtyProcess; 0247 m_process->setPtyChannels(KPtyProcess::StdinChannel); 0248 #endif 0249 0250 m_process->setOutputChannelMode(KProcess::MergedChannels); 0251 m_process->setNextOpenMode(QIODevice::ReadWrite | QIODevice::Unbuffered | QIODevice::Text); 0252 m_process->setProgram(programPath, arguments); 0253 0254 m_readyStdOutConnection = connect(m_process, &QProcess::readyReadStandardOutput, this, [=]() { 0255 readStdout(); 0256 }); 0257 0258 if (m_operationMode == Extract) { 0259 // Extraction jobs need a dedicated post-processing function. 0260 connect(m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &CliInterface::extractProcessFinished); 0261 } else { 0262 connect(m_process, QOverload<int, QProcess::ExitStatus>::of(&QProcess::finished), this, &CliInterface::processFinished); 0263 } 0264 0265 m_stdOutData.clear(); 0266 0267 m_process->start(); 0268 0269 return true; 0270 } 0271 0272 void CliInterface::processFinished(int exitCode, QProcess::ExitStatus exitStatus) 0273 { 0274 m_exitCode = exitCode; 0275 qCDebug(ARK) << "Process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; 0276 0277 if (m_process) { 0278 // handle all the remaining data in the process 0279 readStdout(true); 0280 0281 delete m_process; 0282 m_process = nullptr; 0283 } 0284 0285 // #193908 - #222392 0286 // Don't emit finished() if the job was killed quietly. 0287 if (m_abortingOperation) { 0288 return; 0289 } 0290 0291 if (m_operationMode == Delete || m_operationMode == Move) { 0292 const QStringList removedFullPaths = entryFullPaths(m_removedFiles); 0293 for (const QString &fullPath : removedFullPaths) { 0294 Q_EMIT entryRemoved(fullPath); 0295 } 0296 for (Archive::Entry *e : std::as_const(m_newMovedFiles)) { 0297 Q_EMIT entry(e); 0298 } 0299 m_newMovedFiles.clear(); 0300 } 0301 0302 if (m_operationMode == Add && !isMultiVolume()) { 0303 list(); 0304 } else if (m_operationMode == List && isCorrupt()) { 0305 Kerfuffle::LoadCorruptQuery query(filename()); 0306 query.execute(); 0307 if (!query.responseYes()) { 0308 Q_EMIT cancelled(); 0309 Q_EMIT finished(false); 0310 } else { 0311 Q_EMIT progress(1.0); 0312 Q_EMIT finished(true); 0313 } 0314 } else { 0315 Q_EMIT progress(1.0); 0316 Q_EMIT finished(true); 0317 } 0318 } 0319 0320 void CliInterface::extractProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) 0321 { 0322 Q_ASSERT(m_operationMode == Extract); 0323 0324 m_exitCode = exitCode; 0325 qCDebug(ARK) << "Extraction process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; 0326 0327 if (m_process) { 0328 // Handle all the remaining data in the process. 0329 readStdout(true); 0330 0331 delete m_process; 0332 m_process = nullptr; 0333 } 0334 0335 // Don't emit finished() if the job was killed quietly. 0336 if (m_abortingOperation) { 0337 return; 0338 } 0339 0340 if (m_extractionOptions.alwaysUseTempDir()) { 0341 // unar exits with code 1 if extraction fails. 0342 // This happens at least with wrong passwords or not enough space in the destination folder. 0343 if (m_exitCode == 1) { 0344 if (password().isEmpty()) { 0345 qCWarning(ARK) << "Extraction aborted, destination folder might not have enough space."; 0346 Q_EMIT error(i18n("Extraction failed. Make sure that enough space is available.")); 0347 } else { 0348 qCWarning(ARK) << "Extraction aborted, either the password is wrong or the destination folder doesn't have enough space."; 0349 Q_EMIT error(i18n("Extraction failed. Make sure you provided the correct password and that enough space is available.")); 0350 setPassword(QString()); 0351 } 0352 cleanUpExtracting(); 0353 Q_EMIT finished(false); 0354 return; 0355 } 0356 0357 if (!m_extractionOptions.isDragAndDropEnabled()) { 0358 if (!moveToDestination(QDir::current(), QDir(m_extractDestDir), m_extractionOptions.preservePaths())) { 0359 Q_EMIT error(i18ncp("@info", 0360 "Could not move the extracted file to the destination directory.", 0361 "Could not move the extracted files to the destination directory.", 0362 m_extractedFiles.size())); 0363 cleanUpExtracting(); 0364 Q_EMIT finished(false); 0365 return; 0366 } 0367 0368 cleanUpExtracting(); 0369 } 0370 } 0371 0372 if (m_extractionOptions.isDragAndDropEnabled()) { 0373 const bool droppedFilesMoved = moveDroppedFilesToDest(m_extractedFiles, m_extractDestDir); 0374 if (!droppedFilesMoved) { 0375 cleanUpExtracting(); 0376 return; 0377 } 0378 0379 cleanUpExtracting(); 0380 } 0381 0382 // #395939: make sure we *always* restore the old working dir. 0383 restoreWorkingDirExtraction(); 0384 0385 Q_EMIT progress(1.0); 0386 Q_EMIT finished(true); 0387 } 0388 0389 void CliInterface::continueCopying(bool result) 0390 { 0391 if (!result) { 0392 finishCopying(false); 0393 return; 0394 } 0395 0396 switch (m_subOperation) { 0397 case Extract: 0398 m_subOperation = Add; 0399 m_passedFiles = entriesWithoutChildren(m_passedFiles); 0400 if (!setAddedFiles() || !addFiles(m_tempAddedFiles, m_passedDestination, m_passedOptions)) { 0401 finishCopying(false); 0402 } 0403 break; 0404 case Add: 0405 finishCopying(true); 0406 break; 0407 default: 0408 Q_ASSERT(false); 0409 } 0410 } 0411 0412 bool CliInterface::moveDroppedFilesToDest(const QVector<Archive::Entry *> &files, const QString &finalDest) 0413 { 0414 // Move extracted files from a QTemporaryDir to the final destination. 0415 0416 QDir finalDestDir(finalDest); 0417 qCDebug(ARK) << "Setting final dir to" << finalDest; 0418 0419 bool overwriteAll = false; 0420 bool skipAll = false; 0421 0422 for (const Archive::Entry *file : files) { 0423 QFileInfo relEntry(file->fullPath().remove(file->rootNode)); 0424 QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + file->fullPath()); 0425 QFileInfo absDestEntry(finalDestDir.path() + QLatin1Char('/') + relEntry.filePath()); 0426 0427 if (absSourceEntry.isDir()) { 0428 // For directories, just create the path. 0429 if (!finalDestDir.mkpath(relEntry.filePath())) { 0430 qCWarning(ARK) << "Failed to create directory" << relEntry.filePath() << "in final destination."; 0431 } 0432 0433 } else { 0434 // If destination file exists, prompt the user. 0435 if (absDestEntry.exists()) { 0436 qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; 0437 0438 if (!skipAll && !overwriteAll) { 0439 Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); 0440 query.setNoRenameMode(true); 0441 query.execute(); 0442 0443 if (query.responseOverwrite() || query.responseOverwriteAll()) { 0444 if (query.responseOverwriteAll()) { 0445 overwriteAll = true; 0446 } 0447 if (!QFile::remove(absDestEntry.absoluteFilePath())) { 0448 qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); 0449 } 0450 0451 } else if (query.responseSkip() || query.responseAutoSkip()) { 0452 if (query.responseAutoSkip()) { 0453 skipAll = true; 0454 } 0455 continue; 0456 0457 } else if (query.responseCancelled()) { 0458 Q_EMIT cancelled(); 0459 Q_EMIT finished(false); 0460 return false; 0461 } 0462 0463 } else if (skipAll) { 0464 continue; 0465 } else if (overwriteAll) { 0466 if (!QFile::remove(absDestEntry.absoluteFilePath())) { 0467 qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); 0468 } 0469 } 0470 } 0471 0472 // Create any parent directories. 0473 if (!finalDestDir.mkpath(relEntry.path())) { 0474 qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); 0475 } 0476 0477 // Move files to the final destination. 0478 if (!QFile(absSourceEntry.absoluteFilePath()).rename(absDestEntry.absoluteFilePath())) { 0479 qCWarning(ARK) << "Failed to move file" << absSourceEntry.filePath() << "to final destination."; 0480 Q_EMIT error(i18ncp("@info", 0481 "Could not move the extracted file to the destination directory.", 0482 "Could not move the extracted files to the destination directory.", 0483 m_extractedFiles.size())); 0484 Q_EMIT finished(false); 0485 return false; 0486 } 0487 } 0488 } 0489 return true; 0490 } 0491 0492 bool CliInterface::isEmptyDir(const QDir &dir) 0493 { 0494 QDir d = dir; 0495 d.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); 0496 0497 return d.count() == 0; 0498 } 0499 0500 void CliInterface::cleanUpExtracting() 0501 { 0502 restoreWorkingDirExtraction(); 0503 m_extractTempDir.reset(); 0504 } 0505 0506 void CliInterface::restoreWorkingDirExtraction() 0507 { 0508 if (m_oldWorkingDirExtraction.isEmpty()) { 0509 return; 0510 } 0511 0512 if (!QDir::setCurrent(m_oldWorkingDirExtraction)) { 0513 qCWarning(ARK) << "Failed to restore old working directory:" << m_oldWorkingDirExtraction; 0514 } else { 0515 m_oldWorkingDirExtraction.clear(); 0516 } 0517 } 0518 0519 void CliInterface::finishCopying(bool result) 0520 { 0521 disconnect(this, &CliInterface::finished, this, &CliInterface::continueCopying); 0522 Q_EMIT progress(1.0); 0523 Q_EMIT finished(result); 0524 cleanUp(); 0525 } 0526 0527 bool CliInterface::moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths) 0528 { 0529 qCDebug(ARK) << "Moving extracted files from temp dir" << tempDir.path() << "to final destination" << destDir.path(); 0530 0531 bool overwriteAll = false; 0532 bool skipAll = false; 0533 0534 QDirIterator dirIt(tempDir.path(), QDir::AllEntries | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); 0535 while (dirIt.hasNext()) { 0536 dirIt.next(); 0537 0538 // We skip directories if: 0539 // 1. We are not preserving paths 0540 // 2. The dir is not empty. Only empty directories need to be explicitly moved. 0541 // The non-empty ones are created by QDir::mkpath() below. 0542 if (dirIt.fileInfo().isDir()) { 0543 if (!preservePaths || !isEmptyDir(QDir(dirIt.filePath()))) { 0544 continue; 0545 } 0546 } 0547 0548 QFileInfo relEntry; 0549 if (preservePaths) { 0550 relEntry = QFileInfo(dirIt.filePath().remove(tempDir.path() + QLatin1Char('/'))); 0551 } else { 0552 relEntry = QFileInfo(dirIt.fileName()); 0553 } 0554 0555 QFileInfo absDestEntry(destDir.path() + QLatin1Char('/') + relEntry.filePath()); 0556 0557 if (absDestEntry.exists()) { 0558 qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; 0559 0560 Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); 0561 query.setNoRenameMode(true); 0562 query.execute(); 0563 0564 if (query.responseOverwrite() || query.responseOverwriteAll()) { 0565 if (query.responseOverwriteAll()) { 0566 overwriteAll = true; 0567 } 0568 if (!QFile::remove(absDestEntry.absoluteFilePath())) { 0569 qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); 0570 } 0571 0572 } else if (query.responseSkip() || query.responseAutoSkip()) { 0573 if (query.responseAutoSkip()) { 0574 skipAll = true; 0575 } 0576 continue; 0577 } else if (query.responseCancelled()) { 0578 qCDebug(ARK) << "Copy action cancelled."; 0579 return false; 0580 } 0581 } else if (skipAll) { 0582 continue; 0583 } else if (overwriteAll) { 0584 if (!QFile::remove(absDestEntry.absoluteFilePath())) { 0585 qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); 0586 } 0587 } 0588 0589 if (preservePaths) { 0590 // Create any parent directories. 0591 if (!destDir.mkpath(relEntry.path())) { 0592 qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); 0593 } 0594 } 0595 0596 // Move file to the final destination. 0597 if (!QFile(dirIt.filePath()).rename(absDestEntry.absoluteFilePath())) { 0598 qCWarning(ARK) << "Failed to move file" << dirIt.filePath() << "to final destination."; 0599 return false; 0600 } 0601 } 0602 0603 return true; 0604 } 0605 0606 void CliInterface::setNewMovedFiles(const QVector<Archive::Entry *> &entries, const Archive::Entry *destination, int entriesWithoutChildren) 0607 { 0608 m_newMovedFiles.clear(); 0609 QMap<QString, const Archive::Entry *> entryMap; 0610 for (const Archive::Entry *entry : entries) { 0611 entryMap.insert(entry->fullPath(), entry); 0612 } 0613 0614 QString lastFolder; 0615 0616 QString newPath; 0617 int nameLength = 0; 0618 for (const Archive::Entry *entry : std::as_const(entryMap)) { 0619 if (!lastFolder.isEmpty() && entry->fullPath().startsWith(lastFolder)) { 0620 // Replace last moved or copied folder path with destination path. 0621 int charsCount = entry->fullPath().length() - lastFolder.length(); 0622 if (entriesWithoutChildren > 1) { 0623 charsCount += nameLength; 0624 } 0625 newPath = destination->fullPath() + entry->fullPath().right(charsCount); 0626 } else { 0627 if (entriesWithoutChildren > 1) { 0628 newPath = destination->fullPath() + entry->name(); 0629 } else { 0630 // If there is only one passed file in the list, 0631 // we have to use destination as newPath. 0632 newPath = destination->fullPath(NoTrailingSlash); 0633 } 0634 if (entry->isDir()) { 0635 newPath += QLatin1Char('/'); 0636 nameLength = entry->name().length() + 1; // plus slash 0637 lastFolder = entry->fullPath(); 0638 } else { 0639 nameLength = 0; 0640 lastFolder = QString(); 0641 } 0642 } 0643 Archive::Entry *newEntry = new Archive::Entry(nullptr); 0644 newEntry->copyMetaData(entry); 0645 newEntry->setFullPath(newPath); 0646 m_newMovedFiles << newEntry; 0647 } 0648 } 0649 0650 QStringList CliInterface::extractFilesList(const QVector<Archive::Entry *> &entries) const 0651 { 0652 QStringList filesList; 0653 for (const Archive::Entry *e : entries) { 0654 filesList << escapeFileName(e->fullPath(NoTrailingSlash)); 0655 } 0656 0657 return filesList; 0658 } 0659 0660 void CliInterface::killProcess(bool emitFinished) 0661 { 0662 // TODO: Would be good to unit test #304764/#304178. 0663 0664 if (!m_process) { 0665 return; 0666 } 0667 0668 // waitForFinished() will enter QT's event loop. Disconnect the readyReadStandardOutput 0669 // signal, to avoid an endless recursion, if the process keeps spamming output on stdout. 0670 disconnect(m_readyStdOutConnection); 0671 0672 m_abortingOperation = !emitFinished; 0673 0674 // Give some time for the application to finish gracefully 0675 if (!m_process->waitForFinished(5)) { 0676 m_process->kill(); 0677 0678 // It takes a few hundred ms for the process to be killed. 0679 m_process->waitForFinished(1000); 0680 } 0681 0682 m_abortingOperation = false; 0683 } 0684 0685 bool CliInterface::passwordQuery() 0686 { 0687 Kerfuffle::PasswordNeededQuery query(filename()); 0688 query.execute(); 0689 0690 if (query.responseCancelled()) { 0691 Q_EMIT cancelled(); 0692 // There is no process running, so finished() must be emitted manually. 0693 Q_EMIT finished(false); 0694 return false; 0695 } 0696 0697 setPassword(query.password()); 0698 return true; 0699 } 0700 0701 void CliInterface::cleanUp() 0702 { 0703 qDeleteAll(m_tempAddedFiles); 0704 m_tempAddedFiles.clear(); 0705 QDir::setCurrent(m_oldWorkingDir); 0706 m_tempWorkingDir.reset(); 0707 m_tempAddDir.reset(); 0708 } 0709 0710 void CliInterface::readStdout(bool handleAll) 0711 { 0712 // when hacking this function, please remember the following: 0713 //- standard output comes in unpredictable chunks, this is why 0714 // you can never know if the last part of the output is a complete line or not 0715 //- console applications are not really consistent about what 0716 // characters they send out (newline, backspace, carriage return, 0717 // etc), so keep in mind that this function is supposed to handle 0718 // all those special cases and be the lowest common denominator 0719 0720 if (m_abortingOperation) 0721 return; 0722 0723 Q_ASSERT(m_process); 0724 0725 if (!m_process->bytesAvailable()) { 0726 // if process has no more data, we can just bail out 0727 return; 0728 } 0729 0730 QByteArray dd = m_process->readAllStandardOutput(); 0731 m_stdOutData += dd; 0732 0733 QList<QByteArray> lines = m_stdOutData.split('\n'); 0734 0735 // The reason for this check is that archivers often do not end 0736 // queries (such as file exists, wrong password) on a new line, but 0737 // freeze waiting for input. So we check for errors on the last line in 0738 // all cases. 0739 // TODO: QLatin1String() might not be the best choice here. 0740 // The call to handleLine() at the end of the method uses 0741 // QString::fromLocal8Bit(), for example. 0742 // TODO: The same check methods are called in handleLine(), this 0743 // is suboptimal. 0744 0745 bool wrongPasswordMessage = isWrongPasswordMsg(QLatin1String(lines.last())); 0746 0747 bool foundErrorMessage = (wrongPasswordMessage || isDiskFullMsg(QLatin1String(lines.last())) || isFileExistsMsg(QLatin1String(lines.last()))) 0748 || isPasswordPrompt(QLatin1String(lines.last())); 0749 0750 if (foundErrorMessage) { 0751 handleAll = true; 0752 } 0753 0754 if (wrongPasswordMessage) { 0755 setPassword(QString()); 0756 } 0757 0758 // this is complex, here's an explanation: 0759 // if there is no newline, then there is no guaranteed full line to 0760 // handle in the output. The exception is that it is supposed to handle 0761 // all the data, OR if there's been an error message found in the 0762 // partial data. 0763 if (lines.size() == 1 && !handleAll) { 0764 return; 0765 } 0766 0767 if (handleAll) { 0768 m_stdOutData.clear(); 0769 } else { 0770 // because the last line might be incomplete we leave it for now 0771 // note, this last line may be an empty string if the stdoutdata ends 0772 // with a newline 0773 m_stdOutData = lines.takeLast(); 0774 } 0775 0776 for (const QByteArray &line : std::as_const(lines)) { 0777 if (!line.isEmpty() || (m_listEmptyLines && m_operationMode == List)) { 0778 if (!handleLine(QString::fromLocal8Bit(line))) { 0779 killProcess(); 0780 return; 0781 } 0782 } 0783 } 0784 } 0785 0786 bool CliInterface::setAddedFiles() 0787 { 0788 QDir::setCurrent(m_tempAddDir->path()); 0789 for (const Archive::Entry *file : std::as_const(m_passedFiles)) { 0790 const QString oldPath = m_tempWorkingDir->path() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); 0791 const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + file->name(); 0792 if (!QFile::rename(oldPath, newPath)) { 0793 return false; 0794 } 0795 m_tempAddedFiles << new Archive::Entry(nullptr, file->name()); 0796 } 0797 return true; 0798 } 0799 0800 bool CliInterface::handleLine(const QString &line) 0801 { 0802 // TODO: This should be implemented by each plugin; the way progress is 0803 // shown by each CLI application is subject to a lot of variation. 0804 if ((m_operationMode == Extract || m_operationMode == Add) && m_cliProps->property("captureProgress").toBool()) { 0805 // read the percentage 0806 int pos = line.indexOf(QLatin1Char('%')); 0807 if (pos > 1) { 0808 const int percentage = QStringView(line).mid(pos - 2, 2).toInt(); 0809 Q_EMIT progress(float(percentage) / 100); 0810 return true; 0811 } 0812 } 0813 0814 if (m_operationMode == Extract) { 0815 if (isPasswordPrompt(line)) { 0816 qCDebug(ARK) << "Found a password prompt"; 0817 0818 Kerfuffle::PasswordNeededQuery query(filename()); 0819 query.execute(); 0820 0821 if (query.responseCancelled()) { 0822 Q_EMIT cancelled(); 0823 return false; 0824 } 0825 0826 setPassword(query.password()); 0827 0828 const QString response(password() + QLatin1Char('\n')); 0829 writeToProcess(response.toLocal8Bit()); 0830 0831 return true; 0832 } 0833 0834 if (isDiskFullMsg(line)) { 0835 qCWarning(ARK) << "Found disk full message:" << line; 0836 Q_EMIT error(i18nc("@info", "Extraction failed because the disk is full.")); 0837 return false; 0838 } 0839 0840 if (isWrongPasswordMsg(line)) { 0841 qCWarning(ARK) << "Wrong password!"; 0842 setPassword(QString()); 0843 Q_EMIT error(i18nc("@info", "Extraction failed: Incorrect password")); 0844 return false; 0845 } 0846 0847 if (handleFileExistsMessage(line)) { 0848 return true; 0849 } 0850 0851 return readExtractLine(line); 0852 } 0853 0854 if (m_operationMode == List) { 0855 if (isPasswordPrompt(line)) { 0856 qCDebug(ARK) << "Found a password prompt"; 0857 0858 Kerfuffle::PasswordNeededQuery query(filename()); 0859 query.execute(); 0860 0861 if (query.responseCancelled()) { 0862 Q_EMIT cancelled(); 0863 return false; 0864 } 0865 0866 setPassword(query.password()); 0867 0868 const QString response(password() + QLatin1Char('\n')); 0869 writeToProcess(response.toLocal8Bit()); 0870 0871 return true; 0872 } 0873 0874 if (isWrongPasswordMsg(line)) { 0875 qCWarning(ARK) << "Wrong password!"; 0876 setPassword(QString()); 0877 Q_EMIT error(i18n("Incorrect password.")); 0878 return false; 0879 } 0880 0881 if (isCorruptArchiveMsg(line)) { 0882 qCWarning(ARK) << "Archive corrupt"; 0883 setCorrupt(true); 0884 // Special case: corrupt is not a "fatal" error so we return true here. 0885 return true; 0886 } 0887 0888 return readListLine(line); 0889 } 0890 0891 if (m_operationMode == Delete) { 0892 return readDeleteLine(line); 0893 } 0894 0895 if (m_operationMode == Test) { 0896 if (isPasswordPrompt(line)) { 0897 qCDebug(ARK) << "Found a password prompt"; 0898 0899 Q_EMIT error(i18n("Ark does not currently support testing this archive.")); 0900 return false; 0901 } 0902 0903 if (m_cliProps->isTestPassedMsg(line)) { 0904 qCDebug(ARK) << "Test successful"; 0905 Q_EMIT testSuccess(); 0906 return true; 0907 } 0908 } 0909 0910 if (m_operationMode == Move && isNewMovedFileNamesMsg(line)) { 0911 QString fNames; 0912 for (auto entry : qAsConst(m_newMovedFiles)) { 0913 fNames += QStringLiteral("%1\n").arg(entry->fullPath(NoTrailingSlash)); 0914 } 0915 writeToProcess(fNames.toLocal8Bit()); 0916 return true; 0917 } 0918 0919 return true; 0920 } 0921 0922 bool CliInterface::readDeleteLine(const QString &line) 0923 { 0924 Q_UNUSED(line); 0925 return true; 0926 } 0927 0928 bool CliInterface::handleFileExistsMessage(const QString &line) 0929 { 0930 // Check for a filename and store it. 0931 if (isFileExistsFileName(line)) { 0932 const QStringList fileExistsFileNameRegExp = m_cliProps->property("fileExistsFileNameRegExp").toStringList(); 0933 for (const QString &pattern : fileExistsFileNameRegExp) { 0934 const QRegularExpression rxFileNamePattern(pattern); 0935 const QRegularExpressionMatch rxMatch = rxFileNamePattern.match(line); 0936 0937 if (rxMatch.hasMatch()) { 0938 m_storedFileName = rxMatch.captured(1); 0939 qCWarning(ARK) << "Detected existing file:" << m_storedFileName; 0940 } 0941 } 0942 } 0943 0944 if (!isFileExistsMsg(line)) { 0945 return false; 0946 } 0947 0948 Kerfuffle::OverwriteQuery query(QDir::current().path() + QLatin1Char('/') + m_storedFileName); 0949 query.setNoRenameMode(true); 0950 query.execute(); 0951 0952 QString responseToProcess; 0953 const QStringList choices = m_cliProps->property("fileExistsInput").toStringList(); 0954 0955 if (query.responseOverwrite()) { 0956 responseToProcess = choices.at(0); 0957 } else if (query.responseSkip()) { 0958 responseToProcess = choices.at(1); 0959 } else if (query.responseOverwriteAll()) { 0960 responseToProcess = choices.at(2); 0961 } else if (query.responseAutoSkip()) { 0962 responseToProcess = choices.at(3); 0963 } else if (query.responseCancelled()) { 0964 Q_EMIT cancelled(); 0965 if (choices.count() < 5) { // If the program has no way to cancel the extraction, we resort to killing it 0966 return doKill(); 0967 } 0968 responseToProcess = choices.at(4); 0969 } 0970 0971 Q_ASSERT(!responseToProcess.isEmpty()); 0972 0973 responseToProcess += QLatin1Char('\n'); 0974 0975 writeToProcess(responseToProcess.toLocal8Bit()); 0976 0977 return true; 0978 } 0979 0980 bool CliInterface::doKill() 0981 { 0982 if (m_process) { 0983 killProcess(false); 0984 return true; 0985 } 0986 0987 return false; 0988 } 0989 0990 QString CliInterface::escapeFileName(const QString &fileName) const 0991 { 0992 return fileName; 0993 } 0994 0995 QStringList CliInterface::entryPathDestinationPairs(const QVector<Archive::Entry *> &entriesWithoutChildren, const Archive::Entry *destination) 0996 { 0997 QStringList pairList; 0998 if (entriesWithoutChildren.count() > 1) { 0999 for (const Archive::Entry *file : entriesWithoutChildren) { 1000 pairList << file->fullPath(NoTrailingSlash) << destination->fullPath() + file->name(); 1001 } 1002 } else { 1003 pairList << entriesWithoutChildren.at(0)->fullPath(NoTrailingSlash) << destination->fullPath(NoTrailingSlash); 1004 } 1005 return pairList; 1006 } 1007 1008 void CliInterface::writeToProcess(const QByteArray &data) 1009 { 1010 Q_ASSERT(m_process); 1011 Q_ASSERT(!data.isNull()); 1012 1013 qCDebug(ARK) << "Writing" << data << "to the process"; 1014 1015 #ifdef Q_OS_WIN 1016 m_process->write(data); 1017 #else 1018 m_process->pty()->write(data); 1019 #endif 1020 } 1021 1022 bool CliInterface::addComment(const QString &comment) 1023 { 1024 m_operationMode = Comment; 1025 1026 m_commentTempFile.reset(new QTemporaryFile()); 1027 if (!m_commentTempFile->open()) { 1028 qCWarning(ARK) << "Failed to create temporary file for comment"; 1029 Q_EMIT finished(false); 1030 return false; 1031 } 1032 1033 QTextStream stream(m_commentTempFile.data()); 1034 stream << comment << "\n"; 1035 m_commentTempFile->close(); 1036 1037 if (!runProcess(m_cliProps->property("addProgram").toString(), m_cliProps->commentArgs(filename(), m_commentTempFile->fileName()))) { 1038 return false; 1039 } 1040 m_comment = comment; 1041 return true; 1042 } 1043 1044 QString CliInterface::multiVolumeName() const 1045 { 1046 QString oldSuffix = QMimeDatabase().suffixForFileName(filename()); 1047 QString name; 1048 1049 const QStringList multiVolumeSuffix = m_cliProps->property("multiVolumeSuffix").toStringList(); 1050 for (const QString &multiSuffix : multiVolumeSuffix) { 1051 QString newSuffix = multiSuffix; 1052 newSuffix.replace(QStringLiteral("$Suffix"), oldSuffix); 1053 name = filename().remove(oldSuffix).append(newSuffix); 1054 if (QFileInfo::exists(name)) { 1055 break; 1056 } 1057 } 1058 return name; 1059 } 1060 1061 CliProperties *CliInterface::cliProperties() const 1062 { 1063 return m_cliProps; 1064 } 1065 1066 void CliInterface::onEntry(Archive::Entry *archiveEntry) 1067 { 1068 if (archiveEntry->compressedSizeIsSet) { 1069 m_listedSize += archiveEntry->property("compressedSize").toULongLong(); 1070 if (m_listedSize <= m_archiveSizeOnDisk) { 1071 Q_EMIT progress(float(m_listedSize) / float(m_archiveSizeOnDisk)); 1072 } else { 1073 // In case summed compressed size exceeds archive size on disk. 1074 Q_EMIT progress(1); 1075 } 1076 } 1077 } 1078 1079 bool CliInterface::isPasswordPrompt(const QString &line) 1080 { 1081 Q_UNUSED(line); 1082 return false; 1083 } 1084 1085 bool CliInterface::isWrongPasswordMsg(const QString &line) 1086 { 1087 Q_UNUSED(line); 1088 return false; 1089 } 1090 1091 bool CliInterface::isCorruptArchiveMsg(const QString &line) 1092 { 1093 Q_UNUSED(line); 1094 return false; 1095 } 1096 1097 bool CliInterface::isDiskFullMsg(const QString &line) 1098 { 1099 Q_UNUSED(line); 1100 return false; 1101 } 1102 1103 bool CliInterface::isFileExistsMsg(const QString &line) 1104 { 1105 Q_UNUSED(line); 1106 return false; 1107 } 1108 1109 bool CliInterface::isFileExistsFileName(const QString &line) 1110 { 1111 Q_UNUSED(line); 1112 return false; 1113 } 1114 1115 bool CliInterface::isNewMovedFileNamesMsg(const QString &line) 1116 { 1117 Q_UNUSED(line); 1118 return false; 1119 } 1120 1121 } 1122 1123 #include "moc_cliinterface.cpp"