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"