diff --git a/autotests/plugins/clirarplugin/clirartest.cpp b/autotests/plugins/clirarplugin/clirartest.cpp index 0b603872..e0bc6f06 100644 --- a/autotests/plugins/clirarplugin/clirartest.cpp +++ b/autotests/plugins/clirarplugin/clirartest.cpp @@ -1,213 +1,321 @@ /* * Copyright (c) 2011,2014 Raphael Kubo da Costa * Copyright (c) 2015,2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "clirartest.h" #include #include #include #include #include QTEST_GUILESS_MAIN(CliRarTest) using namespace Kerfuffle; void CliRarTest::initTestCase() { qRegisterMetaType(); const auto plugins = KPluginLoader::findPluginsById(QStringLiteral("kerfuffle"), QStringLiteral("kerfuffle_clirar")); if (plugins.size() == 1) { m_pluginMetadata = plugins.at(0); } } void CliRarTest::testArchive_data() { QTest::addColumn("archivePath"); QTest::addColumn("expectedFileName"); QTest::addColumn("isReadOnly"); QTest::addColumn("isSingleFolder"); QTest::addColumn("expectedEncryptionType"); QTest::addColumn("expectedSubfolderName"); QString archivePath = QFINDTESTDATA("data/one_toplevel_folder.rar"); QTest::newRow("archive with one top-level folder") << archivePath << QFileInfo(archivePath).fileName() << false << true << Archive::Unencrypted << QStringLiteral("A"); } void CliRarTest::testArchive() { if (!m_pluginMetadata.isValid()) { QSKIP("Could not find the clirar plugin. Skipping test.", SkipSingle); } QFETCH(QString, archivePath); Archive *archive = Archive::create(archivePath, m_pluginMetadata, this); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not load the clirar plugin. Skipping test.", SkipSingle); } QFETCH(QString, expectedFileName); QCOMPARE(QFileInfo(archive->fileName()).fileName(), expectedFileName); QFETCH(bool, isReadOnly); QCOMPARE(archive->isReadOnly(), isReadOnly); QFETCH(bool, isSingleFolder); QCOMPARE(archive->isSingleFolderArchive(), isSingleFolder); QFETCH(Archive::EncryptionType, expectedEncryptionType); QCOMPARE(archive->encryptionType(), expectedEncryptionType); QFETCH(QString, expectedSubfolderName); QCOMPARE(archive->subfolderName(), expectedSubfolderName); } void CliRarTest::testList_data() { QTest::addColumn("outputTextFile"); QTest::addColumn("expectedEntriesCount"); // Index of some entry to be tested. QTest::addColumn("someEntryIndex"); // Entry metadata. QTest::addColumn("expectedName"); QTest::addColumn("isDirectory"); QTest::addColumn("isPasswordProtected"); QTest::addColumn("symlinkTarget"); QTest::addColumn("expectedSize"); QTest::addColumn("expectedCompressedSize"); QTest::addColumn("expectedTimestamp"); // Unrar 5 tests QTest::newRow("normal-file-unrar5") << QFINDTESTDATA("data/archive-with-symlink-unrar5.txt") << 8 << 2 << QStringLiteral("rartest/file2.txt") << false << false << QString() << (qulonglong) 14 << (qulonglong) 23 << QStringLiteral("2016-03-21T08:57:36"); QTest::newRow("symlink-unrar5") << QFINDTESTDATA("data/archive-with-symlink-unrar5.txt") << 8 << 3 << QStringLiteral("rartest/linktofile1.txt") << false << false << QStringLiteral("file1.txt") << (qulonglong) 9 << (qulonglong) 9 << QStringLiteral("2016-03-21T08:58:16"); QTest::newRow("encrypted-unrar5") << QFINDTESTDATA("data/archive-encrypted-unrar5.txt") << 7 << 2 << QStringLiteral("rartest/file2.txt") << false << true << QString() << (qulonglong) 14 << (qulonglong) 32 << QStringLiteral("2016-03-21T17:03:36"); QTest::newRow("recovery-record-unrar5") << QFINDTESTDATA("data/archive-recovery-record-unrar5.txt") << 3 << 0 << QStringLiteral("file1.txt") << false << false << QString() << (qulonglong) 32 << (qulonglong) 33 << QStringLiteral("2015-07-26T19:04:38"); QTest::newRow("corrupt-archive-unrar5") << QFINDTESTDATA("data/archive-corrupt-file-header-unrar5.txt") << 8 << 6 << QStringLiteral("dir1/") << true << false << QString() << (qulonglong) 0 << (qulonglong) 0 << QStringLiteral("2015-05-14T01:45:24"); // Unrar 4 tests QTest::newRow("normal-file-unrar4") << QFINDTESTDATA("data/archive-with-symlink-unrar4.txt") << 8 << 2 << QStringLiteral("rartest/file2.txt") << false << false << QString() << (qulonglong) 14 << (qulonglong) 23 << QStringLiteral("2016-03-21T08:57:00"); QTest::newRow("symlink-unrar4") << QFINDTESTDATA("data/archive-with-symlink-unrar4.txt") << 8 << 3 << QStringLiteral("rartest/linktofile1.txt") << false << false << QStringLiteral("file1.txt") << (qulonglong) 9 << (qulonglong) 9 << QStringLiteral("2016-03-21T08:58:00"); QTest::newRow("encrypted-unrar4") << QFINDTESTDATA("data/archive-encrypted-unrar4.txt") << 7 << 2 << QStringLiteral("rartest/file2.txt") << false << true << QString() << (qulonglong) 14 << (qulonglong) 32 << QStringLiteral("2016-03-21T17:03:00"); QTest::newRow("recovery-record-unrar4") << QFINDTESTDATA("data/archive-recovery-record-unrar4.txt") << 3 << 0 << QStringLiteral("file1.txt") << false << false << QString() << (qulonglong) 32 << (qulonglong) 33 << QStringLiteral("2015-07-26T19:04:00"); QTest::newRow("corrupt-archive-unrar4") << QFINDTESTDATA("data/archive-corrupt-file-header-unrar4.txt") << 8 << 6 << QStringLiteral("dir1/") << true << false << QString() << (qulonglong) 0 << (qulonglong) 0 << QStringLiteral("2015-05-14T01:45:00"); /* * Check that the plugin will not crash when reading corrupted archives, which * have lines such as "Unexpected end of archive" or "??? - the file header is * corrupt" instead of a file name and the header string after it. * * See bug 262857 and commit 2042997013432cdc6974f5b26d39893a21e21011. */ QTest::newRow("corrupt-archive-unrar3") << QFINDTESTDATA("data/archive-corrupt-file-header-unrar3.txt") << 1 << 0 << QStringLiteral("some-file.ext") << false << false << QString() << (qulonglong) 732522496 << (qulonglong) 14851208 << QStringLiteral("2010-10-29T20:47:00"); } void CliRarTest::testList() { CliPlugin *rarPlugin = new CliPlugin(this, {QStringLiteral("dummy.rar")}); QSignalSpy signalSpy(rarPlugin, SIGNAL(entry(ArchiveEntry))); QFETCH(QString, outputTextFile); QFETCH(int, expectedEntriesCount); QFile outputText(outputTextFile); QVERIFY(outputText.open(QIODevice::ReadOnly)); QTextStream outputStream(&outputText); while (!outputStream.atEnd()) { const QString line(outputStream.readLine()); QVERIFY(rarPlugin->readListLine(line)); } QCOMPARE(signalSpy.count(), expectedEntriesCount); QFETCH(int, someEntryIndex); QVERIFY(someEntryIndex < signalSpy.count()); ArchiveEntry entry = qvariant_cast(signalSpy.at(someEntryIndex).at(0)); QFETCH(QString, expectedName); QCOMPARE(entry[FileName].toString(), expectedName); QFETCH(bool, isDirectory); QCOMPARE(entry[IsDirectory].toBool(), isDirectory); QFETCH(bool, isPasswordProtected); QCOMPARE(entry[IsPasswordProtected].toBool(), isPasswordProtected); QFETCH(QString, symlinkTarget); QCOMPARE(entry[Link].toString(), symlinkTarget); QFETCH(qulonglong, expectedSize); QCOMPARE(entry[Size].toULongLong(), expectedSize); QFETCH(qulonglong, expectedCompressedSize); QCOMPARE(entry[CompressedSize].toULongLong(), expectedCompressedSize); QFETCH(QString, expectedTimestamp); QCOMPARE(entry[Timestamp].toString(), expectedTimestamp); rarPlugin->deleteLater(); } + +void CliRarTest::testExtractArgs_data() +{ + QTest::addColumn("archiveName"); + QTest::addColumn("files"); + QTest::addColumn("preservePaths"); + QTest::addColumn("password"); + QTest::addColumn("rootNode"); + QTest::addColumn("expectedArgs"); + + QTest::newRow("preserve paths, encrypted, root node") + << QStringLiteral("/tmp/foo.rar") + << QVariantList { + QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), + QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + } + << true << QStringLiteral("1234") << QStringLiteral("aDir") + << QStringList { + QStringLiteral("-kb"), + QStringLiteral("-p-"), + QStringLiteral("x"), + QStringLiteral("-p1234"), + QStringLiteral("-apaDir"), + QStringLiteral("/tmp/foo.rar"), + QStringLiteral("aDir/b.txt"), + QStringLiteral("c.txt"), + }; + + QTest::newRow("preserve paths, unencrypted, root node") + << QStringLiteral("/tmp/foo.rar") + << QVariantList { + QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), + QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + } + << true << QString() << QStringLiteral("aDir") + << QStringList { + QStringLiteral("-kb"), + QStringLiteral("-p-"), + QStringLiteral("x"), + QStringLiteral("-apaDir"), + QStringLiteral("/tmp/foo.rar"), + QStringLiteral("aDir/b.txt"), + QStringLiteral("c.txt"), + }; + + QTest::newRow("without paths, encrypted, root node") + << QStringLiteral("/tmp/foo.rar") + << QVariantList { + QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), + QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + } + << false << QStringLiteral("1234") << QStringLiteral("aDir") + << QStringList { + QStringLiteral("-kb"), + QStringLiteral("-p-"), + QStringLiteral("e"), + QStringLiteral("-p1234"), + QStringLiteral("-apaDir"), + QStringLiteral("/tmp/foo.rar"), + QStringLiteral("aDir/b.txt"), + QStringLiteral("c.txt"), + }; + + QTest::newRow("without paths, unencrypted, root node") + << QStringLiteral("/tmp/foo.rar") + << QVariantList { + QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), + QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + } + << false << QString() << QStringLiteral("aDir") + << QStringList { + QStringLiteral("-kb"), + QStringLiteral("-p-"), + QStringLiteral("e"), + QStringLiteral("-apaDir"), + QStringLiteral("/tmp/foo.rar"), + QStringLiteral("aDir/b.txt"), + QStringLiteral("c.txt"), + }; +} + +void CliRarTest::testExtractArgs() +{ + QFETCH(QString, archiveName); + CliPlugin *rarPlugin = new CliPlugin(this, {QVariant(archiveName)}); + QVERIFY(rarPlugin); + + const QStringList extractArgs = { QStringLiteral("-kb"), + QStringLiteral("-p-"), + QStringLiteral("$PreservePathSwitch"), + QStringLiteral("$PasswordSwitch"), + QStringLiteral("$RootNodeSwitch"), + QStringLiteral("$Archive"), + QStringLiteral("$Files") }; + + QFETCH(QVariantList, files); + QFETCH(bool, preservePaths); + QFETCH(QString, password); + QFETCH(QString, rootNode); + + QStringList replacedArgs = rarPlugin->substituteCopyVariables(extractArgs, files, preservePaths, password, rootNode); + QVERIFY(replacedArgs.size() >= extractArgs.size()); + + QFETCH(QStringList, expectedArgs); + QCOMPARE(replacedArgs, expectedArgs); + + rarPlugin->deleteLater(); +} diff --git a/autotests/plugins/clirarplugin/clirartest.h b/autotests/plugins/clirarplugin/clirartest.h index 3ea9eab6..e338384f 100644 --- a/autotests/plugins/clirarplugin/clirartest.h +++ b/autotests/plugins/clirarplugin/clirartest.h @@ -1,52 +1,54 @@ /* * Copyright (c) 2011 Raphael Kubo da Costa * Copyright (c) 2015,2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CLIRARTEST_H #define CLIRARTEST_H #include "cliplugin.h" #include using namespace Kerfuffle; class CliRarTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testArchive_data(); void testArchive(); void testList_data(); void testList(); + void testExtractArgs_data(); + void testExtractArgs(); private: KPluginMetaData m_pluginMetadata; }; Q_DECLARE_METATYPE(ArchiveEntry) #endif diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp index 67279306..432d1e1f 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,1132 +1,1146 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "cliinterface.h" #include "ark_debug.h" #include "queries.h" #ifdef Q_OS_WIN # include #else # include # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Kerfuffle { CliInterface::CliInterface(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args), m_process(0), m_listEmptyLines(false), m_abortingOperation(false) { //because this interface uses the event loop setWaitForFinishedSignal(true); if (QMetaType::type("QProcess::ExitStatus") == 0) { qRegisterMetaType("QProcess::ExitStatus"); } } void CliInterface::cacheParameterList() { m_param = parameterList(); Q_ASSERT(m_param.contains(ExtractProgram)); Q_ASSERT(m_param.contains(ListProgram)); Q_ASSERT(m_param.contains(PreservePathSwitch)); Q_ASSERT(m_param.contains(FileExistsExpression)); Q_ASSERT(m_param.contains(FileExistsInput)); } CliInterface::~CliInterface() { Q_ASSERT(!m_process); } bool CliInterface::isCliBased() const { return true; } bool CliInterface::findExecutables(bool isReadWrite) { cacheParameterList(); QList execTypes = QList() << ListProgram << ExtractProgram; if (isReadWrite) { execTypes << AddProgram << DeleteProgram; } foreach (const int execType, execTypes) { bool execTypeFound = false; foreach (const QString &program, m_param.value(execType).toStringList()) { if (!QStandardPaths::findExecutable(program).isEmpty()) { qCDebug(ARK) << "Found executable type" << execType << ":" << program; execTypeFound = true; break; } } if (!execTypeFound) { return false; } } return true; } void CliInterface::setListEmptyLines(bool emptyLines) { m_listEmptyLines = emptyLines; } bool CliInterface::list() { resetParsing(); cacheParameterList(); m_operationMode = List; QStringList args = m_param.value(ListArgs).toStringList(); substituteListVariables(args); if (!runProcess(m_param.value(ListProgram).toStringList(), args)) { failOperation(); return false; } emit finished(true); return true; } bool CliInterface::copyFiles(const QVariantList &files, const QString &destinationDirectory, const ExtractionOptions &options) { qCDebug(ARK) << Q_FUNC_INFO << "to" << destinationDirectory; cacheParameterList(); - m_operationMode = Copy; + const QStringList extractArgs = m_param.value(ExtractArgs).toStringList(); - //start preparing the argument list - QStringList args = m_param.value(ExtractArgs).toStringList(); - - //now replace the various elements in the list - for (int i = 0; i < args.size(); ++i) { - QString argument = args.at(i); - qCDebug(ARK) << "Processing argument " << argument; - - if (argument == QLatin1String( "$Archive" )) { - args[i] = filename(); - } - - if (argument == QLatin1String( "$PreservePathSwitch" )) { - QStringList replacementFlags = m_param.value(PreservePathSwitch).toStringList(); - Q_ASSERT(replacementFlags.size() == 2); - - bool preservePaths = options.value(QStringLiteral( "PreservePaths" )).toBool(); - QString theReplacement; - if (preservePaths) { - theReplacement = replacementFlags.at(0); - } else { - theReplacement = replacementFlags.at(1); - } - - if (theReplacement.isEmpty()) { - args.removeAt(i); - --i; //decrement to compensate for the variable we removed - } else { - //but in this case we don't have to decrement, we just - //replace it - args[i] = theReplacement; - } - } - - if (argument == QLatin1String( "$PasswordSwitch" )) { - //if the PasswordSwitch argument has been added, we at least - //assume that the format of the switch has been added as well - Q_ASSERT(m_param.contains(PasswordSwitch)); - - //we will decrement i afterwards - args.removeAt(i); - - //if we get a hint about this being a password protected archive, ask about - //the password in advance. - if ((options.value(QStringLiteral("PasswordProtectedHint")).toBool()) && - (password().isEmpty())) { - qCDebug(ARK) << "Password hint enabled, querying user"; - - Kerfuffle::PasswordNeededQuery query(filename()); - emit userQuery(&query); - query.waitForResponse(); - - if (query.responseCancelled()) { - emit cancelled(); - // There is no process running, so finished() must be emitted manually. - emit finished(false); - failOperation(); - return false; - } - setPassword(query.password()); - } - - QString pass = password(); - - if (!pass.isEmpty()) { - QStringList theSwitch = m_param.value(PasswordSwitch).toStringList(); - for (int j = 0; j < theSwitch.size(); ++j) { - //get the argument part - QString newArg = theSwitch.at(j); - - //substitute the $Path - newArg.replace(QLatin1String( "$Password" ), pass); - - //put it in the arg list - args.insert(i + j, newArg); - ++i; - - } - } - --i; //decrement to compensate for the variable we replaced - } - - if (argument == QLatin1String( "$RootNodeSwitch" )) { - //if the RootNodeSwitch argument has been added, we at least - //assume that the format of the switch has been added as well - Q_ASSERT(m_param.contains(RootNodeSwitch)); - - //we will decrement i afterwards - args.removeAt(i); - - QString rootNode; - if (options.contains(QStringLiteral( "RootNode" ))) { - rootNode = options.value(QStringLiteral( "RootNode" )).toString(); - qCDebug(ARK) << "Set root node " << rootNode; - } - - if (!rootNode.isEmpty()) { - QStringList theSwitch = m_param.value(RootNodeSwitch).toStringList(); - for (int j = 0; j < theSwitch.size(); ++j) { - //get the argument part - QString newArg = theSwitch.at(j); - - //substitute the $Path - newArg.replace(QLatin1String( "$Path" ), rootNode); - - //put it in the arg list - args.insert(i + j, newArg); - ++i; - - } - } - --i; //decrement to compensate for the variable we replaced - } - - if (argument == QLatin1String( "$Files" )) { - args.removeAt(i); - for (int j = 0; j < files.count(); ++j) { - args.insert(i + j, escapeFileName(files.at(j).value().file)); - ++i; - } - --i; + if (extractArgs.contains(QStringLiteral("$PasswordSwitch")) && + options.value(QStringLiteral("PasswordProtectedHint")).toBool() && + password().isEmpty()) { + qCDebug(ARK) << "Password hint enabled, querying user"; + if (!passwordQuery()) { + return false; } } + // Populate the argument list. + const QStringList args = substituteCopyVariables(extractArgs, + files, + options.value(QStringLiteral("PreservePaths")).toBool(), + password(), + options.value(QStringLiteral("RootNode"), QString()).toString()); + QUrl destDir = QUrl(destinationDirectory); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); QString oldCurrentDir; bool useTmpExtractDir = options.value(QStringLiteral("DragAndDrop")).toBool() || options.value(QStringLiteral("AlwaysUseTmpDir")).toBool(); QTemporaryDir tmpExtractDir(QApplication::applicationName() + QLatin1Char('-')); if (useTmpExtractDir) { qCDebug(ARK) << "Using temporary extraction dir:" << tmpExtractDir.path(); if (!tmpExtractDir.isValid()) { qCDebug(ARK) << "Creation of temporary directory failed."; failOperation(); return false; } oldCurrentDir = QDir::currentPath(); destDir = QUrl(tmpExtractDir.path()); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); } if (!runProcess(m_param.value(ExtractProgram).toStringList(), args)) { failOperation(); return false; } if (options.value(QStringLiteral("AlwaysUseTmpDir")).toBool()) { // unar exits with code 1 if the password is wrong. if (m_exitCode == 1) { qCWarning(ARK) << "Wrong password, extraction aborted"; emit error(i18n("Extraction failed due to a wrong password.")); emit finished(false); failOperation(); setPassword(QString()); return false; } if (!options.value(QStringLiteral("DragAndDrop")).toBool()) { if (!moveToDestination(QDir::current(), QDir(destinationDirectory), options[QStringLiteral("PreservePaths")].toBool())) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", files.size())); emit finished(false); return false; } // If we don't do this, the temporary directory will not autodelete itself upon destruction. QDir::setCurrent(oldCurrentDir); } } if (options.value(QStringLiteral("DragAndDrop")).toBool()) { if (!moveDroppedFilesToDest(files, destinationDirectory)) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", files.size())); emit finished(false); return false; } // If we don't do this, the temporary directory will not autodelete itself upon destruction. QDir::setCurrent(oldCurrentDir); } emit finished(true); return true; } bool CliInterface::addFiles(const QStringList & files, const CompressionOptions& options) { cacheParameterList(); m_operationMode = Add; const QString globalWorkDir = options.value(QStringLiteral( "GlobalWorkDir" )).toString(); const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir); if (!globalWorkDir.isEmpty()) { qCDebug(ARK) << "GlobalWorkDir is set, changing dir to " << globalWorkDir; QDir::setCurrent(globalWorkDir); } //start preparing the argument list QStringList args = m_param.value(AddArgs).toStringList(); //now replace the various elements in the list for (int i = 0; i < args.size(); ++i) { const QString argument = args.at(i); qCDebug(ARK) << "Processing argument " << argument; if (argument == QLatin1String( "$Archive" )) { args[i] = filename(); } if (argument == QLatin1String("$PasswordSwitch")) { //if the PasswordSwitch argument has been added, we at least //assume that the format of the switch has been added as well Q_ASSERT(m_param.contains(PasswordSwitch)); //we will decrement i afterwards args.removeAt(i); QString pass = password(); if (!pass.isEmpty()) { QStringList theSwitch = m_param.value(PasswordSwitch).toStringList(); // use the header encryption switch if needed and if provided by the plugin if (isHeaderEncryptionEnabled() && !m_param.value(PasswordHeaderSwitch).toStringList().isEmpty()) { theSwitch = m_param.value(PasswordHeaderSwitch).toStringList(); } for (int j = 0; j < theSwitch.size(); ++j) { //get the argument part QString newArg = theSwitch.at(j); //substitute the $Password newArg.replace(QLatin1String("$Password"), pass); //put it in the arg list args.insert(i + j, newArg); ++i; } } --i; //decrement to compensate for the variable we replaced } if (argument == QLatin1String("$EncryptHeaderSwitch")) { //if the EncryptHeaderSwitch argument has been added, we at least //assume that the format of the switch has been added as well Q_ASSERT(m_param.contains(EncryptHeaderSwitch)); //we will decrement i afterwards args.removeAt(i); QString enabled = isHeaderEncryptionEnabled() ? QStringLiteral("-mhe=on") : QString(); QStringList theSwitch = m_param.value(EncryptHeaderSwitch).toStringList(); for (int j = 0; j < theSwitch.size(); ++j) { //get the argument part QString newArg = theSwitch.at(j); //substitute the $Password newArg.replace(QLatin1String("$Enabled"), enabled); //put it in the arg list args.insert(i + j, newArg); ++i; } --i; //decrement to compensate for the variable we replaced } if (argument == QLatin1String( "$Files" )) { args.removeAt(i); for (int j = 0; j < files.count(); ++j) { // #191821: workDir must be used instead of QDir::current() // so that symlinks aren't resolved automatically // TODO: this kind of call should be moved upwards in the // class hierarchy to avoid code duplication const QString relativeName = workDir.relativeFilePath(files.at(j)); args.insert(i + j, relativeName); ++i; } --i; } } if (!runProcess(m_param.value(AddProgram).toStringList(), args)) { failOperation(); return false; } emit finished(true); return true; } bool CliInterface::deleteFiles(const QList & files) { cacheParameterList(); m_operationMode = Delete; //start preparing the argument list QStringList args = m_param.value(DeleteArgs).toStringList(); //now replace the various elements in the list for (int i = 0; i < args.size(); ++i) { QString argument = args.at(i); qCDebug(ARK) << "Processing argument " << argument; if (argument == QLatin1String( "$Archive" )) { args[i] = filename(); } else if (argument == QLatin1String( "$Files" )) { args.removeAt(i); for (int j = 0; j < files.count(); ++j) { args.insert(i + j, escapeFileName(files.at(j).toString())); ++i; } --i; } } m_removedFiles = files; if (!runProcess(m_param.value(DeleteProgram).toStringList(), args)) { failOperation(); return false; } emit finished(true); return true; } bool CliInterface::runProcess(const QStringList& programNames, const QStringList& arguments) { QString programPath; for (int i = 0; i < programNames.count(); i++) { programPath = QStandardPaths::findExecutable(programNames.at(i)); if (!programPath.isEmpty()) break; } if (programPath.isEmpty()) { const QString names = programNames.join(QStringLiteral(", ")); emit error(xi18ncp("@info", "Failed to locate program %2 on disk.", "Failed to locate programs %2 on disk.", programNames.count(), names)); emit finished(false); return false; } qCDebug(ARK) << "Executing" << programPath << arguments << "within directory" << QDir::currentPath(); if (m_process) { m_process->waitForFinished(); delete m_process; } #ifdef Q_OS_WIN m_process = new KProcess; #else m_process = new KPtyProcess; m_process->setPtyChannels(KPtyProcess::StdinChannel); QEventLoop loop; connect(m_process, static_cast(&KPtyProcess::finished), &loop, &QEventLoop::quit, Qt::DirectConnection); #endif m_process->setOutputChannelMode(KProcess::MergedChannels); m_process->setNextOpenMode(QIODevice::ReadWrite | QIODevice::Unbuffered | QIODevice::Text); m_process->setProgram(programPath, arguments); connect(m_process, SIGNAL(readyReadStandardOutput()), SLOT(readStdout()), Qt::DirectConnection); connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::processFinished, Qt::DirectConnection); m_stdOutData.clear(); m_process->start(); #ifdef Q_OS_WIN bool ret = m_process->waitForFinished(-1); #else bool ret = (loop.exec(QEventLoop::WaitForMoreEvents | QEventLoop::ExcludeUserInputEvents) == 0); #endif Q_ASSERT(!m_process); return ret; } void CliInterface::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { m_exitCode = exitCode; qCDebug(ARK) << "Process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; //if the m_process pointer is gone, then there is nothing to worry //about here if (!m_process) { return; } if (m_operationMode == Delete) { foreach(const QVariant& v, m_removedFiles) { emit entryRemoved(v.toString()); } } //handle all the remaining data in the process readStdout(true); delete m_process; m_process = 0; emit progress(1.0); if (m_operationMode == Add) { list(); return; } } bool CliInterface::moveDroppedFilesToDest(const QVariantList &files, const QString &finalDest) { // Move extracted files from a QTemporaryDir to the final destination. QDir finalDestDir(finalDest); qCDebug(ARK) << "Setting final dir to" << finalDest; bool overwriteAll = false; bool skipAll = false; foreach (const QVariant& file, files) { QFileInfo relEntry(file.value().file.remove(file.value().rootNode)); QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + file.value().file); QFileInfo absDestEntry(finalDestDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absSourceEntry.isDir()) { // For directories, just create the path. if (!finalDestDir.mkpath(relEntry.filePath())) { qCWarning(ARK) << "Failed to create directory" << relEntry.filePath() << "in final destination."; } } else { // If destination file exists, prompt the user. if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; if (!skipAll && !overwriteAll) { Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); emit userQuery(&query); query.waitForResponse(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } } // Create any parent directories. if (!finalDestDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } // Move files to the final destination. if (!QFile(absSourceEntry.absoluteFilePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << absSourceEntry.filePath() << "to final destination."; return false; } } } return true; } bool CliInterface::isEmptyDir(const QDir &dir) { QDir d = dir; d.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); return d.count() == 0; } bool CliInterface::moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths) { qCDebug(ARK) << "Moving extracted files from temp dir" << tempDir.path() << "to final destination" << destDir.path(); bool overwriteAll = false; bool skipAll = false; QDirIterator dirIt(tempDir.path(), QDir::AllEntries | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (dirIt.hasNext()) { dirIt.next(); // We skip directories if: // 1. We are not preserving paths // 2. The dir is not empty. Only empty directories need to be explicitly moved. // The non-empty ones are created by QDir::mkpath() below. if (dirIt.fileInfo().isDir()) { if (!preservePaths || !isEmptyDir(QDir(dirIt.filePath()))) { continue; } } QFileInfo relEntry; if (preservePaths) { relEntry = QFileInfo(dirIt.filePath().remove(tempDir.path() + QLatin1Char('/'))); } else { relEntry = QFileInfo(dirIt.fileName()); } QFileInfo absDestEntry(destDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); emit userQuery(&query); query.waitForResponse(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } if (preservePaths) { // Create any parent directories. if (!destDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } } // Move file to the final destination. if (!QFile(dirIt.filePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << dirIt.filePath() << "to final destination."; return false; } } return true; } +QStringList CliInterface::substituteCopyVariables(const QStringList &extractArgs, const QVariantList &files, bool preservePaths, const QString &password, const QString &rootNode) +{ + // Required if we call this function from unit tests. + cacheParameterList(); + + QStringList args; + foreach (const QString& arg, extractArgs) { + qCDebug(ARK) << "Processing argument" << arg; + + if (arg == QLatin1String("$Archive")) { + args << filename(); + continue; + } + + if (arg == QLatin1String("$PreservePathSwitch")) { + args << preservePathSwitch(preservePaths); + continue; + } + + if (arg == QLatin1String("$PasswordSwitch")) { + + args << passwordSwitch(password); + continue; + } + + if (arg == QLatin1String("$RootNodeSwitch")) { + args << rootNodeSwitch(rootNode); + continue; + } + + if (arg == QLatin1String("$Files")) { + args << copyFilesList(files); + continue; + } + + // Simple argument (e.g. -kb in unrar), nothing to substitute, just add it to the list. + args << arg; + } + + // Remove empty strings, if any. + args.removeAll(QString()); + + return args; +} + +QString CliInterface::preservePathSwitch(bool preservePaths) const +{ + Q_ASSERT(m_param.contains(PreservePathSwitch)); + const QStringList theSwitch = m_param.value(PreservePathSwitch).toStringList(); + Q_ASSERT(theSwitch.size() == 2); + + return (preservePaths ? theSwitch.at(0) : theSwitch.at(1)); +} + +QStringList CliInterface::passwordSwitch(const QString& password) const +{ + if (password.isEmpty()) { + return QStringList(); + } + + Q_ASSERT(m_param.contains(PasswordSwitch)); + + QStringList passwordSwitch = m_param.value(PasswordSwitch).toStringList(); + Q_ASSERT(!passwordSwitch.isEmpty() && passwordSwitch.size() <= 2); + + if (passwordSwitch.size() == 1) { + passwordSwitch[0].replace(QLatin1String("$Password"), password); + } else { + passwordSwitch[1] = password; + } + + return passwordSwitch; +} + +QStringList CliInterface::rootNodeSwitch(const QString &rootNode) const +{ + if (rootNode.isEmpty()) { + return QStringList(); + } + + Q_ASSERT(m_param.contains(RootNodeSwitch)); + + QStringList rootNodeSwitch = m_param.value(RootNodeSwitch).toStringList(); + Q_ASSERT(!rootNodeSwitch.isEmpty() && rootNodeSwitch.size() <= 2); + + if (rootNodeSwitch.size() == 1) { + rootNodeSwitch[0].replace(QLatin1String("$Path"), rootNode); + } else { + rootNodeSwitch[1] = rootNode; + } + + return rootNodeSwitch; +} + +QStringList CliInterface::copyFilesList(const QVariantList& files) const +{ + QStringList filesList; + foreach (const QVariant& f, files) { + filesList << escapeFileName(f.value().file); + } + + return filesList; +} + void CliInterface::failOperation() { // TODO: Would be good to unit test #304764/#304178. doKill(); } +bool CliInterface::passwordQuery() +{ + Kerfuffle::PasswordNeededQuery query(filename()); + emit userQuery(&query); + query.waitForResponse(); + + if (query.responseCancelled()) { + emit cancelled(); + // There is no process running, so finished() must be emitted manually. + emit finished(false); + failOperation(); + return false; + } + + setPassword(query.password()); + return true; +} + void CliInterface::readStdout(bool handleAll) { //when hacking this function, please remember the following: //- standard output comes in unpredictable chunks, this is why //you can never know if the last part of the output is a complete line or not //- console applications are not really consistent about what //characters they send out (newline, backspace, carriage return, //etc), so keep in mind that this function is supposed to handle //all those special cases and be the lowest common denominator if (m_abortingOperation) return; Q_ASSERT(m_process); if (!m_process->bytesAvailable()) { //if process has no more data, we can just bail out return; } //if the process is still not finished (m_process is appearantly not //set to NULL if here), then the operation should definitely not be in //the main thread as this would freeze everything. assert this. Q_ASSERT(QThread::currentThread() != QApplication::instance()->thread()); QByteArray dd = m_process->readAllStandardOutput(); m_stdOutData += dd; QList lines = m_stdOutData.split('\n'); //The reason for this check is that archivers often do not end //queries (such as file exists, wrong password) on a new line, but //freeze waiting for input. So we check for errors on the last line in //all cases. // TODO: QLatin1String() might not be the best choice here. // The call to handleLine() at the end of the method uses // QString::fromLocal8Bit(), for example. // TODO: The same check methods are called in handleLine(), this // is suboptimal. bool wrongPasswordMessage = checkForErrorMessage(QLatin1String( lines.last() ), WrongPasswordPatterns); bool foundErrorMessage = (wrongPasswordMessage || checkForErrorMessage(QLatin1String(lines.last()), DiskFullPatterns) || checkForErrorMessage(QLatin1String(lines.last()), ExtractionFailedPatterns) || checkForPasswordPromptMessage(QLatin1String(lines.last())) || checkForErrorMessage(QLatin1String(lines.last()), FileExistsExpression)); if (foundErrorMessage) { handleAll = true; } if (wrongPasswordMessage) { setPassword(QString()); } //this is complex, here's an explanation: //if there is no newline, then there is no guaranteed full line to //handle in the output. The exception is that it is supposed to handle //all the data, OR if there's been an error message found in the //partial data. if (lines.size() == 1 && !handleAll) { return; } if (handleAll) { m_stdOutData.clear(); } else { //because the last line might be incomplete we leave it for now //note, this last line may be an empty string if the stdoutdata ends //with a newline m_stdOutData = lines.takeLast(); } foreach(const QByteArray& line, lines) { if (!line.isEmpty() || (m_listEmptyLines && m_operationMode == List)) { handleLine(QString::fromLocal8Bit(line)); } } } void CliInterface::handleLine(const QString& line) { // TODO: This should be implemented by each plugin; the way progress is // shown by each CLI application is subject to a lot of variation. if ((m_operationMode == Copy || m_operationMode == Add) && m_param.contains(CaptureProgress) && m_param.value(CaptureProgress).toBool()) { //read the percentage int pos = line.indexOf(QLatin1Char( '%' )); if (pos != -1 && pos > 1) { int percentage = line.midRef(pos - 2, 2).toInt(); emit progress(float(percentage) / 100); return; } } if (m_operationMode == Copy) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); failOperation(); return; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return; } if (checkForErrorMessage(line, DiskFullPatterns)) { qCWarning(ARK) << "Found disk full message:" << line; emit error(i18nc("@info", "Extraction failed because the disk is full.")); failOperation(); return; } if (checkForErrorMessage(line, WrongPasswordPatterns)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18nc("@info", "Extraction failed: Incorrect password")); failOperation(); return; } if (checkForErrorMessage(line, ExtractionFailedPatterns)) { qCWarning(ARK) << "Error in extraction:" << line; emit error(i18n("Extraction failed because of an unexpected error.")); failOperation(); return; } if (handleFileExistsMessage(line)) { return; } } if (m_operationMode == List) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); failOperation(); return; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return; } if (checkForErrorMessage(line, WrongPasswordPatterns)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18n("Incorrect password.")); failOperation(); return; } if (checkForErrorMessage(line, ExtractionFailedPatterns)) { qCWarning(ARK) << "Error in extraction!!"; emit error(i18n("Extraction failed because of an unexpected error.")); failOperation(); return; } if (checkForErrorMessage(line, CorruptArchivePatterns)) { qCWarning(ARK) << "Archive corrupt"; setCorrupt(true); Kerfuffle::LoadCorruptQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (!query.responseYes()) { emit cancelled(); failOperation(); return; } } if (handleFileExistsMessage(line)) { return; } readListLine(line); return; } } bool CliInterface::checkForPasswordPromptMessage(const QString& line) { const QString passwordPromptPattern(m_param.value(PasswordPromptPattern).toString()); if (passwordPromptPattern.isEmpty()) return false; if (m_passwordPromptPattern.pattern().isEmpty()) { m_passwordPromptPattern.setPattern(m_param.value(PasswordPromptPattern).toString()); } if (m_passwordPromptPattern.match(line).hasMatch()) { return true; } return false; } bool CliInterface::handleFileExistsMessage(const QString& line) { // Check for a filename and store it. foreach (const QString &pattern, m_param.value(FileExistsFileName).toStringList()) { const QRegularExpression rxFileNamePattern(pattern); const QRegularExpressionMatch rxMatch = rxFileNamePattern.match(line); if (rxMatch.hasMatch()) { m_storedFileName = rxMatch.captured(1); qCWarning(ARK) << "Detected existing file:" << m_storedFileName; } } if (!checkForErrorMessage(line, FileExistsExpression)) { return false; } Kerfuffle::OverwriteQuery query(QDir::current().path() + QLatin1Char( '/' ) + m_storedFileName); query.setNoRenameMode(true); emit userQuery(&query); qCDebug(ARK) << "Waiting response"; query.waitForResponse(); qCDebug(ARK) << "Finished response"; QString responseToProcess; const QStringList choices = m_param.value(FileExistsInput).toStringList(); if (query.responseOverwrite()) { responseToProcess = choices.at(0); } else if (query.responseSkip()) { responseToProcess = choices.at(1); } else if (query.responseOverwriteAll()) { responseToProcess = choices.at(2); } else if (query.responseAutoSkip()) { responseToProcess = choices.at(3); } else if (query.responseCancelled()) { if (choices.count() < 5) { // If the program has no way to cancel the extraction, we resort to killing it return doKill(); } responseToProcess = choices.at(4); } Q_ASSERT(!responseToProcess.isEmpty()); responseToProcess += QLatin1Char( '\n' ); writeToProcess(responseToProcess.toLocal8Bit()); return true; } bool CliInterface::checkForErrorMessage(const QString& line, int parameterIndex) { QList patterns; if (m_patternCache.contains(parameterIndex)) { patterns = m_patternCache.value(parameterIndex); } else { if (!m_param.contains(parameterIndex)) { return false; } foreach(const QString& rawPattern, m_param.value(parameterIndex).toStringList()) { patterns << QRegularExpression(rawPattern); } m_patternCache[parameterIndex] = patterns; } foreach(const QRegularExpression& pattern, patterns) { if (pattern.match(line).hasMatch()) { return true; } } return false; } bool CliInterface::doKill() { if (m_process) { // Give some time for the application to finish gracefully m_abortingOperation = true; if (!m_process->waitForFinished(5)) { m_process->kill(); } m_abortingOperation = false; return true; } return false; } bool CliInterface::doSuspend() { return false; } bool CliInterface::doResume() { return false; } void CliInterface::substituteListVariables(QStringList& params) { for (int i = 0; i < params.size(); ++i) { const QString parameter = params.at(i); if (parameter == QLatin1String( "$Archive" )) { params[i] = filename(); } if (parameter == QLatin1String("$PasswordSwitch")) { //if the PasswordSwitch argument has been added, we at least //assume that the format of the switch has been added as well Q_ASSERT(m_param.contains(PasswordSwitch)); //we will set it afterwards, if there is a password params.removeAt(i); QString pass = password(); if (!pass.isEmpty()) { QStringList theSwitch = m_param.value(PasswordSwitch).toStringList(); for (int j = 0; j < theSwitch.size(); ++j) { //get the argument part QString newArg = theSwitch.at(j); //substitute the $Password newArg.replace(QLatin1String("$Password"), pass); //put it in the arg list params.insert(i + j, newArg); ++i; } } --i; } } } QString CliInterface::escapeFileName(const QString& fileName) const { return fileName; } void CliInterface::writeToProcess(const QByteArray& data) { Q_ASSERT(m_process); Q_ASSERT(!data.isNull()); qCDebug(ARK) << "Writing" << data << "to the process"; #ifdef Q_OS_WIN m_process->write(data); #else m_process->pty()->write(data); #endif } } diff --git a/kerfuffle/cliinterface.h b/kerfuffle/cliinterface.h index 9dcdfd3f..232abf76 100644 --- a/kerfuffle/cliinterface.h +++ b/kerfuffle/cliinterface.h @@ -1,408 +1,436 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CLIINTERFACE_H #define CLIINTERFACE_H #include "archiveinterface.h" #include "kerfuffle_export.h" #include #include class KProcess; class KPtyProcess; class QDir; namespace Kerfuffle { enum CliInterfaceParameters { ///////////////[ COMMON ]///////////// /** * Bool (default false) * Will look for the %-sign in the stdout while working, in the form of * (2%, 14%, 35%, etc etc), and report progress based upon this */ CaptureProgress = 0, /** * QString * Default: empty * A regexp pattern that matches the program's password prompt. */ PasswordPromptPattern, ///////////////[ LIST ]///////////// /** * QStringList * The names to the program that will handle listing of this * archive (eg "rar"). Will be searched for in PATH */ ListProgram, /** * QStringList * The arguments that are passed to the program above for * listing the archive. Special strings that will be * substituted: * $Archive - the path of the archive */ ListArgs, /** * QStringList (default empty) * List of regexp patterns that indicate a corrupt archive. */ CorruptArchivePatterns, ///////////////[ EXTRACT ]///////////// /** * QStringList * The names to the program that will handle extracting of this * archive (eg "rar"). Will be searched for in PATH */ ExtractProgram, /** * QStringList * The arguments that are passed to the program above for * extracting the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be extracted, if any * $PreservePathSwitch - the flag for extracting with full paths * $RootNodeSwitch - the internal work dir in the archive (for example * when the user has dragged a folder from the archive and wants it * extracted relative to it) * $PasswordSwitch - the switch setting the password. Note that this * will not be inserted unless the listing function has emitted an * entry with the IsPasswordProtected property set to true. */ ExtractArgs, /** * Bool (default false) * When passing directories to the extract program, do not * include trailing slashes * e.g. if the user selected "foo/" and "foo/bar" in the gui, the * paths "foo" and "foo/bar" will be sent to the program. */ NoTrailingSlashes, /** * QStringList * This should be a qstringlist with either two elements. The first * string is what PreservePathSwitch in the ExtractArgs will be replaced * with if PreservePath is True/enabled. The second is for the disabled * case. An empty string means that the argument will not be used in * that case. * Example: for rar, "x" means extract with full paths, and "e" means * extract without full paths. in this case we will use the stringlist * ("x", "e"). Or, for another format that might use the switch * "--extractFull" for preservePaths, and nothing otherwise: we use the * stringlist ("--extractFull", "") */ PreservePathSwitch, /** * QStringList (default empty) * The format of the root node switch. The variable $Path will be * substituted for the path string. * Example: ("--internalPath=$Path) * or ("--path", "$Path") */ RootNodeSwitch, /** * QStringList (default empty) * The format of the root node switch. The variable $Password will be * substituted for the password string. NOTE: supplying passwords * through a virtual terminal is not supported (yet?), because this * is not cross platform compatible. As of KDE 4.3 there are no plans to * change this. * Example: ("-p$Password) * or ("--password", "$Password") */ PasswordSwitch, /** * QStringList * This is a stringlist with regexps, defining how to recognize the last * line in a "File already exists" prompt when extracting. */ FileExistsExpression, /** * QStringList * This is a stringlist with regexps defining how to recognize the line * containing the filename in a "File already exists" prompt when * extracting. It should have one captured string, which is the filename * of the file/folder that already exists. */ FileExistsFileName, /** * int * This sets on what output channel the FileExistsExpression regex * should be applied on, in other words, on what stream the "file * exists" output will appear in. Values accepted: * 0 - Standard error, stderr (default) * 1 - Standard output, stdout */ FileExistsMode, /** * QStringList * The various responses that can be supplied as a response to the * "file exists" prompt. The various items are to be supplied in the * following order: * index 0 - Yes (overwrite) * index 1 - No (skip/do not overwrite) * index 2 - All (overwrite all) * index 3 - Do not overwrite any files (autoskip) * index 4 - Cancel operation */ FileExistsInput, /** * QStringList * Regexp patterns capturing disk is full error messages. */ DiskFullPatterns, ///////////////[ DELETE ]///////////// /** * QStringList * The names to the program that will handle deleting of elements in this * archive format (eg "rar"). Will be searched for in PATH */ DeleteProgram, /** * QStringList * The arguments that are passed to the program above for * deleting from the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be deleted */ DeleteArgs, /** * QStringList * Default: empty * A list of regexp patterns that will cause the extraction to exit * with a general fail message */ ExtractionFailedPatterns, /** * QStringList * Default: empty * A list of regexp patterns that will alert the user that the password * was wrong. */ WrongPasswordPatterns, ///////////////[ ADD ]///////////// /** * QStringList * The names to the program that will handle adding in this * archive format (eg "rar"). Will be searched for in PATH */ AddProgram, /** * QStringList * The arguments that are passed to the program above for * adding to the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be added */ AddArgs, ///////////////[ ENCRYPT ]///////////// /** * QStringList (default empty) * The variable $Password will be * substituted for the password string used to encrypt the header. * Example (rar plugin): ("-hp$Password") */ PasswordHeaderSwitch, /** * QStringList (default empty) * Encrypt the archive header without providing a password string. * It uses $Password from either PasswordSwitch or PasswordHeaderSwitch. * However, there is the $Enabled variable * which is substituted with either 'on' or 'off'. * Example (7z plugin): ("-mhe=$Enabled") */ EncryptHeaderSwitch }; typedef QHash ParameterList; class KERFUFFLE_EXPORT CliInterface : public ReadWriteArchiveInterface { Q_OBJECT public: enum OperationMode { List, Copy, Add, Delete }; OperationMode m_operationMode; explicit CliInterface(QObject *parent, const QVariantList & args); virtual ~CliInterface(); virtual bool list() Q_DECL_OVERRIDE; virtual bool copyFiles(const QList& files, const QString& destinationDirectory, const ExtractionOptions& options) Q_DECL_OVERRIDE; virtual bool addFiles(const QStringList & files, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool deleteFiles(const QList & files) Q_DECL_OVERRIDE; virtual void resetParsing() = 0; virtual ParameterList parameterList() const = 0; virtual bool readListLine(const QString &line) = 0; bool doKill() Q_DECL_OVERRIDE; bool doSuspend() Q_DECL_OVERRIDE; bool doResume() Q_DECL_OVERRIDE; bool findExecutables(bool isReadWrite) Q_DECL_OVERRIDE; bool isCliBased() const Q_DECL_OVERRIDE; /** * Returns the list of characters which are preceded by a * backslash when a file name in an archive is passed to * a program. * * @see setEscapedCharacters(). */ QString escapedCharacters(); // FIXME not implemented? /** * Sets which characters will be preceded by a backslash when * a file name in an archive is passed to a program. * * @see escapedCharacters(). */ void setEscapedCharacters(const QString& characters); // FIXME not implemented? /** * Sets if the listing should include empty lines. * * The default value is false. */ void setListEmptyLines(bool emptyLines); /** * Move all files from @p tmpDir to @p destDir, preserving paths if @p preservePaths is true. * @return Whether the operation has been successfull. */ bool moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths); + QStringList substituteCopyVariables(const QStringList &extractArgs, const QVariantList &files, bool preservePaths, const QString &password, const QString &rootNode); + + /** + * @return The preserve path switch, according to the @p preservePaths extraction option. + */ + QString preservePathSwitch(bool preservePaths) const; + + /** + * @return The password switch with the given @p password. + */ + QStringList passwordSwitch(const QString& password) const; + + /** + * @return The root node switch with the given @p rootNode. + */ + QStringList rootNodeSwitch(const QString& rootNode) const; + + /** + * @return The list of selected files to extract. + */ + QStringList copyFilesList(const QVariantList& files) const; + protected: virtual void handleLine(const QString& line); virtual void cacheParameterList(); void substituteListVariables(QStringList& params); /** * Run @p programName with the given @p arguments. * The method waits until @p programName is finished to exit. * * @param programName The program that will be run (not the whole path). * @param arguments A list of arguments that will be passed to the program. * * @return @c true if the program was found and the process ran correctly, * @c false otherwise. */ bool runProcess(const QStringList& programNames, const QStringList& arguments); void failOperation(); + /** + * Ask the password *before* running any process. + * @return True if the user supplies a password, false otherwise (in which case finished() is emitted). + */ + bool passwordQuery(); + ParameterList m_param; int m_exitCode; protected slots: virtual void readStdout(bool handleAll = false); private: /** * Checks whether a line of the program's output is a password prompt. * * It uses the regular expression in the @c PasswordPromptPattern parameter * for the check. * * @param line A line of the program's output. * * @return @c true if the given @p line is a password prompt, @c false * otherwise. */ bool checkForPasswordPromptMessage(const QString& line); bool handleFileExistsMessage(const QString& filename); bool checkForErrorMessage(const QString& line, int parameterIndex); /** * Performs any additional escaping and processing on @p fileName * before passing it to the underlying process. * * The default implementation returns @p fileName unchanged. * * @param fileName String to escape. */ virtual QString escapeFileName(const QString &fileName) const; /** * Wrapper around KProcess::write() or KPtyDevice::write(), depending on * the platform. */ void writeToProcess(const QByteArray& data); bool moveDroppedFilesToDest(const QVariantList &files, const QString &finalDest); /** * @return Whether @p dir is an empty directory. */ bool isEmptyDir(const QDir &dir); QByteArray m_stdOutData; QRegularExpression m_passwordPromptPattern; QHash > m_patternCache; #ifdef Q_OS_WIN KProcess *m_process; #else KPtyProcess *m_process; #endif QVariantList m_removedFiles; bool m_listEmptyLines; bool m_abortingOperation; QString m_storedFileName; private slots: void processFinished(int exitCode, QProcess::ExitStatus exitStatus); }; } #endif /* CLIINTERFACE_H */