diff --git a/autotests/plugins/clirarplugin/clirartest.h b/autotests/plugins/clirarplugin/clirartest.h --- a/autotests/plugins/clirarplugin/clirartest.h +++ b/autotests/plugins/clirarplugin/clirartest.h @@ -42,6 +42,8 @@ void testArchive(); void testList_data(); void testList(); + void testExtractArgs_data(); + void testExtractArgs(); private: KPluginMetaData m_pluginMetadata; diff --git a/autotests/plugins/clirarplugin/clirartest.cpp b/autotests/plugins/clirarplugin/clirartest.cpp --- a/autotests/plugins/clirarplugin/clirartest.cpp +++ b/autotests/plugins/clirarplugin/clirartest.cpp @@ -211,3 +211,111 @@ 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/kerfuffle/cliinterface.h b/kerfuffle/cliinterface.h --- a/kerfuffle/cliinterface.h +++ b/kerfuffle/cliinterface.h @@ -317,6 +317,28 @@ */ 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); @@ -337,6 +359,12 @@ 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; diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -144,133 +144,25 @@ 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()); @@ -737,12 +629,134 @@ 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: