diff --git a/autotests/kerfuffle/jobstest.cpp b/autotests/kerfuffle/jobstest.cpp index 7a748c09..dd7db1af 100644 --- a/autotests/kerfuffle/jobstest.cpp +++ b/autotests/kerfuffle/jobstest.cpp @@ -1,450 +1,453 @@ /* * Copyright (c) 2010-2011 Raphael Kubo da Costa * Copyright (c) 2016 Elvis Angelaccio * * 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 "jsonarchiveinterface.h" #include "kerfuffle/jobs.h" #include "kerfuffle/archiveentry.h" +#include + #include #include #include using namespace Kerfuffle; class JobsTest : public QObject { Q_OBJECT public: JobsTest(); protected Q_SLOTS: void init(); void slotNewEntry(Archive::Entry *entry); private Q_SLOTS: // ListJob-related tests void testLoadJob_data(); void testLoadJob(); // ExtractJob-related tests void testExtractJobAccessors(); void testTempExtractJob(); // DeleteJob-related tests void testRemoveEntries_data(); void testRemoveEntries(); // AddJob-related tests void testAddEntries_data(); void testAddEntries(); private: JSONArchiveInterface *createArchiveInterface(const QString& filePath); QVector listEntries(JSONArchiveInterface *iface); void startAndWaitForResult(KJob *job); QVector m_entries; QEventLoop m_eventLoop; }; QTEST_GUILESS_MAIN(JobsTest) JobsTest::JobsTest() : QObject(Q_NULLPTR) , m_eventLoop(this) { } void JobsTest::init() { m_entries.clear(); } void JobsTest::slotNewEntry(Archive::Entry *entry) { m_entries.append(entry); } JSONArchiveInterface *JobsTest::createArchiveInterface(const QString& filePath) { - JSONArchiveInterface *iface = new JSONArchiveInterface(this, {filePath}); + JSONArchiveInterface *iface = new JSONArchiveInterface(this, {filePath, + QVariant().fromValue(KPluginMetaData())}); if (!iface->open()) { qDebug() << "Could not open" << filePath; return Q_NULLPTR; } return iface; } QVector JobsTest::listEntries(JSONArchiveInterface *iface) { m_entries.clear(); auto job = new LoadJob(iface); connect(job, &Job::newEntry, this, &JobsTest::slotNewEntry); startAndWaitForResult(job); return m_entries; } void JobsTest::startAndWaitForResult(KJob *job) { connect(job, &KJob::result, &m_eventLoop, &QEventLoop::quit); job->start(); m_eventLoop.exec(); } void JobsTest::testLoadJob_data() { QTest::addColumn("jsonArchive"); QTest::addColumn("expectedExtractedFilesSize"); QTest::addColumn("isPasswordProtected"); QTest::addColumn("isSingleFolder"); QTest::addColumn("expectedEntryNames"); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << 0LL << false << false << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")}; QTest::newRow("archive002.json") << QFINDTESTDATA("data/archive002.json") << 45959LL << false << false << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")}; QTest::newRow("archive-deepsinglehierarchy.json") << QFINDTESTDATA("data/archive-deepsinglehierarchy.json") << 0LL << false << true << QStringList { // Depth-first order! QStringLiteral("aDir/"), QStringLiteral("aDir/aDirInside/"), QStringLiteral("aDir/aDirInside/anotherDir/"), QStringLiteral("aDir/aDirInside/anotherDir/file.txt"), QStringLiteral("aDir/b.txt") }; QTest::newRow("archive-multiplefolders.json") << QFINDTESTDATA("data/archive-multiplefolders.json") << 0LL << false << false << QStringList {QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("anotherDir/"), QStringLiteral("anotherDir/file.txt")}; QTest::newRow("archive-nodir-manyfiles.json") << QFINDTESTDATA("data/archive-nodir-manyfiles.json") << 0LL << false << false << QStringList {QStringLiteral("a.txt"), QStringLiteral("file.txt")}; QTest::newRow("archive-onetopfolder.json") << QFINDTESTDATA("data/archive-onetopfolder.json") << 0LL << false << true << QStringList {QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt")}; QTest::newRow("archive-password.json") << QFINDTESTDATA("data/archive-password.json") << 0LL << true << false // Possibly unexpected behavior of listing: // 1. Directories are listed before files, if they are empty! // 2. Files are sorted alphabetically. << QStringList {QStringLiteral("aDirectory/"), QStringLiteral("bar.txt"), QStringLiteral("foo.txt")}; QTest::newRow("archive-singlefile.json") << QFINDTESTDATA("data/archive-singlefile.json") << 0LL << false << false << QStringList {QStringLiteral("a.txt")}; QTest::newRow("archive-emptysinglefolder.json") << QFINDTESTDATA("data/archive-emptysinglefolder.json") << 0LL << false << true << QStringList {QStringLiteral("aDir/")}; QTest::newRow("archive-unorderedsinglefolder.json") << QFINDTESTDATA("data/archive-unorderedsinglefolder.json") << 0LL << false << true << QStringList { QStringLiteral("aDir/"), QStringLiteral("aDir/anotherDir/"), QStringLiteral("aDir/anotherDir/bar.txt"), QStringLiteral("aDir/foo.txt") }; } void JobsTest::testLoadJob() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); auto loadJob = new LoadJob(iface); loadJob->setAutoDelete(false); startAndWaitForResult(loadJob); QFETCH(qlonglong, expectedExtractedFilesSize); QCOMPARE(loadJob->extractedFilesSize(), expectedExtractedFilesSize); QFETCH(bool, isPasswordProtected); QCOMPARE(loadJob->isPasswordProtected(), isPasswordProtected); QFETCH(bool, isSingleFolder); QCOMPARE(loadJob->isSingleFolderArchive(), isSingleFolder); QFETCH(QStringList, expectedEntryNames); auto archiveEntries = listEntries(iface); QCOMPARE(archiveEntries.size(), expectedEntryNames.size()); for (int i = 0; i < archiveEntries.size(); i++) { QCOMPARE(archiveEntries.at(i)->fullPath(), expectedEntryNames.at(i)); } loadJob->deleteLater(); } void JobsTest::testExtractJobAccessors() { JSONArchiveInterface *iface = createArchiveInterface(QFINDTESTDATA("data/archive001.json")); ExtractJob *job = new ExtractJob(QVector(), QStringLiteral("/tmp/some-dir"), ExtractionOptions(), iface); QCOMPARE(job->destinationDirectory(), QLatin1String("/tmp/some-dir")); QVERIFY(job->extractionOptions().preservePaths()); job->setAutoDelete(false); startAndWaitForResult(job); QCOMPARE(job->destinationDirectory(), QLatin1String("/tmp/some-dir")); delete job; ExtractionOptions options; options.setPreservePaths(false); job = new ExtractJob(QVector(), QStringLiteral("/root"), options, iface); QCOMPARE(job->destinationDirectory(), QLatin1String("/root")); QVERIFY(!job->extractionOptions().preservePaths()); job->setAutoDelete(false); startAndWaitForResult(job); QCOMPARE(job->destinationDirectory(), QLatin1String("/root")); QVERIFY(!job->extractionOptions().preservePaths()); delete job; } void JobsTest::testTempExtractJob() { JSONArchiveInterface *iface = createArchiveInterface(QFINDTESTDATA("data/archive-malicious.json")); PreviewJob *job = new PreviewJob(new Archive::Entry(this, QStringLiteral("anotherDir/../../file.txt")), false, iface); const QString tempDirPath = job->tempDir()->path(); QVERIFY(QFileInfo::exists(tempDirPath)); QVERIFY(job->validatedFilePath().endsWith(QLatin1String("anotherDir/file.txt"))); QVERIFY(job->extractionOptions().preservePaths()); job->setAutoDelete(false); startAndWaitForResult(job); QVERIFY(job->validatedFilePath().endsWith(QLatin1String("anotherDir/file.txt"))); QVERIFY(job->extractionOptions().preservePaths()); delete job->tempDir(); QVERIFY(!QFileInfo::exists(tempDirPath)); delete job; } void JobsTest::testRemoveEntries_data() { QTest::addColumn("jsonArchive"); QTest::addColumn>("entries"); QTest::addColumn>("entriesToDelete"); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QVector { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QVector {new Archive::Entry(this, QStringLiteral("c.txt"))}; QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QVector { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QVector { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) }; // Error test: if we delete non-existent entries, the archive must not change. QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QVector { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QVector {new Archive::Entry(this, QStringLiteral("foo.txt"))}; } void JobsTest::testRemoveEntries() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); QFETCH(QVector, entries); QFETCH(QVector, entriesToDelete); QStringList fullPathsToDelete = iface->entryFullPaths(entriesToDelete); QVector expectedRemainingEntries; Q_FOREACH (Archive::Entry *entry, entries) { if (!fullPathsToDelete.contains(entry->fullPath())) { expectedRemainingEntries.append(entry); } } DeleteJob *deleteJob = new DeleteJob(entriesToDelete, iface); startAndWaitForResult(deleteJob); auto remainingEntries = listEntries(iface); QCOMPARE(remainingEntries.size(), expectedRemainingEntries.size()); for (int i = 0; i < remainingEntries.size(); i++) { QCOMPARE(*remainingEntries.at(i), *expectedRemainingEntries.at(i)); } iface->deleteLater(); } void JobsTest::testAddEntries_data() { QTest::addColumn("jsonArchive"); QTest::addColumn>("originalEntries"); QTest::addColumn>("entriesToAdd"); QTest::addColumn("destinationEntry"); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QVector { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QVector { new Archive::Entry(this, QStringLiteral("foo.txt")) } << new Archive::Entry(this); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QVector { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QVector { new Archive::Entry(this, QStringLiteral("foo.txt")), new Archive::Entry(this, QStringLiteral("bar.txt")) } << new Archive::Entry(this); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QVector { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QVector { new Archive::Entry(this, QStringLiteral("foo.txt")), new Archive::Entry(this, QStringLiteral("bar.txt")) } << new Archive::Entry(this, QStringLiteral("aDir/")); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QVector { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QVector {new Archive::Entry(this, QStringLiteral("c.txt"))} << new Archive::Entry(this, QStringLiteral("aDir/")); // Error test: if we add an already existent entry, the archive must not change. QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QVector { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QVector {new Archive::Entry(this, QStringLiteral("c.txt"))} << new Archive::Entry(this); } void JobsTest::testAddEntries() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); QFETCH(QVector, originalEntries); QStringList originalFullPaths = QStringList(); Q_FOREACH (const Archive::Entry *entry, originalEntries) { originalFullPaths.append(entry->fullPath()); } auto currentEntries = listEntries(iface); QCOMPARE(currentEntries.size(), originalEntries.size()); QFETCH(QVector, entriesToAdd); QFETCH(Archive::Entry*, destinationEntry); AddJob *addJob = new AddJob(entriesToAdd, destinationEntry, CompressionOptions(), iface); startAndWaitForResult(addJob); QStringList expectedAddedFullPaths = QStringList(); const QString destinationPath = destinationEntry->fullPath(); int expectedEntriesCount = originalEntries.size(); Q_FOREACH (const Archive::Entry *entry, entriesToAdd) { const QString fullPath = destinationPath + entry->fullPath(); if (!originalFullPaths.contains(fullPath)) { expectedEntriesCount++; expectedAddedFullPaths << destinationPath + entry->fullPath(); } } currentEntries = listEntries(iface); QCOMPARE(currentEntries.size(), expectedEntriesCount); QStringList currentFullPaths = QStringList(); Q_FOREACH (const Archive::Entry* entry, currentEntries) { currentFullPaths << entry->fullPath(); } Q_FOREACH (const QString &fullPath, expectedAddedFullPaths) { QVERIFY(currentFullPaths.contains(fullPath)); } iface->deleteLater(); } #include "jobstest.moc" diff --git a/autotests/plugins/cli7zplugin/cli7ztest.cpp b/autotests/plugins/cli7zplugin/cli7ztest.cpp index 769b9888..cdcfac07 100644 --- a/autotests/plugins/cli7zplugin/cli7ztest.cpp +++ b/autotests/plugins/cli7zplugin/cli7ztest.cpp @@ -1,445 +1,454 @@ /* * Copyright (c) 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 "cli7ztest.h" #include "testhelper.h" #include #include #include #include #include QTEST_GUILESS_MAIN(Cli7zTest) using namespace Kerfuffle; void Cli7zTest::initTestCase() { m_plugin = new Plugin(this); foreach (Plugin *plugin, m_pluginManger.availablePlugins()) { if (plugin->metaData().pluginId() == QStringLiteral("kerfuffle_cli7z")) { m_plugin = plugin; return; } } } void Cli7zTest::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.7z"); QTest::newRow("archive with one top-level folder") << archivePath << QFileInfo(archivePath).fileName() << false << true << Archive::Unencrypted << QStringLiteral("A"); } void Cli7zTest::testArchive() { if (!m_plugin->isValid()) { QSKIP("cli7z plugin not available. Skipping test.", SkipSingle); } QFETCH(QString, archivePath); auto loadJob = Archive::load(archivePath, m_plugin, this); QVERIFY(loadJob); TestHelper::startAndWaitForResult(loadJob); auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not load the cli7z 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->isSingleFolder(), isSingleFolder); QFETCH(Archive::EncryptionType, expectedEncryptionType); QCOMPARE(archive->encryptionType(), expectedEncryptionType); QFETCH(QString, expectedSubfolderName); QCOMPARE(archive->subfolderName(), expectedSubfolderName); } void Cli7zTest::testList_data() { QTest::addColumn("outputTextFile"); QTest::addColumn("expectedEntriesCount"); QTest::addColumn("isMultiVolume"); // Is zero for non-multi-volume archives: QTest::addColumn("numberOfVolumes"); QTest::addColumn("compressionMethods"); // Index of some entry to be tested. QTest::addColumn("someEntryIndex"); // Entry metadata. QTest::addColumn("expectedName"); QTest::addColumn("isDirectory"); QTest::addColumn("isPasswordProtected"); QTest::addColumn("expectedSize"); QTest::addColumn("expectedTimestamp"); // p7zip version 16.02 tests QTest::newRow("normal-file-1602") << QFINDTESTDATA("data/archive-with-symlink-1602.txt") << 10 << false << 0 << QStringList{QStringLiteral("LZMA2")} << 4 << QStringLiteral("testarchive/dir2/file2.txt") << false << false << (qulonglong) 32 << QStringLiteral("2015-05-17T20:41:48"); QTest::newRow("encrypted-1602") << QFINDTESTDATA("data/archive-encrypted-1602.txt") << 4 << false << 0 << QStringList{QStringLiteral("LZMA2"), QStringLiteral("7zAES")} << 1 << QStringLiteral("file2.txt") << false << true << (qulonglong) 14 << QStringLiteral("2016-03-02T22:37:55"); QTest::newRow("multi-volume-1602") << QFINDTESTDATA("data/archive-multivol-1602.txt") << 2 << true << 5 << QStringList{QStringLiteral("LZMA2")} << 1 << QStringLiteral("largefile2") << false << false << (qulonglong) 2097152 << QStringLiteral("2016-07-17T11:26:19"); // p7zip version 15.14 tests QTest::newRow("normal-file-1514") << QFINDTESTDATA("data/archive-with-symlink-1514.txt") << 10 << false << 0 << QStringList{QStringLiteral("LZMA2")} << 4 << QStringLiteral("testarchive/dir2/file2.txt") << false << false << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); QTest::newRow("encrypted-1514") << QFINDTESTDATA("data/archive-encrypted-1514.txt") << 9 << false << 0 << QStringList{QStringLiteral("LZMA2"), QStringLiteral("7zAES")} << 3 << QStringLiteral("testarchive/dir1/file1.txt") << false << true << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); // p7zip version 15.09 tests QTest::newRow("normal-file-1509") << QFINDTESTDATA("data/archive-with-symlink-1509.txt") << 10 << false << 0 << QStringList{QStringLiteral("LZMA2")} << 4 << QStringLiteral("testarchive/dir2/file2.txt") << false << false << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); QTest::newRow("encrypted-1509") << QFINDTESTDATA("data/archive-encrypted-1509.txt") << 9 << false << 0 << QStringList{QStringLiteral("LZMA2"), QStringLiteral("7zAES")} << 3 << QStringLiteral("testarchive/dir1/file1.txt") << false << true << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); // p7zip version 9.38.1 tests QTest::newRow("normal-file-9381") << QFINDTESTDATA("data/archive-with-symlink-9381.txt") << 10 << false << 0 << QStringList{QStringLiteral("LZMA2")} << 4 << QStringLiteral("testarchive/dir2/file2.txt") << false << false << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); QTest::newRow("encrypted-9381") << QFINDTESTDATA("data/archive-encrypted-9381.txt") << 9 << false << 0 << QStringList{QStringLiteral("LZMA2"), QStringLiteral("7zAES")} << 3 << QStringLiteral("testarchive/dir1/file1.txt") << false << true << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); } void Cli7zTest::testList() { qRegisterMetaType("Archive::Entry*"); - CliPlugin *plugin = new CliPlugin(this, {QStringLiteral("dummy.7z")}); + CliPlugin *plugin = new CliPlugin(this, {QStringLiteral("dummy.7z"), + QVariant::fromValue(m_plugin->metaData())}); QSignalSpy signalSpyEntry(plugin, &CliPlugin::entry); QSignalSpy signalSpyCompMethod(plugin, &CliPlugin::compressionMethodFound); 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(plugin->readListLine(line)); } QCOMPARE(signalSpyEntry.count(), expectedEntriesCount); QFETCH(bool, isMultiVolume); QCOMPARE(plugin->isMultiVolume(), isMultiVolume); QFETCH(int, numberOfVolumes); QCOMPARE(plugin->numberOfVolumes(), numberOfVolumes); QCOMPARE(signalSpyCompMethod.count(), 1); QFETCH(QStringList, compressionMethods); QCOMPARE(signalSpyCompMethod.at(0).at(0).toStringList(), compressionMethods); QFETCH(int, someEntryIndex); QVERIFY(someEntryIndex < signalSpyEntry.count()); Archive::Entry *entry = signalSpyEntry.at(someEntryIndex).at(0).value(); QFETCH(QString, expectedName); QCOMPARE(entry->fullPath(), expectedName); QFETCH(bool, isDirectory); QCOMPARE(entry->isDir(), isDirectory); QFETCH(bool, isPasswordProtected); QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected); QFETCH(qulonglong, expectedSize); QCOMPARE(entry->property("size").toULongLong(), expectedSize); QFETCH(QString, expectedTimestamp); QCOMPARE(entry->property("timestamp").toString(), expectedTimestamp); plugin->deleteLater(); } void Cli7zTest::testListArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.7z") << QString() << QStringList { QStringLiteral("l"), QStringLiteral("-slt"), QStringLiteral("/tmp/foo.7z") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.7z") << QStringLiteral("1234") << QStringList { QStringLiteral("l"), QStringLiteral("-slt"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.7z") }; } void Cli7zTest::testListArgs() { + if (!m_plugin->isValid()) { + QSKIP("cli7z plugin not available. Skipping test.", SkipSingle); + } + QFETCH(QString, archiveName); - CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); + CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName), + QVariant::fromValue(m_plugin->metaData())}); QVERIFY(plugin); - const QStringList listArgs = { QStringLiteral("l"), - QStringLiteral("-slt"), - QStringLiteral("$PasswordSwitch"), - QStringLiteral("$Archive") }; - QFETCH(QString, password); - const auto replacedArgs = plugin->substituteListVariables(listArgs, password); + + const auto replacedArgs = plugin->cliProperties()->listArgs(archiveName, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void Cli7zTest::testAddArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("encryptHeader"); QTest::addColumn("compressionLevel"); QTest::addColumn("compressionMethod"); QTest::addColumn("volumeSize"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.7z") << QString() << false << 5 << QStringLiteral("LZMA2") << 0UL << QStringList { QStringLiteral("a"), - QStringLiteral("/tmp/foo.7z"), + QStringLiteral("-l"), QStringLiteral("-mx=5"), - QStringLiteral("-m0=LZMA2") + QStringLiteral("-m0=LZMA2"), + QStringLiteral("/tmp/foo.7z") }; QTest::newRow("encrypted") << QStringLiteral("/tmp/foo.7z") << QStringLiteral("1234") << false << 5 << QStringLiteral("LZMA2") << 0UL << QStringList { QStringLiteral("a"), - QStringLiteral("/tmp/foo.7z"), + QStringLiteral("-l"), QStringLiteral("-p1234"), QStringLiteral("-mx=5"), - QStringLiteral("-m0=LZMA2") + QStringLiteral("-m0=LZMA2"), + QStringLiteral("/tmp/foo.7z") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.7z") << QStringLiteral("1234") << true << 5 << QStringLiteral("LZMA2") << 0UL << QStringList { QStringLiteral("a"), - QStringLiteral("/tmp/foo.7z"), + QStringLiteral("-l"), QStringLiteral("-p1234"), QStringLiteral("-mhe=on"), QStringLiteral("-mx=5"), - QStringLiteral("-m0=LZMA2") + QStringLiteral("-m0=LZMA2"), + QStringLiteral("/tmp/foo.7z") }; QTest::newRow("multi-volume") << QStringLiteral("/tmp/foo.7z") << QString() << false << 5 << QStringLiteral("LZMA2") << 2500UL << QStringList { QStringLiteral("a"), - QStringLiteral("/tmp/foo.7z"), + QStringLiteral("-l"), QStringLiteral("-mx=5"), QStringLiteral("-m0=LZMA2"), - QStringLiteral("-v2500k") + QStringLiteral("-v2500k"), + QStringLiteral("/tmp/foo.7z") }; QTest::newRow("comp-method-bzip2") << QStringLiteral("/tmp/foo.7z") << QString() << false << 5 << QStringLiteral("BZip2") << 0UL << QStringList { QStringLiteral("a"), - QStringLiteral("/tmp/foo.7z"), + QStringLiteral("-l"), QStringLiteral("-mx=5"), - QStringLiteral("-m0=BZip2") + QStringLiteral("-m0=BZip2"), + QStringLiteral("/tmp/foo.7z") }; } void Cli7zTest::testAddArgs() { + if (!m_plugin->isValid()) { + QSKIP("cli7z plugin not available. Skipping test.", SkipSingle); + } + QFETCH(QString, archiveName); - CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); + CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName), + QVariant::fromValue(m_plugin->metaData())}); QVERIFY(plugin); - const QStringList addArgs = { QStringLiteral("a"), - QStringLiteral("$Archive"), - QStringLiteral("$PasswordSwitch"), - QStringLiteral("$CompressionLevelSwitch"), - QStringLiteral("$CompressionMethodSwitch"), - QStringLiteral("$MultiVolumeSwitch"), - QStringLiteral("$Files") }; - QFETCH(QString, password); QFETCH(bool, encryptHeader); QFETCH(int, compressionLevel); QFETCH(ulong, volumeSize); QFETCH(QString, compressionMethod); - QStringList replacedArgs = plugin->substituteAddVariables(addArgs, {}, password, encryptHeader, compressionLevel, volumeSize, compressionMethod); + const auto replacedArgs = plugin->cliProperties()->addArgs(archiveName, {}, password, encryptHeader, compressionLevel, compressionMethod, volumeSize); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void Cli7zTest::testExtractArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn>("files"); QTest::addColumn("preservePaths"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("preserve paths, encrypted") << QStringLiteral("/tmp/foo.7z") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << true << QStringLiteral("1234") << QStringList { QStringLiteral("x"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("preserve paths, unencrypted") << QStringLiteral("/tmp/foo.7z") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << true << QString() << QStringList { QStringLiteral("x"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, encrypted") << QStringLiteral("/tmp/foo.7z") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << false << QStringLiteral("1234") << QStringList { QStringLiteral("e"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, unencrypted") << QStringLiteral("/tmp/foo.7z") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << false << QString() << QStringList { QStringLiteral("e"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; } void Cli7zTest::testExtractArgs() { + if (!m_plugin->isValid()) { + QSKIP("cli7z plugin not available. Skipping test.", SkipSingle); + } + QFETCH(QString, archiveName); - CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); + CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName), + QVariant::fromValue(m_plugin->metaData())}); QVERIFY(plugin); - const QStringList extractArgs = { QStringLiteral("$PreservePathSwitch"), - QStringLiteral("$PasswordSwitch"), - QStringLiteral("$Archive"), - QStringLiteral("$Files") }; - QFETCH(QVector, files); + QStringList filesList; + foreach (const Archive::Entry *e, files) { + filesList << e->fullPath(NoTrailingSlash); + } + QFETCH(bool, preservePaths); QFETCH(QString, password); - QStringList replacedArgs = plugin->substituteExtractVariables(extractArgs, files, preservePaths, password); - QVERIFY(replacedArgs.size() >= extractArgs.size()); + const auto replacedArgs = plugin->cliProperties()->extractArgs(archiveName, filesList, preservePaths, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } + diff --git a/autotests/plugins/clirarplugin/clirartest.cpp b/autotests/plugins/clirarplugin/clirartest.cpp index b629b05b..3920d4f0 100644 --- a/autotests/plugins/clirarplugin/clirartest.cpp +++ b/autotests/plugins/clirarplugin/clirartest.cpp @@ -1,508 +1,508 @@ /* * 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() { m_plugin = new Plugin(this); foreach (Plugin *plugin, m_pluginManger.availablePlugins()) { if (plugin->metaData().pluginId() == QStringLiteral("kerfuffle_clirar")) { m_plugin = plugin; return; } } } 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_plugin->isValid()) { QSKIP("clirar plugin not available. Skipping test.", SkipSingle); } QFETCH(QString, archivePath); auto loadJob = Archive::load(archivePath, m_plugin, this); QVERIFY(loadJob); TestHelper::startAndWaitForResult(loadJob); auto archive = loadJob->archive(); 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->isSingleFolder(), 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("errorMessage"); QTest::addColumn("expectedEntriesCount"); QTest::addColumn("isMultiVolume"); // Is zero for non-multi-volume archives: QTest::addColumn("numberOfVolumes"); QTest::addColumn("compressionMethods"); // 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") << QString() << 8 << false << 0 << QStringList{QStringLiteral("RAR4")} << 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") << QString() << 8 << false << 0 << QStringList{QStringLiteral("RAR4")} << 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") << QString() << 7 << false << 0 << QStringList{QStringLiteral("RAR4")} << 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") << QString() << 3 << false << 0 << QStringList{QStringLiteral("RAR4")} << 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") << QString() << 8 << false << 0 << QStringList{QStringLiteral("RAR4")} << 6 << QStringLiteral("dir1/") << true << false << QString() << (qulonglong) 0 << (qulonglong) 0 << QStringLiteral("2015-05-14T01:45:24"); //Note: The number of entries will be the total number of all entries in all volumes, i.e. if a file spans 3 volumes it will count as 3 entries. QTest::newRow("multivolume-archive-unrar5") << QFINDTESTDATA("data/archive-multivol-unrar5.txt") << QString() << 6 << true << 5 << QStringList{QStringLiteral("RAR4")} << 5 << QStringLiteral("largefile2") << false << false << QString() << (qulonglong) 2097152 << (qulonglong) 11231 << QStringLiteral("2016-07-17T11:26:19"); QTest::newRow("RAR5-open-with-unrar5") << QFINDTESTDATA("data/archive-RARv5-unrar5.txt") << QString() << 9 << false << 0 << QStringList{QStringLiteral("RAR5")} << 4 << QStringLiteral("testarchive/dir1/file1.txt") << false << false << QString() << (qulonglong) 32 << (qulonglong) 32 << QStringLiteral("2015-05-17T20:41:48"); // Unrar 4 tests QTest::newRow("normal-file-unrar4") << QFINDTESTDATA("data/archive-with-symlink-unrar4.txt") << QString() << 8 << false << 0 << QStringList{QStringLiteral("RAR4")} << 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") << QString() << 8 << false << 0 << QStringList{QStringLiteral("RAR4")} << 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") << QString() << 7 << false << 0 << QStringList{QStringLiteral("RAR4")} << 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") << QString() << 3 << false << 0 << QStringList{QStringLiteral("RAR4")} << 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") << QString() << 8 << false << 0 << QStringList{QStringLiteral("RAR4")} << 6 << QStringLiteral("dir1/") << true << false << QString() << (qulonglong) 0 << (qulonglong) 0 << QStringLiteral("2015-05-14T01:45:00"); QTest::newRow("RAR5-open-with-unrar4") << QFINDTESTDATA("data/archive-RARv5-unrar4.txt") << QStringLiteral("Your unrar executable is version 4.20, which is too old to handle this archive. Please update to a more recent version.") << 0 << false << 0 << QStringList() << 0 << QString() << true << false << QString() << (qulonglong) 0 << (qulonglong) 0 << QString(); //Note: The number of entries will be the total number of all entries in all volumes, i.e. if a file spans 3 volumes it will count as 3 entries. QTest::newRow("multivolume-archive-unrar4") << QFINDTESTDATA("data/archive-multivol-unrar4.txt") << QString() << 6 << true << 5 << QStringList{QStringLiteral("RAR4")} << 5 << QStringLiteral("largefile2") << false << false << QString() << (qulonglong) 2097152 << (qulonglong) 11231 << QStringLiteral("2016-07-17T11:26:00"); // Unrar 3 tests QTest::newRow("RAR5-open-with-unrar3") << QFINDTESTDATA("data/archive-RARv5-unrar3.txt") << QStringLiteral("Unrar reported a non-RAR archive. The installed unrar version (3.71) is old. Try updating your unrar.") << 0 << false << 0 << QStringList() << 0 << QString() << true << false << QString() << (qulonglong) 0 << (qulonglong) 0 << QString(); /* * 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") << QString() << 1 << true << 1 << QStringList{QStringLiteral("RAR4")} << 0 << QStringLiteral("some-file.ext") << false << false << QString() << (qulonglong) 732522496 << (qulonglong) 14851208 << QStringLiteral("2010-10-29T20:47:00"); } void CliRarTest::testList() { qRegisterMetaType("Archive::Entry*"); - CliPlugin *rarPlugin = new CliPlugin(this, {QStringLiteral("dummy.rar")}); + CliPlugin *rarPlugin = new CliPlugin(this, {QStringLiteral("dummy.rar"), + QVariant::fromValue(m_plugin->metaData())}); QSignalSpy signalSpyEntry(rarPlugin, &CliPlugin::entry); QSignalSpy signalSpyCompMethod(rarPlugin, &CliPlugin::compressionMethodFound); QSignalSpy signalSpyError(rarPlugin, &CliPlugin::error); 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()); if (!rarPlugin->readListLine(line)) { break; } } QFETCH(QString, errorMessage); if (!errorMessage.isEmpty()) { QCOMPARE(signalSpyError.count(), 1); QCOMPARE(signalSpyError.at(0).at(0).toString(), errorMessage); return; } QCOMPARE(signalSpyEntry.count(), expectedEntriesCount); QFETCH(bool, isMultiVolume); QCOMPARE(rarPlugin->isMultiVolume(), isMultiVolume); QFETCH(int, numberOfVolumes); QCOMPARE(rarPlugin->numberOfVolumes(), numberOfVolumes); QVERIFY(signalSpyCompMethod.count() > 0); QFETCH(QStringList, compressionMethods); if (!compressionMethods.isEmpty()) { QCOMPARE(signalSpyCompMethod.at(0).at(0).toStringList(), compressionMethods); } QFETCH(int, someEntryIndex); QVERIFY(someEntryIndex < signalSpyEntry.count()); Archive::Entry *entry = signalSpyEntry.at(someEntryIndex).at(0).value(); QFETCH(QString, expectedName); QCOMPARE(entry->fullPath(), expectedName); QFETCH(bool, isDirectory); QCOMPARE(entry->isDir(), isDirectory); QFETCH(bool, isPasswordProtected); QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected); QFETCH(QString, symlinkTarget); QCOMPARE(entry->property("link").toString(), symlinkTarget); QFETCH(qulonglong, expectedSize); QCOMPARE(entry->property("size").toULongLong(), expectedSize); QFETCH(qulonglong, expectedCompressedSize); QCOMPARE(entry->property("compressedSize").toULongLong(), expectedCompressedSize); QFETCH(QString, expectedTimestamp); QCOMPARE(entry->property("timestamp").toString(), expectedTimestamp); rarPlugin->deleteLater(); } void CliRarTest::testListArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.rar") << QString() << QStringList { QStringLiteral("vt"), QStringLiteral("-v"), QStringLiteral("/tmp/foo.rar") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.rar") << QStringLiteral("1234") << QStringList { QStringLiteral("vt"), QStringLiteral("-v"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.rar") }; } void CliRarTest::testListArgs() { + if (!m_plugin->isValid()) { + QSKIP("clirar plugin not available. Skipping test.", SkipSingle); + } + QFETCH(QString, archiveName); - CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); + CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName), + QVariant::fromValue(m_plugin->metaData())}); QVERIFY(plugin); - const QStringList listArgs = { QStringLiteral("vt"), - QStringLiteral("-v"), - QStringLiteral("$PasswordSwitch"), - QStringLiteral("$Archive") }; - QFETCH(QString, password); - const auto replacedArgs = plugin->substituteListVariables(listArgs, password); + const auto replacedArgs = plugin->cliProperties()->listArgs(archiveName, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void CliRarTest::testAddArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("encryptHeader"); QTest::addColumn("compressionLevel"); QTest::addColumn("compressionMethod"); QTest::addColumn("volumeSize"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.rar") << QString() << false << 3 << QStringLiteral("RAR4") << 0UL << QStringList { QStringLiteral("a"), - QStringLiteral("/tmp/foo.rar"), QStringLiteral("-m3"), - QStringLiteral("-ma4") + QStringLiteral("-ma4"), + QStringLiteral("/tmp/foo.rar") }; QTest::newRow("encrypted") << QStringLiteral("/tmp/foo.rar") << QStringLiteral("1234") << false << 3 << QString() << 0UL << QStringList { QStringLiteral("a"), - QStringLiteral("/tmp/foo.rar"), QStringLiteral("-p1234"), - QStringLiteral("-m3") + QStringLiteral("-m3"), + QStringLiteral("/tmp/foo.rar") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.rar") << QStringLiteral("1234") << true << 3 << QString() << 0UL << QStringList { QStringLiteral("a"), - QStringLiteral("/tmp/foo.rar"), QStringLiteral("-hp1234"), - QStringLiteral("-m3") + QStringLiteral("-m3"), + QStringLiteral("/tmp/foo.rar") }; QTest::newRow("multi-volume") << QStringLiteral("/tmp/foo.rar") << QString() << false << 3 << QString() << 2500UL << QStringList { QStringLiteral("a"), - QStringLiteral("/tmp/foo.rar"), QStringLiteral("-m3"), - QStringLiteral("-v2500k") + QStringLiteral("-v2500k"), + QStringLiteral("/tmp/foo.rar") }; QTest::newRow("comp-method-RAR5") << QStringLiteral("/tmp/foo.rar") << QString() << false << 3 << QStringLiteral("RAR5") << 0UL << QStringList { QStringLiteral("a"), - QStringLiteral("/tmp/foo.rar"), QStringLiteral("-m3"), - QStringLiteral("-ma5") + QStringLiteral("-ma5"), + QStringLiteral("/tmp/foo.rar") }; } void CliRarTest::testAddArgs() { - QFETCH(QString, archiveName); - CliPlugin *rarPlugin = new CliPlugin(this, {QVariant(archiveName)}); - QVERIFY(rarPlugin); + if (!m_plugin->isValid()) { + QSKIP("clirar plugin not available. Skipping test.", SkipSingle); + } - const QStringList addArgs = { QStringLiteral("a"), - QStringLiteral("$Archive"), - QStringLiteral("$PasswordSwitch"), - QStringLiteral("$CompressionLevelSwitch"), - QStringLiteral("$CompressionMethodSwitch"), - QStringLiteral("$MultiVolumeSwitch"), - QStringLiteral("$Files") }; + QFETCH(QString, archiveName); + CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName), + QVariant::fromValue(m_plugin->metaData())}); + QVERIFY(plugin); QFETCH(QString, password); QFETCH(bool, encryptHeader); QFETCH(int, compressionLevel); QFETCH(QString, compressionMethod); QFETCH(ulong, volumeSize); - QStringList replacedArgs = rarPlugin->substituteAddVariables(addArgs, {}, password, encryptHeader, compressionLevel, volumeSize, compressionMethod); + const auto replacedArgs = plugin->cliProperties()->addArgs(archiveName, {}, password, encryptHeader, compressionLevel, compressionMethod, volumeSize); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); - rarPlugin->deleteLater(); + plugin->deleteLater(); } void CliRarTest::testExtractArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn>("files"); QTest::addColumn("preservePaths"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("preserve paths, encrypted") << QStringLiteral("/tmp/foo.rar") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << true << QStringLiteral("1234") << QStringList { + QStringLiteral("x"), QStringLiteral("-kb"), QStringLiteral("-p-"), - QStringLiteral("x"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("preserve paths, unencrypted") << QStringLiteral("/tmp/foo.rar") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << true << QString() << QStringList { + QStringLiteral("x"), QStringLiteral("-kb"), QStringLiteral("-p-"), - QStringLiteral("x"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, encrypted") << QStringLiteral("/tmp/foo.rar") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << false << QStringLiteral("1234") << QStringList { + QStringLiteral("e"), QStringLiteral("-kb"), QStringLiteral("-p-"), - QStringLiteral("e"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, unencrypted") << QStringLiteral("/tmp/foo.rar") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << false << QString() << QStringList { + QStringLiteral("e"), QStringLiteral("-kb"), QStringLiteral("-p-"), - QStringLiteral("e"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; } void CliRarTest::testExtractArgs() { - QFETCH(QString, archiveName); - CliPlugin *rarPlugin = new CliPlugin(this, {QVariant(archiveName)}); - QVERIFY(rarPlugin); + if (!m_plugin->isValid()) { + QSKIP("clirar plugin not available. Skipping test.", SkipSingle); + } - const QStringList extractArgs = { QStringLiteral("-kb"), - QStringLiteral("-p-"), - QStringLiteral("$PreservePathSwitch"), - QStringLiteral("$PasswordSwitch"), - QStringLiteral("$Archive"), - QStringLiteral("$Files") }; + QFETCH(QString, archiveName); + CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName), + QVariant::fromValue(m_plugin->metaData())}); + QVERIFY(plugin); QFETCH(QVector, files); + QStringList filesList; + foreach (const Archive::Entry *e, files) { + filesList << e->fullPath(NoTrailingSlash); + } + QFETCH(bool, preservePaths); QFETCH(QString, password); - QStringList replacedArgs = rarPlugin->substituteExtractVariables(extractArgs, files, preservePaths, password); - QVERIFY(replacedArgs.size() >= extractArgs.size()); + const auto replacedArgs = plugin->cliProperties()->extractArgs(archiveName, filesList, preservePaths, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); - rarPlugin->deleteLater(); + plugin->deleteLater(); } diff --git a/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp b/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp index 52d71987..bded1665 100644 --- a/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp +++ b/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp @@ -1,398 +1,404 @@ /* * Copyright (C) 2016 Elvis Angelaccio * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliunarchivertest.h" #include "jobs.h" #include "testhelper.h" #include #include #include #include #include #include QTEST_GUILESS_MAIN(CliUnarchiverTest) using namespace Kerfuffle; void CliUnarchiverTest::initTestCase() { m_plugin = new Plugin(this); foreach (Plugin *plugin, m_pluginManger.availablePlugins()) { if (plugin->metaData().pluginId() == QStringLiteral("kerfuffle_cliunarchiver")) { m_plugin = plugin; return; } } } void CliUnarchiverTest::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() << true << true << Archive::Unencrypted << QStringLiteral("A"); archivePath = QFINDTESTDATA("data/multiple_toplevel_entries.rar"); QTest::newRow("archive with multiple top-level entries") << archivePath << QFileInfo(archivePath).fileName() << true << false << Archive::Unencrypted << QStringLiteral("multiple_toplevel_entries"); archivePath = QFINDTESTDATA("data/encrypted_entries.rar"); QTest::newRow("archive with encrypted entries") << archivePath << QFileInfo(archivePath).fileName() << true << true << Archive::Encrypted << QStringLiteral("A"); } void CliUnarchiverTest::testArchive() { if (!m_plugin->isValid()) { QSKIP("cliunarchiver plugin not available. Skipping test.", SkipSingle); } QFETCH(QString, archivePath); auto loadJob = Archive::load(archivePath, m_plugin, this); QVERIFY(loadJob); TestHelper::startAndWaitForResult(loadJob); auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not load the cliunarchiver 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->isSingleFolder(), isSingleFolder); QFETCH(Archive::EncryptionType, expectedEncryptionType); QCOMPARE(archive->encryptionType(), expectedEncryptionType); QFETCH(QString, expectedSubfolderName); QCOMPARE(archive->subfolderName(), expectedSubfolderName); } void CliUnarchiverTest::testList_data() { QTest::addColumn("jsonFilePath"); 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("expectedSize"); QTest::addColumn("expectedCompressedSize"); QTest::addColumn("expectedTimestamp"); QTest::newRow("archive with one top-level folder") << QFINDTESTDATA("data/one_toplevel_folder.json") << 9 << 6 << QStringLiteral("A/B/C/") << true << false << (qulonglong) 0 << (qulonglong) 0 << QStringLiteral("2015-12-21T16:57:20+01:00"); QTest::newRow("archive with multiple top-level entries") << QFINDTESTDATA("data/multiple_toplevel_entries.json") << 12 << 4 << QStringLiteral("data/A/B/test1.txt") << false << false << (qulonglong) 7 << (qulonglong) 7 << QStringLiteral("2015-12-21T16:56:28+01:00"); QTest::newRow("archive with encrypted entries") << QFINDTESTDATA("data/encrypted_entries.json") << 9 << 5 << QStringLiteral("A/test1.txt") << false << true << (qulonglong) 7 << (qulonglong) 32 << QStringLiteral("2015-12-21T16:56:28+01:00"); QTest::newRow("huge archive") << QFINDTESTDATA("data/huge_archive.json") << 250 << 8 << QStringLiteral("PsycOPacK/Base Dictionnaries/att800") << false << false << (qulonglong) 593687 << (qulonglong) 225219 << QStringLiteral("2011-08-14T03:10:10+02:00"); } void CliUnarchiverTest::testList() { qRegisterMetaType("Archive::Entry*"); - CliPlugin *unarPlugin = new CliPlugin(this, {QStringLiteral("dummy.rar")}); - QSignalSpy signalSpy(unarPlugin, &CliPlugin::entry); + CliPlugin *plugin = new CliPlugin(this, {QStringLiteral("dummy.rar"), + QVariant::fromValue(m_plugin->metaData())}); + QSignalSpy signalSpy(plugin, &CliPlugin::entry); QFETCH(QString, jsonFilePath); QFETCH(int, expectedEntriesCount); QFile jsonFile(jsonFilePath); QVERIFY(jsonFile.open(QIODevice::ReadOnly)); QTextStream stream(&jsonFile); - unarPlugin->setJsonOutput(stream.readAll()); + plugin->setJsonOutput(stream.readAll()); QCOMPARE(signalSpy.count(), expectedEntriesCount); QFETCH(int, someEntryIndex); QVERIFY(someEntryIndex < signalSpy.count()); Archive::Entry *entry = signalSpy.at(someEntryIndex).at(0).value(); QFETCH(QString, expectedName); QCOMPARE(entry->fullPath(), expectedName); QFETCH(bool, isDirectory); QCOMPARE(entry->isDir(), isDirectory); QFETCH(bool, isPasswordProtected); QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected); QFETCH(qulonglong, expectedSize); QCOMPARE(entry->property("size").toULongLong(), expectedSize); QFETCH(qulonglong, expectedCompressedSize); QCOMPARE(entry->property("compressedSize").toULongLong(), expectedCompressedSize); QFETCH(QString, expectedTimestamp); QCOMPARE(entry->property("timestamp").toString(), expectedTimestamp); - unarPlugin->deleteLater(); + plugin->deleteLater(); } void CliUnarchiverTest::testListArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.rar") << QString() << QStringList { QStringLiteral("-json"), QStringLiteral("/tmp/foo.rar") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.rar") << QStringLiteral("1234") << QStringList { QStringLiteral("-json"), - QStringLiteral("/tmp/foo.rar"), QStringLiteral("-password"), - QStringLiteral("1234") + QStringLiteral("1234"), + QStringLiteral("/tmp/foo.rar") }; } void CliUnarchiverTest::testListArgs() { + if (!m_plugin->isValid()) { + QSKIP("cliunarchiver plugin not available. Skipping test.", SkipSingle); + } + QFETCH(QString, archiveName); - CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); + CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName), + QVariant::fromValue(m_plugin->metaData())}); QVERIFY(plugin); - const QStringList listArgs = { QStringLiteral("-json"), - QStringLiteral("$Archive"), - QStringLiteral("$PasswordSwitch") }; - QFETCH(QString, password); - const auto replacedArgs = plugin->substituteListVariables(listArgs, password); + const auto replacedArgs = plugin->cliProperties()->listArgs(archiveName, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void CliUnarchiverTest::testExtraction_data() { QTest::addColumn("archivePath"); QTest::addColumn>("entriesToExtract"); QTest::addColumn("extractionOptions"); QTest::addColumn("expectedExtractedEntriesCount"); ExtractionOptions defaultOptions; defaultOptions.setAlwaysUseTempDir(true); ExtractionOptions optionsNoPaths = defaultOptions; optionsNoPaths.setPreservePaths(false); ExtractionOptions dragAndDropOptions = defaultOptions; dragAndDropOptions.setDragAndDropEnabled(true); QTest::newRow("extract the whole multiple_toplevel_entries.rar") << QFINDTESTDATA("data/multiple_toplevel_entries.rar") << QVector() << defaultOptions << 12; QTest::newRow("extract selected entries from a rar, without paths") << QFINDTESTDATA("data/one_toplevel_folder.rar") << QVector { new Archive::Entry(this, QStringLiteral("A/test2.txt"), QStringLiteral("A")), new Archive::Entry(this, QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B")) } << optionsNoPaths << 2; QTest::newRow("extract selected entries from a rar, preserve paths") << QFINDTESTDATA("data/one_toplevel_folder.rar") << QVector { new Archive::Entry(this, QStringLiteral("A/test2.txt"), QStringLiteral("A")), new Archive::Entry(this, QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B")) } << defaultOptions << 4; QTest::newRow("extract selected entries from a rar, drag-and-drop") << QFINDTESTDATA("data/one_toplevel_folder.rar") << QVector { new Archive::Entry(this, QStringLiteral("A/B/C/"), QStringLiteral("A/B/")), new Archive::Entry(this, QStringLiteral("A/test2.txt"), QStringLiteral("A/")), new Archive::Entry(this, QStringLiteral("A/B/C/test1.txt"), QStringLiteral("A/B/")), new Archive::Entry(this, QStringLiteral("A/B/C/test2.txt"), QStringLiteral("A/B/")) } << dragAndDropOptions << 4; QTest::newRow("rar with empty folders") << QFINDTESTDATA("data/empty_folders.rar") << QVector() << defaultOptions << 5; QTest::newRow("rar with hidden folder and files") << QFINDTESTDATA("data/hidden_files.rar") << QVector() << defaultOptions << 4; } // TODO: we can remove this test (which is duplicated from kerfuffle/archivetest) // if we ever ends up using a temp dir for any cliplugin, instead of only for cliunarchiver. void CliUnarchiverTest::testExtraction() { if (!m_plugin->isValid()) { QSKIP("cliunarchiver plugin not available. Skipping test.", SkipSingle); } QFETCH(QString, archivePath); auto loadJob = Archive::load(archivePath, m_plugin, this); QVERIFY(loadJob); TestHelper::startAndWaitForResult(loadJob); auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not load the cliunarchiver plugin. Skipping test.", SkipSingle); } QTemporaryDir destDir; if (!destDir.isValid()) { QSKIP("Could not create a temporary directory for extraction. Skipping test.", SkipSingle); } QFETCH(QVector, entriesToExtract); QFETCH(ExtractionOptions, extractionOptions); auto extractionJob = archive->extractFiles(entriesToExtract, destDir.path(), extractionOptions); QEventLoop eventLoop(this); connect(extractionJob, &KJob::result, &eventLoop, &QEventLoop::quit); extractionJob->start(); eventLoop.exec(); // krazy:exclude=crashy QFETCH(int, expectedExtractedEntriesCount); int extractedEntriesCount = 0; QDirIterator dirIt(destDir.path(), QDir::AllEntries | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (dirIt.hasNext()) { extractedEntriesCount++; dirIt.next(); } QCOMPARE(extractedEntriesCount, expectedExtractedEntriesCount); archive->deleteLater(); } void CliUnarchiverTest::testExtractArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn>("files"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("encrypted, multiple files") << QStringLiteral("/tmp/foo.rar") << QVector { new Archive::Entry(this, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << QStringLiteral("1234") << QStringList { QStringLiteral("-D"), + QStringLiteral("-password"), + QStringLiteral("1234"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), - QStringLiteral("c.txt"), - QStringLiteral("-password"), - QStringLiteral("1234") + QStringLiteral("c.txt") }; QTest::newRow("unencrypted, multiple files") << QStringLiteral("/tmp/foo.rar") << QVector { new Archive::Entry(this, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << QString() << QStringList { QStringLiteral("-D"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; } void CliUnarchiverTest::testExtractArgs() { + if (!m_plugin->isValid()) { + QSKIP("cliunarchiver plugin not available. Skipping test.", SkipSingle); + } + QFETCH(QString, archiveName); - CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); + CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName), + QVariant::fromValue(m_plugin->metaData())}); QVERIFY(plugin); - const QStringList extractArgs = { QStringLiteral("-D"), - QStringLiteral("$Archive"), - QStringLiteral("$Files"), - QStringLiteral("$PasswordSwitch") }; - QFETCH(QVector, files); + QStringList filesList; + foreach (const Archive::Entry *e, files) { + filesList << e->fullPath(NoTrailingSlash); + } + QFETCH(QString, password); - QStringList replacedArgs = plugin->substituteExtractVariables(extractArgs, files, false, password); - QVERIFY(replacedArgs.size() >= extractArgs.size()); + const auto replacedArgs = plugin->cliProperties()->extractArgs(archiveName, filesList, false, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } diff --git a/autotests/plugins/clizipplugin/cliziptest.cpp b/autotests/plugins/clizipplugin/cliziptest.cpp index d77a6b02..22a5b2ee 100644 --- a/autotests/plugins/clizipplugin/cliziptest.cpp +++ b/autotests/plugins/clizipplugin/cliziptest.cpp @@ -1,217 +1,231 @@ /* * Copyright (c) 2016 Elvis Angelaccio * * 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 "cliziptest.h" #include QTEST_GUILESS_MAIN(CliZipTest) using namespace Kerfuffle; +void CliZipTest::initTestCase() +{ + m_plugin = new Plugin(this); + foreach (Plugin *plugin, m_pluginManger.availablePlugins()) { + if (plugin->metaData().pluginId() == QStringLiteral("kerfuffle_clizip")) { + m_plugin = plugin; + return; + } + } +} + void CliZipTest::testListArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("expectedArgs"); QTest::newRow("fake zip") << QStringLiteral("/tmp/foo.zip") << QStringList { QStringLiteral("-l"), QStringLiteral("-T"), QStringLiteral("-z"), QStringLiteral("/tmp/foo.zip") }; } void CliZipTest::testListArgs() { + if (!m_plugin->isValid()) { + QSKIP("clizip plugin not available. Skipping test.", SkipSingle); + } + QFETCH(QString, archiveName); - CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); + CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName), + QVariant::fromValue(m_plugin->metaData())}); QVERIFY(plugin); - const QStringList listArgs = { QStringLiteral("-l"), - QStringLiteral("-T"), - QStringLiteral("-z"), - QStringLiteral("$Archive") }; - - const auto replacedArgs = plugin->substituteListVariables(listArgs, QString()); + const auto replacedArgs = plugin->cliProperties()->listArgs(archiveName, QString()); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void CliZipTest::testAddArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("compressionLevel"); QTest::addColumn("compressionMethod"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.zip") - << QString() << 3 << QStringLiteral("deflate") + << QString() << 3 << QStringLiteral("Deflate") << QStringList { QStringLiteral("-r"), - QStringLiteral("/tmp/foo.zip"), QStringLiteral("-3"), - QStringLiteral("-Zdeflate") + QStringLiteral("-Zdeflate"), + QStringLiteral("/tmp/foo.zip") }; QTest::newRow("encrypted") << QStringLiteral("/tmp/foo.zip") << QStringLiteral("1234") << 3 << QString() << QStringList { QStringLiteral("-r"), - QStringLiteral("/tmp/foo.zip"), QStringLiteral("-P1234"), - QStringLiteral("-3") + QStringLiteral("-3"), + QStringLiteral("/tmp/foo.zip") }; QTest::newRow("comp-method-bzip2") << QStringLiteral("/tmp/foo.zip") - << QString() << 3 << QStringLiteral("bzip2") + << QString() << 3 << QStringLiteral("BZip2") << QStringList { QStringLiteral("-r"), - QStringLiteral("/tmp/foo.zip"), QStringLiteral("-3"), - QStringLiteral("-Zbzip2") + QStringLiteral("-Zbzip2"), + QStringLiteral("/tmp/foo.zip") }; } void CliZipTest::testAddArgs() { + if (!m_plugin->isValid()) { + QSKIP("clizip plugin not available. Skipping test.", SkipSingle); + } + QFETCH(QString, archiveName); - CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); + CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName), + QVariant::fromValue(m_plugin->metaData())}); QVERIFY(plugin); - const QStringList addArgs = { QStringLiteral("-r"), - QStringLiteral("$Archive"), - QStringLiteral("$PasswordSwitch"), - QStringLiteral("$CompressionLevelSwitch"), - QStringLiteral("$CompressionMethodSwitch"), - QStringLiteral("$Files") }; - QFETCH(QString, password); QFETCH(int, compressionLevel); QFETCH(QString, compressionMethod); - QStringList replacedArgs = plugin->substituteAddVariables(addArgs, {}, password, false, compressionLevel, 0, compressionMethod); + const auto replacedArgs = plugin->cliProperties()->addArgs(archiveName, {}, password, false, compressionLevel, compressionMethod, 0); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void CliZipTest::testExtractArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn>("files"); QTest::addColumn("preservePaths"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("preserve paths, encrypted") << QStringLiteral("/tmp/foo.zip") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << true << QStringLiteral("1234") << QStringList { QStringLiteral("-P1234"), QStringLiteral("/tmp/foo.zip"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("preserve paths, unencrypted") << QStringLiteral("/tmp/foo.zip") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << true << QString() << QStringList { QStringLiteral("/tmp/foo.zip"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, encrypted") << QStringLiteral("/tmp/foo.zip") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << false << QStringLiteral("1234") << QStringList { QStringLiteral("-j"), QStringLiteral("-P1234"), QStringLiteral("/tmp/foo.zip"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, unencrypted") << QStringLiteral("/tmp/foo.zip") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << false << QString() << QStringList { QStringLiteral("-j"), QStringLiteral("/tmp/foo.zip"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; } void CliZipTest::testExtractArgs() { + if (!m_plugin->isValid()) { + QSKIP("clizip plugin not available. Skipping test.", SkipSingle); + } + QFETCH(QString, archiveName); - CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); + CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName), + QVariant::fromValue(m_plugin->metaData())}); QVERIFY(plugin); - const QStringList extractArgs = { QStringLiteral("$PreservePathSwitch"), - QStringLiteral("$PasswordSwitch"), - QStringLiteral("$Archive"), - QStringLiteral("$Files") }; - QFETCH(QVector, files); + QStringList filesList; + foreach (const Archive::Entry *e, files) { + filesList << e->fullPath(NoTrailingSlash); + } + QFETCH(bool, preservePaths); QFETCH(QString, password); - QStringList replacedArgs = plugin->substituteExtractVariables(extractArgs, files, preservePaths, password); + const auto replacedArgs = plugin->cliProperties()->extractArgs(archiveName, filesList, preservePaths, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } diff --git a/autotests/plugins/clizipplugin/cliziptest.h b/autotests/plugins/clizipplugin/cliziptest.h index 6f8aed24..4b253714 100644 --- a/autotests/plugins/clizipplugin/cliziptest.h +++ b/autotests/plugins/clizipplugin/cliziptest.h @@ -1,49 +1,54 @@ /* * Copyright (c) 2016 Elvis Angelaccio * * 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 CLIZIPTEST_H #define CLIZIPTEST_H #include "cliplugin.h" #include "autotests/testhelper/testhelper.h" #include "kerfuffle/jobs.h" +#include "pluginmanager.h" using namespace Kerfuffle; class CliZipTest : public QObject { Q_OBJECT private Q_SLOTS: - + void initTestCase(); void testListArgs_data(); void testListArgs(); void testAddArgs_data(); void testAddArgs(); void testExtractArgs_data(); void testExtractArgs(); + +private: + PluginManager m_pluginManger; + Plugin *m_plugin; }; #endif diff --git a/kerfuffle/CMakeLists.txt b/kerfuffle/CMakeLists.txt index f114bd0e..663e2ef9 100644 --- a/kerfuffle/CMakeLists.txt +++ b/kerfuffle/CMakeLists.txt @@ -1,69 +1,70 @@ ########### next target ############### set(kerfuffle_SRCS archiveformat.cpp archive_kerfuffle.cpp archiveinterface.cpp extractionsettingspage.cpp previewsettingspage.cpp settingspage.cpp jobs.cpp adddialog.cpp compressionoptionswidget.cpp createdialog.cpp extractiondialog.cpp propertiesdialog.cpp queries.cpp addtoarchive.cpp cliinterface.cpp + cliproperties.cpp mimetypes.cpp plugin.cpp pluginmanager.cpp archiveentry.cpp options.cpp ) kconfig_add_kcfg_files(kerfuffle_SRCS settings.kcfgc GENERATE_MOC) ki18n_wrap_ui(kerfuffle_SRCS createdialog.ui extractiondialog.ui extractionsettings.ui previewsettings.ui propertiesdialog.ui compressionoptionswidget.ui ) ecm_qt_declare_logging_category(kerfuffle_SRCS HEADER ark_debug.h IDENTIFIER ARK CATEGORY_NAME ark.kerfuffle) add_library(kerfuffle SHARED ${kerfuffle_SRCS}) generate_export_header(kerfuffle BASE_NAME kerfuffle) target_link_libraries(kerfuffle PUBLIC KF5::IconThemes KF5::Pty KF5::Service KF5::I18n KF5::WidgetsAddons PRIVATE Qt5::Concurrent KF5::KIOCore KF5::KIOWidgets KF5::KIOFileWidgets ) set_target_properties(kerfuffle PROPERTIES VERSION ${KERFUFFLE_VERSION_STRING} SOVERSION ${KERFUFFLE_SOVERSION}) install(TARGETS kerfuffle ${KDE_INSTALL_TARGETS_DEFAULT_ARGS} LIBRARY NAMELINK_SKIP) install(FILES kerfufflePlugin.desktop DESTINATION ${KDE_INSTALL_KSERVICETYPES5DIR}) install(FILES ark.kcfg DESTINATION ${KDE_INSTALL_KCFGDIR}) install(FILES mime/kerfuffle.xml DESTINATION ${KDE_INSTALL_MIMEDIR}) if(SharedMimeInfo_FOUND) update_xdg_mimetypes(${KDE_INSTALL_MIMEDIR}) endif() diff --git a/kerfuffle/archive_kerfuffle.cpp b/kerfuffle/archive_kerfuffle.cpp index 78e2edbf..7b807490 100644 --- a/kerfuffle/archive_kerfuffle.cpp +++ b/kerfuffle/archive_kerfuffle.cpp @@ -1,517 +1,518 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008 Harald Hvaal * Copyright (c) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * 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 "archive_kerfuffle.h" #include "archiveentry.h" #include "archiveinterface.h" #include "jobs.h" #include "mimetypes.h" #include "pluginmanager.h" #include #include #include #include namespace Kerfuffle { Archive *Archive::create(const QString &fileName, QObject *parent) { return create(fileName, QString(), parent); } Archive *Archive::create(const QString &fileName, const QString &fixedMimeType, QObject *parent) { qCDebug(ARK) << "Going to create archive" << fileName; PluginManager pluginManager; const QMimeType mimeType = fixedMimeType.isEmpty() ? determineMimeType(fileName) : QMimeDatabase().mimeTypeForName(fixedMimeType); const QVector offers = pluginManager.preferredPluginsFor(mimeType); if (offers.isEmpty()) { qCCritical(ARK) << "Could not find a plugin to handle" << fileName; return new Archive(NoPlugin, parent); } Archive *archive = Q_NULLPTR; foreach (Plugin *plugin, offers) { archive = create(fileName, plugin, parent); // Use the first valid plugin, according to the priority sorting. if (archive->isValid()) { return archive; } } qCCritical(ARK) << "Failed to find a usable plugin for" << fileName; return archive; } Archive *Archive::create(const QString &fileName, Plugin *plugin, QObject *parent) { Q_ASSERT(plugin); qCDebug(ARK) << "Checking plugin" << plugin->metaData().pluginId(); KPluginFactory *factory = KPluginLoader(plugin->metaData().fileName()).factory(); if (!factory) { qCWarning(ARK) << "Invalid plugin factory for" << plugin->metaData().pluginId(); return new Archive(FailedPlugin, parent); } - const QVariantList args = {QVariant(QFileInfo(fileName).absoluteFilePath())}; + const QVariantList args = {QVariant(QFileInfo(fileName).absoluteFilePath()), + QVariant().fromValue(plugin->metaData())}; ReadOnlyArchiveInterface *iface = factory->create(Q_NULLPTR, args); if (!iface) { qCWarning(ARK) << "Could not create plugin instance" << plugin->metaData().pluginId(); return new Archive(FailedPlugin, parent); } if (!plugin->isValid()) { qCDebug(ARK) << "Cannot use plugin" << plugin->metaData().pluginId() << "- check whether" << plugin->readOnlyExecutables() << "are installed."; return new Archive(FailedPlugin, parent); } qCDebug(ARK) << "Successfully loaded plugin" << plugin->metaData().pluginId(); return new Archive(iface, !plugin->isReadWrite(), parent); } BatchExtractJob *Archive::batchExtract(const QString &fileName, const QString &destination, bool autoSubfolder, bool preservePaths, QObject *parent) { auto loadJob = load(fileName, parent); auto batchJob = new BatchExtractJob(loadJob, destination, autoSubfolder, preservePaths); return batchJob; } CreateJob *Archive::create(const QString &fileName, const QString &mimeType, const QVector &entries, const CompressionOptions &options, QObject *parent) { auto archive = create(fileName, mimeType, parent); auto createJob = new CreateJob(archive, entries, options); return createJob; } Archive *Archive::createEmpty(const QString &fileName, const QString &mimeType, QObject *parent) { auto archive = create(fileName, mimeType, parent); Q_ASSERT(archive->isEmpty()); return archive; } LoadJob *Archive::load(const QString &fileName, QObject *parent) { return load(fileName, QString(), parent); } LoadJob *Archive::load(const QString &fileName, const QString &mimeType, QObject *parent) { auto archive = create(fileName, mimeType, parent); auto loadJob = new LoadJob(archive); return loadJob; } LoadJob *Archive::load(const QString &fileName, Plugin *plugin, QObject *parent) { auto archive = create(fileName, plugin, parent); auto loadJob = new LoadJob(archive); return loadJob; } Archive::Archive(ArchiveError errorCode, QObject *parent) : QObject(parent) , m_iface(Q_NULLPTR) , m_error(errorCode) { qCDebug(ARK) << "Created archive instance with error"; } Archive::Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent) : QObject(parent) , m_iface(archiveInterface) , m_isReadOnly(isReadOnly) , m_isSingleFolder(false) , m_isMultiVolume(false) , m_extractedFilesSize(0) , m_error(NoError) , m_encryptionType(Unencrypted) { qCDebug(ARK) << "Created archive instance"; Q_ASSERT(m_iface); m_iface->setParent(this); connect(m_iface, &ReadOnlyArchiveInterface::compressionMethodFound, this, &Archive::onCompressionMethodFound); } void Archive::onCompressionMethodFound(const QStringList &methods) { // If other methods are found, we dont report "Store" method. QStringList processedMethods = methods; if (processedMethods.size() > 1 && processedMethods.contains(QStringLiteral("Store"))) { processedMethods.removeOne(QStringLiteral("Store")); } setProperty("compressionMethods", processedMethods); } Archive::~Archive() { } QString Archive::completeBaseName() const { const QString suffix = QFileInfo(fileName()).suffix(); QString base = QFileInfo(fileName()).completeBaseName(); // Special case for compressed tar archives. if (base.right(4).toUpper() == QLatin1String(".TAR")) { base.chop(4); // Multi-volume 7z's are named name.7z.001. } else if (base.right(3).toUpper() == QLatin1String(".7Z")) { base.chop(3); // Multi-volume zip's are named name.zip.001. } else if (base.right(4).toUpper() == QLatin1String(".ZIP")) { base.chop(4); // For multivolume rar's we want to remove the ".partNNN" suffix. } else if (suffix.toUpper() == QLatin1String("RAR")) { base.remove(QRegularExpression(QStringLiteral("\\.part[0-9]{1,3}$"))); } return base; } QString Archive::fileName() const { return isValid() ? m_iface->filename() : QString(); } QString Archive::comment() const { return isValid() ? m_iface->comment() : QString(); } CommentJob* Archive::addComment(const QString &comment) { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to add comment:" << comment; Q_ASSERT(!isReadOnly()); CommentJob *job = new CommentJob(comment, static_cast(m_iface)); return job; } TestJob* Archive::testArchive() { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to test archive"; TestJob *job = new TestJob(m_iface); return job; } QMimeType Archive::mimeType() { if (!isValid()) { return QMimeType(); } if (!m_mimeType.isValid()) { m_mimeType = determineMimeType(fileName()); } return m_mimeType; } bool Archive::isEmpty() const { return (numberOfEntries() == 0); } bool Archive::isReadOnly() const { return isValid() ? (m_iface->isReadOnly() || m_isReadOnly || (isMultiVolume() && (numberOfEntries() > 0))) : false; } bool Archive::isSingleFolder() const { if (!isValid()) { return false; } return m_isSingleFolder; } bool Archive::hasComment() const { return isValid() ? !comment().isEmpty() : false; } bool Archive::isMultiVolume() const { if (!isValid()) { return false; } return m_iface->isMultiVolume(); } void Archive::setMultiVolume(bool value) { m_iface->setMultiVolume(value); } int Archive::numberOfVolumes() const { return m_iface->numberOfVolumes(); } Archive::EncryptionType Archive::encryptionType() const { if (!isValid()) { return Unencrypted; } return m_encryptionType; } QString Archive::password() const { return m_iface->password(); } uint Archive::numberOfEntries() const { if (!isValid()) { return 0; } return m_iface->numberOfEntries(); } qulonglong Archive::unpackedSize() const { if (!isValid()) { return 0; } return m_extractedFilesSize; } qulonglong Archive::packedSize() const { return isValid() ? QFileInfo(fileName()).size() : 0; } QString Archive::subfolderName() const { if (!isValid()) { return QString(); } return m_subfolderName; } bool Archive::isValid() const { return m_iface && (m_error == NoError); } ArchiveError Archive::error() const { return m_error; } DeleteJob* Archive::deleteFiles(QVector &entries) { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to delete" << entries.size() << "entries"; if (m_iface->isReadOnly()) { return 0; } DeleteJob *newJob = new DeleteJob(entries, static_cast(m_iface)); return newJob; } AddJob* Archive::addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options) { if (!isValid()) { return Q_NULLPTR; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions.setEncryptedArchiveHint(true); } qCDebug(ARK) << "Going to add files" << files << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); AddJob *newJob = new AddJob(files, destination, newOptions, static_cast(m_iface)); connect(newJob, &AddJob::result, this, &Archive::onAddFinished); return newJob; } MoveJob* Archive::moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options) { if (!isValid()) { return Q_NULLPTR; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions.setEncryptedArchiveHint(true); } qCDebug(ARK) << "Going to move files" << files << "to destinatian" << destination << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); MoveJob *newJob = new MoveJob(files, destination, newOptions, static_cast(m_iface)); return newJob; } CopyJob* Archive::copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { if (!isValid()) { return Q_NULLPTR; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions.setEncryptedArchiveHint(true); } qCDebug(ARK) << "Going to copy files" << files << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); CopyJob *newJob = new CopyJob(files, destination, newOptions, static_cast(m_iface)); return newJob; } ExtractJob* Archive::extractFiles(const QVector &files, const QString &destinationDir, const ExtractionOptions &options) { if (!isValid()) { return Q_NULLPTR; } ExtractionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions.setEncryptedArchiveHint(true); } ExtractJob *newJob = new ExtractJob(files, destinationDir, newOptions, m_iface); return newJob; } PreviewJob *Archive::preview(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } PreviewJob *job = new PreviewJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } OpenJob *Archive::open(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } OpenJob *job = new OpenJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } OpenWithJob *Archive::openWith(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } OpenWithJob *job = new OpenWithJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } void Archive::encrypt(const QString &password, bool encryptHeader) { if (!isValid()) { return; } m_iface->setPassword(password); m_iface->setHeaderEncryptionEnabled(encryptHeader); m_encryptionType = encryptHeader ? HeaderEncrypted : Encrypted; } void Archive::onAddFinished(KJob* job) { //if the archive was previously a single folder archive and an add job //has successfully finished, then it is no longer a single folder //archive (for the current implementation, which does not allow adding //folders/files other places than the root. //TODO: handle the case of creating a new file and singlefolderarchive //then. if (m_isSingleFolder && !job->error()) { m_isSingleFolder = false; } } void Archive::onUserQuery(Query* query) { query->execute(); } QString Archive::multiVolumeName() const { return m_iface->multiVolumeName(); } ReadOnlyArchiveInterface *Archive::interface() { return m_iface; } } // namespace Kerfuffle diff --git a/kerfuffle/archive_kerfuffle.h b/kerfuffle/archive_kerfuffle.h index b3daa8bb..80ebb547 100644 --- a/kerfuffle/archive_kerfuffle.h +++ b/kerfuffle/archive_kerfuffle.h @@ -1,245 +1,247 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008 Harald Hvaal * Copyright (c) 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 ARCHIVE_H #define ARCHIVE_H #include "kerfuffle_export.h" #include "options.h" #include #include #include #include #include namespace Kerfuffle { class LoadJob; class BatchExtractJob; class CreateJob; class ExtractJob; class DeleteJob; class AddJob; class MoveJob; class CopyJob; class CommentJob; class TestJob; class OpenJob; class OpenWithJob; class Plugin; class PreviewJob; class Query; class ReadOnlyArchiveInterface; enum ArchiveError { NoError = 0, NoPlugin, FailedPlugin }; class KERFUFFLE_EXPORT Archive : public QObject { Q_OBJECT /** * Complete base name, without the "tar" extension (if any). */ Q_PROPERTY(QString completeBaseName READ completeBaseName CONSTANT) Q_PROPERTY(QString fileName READ fileName CONSTANT) Q_PROPERTY(QString comment READ comment CONSTANT) Q_PROPERTY(QMimeType mimeType READ mimeType CONSTANT) Q_PROPERTY(bool isEmpty READ isEmpty) Q_PROPERTY(bool isReadOnly READ isReadOnly CONSTANT) Q_PROPERTY(bool isSingleFolder MEMBER m_isSingleFolder READ isSingleFolder) Q_PROPERTY(bool isMultiVolume READ isMultiVolume WRITE setMultiVolume) Q_PROPERTY(bool numberOfVolumes READ numberOfVolumes) Q_PROPERTY(EncryptionType encryptionType MEMBER m_encryptionType READ encryptionType) Q_PROPERTY(uint numberOfEntries READ numberOfEntries) Q_PROPERTY(qulonglong unpackedSize MEMBER m_extractedFilesSize READ unpackedSize) Q_PROPERTY(qulonglong packedSize READ packedSize) Q_PROPERTY(QString subfolderName MEMBER m_subfolderName READ subfolderName) Q_PROPERTY(QString password READ password) Q_PROPERTY(QStringList compressionMethods MEMBER m_compressionMethods) public: enum EncryptionType { Unencrypted, Encrypted, HeaderEncrypted }; Q_ENUM(EncryptionType) class Entry; QString completeBaseName() const; QString fileName() const; QString comment() const; QMimeType mimeType(); bool isEmpty() const; bool isReadOnly() const; bool isSingleFolder() const; bool isMultiVolume() const; void setMultiVolume(bool value); bool hasComment() const; int numberOfVolumes() const; EncryptionType encryptionType() const; QString password() const; uint numberOfEntries() const; qulonglong unpackedSize() const; qulonglong packedSize() const; QString subfolderName() const; QString multiVolumeName() const; ReadOnlyArchiveInterface *interface(); /** * @return Batch extraction job for @p filename to @p destination. * @param autoSubfolder Whether the job will extract into a subfolder. * @param preservePaths Whether the job will preserve paths. * @param parent The parent for the archive. */ static BatchExtractJob *batchExtract(const QString &fileName, const QString &destination, bool autoSubfolder, bool preservePaths, QObject *parent = Q_NULLPTR); /** * @return Job to create an archive for the given @p entries. * @param fileName The name of the new archive. * @param mimeType The mimetype of the new archive. */ static CreateJob* create(const QString &fileName, const QString &mimeType, const QVector &entries, const CompressionOptions& options, QObject *parent = Q_NULLPTR); /** * @return An empty archive with name @p fileName, mimetype @p mimeType and @p parent as parent. */ static Archive *createEmpty(const QString &fileName, const QString &mimeType, QObject *parent = Q_NULLPTR); /** * @return Job to load the archive @p fileName. * @param parent The parent of the archive that will be loaded. */ static LoadJob* load(const QString &fileName, QObject *parent = Q_NULLPTR); /** * @return Job to load the archive @p fileName with mimetype @p mimeType. * @param parent The parent of the archive that will be loaded. */ static LoadJob* load(const QString &fileName, const QString &mimeType, QObject *parent = Q_NULLPTR); /** * @return Job to load the archive @p fileName by using @p plugin. * @param parent The parent of the archive that will be loaded. */ static LoadJob* load(const QString &fileName, Plugin *plugin, QObject *parent = Q_NULLPTR); ~Archive(); ArchiveError error() const; bool isValid() const; DeleteJob* deleteFiles(QVector &entries); CommentJob* addComment(const QString &comment); TestJob* testArchive(); /** * Compression options that should be handled by all interfaces: * * GlobalWorkDir - Change to this dir before adding the new files. * The path names should then be added relative to this directory. */ AddJob* addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options = CompressionOptions()); /** * Renames or moves entries within the archive. * * @param files All the renamed or moved files and their child entries (for renaming a directory too). * @param destination New entry name (for renaming) or destination folder (for moving). * If ReadOnlyArchiveInterface::entriesWithoutChildren(files).count() returns 1, then it's renaming, * so you must specify the resulted entry name, even if it's not going to be changed. * Otherwise (if count is more than 1) it's moving, so destination must conatin only targeted folder path * or be empty, if moving to the root. */ MoveJob* moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options = CompressionOptions()); /** * Copies entries within the archive. * * @param files All the renamed or moved files and their child entries (for renaming a directory too). * @param destination Destination path. It must conatin only targeted folder path or be empty, * if copying to the root. */ CopyJob* copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options = CompressionOptions()); ExtractJob* extractFiles(const QVector &files, const QString &destinationDir, const ExtractionOptions &options = ExtractionOptions()); PreviewJob* preview(Archive::Entry *entry); OpenJob* open(Archive::Entry *entry); OpenWithJob* openWith(Archive::Entry *entry); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ void encrypt(const QString &password, bool encryptHeader); private slots: void onAddFinished(KJob*); void onUserQuery(Kerfuffle::Query*); void onCompressionMethodFound(const QStringList &methods); private: Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent = 0); Archive(ArchiveError errorCode, QObject *parent = 0); static Archive *create(const QString &fileName, QObject *parent = 0); static Archive *create(const QString &fileName, const QString &fixedMimeType, QObject *parent = 0); /** * Create an archive instance from a given @p plugin. * @param fileName The name of the archive. * @return A valid archive if the plugin could be loaded, an invalid one otherwise (with the FailedPlugin error set). */ static Archive *create(const QString &fileName, Plugin *plugin, QObject *parent = Q_NULLPTR); ReadOnlyArchiveInterface *m_iface; bool m_isReadOnly; bool m_isSingleFolder; bool m_isMultiVolume; QString m_subfolderName; qulonglong m_extractedFilesSize; ArchiveError m_error; EncryptionType m_encryptionType; qulonglong m_numberOfFiles; qulonglong m_numberOfFolders; QMimeType m_mimeType; QStringList m_compressionMethods; }; } // namespace Kerfuffle +Q_DECLARE_METATYPE(KPluginMetaData) + #endif // ARCHIVE_H diff --git a/kerfuffle/archiveformat.cpp b/kerfuffle/archiveformat.cpp index a32b8ec8..454c3f78 100644 --- a/kerfuffle/archiveformat.cpp +++ b/kerfuffle/archiveformat.cpp @@ -1,149 +1,146 @@ /* * Copyright (c) 2016 Elvis Angelaccio * * 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 "archiveformat.h" +#include "ark_debug.h" #include namespace Kerfuffle { ArchiveFormat::ArchiveFormat() : m_encryptionType(Archive::Unencrypted) { } ArchiveFormat::ArchiveFormat(const QMimeType& mimeType, Archive::EncryptionType encryptionType, int minCompLevel, int maxCompLevel, int defaultCompLevel, bool supportsWriteComment, bool supportsTesting, bool supportsMultiVolume, - QStringList compressionMethods, + QVariantMap compressionMethods, QString defaultCompressionMethod) : m_mimeType(mimeType), m_encryptionType(encryptionType), m_minCompressionLevel(minCompLevel), m_maxCompressionLevel(maxCompLevel), m_defaultCompressionLevel(defaultCompLevel), m_supportsWriteComment(supportsWriteComment), m_supportsTesting(supportsTesting), m_supportsMultiVolume(supportsMultiVolume), m_compressionMethods(compressionMethods), m_defaultCompressionMethod(defaultCompressionMethod) { } ArchiveFormat ArchiveFormat::fromMetadata(const QMimeType& mimeType, const KPluginMetaData& metadata) { const QJsonObject json = metadata.rawData(); foreach (const QString& mime, metadata.mimeTypes()) { if (mimeType.name() != mime) { continue; } const QJsonObject formatProps = json[mime].toObject(); int minCompLevel = formatProps[QStringLiteral("CompressionLevelMin")].toInt(); int maxCompLevel = formatProps[QStringLiteral("CompressionLevelMax")].toInt(); int defaultCompLevel = formatProps[QStringLiteral("CompressionLevelDefault")].toInt(); bool supportsWriteComment = formatProps[QStringLiteral("SupportsWriteComment")].toBool(); bool supportsTesting = formatProps[QStringLiteral("SupportsTesting")].toBool(); bool supportsMultiVolume = formatProps[QStringLiteral("SupportsMultiVolume")].toBool(); - QStringList compressionMethods; - QJsonArray array = formatProps[QStringLiteral("CompressionMethods")].toArray(); - foreach (const QJsonValue &value, array) { - compressionMethods.append(value.toString()); - } + QVariantMap compressionMethods = formatProps[QStringLiteral("CompressionMethods")].toObject().toVariantMap(); QString defaultCompMethod = formatProps[QStringLiteral("CompressionMethodDefault")].toString(); Archive::EncryptionType encType = Archive::Unencrypted; if (formatProps[QStringLiteral("HeaderEncryption")].toBool()) { encType = Archive::HeaderEncrypted; } else if (formatProps[QStringLiteral("Encryption")].toBool()) { encType = Archive::Encrypted; } return ArchiveFormat(mimeType, encType, minCompLevel, maxCompLevel, defaultCompLevel, supportsWriteComment, supportsTesting, supportsMultiVolume, compressionMethods, defaultCompMethod); } return ArchiveFormat(); } bool ArchiveFormat::isValid() const { return m_mimeType.isValid(); } Archive::EncryptionType ArchiveFormat::encryptionType() const { return m_encryptionType; } int ArchiveFormat::minCompressionLevel() const { return m_minCompressionLevel; } int ArchiveFormat::maxCompressionLevel() const { return m_maxCompressionLevel; } int ArchiveFormat::defaultCompressionLevel() const { return m_defaultCompressionLevel; } bool ArchiveFormat::supportsWriteComment() const { return m_supportsWriteComment; } bool ArchiveFormat::supportsTesting() const { return m_supportsTesting; } bool ArchiveFormat::supportsMultiVolume() const { return m_supportsMultiVolume; } -QStringList ArchiveFormat::compressionMethods() const +QVariantMap ArchiveFormat::compressionMethods() const { return m_compressionMethods; } QString ArchiveFormat::defaultCompressionMethod() const { return m_defaultCompressionMethod; } } diff --git a/kerfuffle/archiveformat.h b/kerfuffle/archiveformat.h index 4e741aa2..c0938578 100644 --- a/kerfuffle/archiveformat.h +++ b/kerfuffle/archiveformat.h @@ -1,90 +1,90 @@ /* * Copyright (c) 2016 Elvis Angelaccio * * 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 ARCHIVEFORMAT_H #define ARCHIVEFORMAT_H #include "archive_kerfuffle.h" #include namespace Kerfuffle { class KERFUFFLE_EXPORT ArchiveFormat { public: explicit ArchiveFormat(); explicit ArchiveFormat(const QMimeType& mimeType, Kerfuffle::Archive::EncryptionType encryptionType, int minCompLevel, int maxCompLevel, int defaultCompLevel, bool supportsWriteComment, bool supportsTesting, bool suppportsMultiVolume, - QStringList compressionMethods, + QVariantMap compressionMethods, QString defaultCompressionMethod); /** * @return The archive format of the given @p mimeType, according to the given @p metadata. */ static ArchiveFormat fromMetadata(const QMimeType& mimeType, const KPluginMetaData& metadata); /** * @return Whether the format is associated to a valid mimetype. */ bool isValid() const; /** * @return The encryption type supported by the archive format. */ Kerfuffle::Archive::EncryptionType encryptionType() const; int minCompressionLevel() const; int maxCompressionLevel() const; int defaultCompressionLevel() const; bool supportsWriteComment() const; bool supportsTesting() const; bool supportsMultiVolume() const; - QStringList compressionMethods() const; + QVariantMap compressionMethods() const; QString defaultCompressionMethod() const; private: QMimeType m_mimeType; Kerfuffle::Archive::EncryptionType m_encryptionType; int m_minCompressionLevel; int m_maxCompressionLevel; int m_defaultCompressionLevel; bool m_supportsWriteComment; bool m_supportsTesting; bool m_supportsMultiVolume; - QStringList m_compressionMethods; + QVariantMap m_compressionMethods; QString m_defaultCompressionMethod; }; } #endif // ARCHIVEFORMAT_H diff --git a/kerfuffle/archiveinterface.cpp b/kerfuffle/archiveinterface.cpp index d9e70908..67281356 100644 --- a/kerfuffle/archiveinterface.cpp +++ b/kerfuffle/archiveinterface.cpp @@ -1,284 +1,294 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * 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 "archiveinterface.h" #include "ark_debug.h" +#include "mimetypes.h" #include #include #include #include namespace Kerfuffle { ReadOnlyArchiveInterface::ReadOnlyArchiveInterface(QObject *parent, const QVariantList & args) : QObject(parent) , m_numberOfVolumes(0) , m_numberOfEntries(0) , m_waitForFinishedSignal(false) , m_isHeaderEncryptionEnabled(false) , m_isCorrupt(false) , m_isMultiVolume(false) { + Q_ASSERT(args.size() >= 2); + qCDebug(ARK) << "Created read-only interface for" << args.first().toString(); m_filename = args.first().toString(); + m_mimetype = determineMimeType(m_filename); connect(this, &ReadOnlyArchiveInterface::entry, this, &ReadOnlyArchiveInterface::onEntry); + m_metaData = args.at(1).value(); } ReadOnlyArchiveInterface::~ReadOnlyArchiveInterface() { } void ReadOnlyArchiveInterface::onEntry(Archive::Entry *archiveEntry) { Q_UNUSED(archiveEntry) m_numberOfEntries++; } QString ReadOnlyArchiveInterface::filename() const { return m_filename; } QString ReadOnlyArchiveInterface::comment() const { return m_comment; } bool ReadOnlyArchiveInterface::isReadOnly() const { return true; } bool ReadOnlyArchiveInterface::open() { return true; } void ReadOnlyArchiveInterface::setPassword(const QString &password) { m_password = password; } void ReadOnlyArchiveInterface::setHeaderEncryptionEnabled(bool enabled) { m_isHeaderEncryptionEnabled = enabled; } QString ReadOnlyArchiveInterface::password() const { return m_password; } bool ReadOnlyArchiveInterface::doKill() { //default implementation return false; } bool ReadOnlyArchiveInterface::doSuspend() { //default implementation return false; } bool ReadOnlyArchiveInterface::doResume() { //default implementation return false; } void ReadOnlyArchiveInterface::setCorrupt(bool isCorrupt) { m_isCorrupt = isCorrupt; } bool ReadOnlyArchiveInterface::isCorrupt() const { return m_isCorrupt; } bool ReadOnlyArchiveInterface::isMultiVolume() const { return m_isMultiVolume; } void ReadOnlyArchiveInterface::setMultiVolume(bool value) { m_isMultiVolume = value; } int ReadOnlyArchiveInterface::numberOfVolumes() const { return m_numberOfVolumes; } QString ReadOnlyArchiveInterface::multiVolumeName() const { return filename(); } -ReadWriteArchiveInterface::ReadWriteArchiveInterface(QObject *parent, const QVariantList & args) +ReadWriteArchiveInterface::ReadWriteArchiveInterface(QObject *parent, const QVariantList &args) : ReadOnlyArchiveInterface(parent, args) { qCDebug(ARK) << "Created read-write interface for" << args.first().toString(); connect(this, &ReadWriteArchiveInterface::entryRemoved, this, &ReadWriteArchiveInterface::onEntryRemoved); } ReadWriteArchiveInterface::~ReadWriteArchiveInterface() { } bool ReadOnlyArchiveInterface::waitForFinishedSignal() { return m_waitForFinishedSignal; } int ReadOnlyArchiveInterface::moveRequiredSignals() const { return 1; } int ReadOnlyArchiveInterface::copyRequiredSignals() const { return 1; } void ReadOnlyArchiveInterface::setWaitForFinishedSignal(bool value) { m_waitForFinishedSignal = value; } QStringList ReadOnlyArchiveInterface::entryFullPaths(const QVector &entries, PathFormat format) { QStringList filesList; foreach (const Archive::Entry *file, entries) { filesList << file->fullPath(format); } return filesList; } QVector ReadOnlyArchiveInterface::entriesWithoutChildren(const QVector &entries) { // QMap is easy way to get entries sorted by their fullPath. QMap sortedEntries; foreach (Archive::Entry *entry, entries) { sortedEntries.insert(entry->fullPath(), entry); } QVector filteredEntries; QString lastFolder; foreach (Archive::Entry *entry, sortedEntries) { if (lastFolder.count() > 0 && entry->fullPath().startsWith(lastFolder)) { continue; } lastFolder = (entry->fullPath().right(1) == QLatin1String("/")) ? entry->fullPath() : QString(); filteredEntries << entry; } return filteredEntries; } QStringList ReadOnlyArchiveInterface::entryPathsFromDestination(QStringList entries, const Archive::Entry *destination, int entriesWithoutChildren) { QStringList paths = QStringList(); entries.sort(); QString lastFolder; const QString destinationPath = (destination == Q_NULLPTR) ? QString() : destination->fullPath(); QString newPath; int nameLength = 0; foreach (const QString &entryPath, entries) { if (lastFolder.count() > 0 && entryPath.startsWith(lastFolder)) { // Replace last moved or copied folder path with destination path. int charsCount = entryPath.count() - lastFolder.count(); if (entriesWithoutChildren != 1) { charsCount += nameLength; } newPath = destinationPath + entryPath.right(charsCount); } else { const QString name = entryPath.split(QLatin1Char('/'), QString::SkipEmptyParts).last(); if (entriesWithoutChildren != 1) { newPath = destinationPath + name; if (entryPath.right(1) == QLatin1String("/")) { newPath += QLatin1Char('/'); } } else { // If the mode is set to Move and there is only one passed file in the list, // we have to use destination as newPath. newPath = destinationPath; } if (entryPath.right(1) == QLatin1String("/")) { nameLength = name.count() + 1; // plus slash lastFolder = entryPath; } else { nameLength = 0; lastFolder = QString(); } } paths << newPath; } return paths; } bool ReadOnlyArchiveInterface::isHeaderEncryptionEnabled() const { return m_isHeaderEncryptionEnabled; } +QMimeType ReadOnlyArchiveInterface::mimetype() const +{ + return m_mimetype; +} + bool ReadWriteArchiveInterface::isReadOnly() const { // We set corrupt archives to read-only to avoid add/delete actions, that // are likely to fail anyway. if (isCorrupt()) { return true; } QFileInfo fileInfo(filename()); if (fileInfo.exists()) { return !fileInfo.isWritable(); } else { return !fileInfo.dir().exists(); // TODO: Should also check if we can create a file in that directory } } int ReadOnlyArchiveInterface::numberOfEntries() const { return m_numberOfEntries; } void ReadWriteArchiveInterface::onEntryRemoved(const QString &path) { Q_UNUSED(path) m_numberOfEntries--; } } // namespace Kerfuffle diff --git a/kerfuffle/archiveinterface.h b/kerfuffle/archiveinterface.h index afb19ed1..4b009f37 100644 --- a/kerfuffle/archiveinterface.h +++ b/kerfuffle/archiveinterface.h @@ -1,226 +1,229 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * 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 ARCHIVEINTERFACE_H #define ARCHIVEINTERFACE_H #include "archive_kerfuffle.h" #include "archive_entry.h" #include "kerfuffle_export.h" #include "kerfuffle/archiveentry.h" #include #include #include #include namespace Kerfuffle { class Query; class KERFUFFLE_EXPORT ReadOnlyArchiveInterface: public QObject { Q_OBJECT public: - explicit ReadOnlyArchiveInterface(QObject *parent, const QVariantList & args); + explicit ReadOnlyArchiveInterface(QObject *parent, const QVariantList &args); virtual ~ReadOnlyArchiveInterface(); /** * Returns the filename of the archive currently being handled. */ QString filename() const; /** * Returns the comment of the archive. */ QString comment() const; /** * @return The password of the archive, if any. */ QString password() const; bool isMultiVolume() const; int numberOfVolumes() const; /** * Returns whether the file can only be read. * * @return @c true The file cannot be written. * @return @c false The file can be read and written. */ virtual bool isReadOnly() const; virtual bool open(); /** * List archive contents. * This runs the process of reading archive contents. * When subclassing, you can block as long as you need (unless you called setWaitForFinishedSignal(true)). * @returns whether the listing succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ virtual bool list() = 0; virtual bool testArchive() = 0; void setPassword(const QString &password); void setHeaderEncryptionEnabled(bool enabled); /** * Extract files from archive. * Globally recognized extraction options: * @li PreservePaths - preserve file paths (extract flat if false) * @li RootNode - node in the archive which will correspond to the @arg destinationDirectory * When subclassing, you can block as long as you need (unless you called setWaitForFinishedSignal(true)). * @returns whether the listing succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ virtual bool extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) = 0; bool waitForFinishedSignal(); /** * Returns count of required finish signals for a job to be finished. * * These two methods are used by move and copy jobs, which in some plugins implementations have to call * several processes sequentually. For instance, moving entries in zip archive is only possible if * extracting the entries, deleting them, recreating destination folder structure and adding them back again. */ virtual int moveRequiredSignals() const; virtual int copyRequiredSignals() const; /** * Returns the list of filenames retrieved from the list of entries. */ static QStringList entryFullPaths(const QVector &entries, PathFormat format = WithTrailingSlash); /** * Returns the list of the entries, excluding their children. * * This method relies on entries paths so doesn't require parents to be set. */ static QVector entriesWithoutChildren(const QVector &entries); /** * Returns the string list of entry paths, which will be a result of adding/moving/copying entries. * * @param entries The entries which will be added/moved/copied. * @param destination Destination path within the archive to which entries have to be added. For renaming an entry * the path has to contain a new filename too. * @param entriesWithoutChildren Entries count, excluding their children. For AddJob or CopyJob 0 MUST be passed. * * @return For entries * some/dir/ * some/dir/entry * some/dir/some/entry * some/another/entry * and destination * some/destination * will return * some/destination/dir/ * some/destination/dir/entry * some/destination/dir/some/enty * some/destination/entry */ static QStringList entryPathsFromDestination(QStringList entries, const Archive::Entry *destination, int entriesWithoutChildren); virtual bool doKill(); virtual bool doSuspend(); virtual bool doResume(); bool isHeaderEncryptionEnabled() const; virtual QString multiVolumeName() const; void setMultiVolume(bool value); int numberOfEntries() const; + QMimeType mimetype() const; signals: void cancelled(); void error(const QString &message, const QString &details = QString()); void entry(Archive::Entry *archiveEntry); void progress(double progress); void info(const QString &info); void finished(bool result); void userQuery(Query *query); void testSuccess(); void compressionMethodFound(const QStringList); protected: /** * Setting this option to true will not run the functions in their own thread. * Instead it will be necessary to call finished(bool) when the operation is actually finished. */ void setWaitForFinishedSignal(bool value); void setCorrupt(bool isCorrupt); bool isCorrupt() const; QString m_comment; int m_numberOfVolumes; int m_numberOfEntries; + KPluginMetaData m_metaData; private: QString m_filename; + QMimeType m_mimetype; QString m_password; bool m_waitForFinishedSignal; bool m_isHeaderEncryptionEnabled; bool m_isCorrupt; bool m_isMultiVolume; private slots: void onEntry(Archive::Entry *archiveEntry); }; class KERFUFFLE_EXPORT ReadWriteArchiveInterface: public ReadOnlyArchiveInterface { Q_OBJECT public: enum OperationMode { List, Extract, Add, Move, Copy, Delete, Comment, Test }; - explicit ReadWriteArchiveInterface(QObject *parent, const QVariantList & args); + explicit ReadWriteArchiveInterface(QObject *parent, const QVariantList &args); virtual ~ReadWriteArchiveInterface(); bool isReadOnly() const Q_DECL_OVERRIDE; virtual bool addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd = 0) = 0; virtual bool moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options) = 0; virtual bool copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options) = 0; virtual bool deleteFiles(const QVector &files) = 0; virtual bool addComment(const QString &comment) = 0; signals: void entryRemoved(const QString &path); private slots: void onEntryRemoved(const QString &path); }; } // namespace Kerfuffle #endif // ARCHIVEINTERFACE_H diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp index f0d2bdb4..9482d7a6 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,1552 +1,1124 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * 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 #include #include namespace Kerfuffle { CliInterface::CliInterface(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args), m_process(0), m_abortingOperation(false), m_listEmptyLines(false), m_extractTempDir(Q_NULLPTR), m_commentTempFile(Q_NULLPTR) { //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)); + m_cliProps = new CliProperties(this, m_metaData, mimetype()); } CliInterface::~CliInterface() { Q_ASSERT(!m_process); delete m_commentTempFile; } void CliInterface::setListEmptyLines(bool emptyLines) { m_listEmptyLines = emptyLines; } int CliInterface::copyRequiredSignals() const { return 2; } bool CliInterface::list() { resetParsing(); - cacheParameterList(); m_operationMode = List; m_numberOfEntries = 0; - const auto args = substituteListVariables(m_param.value(ListArgs).toStringList(), password()); - - if (!runProcess(m_param.value(ListProgram).toStringList(), args)) { + if (!runProcess(m_cliProps->property("listProgram").toString(), m_cliProps->listArgs(filename(), password()))) { return false; } return true; } bool CliInterface::extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) { qCDebug(ARK) << Q_FUNC_INFO << "to" << destinationDirectory; - cacheParameterList(); m_operationMode = Extract; m_extractionOptions = options; m_extractedFiles = files; m_extractDestDir = destinationDirectory; - const QStringList extractArgs = m_param.value(ExtractArgs).toStringList(); - if (extractArgs.contains(QStringLiteral("$PasswordSwitch")) && options.encryptedArchiveHint() && password().isEmpty()) { + + + if (!m_cliProps->property("passwordSwitch").toString().isEmpty() && options.encryptedArchiveHint() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } - // Populate the argument list. - const QStringList args = substituteExtractVariables(extractArgs, - files, - options.preservePaths(), - password()); - QUrl destDir = QUrl(destinationDirectory); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); const bool useTmpExtractDir = options.isDragAndDropEnabled() || options.alwaysUseTempDir(); if (useTmpExtractDir) { Q_ASSERT(!m_extractTempDir); m_extractTempDir = new QTemporaryDir(QApplication::applicationName() + QLatin1Char('-')); qCDebug(ARK) << "Using temporary extraction dir:" << m_extractTempDir->path(); if (!m_extractTempDir->isValid()) { qCDebug(ARK) << "Creation of temporary directory failed."; emit finished(false); return false; } m_oldWorkingDir = QDir::currentPath(); destDir = QUrl(m_extractTempDir->path()); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); } - if (!runProcess(m_param.value(ExtractProgram).toStringList(), args)) { + if (!runProcess(m_cliProps->property("extractProgram").toString(), + m_cliProps->extractArgs(filename(), + extractFilesList(files), + options.preservePaths(), + password()))) { return false; } return true; } bool CliInterface::addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd) { Q_UNUSED(numberOfEntriesToAdd) - cacheParameterList(); - m_operationMode = Add; - const QStringList addArgs = m_param.value(AddArgs).toStringList(); - QVector filesToPass = QVector(); // If destination path is specified, we have recreate its structure inside the temp directory // and then place symlinks of targeted files there. const QString destinationPath = (destination == Q_NULLPTR) ? QString() : destination->fullPath(); qCDebug(ARK) << "Adding" << files.count() << "file(s) to destination:" << destinationPath; if (!destinationPath.isEmpty()) { m_extractTempDir = new QTemporaryDir(); const QString absoluteDestinationPath = m_extractTempDir->path() + QLatin1Char('/') + destinationPath; QDir qDir; qDir.mkpath(absoluteDestinationPath); QObject *preservedParent = Q_NULLPTR; foreach (Archive::Entry *file, files) { // The entries may have parent. We have to save and apply it to our new entry in order to prevent memory // leaks. if (preservedParent == Q_NULLPTR) { preservedParent = file->parent(); } const QString filePath = QDir::currentPath() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); const QString newFilePath = absoluteDestinationPath + file->fullPath(NoTrailingSlash); if (QFile::link(filePath, newFilePath)) { qCDebug(ARK) << "Symlink's created:" << filePath << newFilePath; } else { qCDebug(ARK) << "Can't create symlink" << filePath << newFilePath; delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; return false; } } qCDebug(ARK) << "Changing working dir again to " << m_extractTempDir->path(); QDir::setCurrent(m_extractTempDir->path()); filesToPass.push_back(new Archive::Entry(preservedParent, destinationPath.split(QLatin1Char('/'), QString::SkipEmptyParts).at(0))); } else { filesToPass = files; } - if (addArgs.contains(QStringLiteral("$PasswordSwitch")) && options.encryptedArchiveHint() && password().isEmpty()) { + if (!m_cliProps->property("passwordSwitch").toString().isEmpty() && options.encryptedArchiveHint() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } - const auto args = substituteAddVariables(m_param.value(AddArgs).toStringList(), - filesToPass, - password(), - isHeaderEncryptionEnabled(), - options.compressionLevel(), - options.volumeSize(), - options.compressionMethod()); - - return runProcess(m_param.value(AddProgram).toStringList(), args); + return runProcess(m_cliProps->property("addProgram").toString(), + m_cliProps->addArgs(filename(), + entryFullPaths(filesToPass, NoTrailingSlash), + password(), + isHeaderEncryptionEnabled(), + options.compressionLevel(), + options.compressionMethod(), + options.volumeSize())); } bool CliInterface::moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(options); - cacheParameterList(); m_operationMode = Move; m_removedFiles = files; QVector withoutChildren = entriesWithoutChildren(files); setNewMovedFiles(files, destination, withoutChildren.count()); - const auto moveArgs = m_param.value(MoveArgs).toStringList(); - - const auto args = substituteMoveVariables(moveArgs, withoutChildren, destination, password()); - - return runProcess(m_param.value(MoveProgram).toStringList(), args); + return runProcess(m_cliProps->property("moveProgram").toString(), + m_cliProps->moveArgs(filename(), + withoutChildren, + destination, + password())); } bool CliInterface::copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { m_oldWorkingDir = QDir::currentPath(); m_tempExtractDir = new QTemporaryDir(); m_tempAddDir = new QTemporaryDir(); QDir::setCurrent(m_tempExtractDir->path()); m_passedFiles = files; m_passedDestination = destination; m_passedOptions = options; m_numberOfEntries = 0; m_subOperation = Extract; connect(this, &CliInterface::finished, this, &CliInterface::continueCopying); return extractFiles(files, QDir::currentPath(), ExtractionOptions()); } bool CliInterface::deleteFiles(const QVector &files) { - cacheParameterList(); m_operationMode = Delete; m_removedFiles = files; - const auto deleteArgs = m_param.value(DeleteArgs).toStringList(); - - const auto args = substituteDeleteVariables(deleteArgs, - files, - password()); - - return runProcess(m_param.value(DeleteProgram).toStringList(), args); + return runProcess(m_cliProps->property("deleteProgram").toString(), + m_cliProps->deleteArgs(filename(), files, password())); } bool CliInterface::testArchive() { resetParsing(); - cacheParameterList(); m_operationMode = Test; - const auto args = substituteTestVariables(m_param.value(TestArgs).toStringList(), password()); - - return runProcess(m_param.value(TestProgram).toStringList(), args); + return runProcess(m_cliProps->property("testProgram").toString(), m_cliProps->testArgs(filename(), password())); } -bool CliInterface::runProcess(const QStringList& programNames, const QStringList& arguments) +bool CliInterface::runProcess(const QString& programName, const QStringList& arguments) { Q_ASSERT(!m_process); - QString programPath; - for (int i = 0; i < programNames.count(); i++) { - programPath = QStandardPaths::findExecutable(programNames.at(i)); - if (!programPath.isEmpty()) - break; - } + QString programPath = QStandardPaths::findExecutable(programName); 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 error(xi18nc("@info", "Failed to locate program %2 on disk.", programName)); emit finished(false); return false; } qCDebug(ARK) << "Executing" << programPath << arguments << "within directory" << QDir::currentPath(); #ifdef Q_OS_WIN m_process = new KProcess; #else m_process = new KPtyProcess; m_process->setPtyChannels(KPtyProcess::StdinChannel); #endif m_process->setOutputChannelMode(KProcess::MergedChannels); m_process->setNextOpenMode(QIODevice::ReadWrite | QIODevice::Unbuffered | QIODevice::Text); m_process->setProgram(programPath, arguments); connect(m_process, &QProcess::readyReadStandardOutput, this, [=]() { readStdout(); }); if (m_operationMode == Extract) { // Extraction jobs need a dedicated post-processing function. connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::extractProcessFinished); } else { connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::processFinished); } m_stdOutData.clear(); m_process->start(); return true; } void CliInterface::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { m_exitCode = exitCode; qCDebug(ARK) << "Process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { //handle all the remaining data in the process readStdout(true); delete m_process; m_process = Q_NULLPTR; } // #193908 - #222392 // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_operationMode == Delete || m_operationMode == Move) { QStringList removedFullPaths = entryFullPaths(m_removedFiles); foreach (const QString &fullPath, removedFullPaths) { emit entryRemoved(fullPath); } foreach (Archive::Entry *e, m_newMovedFiles) { emit entry(e); } m_newMovedFiles.clear(); } if (m_operationMode == Add && !isMultiVolume()) { if (m_extractTempDir) { delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; } list(); } else if (m_operationMode == List && isCorrupt()) { Kerfuffle::LoadCorruptQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (!query.responseYes()) { emit cancelled(); emit finished(false); } else { emit progress(1.0); emit finished(true); } } else { emit progress(1.0); emit finished(true); } } void CliInterface::extractProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_ASSERT(m_operationMode == Extract); m_exitCode = exitCode; qCDebug(ARK) << "Extraction process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { // Handle all the remaining data in the process. readStdout(true); delete m_process; m_process = Q_NULLPTR; } // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_extractionOptions.alwaysUseTempDir()) { // unar exits with code 1 if extraction fails. // This happens at least with wrong passwords or not enough space in the destination folder. if (m_exitCode == 1) { if (password().isEmpty()) { qCWarning(ARK) << "Extraction aborted, destination folder might not have enough space."; emit error(i18n("Extraction failed. Make sure that enough space is available.")); } else { qCWarning(ARK) << "Extraction aborted, either the password is wrong or the destination folder doesn't have enough space."; emit error(i18n("Extraction failed. Make sure you provided the correct password and that enough space is available.")); setPassword(QString()); } cleanUpExtracting(); emit finished(false); return; } if (!m_extractionOptions.isDragAndDropEnabled()) { if (!moveToDestination(QDir::current(), QDir(m_extractDestDir), m_extractionOptions.preservePaths())) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_extractedFiles.size())); cleanUpExtracting(); emit finished(false); return; } cleanUpExtracting(); } } if (m_extractionOptions.isDragAndDropEnabled()) { if (!moveDroppedFilesToDest(m_extractedFiles, m_extractDestDir)) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_extractedFiles.size())); cleanUpExtracting(); emit finished(false); return; } cleanUpExtracting(); } emit progress(1.0); emit finished(true); } void CliInterface::continueCopying(bool result) { if (!result) { finishCopying(false); return; } switch (m_subOperation) { case Extract: m_subOperation = Add; m_passedFiles = entriesWithoutChildren(m_passedFiles); if (!setAddedFiles() || !addFiles(m_tempAddedFiles, m_passedDestination, m_passedOptions)) { finishCopying(false); } break; case Add: finishCopying(true); break; default: Q_ASSERT(false); } } bool CliInterface::moveDroppedFilesToDest(const QVector &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 Archive::Entry *file, files) { QFileInfo relEntry(file->fullPath().remove(file->rootNode)); QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + file->fullPath()); 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; } void CliInterface::cleanUpExtracting() { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } if (m_extractTempDir) { delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; } } void CliInterface::finishCopying(bool result) { disconnect(this, &CliInterface::finished, this, &CliInterface::continueCopying); emit progress(1.0); emit finished(result); cleanUp(); } 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::Hidden | 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::substituteListVariables(const QStringList &listArgs, const QString &password) -{ - // Required if we call this function from unit tests. - cacheParameterList(); - - QStringList args; - foreach (const QString& arg, listArgs) { - if (arg == QLatin1String("$Archive")) { - args << filename(); - continue; - } - - if (arg == QLatin1String("$PasswordSwitch")) { - args << passwordSwitch(password); - continue; - } - - // Simple argument (e.g. -slt in 7z), nothing to substitute, just add it to the list. - args << arg; - } - - // Remove empty strings, if any. - args.removeAll(QString()); - - return args; -} - -QStringList CliInterface::substituteExtractVariables(const QStringList &extractArgs, const QVector &entries, bool preservePaths, const QString &password) -{ - // 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("$Files")) { - args << extractFilesList(entries); - 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; -} - -QStringList CliInterface::substituteAddVariables(const QStringList &addArgs, const QVector &entries, const QString &password, bool encryptHeader, int compLevel, ulong volumeSize, QString compMethod) -{ - // Required if we call this function from unit tests. - cacheParameterList(); - - QStringList args; - foreach (const QString& arg, addArgs) { - qCDebug(ARK) << "Processing argument " << arg; - - if (arg == QLatin1String("$Archive")) { - args << filename(); - continue; - } - - if (arg == QLatin1String("$PasswordSwitch")) { - args << (encryptHeader ? passwordHeaderSwitch(password) : passwordSwitch(password)); - continue; - } - - if (arg == QLatin1String("$CompressionLevelSwitch")) { - args << compressionLevelSwitch(compLevel); - continue; - } - - if (arg == QLatin1String("$CompressionMethodSwitch")) { - args << compressionMethodSwitch(compMethod); - continue; - } - - if (arg == QLatin1String("$MultiVolumeSwitch")) { - args << multiVolumeSwitch(volumeSize); - continue; - } - - if (arg == QLatin1String("$Files")) { - args << entryFullPaths(entries, NoTrailingSlash); - continue; - } - - // Simple argument (e.g. a in 7z), nothing to substitute, just add it to the list. - args << arg; - } - - // Remove empty strings, if any. - args.removeAll(QString()); - - return args; -} - -QStringList CliInterface::substituteMoveVariables(const QStringList &moveArgs, const QVector &entriesWithoutChildren, const Archive::Entry *destination, const QString &password) -{ - // Required if we call this function from unit tests. - cacheParameterList(); - - QStringList args; - foreach (const QString& arg, moveArgs) { - qCDebug(ARK) << "Processing argument " << arg; - - if (arg == QLatin1String("$Archive")) { - args << filename(); - continue; - } - - if (arg == QLatin1String("$PasswordSwitch")) { - args << passwordSwitch(password); - continue; - } - - if (arg == QLatin1String("$PathPairs")) { - args << entryPathDestinationPairs(entriesWithoutChildren, destination); - continue; - } - - // Simple argument (e.g. a in 7z), nothing to substitute, just add it to the list. - args << arg; - } - - // Remove empty strings, if any. - args.removeAll(QString()); - - return args; -} - -QStringList CliInterface::substituteDeleteVariables(const QStringList &deleteArgs, const QVector &entries, const QString &password) -{ - cacheParameterList(); - - QStringList args; - foreach (const QString& arg, deleteArgs) { - qCDebug(ARK) << "Processing argument" << arg; - - if (arg == QLatin1String("$Archive")) { - args << filename(); - continue; - } - - if (arg == QLatin1String("$PasswordSwitch")) { - args << passwordSwitch(password); - continue; - } - - if (arg == QLatin1String("$Files")) { - foreach (const Archive::Entry *e, entries) { - args << escapeFileName(e->fullPath(NoTrailingSlash)); - } - continue; - } - - // Simple argument (e.g. d in rar), nothing to substitute, just add it to the list. - args << arg; - } - - // Remove empty strings, if any. - args.removeAll(QString()); - - return args; -} - -QStringList CliInterface::substituteCommentVariables(const QStringList &commentArgs, const QString &commentFile) -{ - // Required if we call this function from unit tests. - cacheParameterList(); - - QStringList args; - foreach (const QString& arg, commentArgs) { - qCDebug(ARK) << "Processing argument " << arg; - - if (arg == QLatin1String("$Archive")) { - args << filename(); - continue; - } - - if (arg == QLatin1String("$CommentSwitch")) { - QString commentSwitch = m_param.value(CommentSwitch).toString(); - commentSwitch.replace(QStringLiteral("$CommentFile"), commentFile); - args << commentSwitch; - continue; - } - - args << arg; - } - - // Remove empty strings, if any. - args.removeAll(QString()); - - return args; -} - -QStringList CliInterface::substituteTestVariables(const QStringList &testArgs, const QString &password) -{ - // Required if we call this function from unit tests. - cacheParameterList(); - - QStringList args; - foreach (const QString& arg, testArgs) { - qCDebug(ARK) << "Processing argument " << arg; - - if (arg == QLatin1String("$Archive")) { - args << filename(); - continue; - } - - if (arg == QLatin1String("$PasswordSwitch")) { - args << passwordSwitch(password); - continue; - } - - args << arg; - } - - // Remove empty strings, if any. - args.removeAll(QString()); - - return args; -} - void CliInterface::setNewMovedFiles(const QVector &entries, const Archive::Entry *destination, int entriesWithoutChildren) { m_newMovedFiles.clear(); QMap entryMap; foreach (const Archive::Entry* entry, entries) { entryMap.insert(entry->fullPath(), entry); } QString lastFolder; QString newPath; int nameLength = 0; foreach (const Archive::Entry* entry, entryMap) { if (lastFolder.count() > 0 && entry->fullPath().startsWith(lastFolder)) { // Replace last moved or copied folder path with destination path. int charsCount = entry->fullPath().count() - lastFolder.count(); if (entriesWithoutChildren > 1) { charsCount += nameLength; } newPath = destination->fullPath() + entry->fullPath().right(charsCount); } else { if (entriesWithoutChildren > 1) { newPath = destination->fullPath() + entry->name(); } else { // If there is only one passed file in the list, // we have to use destination as newPath. newPath = destination->fullPath(NoTrailingSlash); } if (entry->isDir()) { newPath += QLatin1Char('/'); nameLength = entry->name().count() + 1; // plus slash lastFolder = entry->fullPath(); } else { nameLength = 0; lastFolder = QString(); } } Archive::Entry *newEntry = new Archive::Entry(Q_NULLPTR); newEntry->copyMetaData(entry); newEntry->setFullPath(newPath); m_newMovedFiles << newEntry; } } -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::passwordHeaderSwitch(const QString& password) const -{ - if (password.isEmpty()) { - return QStringList(); - } - - Q_ASSERT(m_param.contains(PasswordHeaderSwitch)); - - QStringList passwordHeaderSwitch = m_param.value(PasswordHeaderSwitch).toStringList(); - Q_ASSERT(!passwordHeaderSwitch.isEmpty() && passwordHeaderSwitch.size() <= 2); - - if (passwordHeaderSwitch.size() == 1) { - passwordHeaderSwitch[0].replace(QLatin1String("$Password"), password); - } else { - passwordHeaderSwitch[1] = password; - } - - return passwordHeaderSwitch; -} - -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; -} - -QString CliInterface::compressionLevelSwitch(int level) const -{ - if (level < 0 || level > 9) { - return QString(); - } - - Q_ASSERT(m_param.contains(CompressionLevelSwitch)); - - QString compLevelSwitch = m_param.value(CompressionLevelSwitch).toString(); - Q_ASSERT(!compLevelSwitch.isEmpty()); - - compLevelSwitch.replace(QLatin1String("$CompressionLevel"), QString::number(level)); - - return compLevelSwitch; -} - -QString CliInterface::compressionMethodSwitch(const QString &method) const -{ - if (method.isEmpty()) { - return QString(); - } - - Q_ASSERT(m_param.contains(CompressionMethodSwitch)); - QString compMethodSwitch = m_param.value(CompressionMethodSwitch).toString(); - Q_ASSERT(!compMethodSwitch.isEmpty()); - - compMethodSwitch.replace(QLatin1String("$CompressionMethod"), method); - - return compMethodSwitch; -} - -QString CliInterface::multiVolumeSwitch(ulong volumeSize) const -{ - // The maximum value we allow in the QDoubleSpinBox is 1000MB. Converted to - // KB this is 1024000. - if (volumeSize <= 0 || volumeSize > 1024000) { - return QString(); - } - - Q_ASSERT(m_param.contains(MultiVolumeSwitch)); - - QString multiVolumeSwitch = m_param.value(MultiVolumeSwitch).toString(); - Q_ASSERT(!multiVolumeSwitch.isEmpty()); - - multiVolumeSwitch.replace(QLatin1String("$VolumeSize"), QString::number(volumeSize)); - - return multiVolumeSwitch; -} - QStringList CliInterface::extractFilesList(const QVector &entries) const { QStringList filesList; foreach (const Archive::Entry *e, entries) { filesList << escapeFileName(e->fullPath(NoTrailingSlash)); } return filesList; } void CliInterface::killProcess(bool emitFinished) { // TODO: Would be good to unit test #304764/#304178. if (!m_process) { return; } m_abortingOperation = !emitFinished; // Give some time for the application to finish gracefully if (!m_process->waitForFinished(5)) { m_process->kill(); // It takes a few hundred ms for the process to be killed. m_process->waitForFinished(1000); } m_abortingOperation = false; } 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); return false; } setPassword(query.password()); return true; } void CliInterface::cleanUp() { qDeleteAll(m_tempAddedFiles); m_tempAddedFiles.clear(); QDir::setCurrent(m_oldWorkingDir); delete m_tempExtractDir; m_tempExtractDir = Q_NULLPTR; delete m_tempAddDir; m_tempAddDir = Q_NULLPTR; } 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; } 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 wrongPasswordMessage = m_cliProps->isWrongPasswordMsg(QLatin1String(lines.last())); bool foundErrorMessage = (wrongPasswordMessage || - checkForErrorMessage(QLatin1String(lines.last()), DiskFullPatterns) || - checkForErrorMessage(QLatin1String(lines.last()), ExtractionFailedPatterns) || - checkForPasswordPromptMessage(QLatin1String(lines.last())) || - checkForErrorMessage(QLatin1String(lines.last()), FileExistsExpression)); + m_cliProps->isDiskFullMsg(QLatin1String(lines.last())) || + m_cliProps->isExtractionFailedMsg(QLatin1String(lines.last())) || + m_cliProps->isfileExistsMsg(QLatin1String(lines.last()))) || + m_cliProps->isPasswordPrompt(QLatin1String(lines.last())); 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)) { if (!handleLine(QString::fromLocal8Bit(line))) { killProcess(); return; } } } } bool CliInterface::setAddedFiles() { QDir::setCurrent(m_tempAddDir->path()); foreach (const Archive::Entry *file, m_passedFiles) { const QString oldPath = m_tempExtractDir->path() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + file->name(); if (!QFile::rename(oldPath, newPath)) { return false; } m_tempAddedFiles << new Archive::Entry(Q_NULLPTR, file->name()); } return true; } bool 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 == Extract || m_operationMode == Add) && m_param.contains(CaptureProgress) && m_param.value(CaptureProgress).toBool()) { + if ((m_operationMode == Extract || m_operationMode == Add) && m_cliProps->property("captureProgress").toBool()) { //read the percentage int pos = line.indexOf(QLatin1Char( '%' )); if (pos > 1) { int percentage = line.midRef(pos - 2, 2).toInt(); emit progress(float(percentage) / 100); return true; } } if (m_operationMode == Extract) { - if (checkForPasswordPromptMessage(line)) { + if (m_cliProps->isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return true; } - if (checkForErrorMessage(line, DiskFullPatterns)) { + if (m_cliProps->isDiskFullMsg(line)) { qCWarning(ARK) << "Found disk full message:" << line; emit error(i18nc("@info", "Extraction failed because the disk is full.")); return false; } - if (checkForErrorMessage(line, WrongPasswordPatterns)) { + if (m_cliProps->isWrongPasswordMsg(line)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18nc("@info", "Extraction failed: Incorrect password")); return false; } - if (checkForErrorMessage(line, ExtractionFailedPatterns)) { + if (m_cliProps->isExtractionFailedMsg(line)) { qCWarning(ARK) << "Error in extraction:" << line; emit error(i18n("Extraction failed because of an unexpected error.")); return false; } if (handleFileExistsMessage(line)) { return true; } } if (m_operationMode == List) { - if (checkForPasswordPromptMessage(line)) { + if (m_cliProps->isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return true; } - if (checkForErrorMessage(line, WrongPasswordPatterns)) { + if (m_cliProps->isWrongPasswordMsg(line)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18n("Incorrect password.")); return false; } - if (checkForErrorMessage(line, ExtractionFailedPatterns)) { + if (m_cliProps->isExtractionFailedMsg(line)) { qCWarning(ARK) << "Error in extraction!!"; emit error(i18n("Extraction failed because of an unexpected error.")); return false; } - if (checkForErrorMessage(line, CorruptArchivePatterns)) { + if (m_cliProps->isCorruptArchiveMsg(line)) { qCWarning(ARK) << "Archive corrupt"; setCorrupt(true); // Special case: corrupt is not a "fatal" error so we return true here. return true; } if (handleFileExistsMessage(line)) { return true; } return readListLine(line); } if (m_operationMode == Test) { - if (checkForPasswordPromptMessage(line)) { + if (m_cliProps->isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; emit error(i18n("Ark does not currently support testing this archive.")); return false; } - if (checkForTestSuccessMessage(line)) { + if (m_cliProps->isTestPassedMsg(line)) { qCDebug(ARK) << "Test successful"; emit testSuccess(); return true; } } return true; } -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()) { + foreach (const QString &pattern, m_cliProps->property("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)) { + if (!m_cliProps->isfileExistsMsg(line)) { 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(); + const QStringList choices = m_cliProps->property("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::checkForTestSuccessMessage(const QString& line) -{ - const QRegularExpression rx(m_param.value(TestPassedPattern).toString()); - const QRegularExpressionMatch rxMatch = rx.match(line); - if (rxMatch.hasMatch()) { - return true; - } - return false; -} - bool CliInterface::doKill() { if (m_process) { killProcess(false); return true; } return false; } bool CliInterface::doSuspend() { return false; } bool CliInterface::doResume() { return false; } QString CliInterface::escapeFileName(const QString& fileName) const { return fileName; } QStringList CliInterface::entryPathDestinationPairs(const QVector &entriesWithoutChildren, const Archive::Entry *destination) { QStringList pairList; if (entriesWithoutChildren.count() > 1) { foreach (const Archive::Entry *file, entriesWithoutChildren) { pairList << file->fullPath(NoTrailingSlash) << destination->fullPath() + file->name(); } } else { pairList << entriesWithoutChildren.at(0)->fullPath(NoTrailingSlash) << destination->fullPath(NoTrailingSlash); } return pairList; } 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 } bool CliInterface::addComment(const QString &comment) { - cacheParameterList(); - m_operationMode = Comment; m_commentTempFile = new QTemporaryFile; if (!m_commentTempFile->open()) { qCWarning(ARK) << "Failed to create temporary file for comment"; emit finished(false); return false; } QTextStream stream(m_commentTempFile); stream << comment << endl; m_commentTempFile->close(); - const auto args = substituteCommentVariables(m_param.value(CommentArgs).toStringList(), - m_commentTempFile->fileName()); - - if (!runProcess(m_param.value(AddProgram).toStringList(), args)) { + if (!runProcess(m_cliProps->property("addProgram").toString(), + m_cliProps->commentArgs(filename(), m_commentTempFile->fileName()))) { return false; } m_comment = comment; return true; } QString CliInterface::multiVolumeName() const { QString oldSuffix = QMimeDatabase().suffixForFileName(filename()); QString name; - foreach (const QString &multiSuffix, m_param.value(MultiVolumeSuffix).toStringList()) { + foreach (const QString &multiSuffix, m_cliProps->property("multiVolumeSuffix").toStringList()) { QString newSuffix = multiSuffix; newSuffix.replace(QStringLiteral("$Suffix"), oldSuffix); name = filename().remove(oldSuffix).append(newSuffix); if (QFileInfo::exists(name)) { break; } } return name; } +CliProperties *CliInterface::cliProperties() const +{ + return m_cliProps; +} + } diff --git a/kerfuffle/cliinterface.h b/kerfuffle/cliinterface.h index e74775ab..bc7bb64d 100644 --- a/kerfuffle/cliinterface.h +++ b/kerfuffle/cliinterface.h @@ -1,522 +1,228 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * 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 "archiveentry.h" +#include "cliproperties.h" #include "kerfuffle_export.h" #include "part/archivemodel.h" #include #include class KProcess; class KPtyProcess; class QDir; class QTemporaryDir; class QTemporaryFile; 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 - * $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 $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, - /** - * QString - * The format of the compression level switch. The variable $CompressionLevel - * will be substituted for the level. - * Example: ("-mx=$CompressionLevel) - */ - CompressionLevelSwitch, - /** - * 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, - - ///////////////[ MOVE ]///////////// - - /** - * QStringList - * The names to the program that will handle adding in this - * archive format (eg "rar"). Will be searched for in PATH - */ - MoveProgram, - /** - * QStringList - * The arguments that are passed to the program above for - * moving inside the archive. Special strings that will be - * substituted: - * $Archive - the path of the archive - * $Files - the files selected to be moved - * $Destinations - new path of each file selected to be moved - */ - MoveArgs, - - ///////////////[ 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, - - ///////////////[ COMMENT ]///////////// - - /** - * QStringList - * The arguments that are passed to AddProgram when adding - * a comment. - */ - CommentArgs, - /** - * QString - * The variable $CommentFile will be substituted for the file - * containing the comment. - * Example (rar plugin): -z$CommentFile - */ - CommentSwitch, - TestProgram, - TestArgs, - TestPassedPattern, - MultiVolumeSwitch, - MultiVolumeSuffix, - CompressionMethodSwitch -}; - -typedef QHash ParameterList; - class KERFUFFLE_EXPORT CliInterface : public ReadWriteArchiveInterface { Q_OBJECT public: OperationMode m_operationMode; explicit CliInterface(QObject *parent, const QVariantList & args); virtual ~CliInterface(); virtual int copyRequiredSignals() const Q_DECL_OVERRIDE; virtual bool list() Q_DECL_OVERRIDE; virtual bool extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) Q_DECL_OVERRIDE; virtual bool addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd = 0) Q_DECL_OVERRIDE; virtual bool moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool deleteFiles(const QVector &files) Q_DECL_OVERRIDE; virtual bool addComment(const QString &comment) Q_DECL_OVERRIDE; virtual bool testArchive() 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; /** * 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 successful. */ bool moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths); - QStringList substituteListVariables(const QStringList &listArgs, const QString &password); - QStringList substituteExtractVariables(const QStringList &extractArgs, const QVector &entries, bool preservePaths, const QString &password); - QStringList substituteAddVariables(const QStringList &addArgs, const QVector &entries, const QString &password, bool encryptHeader, int compLevel, ulong volumeSize, QString compMethod); - QStringList substituteMoveVariables(const QStringList &moveArgs, const QVector &entriesWithoutChildren, const Archive::Entry *destination, const QString &password); - QStringList substituteDeleteVariables(const QStringList &deleteArgs, const QVector &entries, const QString &password); - QStringList substituteCommentVariables(const QStringList &commentArgs, const QString &commentFile); - QStringList substituteTestVariables(const QStringList &testArgs, const QString &password); - /** * @see ArchiveModel::entryPathsFromDestination */ void setNewMovedFiles(const QVector &entries, const Archive::Entry *destination, int entriesWithoutChildren); - /** - * @return The preserve path switch, according to the @p preservePaths extraction option. - */ - QString preservePathSwitch(bool preservePaths) const; - - /** - * @return The password header-switch with the given @p password. - */ - virtual QStringList passwordHeaderSwitch(const QString& password) const; - - /** - * @return The password switch with the given @p password. - */ - QStringList passwordSwitch(const QString& password) const; - - /** - * @return The compression level switch with the given @p level. - */ - QString compressionLevelSwitch(int level) const; - - virtual QString compressionMethodSwitch(const QString &method) const; - QString multiVolumeSwitch(ulong volumeSize) const; - /** * @return The list of selected files to extract. */ QStringList extractFilesList(const QVector &files) const; QString multiVolumeName() const Q_DECL_OVERRIDE; + CliProperties *cliProperties() const; + protected: bool setAddedFiles(); /** * Handles the given @p line. * @return True if the line is ok. False if the line contains/triggers a "fatal" error * or a canceled user query. If false is returned, the caller is supposed to call killProcess(). */ virtual bool handleLine(const QString& line); - bool checkForErrorMessage(const QString& line, int parameterIndex); - - /** - * 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); - - virtual void cacheParameterList(); - /** * Run @p programName with the given @p arguments. * * @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 was started correctly, * @c false otherwise (in which case finished(false) is emitted). */ - bool runProcess(const QStringList& programNames, const QStringList& arguments); + bool runProcess(const QString& programName, const QStringList& arguments); /** * Kill the running process. The finished signal is emitted according to @p emitFinished. */ void killProcess(bool emitFinished = true); /** * 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(); void cleanUp(); + CliProperties *m_cliProps; QString m_oldWorkingDir; QTemporaryDir *m_tempExtractDir; QTemporaryDir *m_tempAddDir; OperationMode m_subOperation; QVector m_passedFiles; QVector m_tempAddedFiles; Archive::Entry *m_passedDestination; CompressionOptions m_passedOptions; - ParameterList m_param; - #ifdef Q_OS_WIN KProcess *m_process; #else KPtyProcess *m_process; #endif bool m_abortingOperation; protected slots: virtual void readStdout(bool handleAll = false); private: bool handleFileExistsMessage(const QString& filename); - bool checkForTestSuccessMessage(const QString& line); - - /** - * 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; /** * Returns a list of path pairs which will be supplied to rn command. * [ ... ] * Also constructs a list of new entries resulted in moving. * * @param entriesWithoutChildren List of archive entries * @param destination Must be a directory entry if QList contains more that one entry */ QStringList entryPathDestinationPairs(const QVector &entriesWithoutChildren, const Archive::Entry *destination); /** * Wrapper around KProcess::write() or KPtyDevice::write(), depending on * the platform. */ void writeToProcess(const QByteArray& data); bool moveDroppedFilesToDest(const QVector &files, const QString &finalDest); /** * @return Whether @p dir is an empty directory. */ bool isEmptyDir(const QDir &dir); + /** + * 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; + void cleanUpExtracting(); void finishCopying(bool result); QByteArray m_stdOutData; QRegularExpression m_passwordPromptPattern; QHash > m_patternCache; QVector m_removedFiles; QVector m_newMovedFiles; int m_exitCode; bool m_listEmptyLines; QString m_storedFileName; ExtractionOptions m_extractionOptions; QString m_extractDestDir; QTemporaryDir *m_extractTempDir; QTemporaryFile *m_commentTempFile; QVector m_extractedFiles; protected slots: virtual void processFinished(int exitCode, QProcess::ExitStatus exitStatus); private slots: void extractProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); void continueCopying(bool result); }; } #endif /* CLIINTERFACE_H */ diff --git a/kerfuffle/cliproperties.cpp b/kerfuffle/cliproperties.cpp new file mode 100644 index 00000000..b9afd0f9 --- /dev/null +++ b/kerfuffle/cliproperties.cpp @@ -0,0 +1,342 @@ +/* + * ark -- archiver for the KDE project + * + * Copyright (C) 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 "cliproperties.h" +#include "ark_debug.h" +#include "archiveformat.h" +#include "pluginmanager.h" + +namespace Kerfuffle +{ + +CliProperties::CliProperties(QObject *parent, const KPluginMetaData &metaData, const QMimeType &archiveType) + : QObject(parent) + , m_mimeType(archiveType) + , m_metaData(metaData) +{ +} + +QStringList CliProperties::addArgs(const QString &archive, const QStringList &files, const QString &password, bool headerEncryption, int compressionLevel, const QString &compressionMethod, uint volumeSize) +{ + QStringList args; + foreach (const QString &s, m_addSwitch) { + args << s; + } + if (!password.isEmpty()) { + args << substitutePasswordSwitch(password, headerEncryption); + } + if (compressionLevel > -1) { + args << substituteCompressionLevelSwitch(compressionLevel); + } + if (!compressionMethod.isEmpty()) { + args << substituteCompressionMethodSwitch(compressionMethod); + } + if (volumeSize > 0) { + args << substituteMultiVolumeSwitch(volumeSize); + } + args << archive; + args << files; + + args.removeAll(QString()); + return args; +} + +QStringList CliProperties::commentArgs(const QString &archive, const QString &commentfile) +{ + QStringList args; + foreach (const QString &s, substituteCommentSwitch(commentfile)) { + args << s; + } + args << archive; + + args.removeAll(QString()); + return args; +} + +QStringList CliProperties::deleteArgs(const QString &archive, const QVector &files, const QString &password) +{ + QStringList args; + args << m_deleteSwitch; + if (!password.isEmpty()) { + args << substitutePasswordSwitch(password); + } + args << archive; + foreach (const Archive::Entry *e, files) { + args << e->fullPath(NoTrailingSlash); + } + + args.removeAll(QString()); + return args; +} + +QStringList CliProperties::extractArgs(const QString &archive, const QStringList &files, bool preservePaths, const QString &password) +{ + QStringList args; + + if (preservePaths && !m_extractSwitch.isEmpty()) { + args << m_extractSwitch; + } else if (!preservePaths && !m_extractSwitchNoPreserve.isEmpty()) { + args << m_extractSwitchNoPreserve; + } + + if (!password.isEmpty()) { + args << substitutePasswordSwitch(password); + } + args << archive; + args << files; + + args.removeAll(QString()); + return args; +} + +QStringList CliProperties::listArgs(const QString &archive, const QString &password) +{ + QStringList args; + foreach (const QString &s, m_listSwitch) { + args << s; + } + if (!password.isEmpty()) { + args << substitutePasswordSwitch(password); + } + args << archive; + + args.removeAll(QString()); + return args; +} + +QStringList CliProperties::moveArgs(const QString &archive, const QVector &entries, Archive::Entry *destination, const QString &password) +{ + QStringList args; + args << m_moveSwitch; + if (!password.isEmpty()) { + args << substitutePasswordSwitch(password); + } + args << archive; + if (entries.count() > 1) { + foreach (const Archive::Entry *file, entries) { + args << file->fullPath(NoTrailingSlash) << destination->fullPath() + file->name(); + } + } else { + args << entries.at(0)->fullPath(NoTrailingSlash) << destination->fullPath(NoTrailingSlash); + } + + args.removeAll(QString()); + return args; +} + +QStringList CliProperties::testArgs(const QString &archive, const QString &password) +{ + QStringList args; + foreach (const QString &s, m_testSwitch) { + args << s; + } + if (!password.isEmpty()) { + args << substitutePasswordSwitch(password); + } + args << archive; + + args.removeAll(QString()); + return args; +} + +QStringList CliProperties::substituteCommentSwitch(const QString &commentfile) const +{ + Q_ASSERT(!commentfile.isEmpty()); + + Q_ASSERT(ArchiveFormat::fromMetadata(m_mimeType, m_metaData).supportsWriteComment()); + + QStringList commentSwitches = m_commentSwitch; + Q_ASSERT(!commentSwitches.isEmpty()); + + QMutableListIterator i(commentSwitches); + while (i.hasNext()) { + i.next(); + i.value().replace(QLatin1String("$CommentFile"), commentfile); + } + + return commentSwitches; +} + +QStringList CliProperties::substitutePasswordSwitch(const QString &password, bool headerEnc) const +{ + if (password.isEmpty()) { + return QStringList(); + } + + Archive::EncryptionType encryptionType = ArchiveFormat::fromMetadata(m_mimeType, m_metaData).encryptionType(); + Q_ASSERT(encryptionType != Archive::EncryptionType::Unencrypted); + + QStringList passwordSwitch; + if (headerEnc) { + passwordSwitch = m_passwordSwitchHeaderEnc; + } else { + passwordSwitch = m_passwordSwitch; + } + Q_ASSERT(!passwordSwitch.isEmpty()); + + QMutableListIterator i(passwordSwitch); + while (i.hasNext()) { + i.next(); + i.value().replace(QLatin1String("$Password"), password); + } + + return passwordSwitch; +} + +QString CliProperties::substituteCompressionLevelSwitch(int level) const +{ + if (level < 0 || level > 9) { + return QString(); + } + + Q_ASSERT(ArchiveFormat::fromMetadata(m_mimeType, m_metaData).maxCompressionLevel() != -1); + + QString compLevelSwitch = m_compressionLevelSwitch; + Q_ASSERT(!compLevelSwitch.isEmpty()); + + compLevelSwitch.replace(QLatin1String("$CompressionLevel"), QString::number(level)); + + return compLevelSwitch; +} + +QString CliProperties::substituteCompressionMethodSwitch(const QString &method) const +{ + if (method.isEmpty()) { + return QString(); + } + + Q_ASSERT(!ArchiveFormat::fromMetadata(m_mimeType, m_metaData).compressionMethods().isEmpty()); + + QString compMethodSwitch = m_compressionMethodSwitch[m_mimeType.name()].toString(); + Q_ASSERT(!compMethodSwitch.isEmpty()); + + QString cliMethod = ArchiveFormat::fromMetadata(m_mimeType, m_metaData).compressionMethods().value(method).toString(); + + compMethodSwitch.replace(QLatin1String("$CompressionMethod"), cliMethod); + + return compMethodSwitch; +} + +QString CliProperties::substituteMultiVolumeSwitch(uint volumeSize) const +{ + // The maximum value we allow in the QDoubleSpinBox is 1000MB. Converted to + // KB this is 1024000. + if (volumeSize <= 0 || volumeSize > 1024000) { + return QString(); + } + + Q_ASSERT(ArchiveFormat::fromMetadata(m_mimeType, m_metaData).supportsMultiVolume()); + + QString multiVolumeSwitch = m_multiVolumeSwitch; + Q_ASSERT(!multiVolumeSwitch.isEmpty()); + + multiVolumeSwitch.replace(QLatin1String("$VolumeSize"), QString::number(volumeSize)); + + return multiVolumeSwitch; +} + +bool CliProperties::isPasswordPrompt(const QString &line) +{ + foreach(const QString &rx, m_passwordPromptPatterns) { + if (QRegularExpression(rx).match(line).hasMatch()) { + return true; + } + } + return false; +} + +bool CliProperties::isWrongPasswordMsg(const QString &line) +{ + foreach(const QString &rx, m_wrongPasswordPatterns) { + if (QRegularExpression(rx).match(line).hasMatch()) { + return true; + } + } + return false; +} + +bool CliProperties::isTestPassedMsg(const QString &line) +{ + foreach(const QString &rx, m_testPassedPatterns) { + if (QRegularExpression(rx).match(line).hasMatch()) { + return true; + } + } + return false; +} + +bool CliProperties::isfileExistsMsg(const QString &line) +{ + foreach(const QString &rx, m_fileExistsPatterns) { + if (QRegularExpression(rx).match(line).hasMatch()) { + return true; + } + } + return false; +} + +bool CliProperties::isFileExistsFileName(const QString &line) +{ + foreach(const QString &rx, m_fileExistsFileName) { + if (QRegularExpression(rx).match(line).hasMatch()) { + return true; + } + } + return false; +} + +bool CliProperties::isExtractionFailedMsg(const QString &line) +{ + foreach(const QString &rx, m_extractionFailedPatterns) { + if (QRegularExpression(rx).match(line).hasMatch()) { + return true; + } + } + return false; +} + +bool CliProperties::isCorruptArchiveMsg(const QString &line) +{ + foreach(const QString &rx, m_corruptArchivePatterns) { + if (QRegularExpression(rx).match(line).hasMatch()) { + return true; + } + } + return false; +} + +bool CliProperties::isDiskFullMsg(const QString &line) +{ + foreach(const QString &rx, m_diskFullPatterns) { + if (QRegularExpression(rx).match(line).hasMatch()) { + return true; + } + } + return false; +} + +} diff --git a/kerfuffle/cliproperties.h b/kerfuffle/cliproperties.h new file mode 100644 index 00000000..0716e626 --- /dev/null +++ b/kerfuffle/cliproperties.h @@ -0,0 +1,152 @@ +/* + * ark -- archiver for the KDE project + * + * Copyright (C) 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 CLIPROPERTIES_H +#define CLIPROPERTIES_H + +#include "archiveinterface.h" +#include "kerfuffle_export.h" + +#include + +namespace Kerfuffle +{ + +class KERFUFFLE_EXPORT CliProperties: public QObject +{ + Q_OBJECT + + Q_PROPERTY(QString addProgram MEMBER m_addProgram) + Q_PROPERTY(QString deleteProgram MEMBER m_deleteProgram) + Q_PROPERTY(QString extractProgram MEMBER m_extractProgram) + Q_PROPERTY(QString listProgram MEMBER m_listProgram) + Q_PROPERTY(QString moveProgram MEMBER m_moveProgram) + Q_PROPERTY(QString testProgram MEMBER m_testProgram) + + Q_PROPERTY(QStringList addSwitch MEMBER m_addSwitch) + Q_PROPERTY(QStringList commentSwitch MEMBER m_commentSwitch) + Q_PROPERTY(QString deleteSwitch MEMBER m_deleteSwitch) + Q_PROPERTY(QStringList extractSwitch MEMBER m_extractSwitch) + Q_PROPERTY(QStringList extractSwitchNoPreserve MEMBER m_extractSwitchNoPreserve) + Q_PROPERTY(QStringList listSwitch MEMBER m_listSwitch) + Q_PROPERTY(QString moveSwitch MEMBER m_moveSwitch) + Q_PROPERTY(QStringList testSwitch MEMBER m_testSwitch) + + Q_PROPERTY(QStringList passwordSwitch MEMBER m_passwordSwitch) + Q_PROPERTY(QStringList passwordSwitchHeaderEnc MEMBER m_passwordSwitchHeaderEnc) + Q_PROPERTY(QString compressionLevelSwitch MEMBER m_compressionLevelSwitch) + Q_PROPERTY(QHash compressionMethodSwitch MEMBER m_compressionMethodSwitch) + Q_PROPERTY(QString multiVolumeSwitch MEMBER m_multiVolumeSwitch) + + Q_PROPERTY(QStringList passwordPromptPatterns MEMBER m_passwordPromptPatterns) + Q_PROPERTY(QStringList wrongPasswordPatterns MEMBER m_wrongPasswordPatterns) + Q_PROPERTY(QStringList testPassedPatterns MEMBER m_testPassedPatterns) + Q_PROPERTY(QStringList fileExistsPatterns MEMBER m_fileExistsPatterns) + Q_PROPERTY(QStringList fileExistsFileName MEMBER m_fileExistsFileName) + Q_PROPERTY(QStringList extractionFailedPatterns MEMBER m_extractionFailedPatterns) + Q_PROPERTY(QStringList corruptArchivePatterns MEMBER m_corruptArchivePatterns) + Q_PROPERTY(QStringList diskFullPatterns MEMBER m_diskFullPatterns) + + Q_PROPERTY(QStringList fileExistsInput MEMBER m_fileExistsInput) + Q_PROPERTY(QStringList multiVolumeSuffix MEMBER m_multiVolumeSuffix) + + Q_PROPERTY(bool captureProgress MEMBER m_captureProgress) + +public: + explicit CliProperties(QObject *parent, const KPluginMetaData &metaData, const QMimeType &archiveType); + + QStringList addArgs(const QString &archive, + const QStringList &files, + const QString &password, + bool headerEncryption, + int compressionLevel, + const QString &compressionMethod, + uint volumeSize); + QStringList commentArgs(const QString &archive, const QString &commentfile); + QStringList deleteArgs(const QString &archive, const QVector &files, const QString &password); + QStringList extractArgs(const QString &archive, const QStringList &files, bool preservePaths, const QString &password); + QStringList listArgs(const QString &archive, const QString &password); + QStringList moveArgs(const QString &archive, const QVector &entries, Archive::Entry *destination, const QString &password); + QStringList testArgs(const QString &archive, const QString &password); + + bool isPasswordPrompt(const QString &line); + bool isWrongPasswordMsg(const QString &line); + bool isTestPassedMsg(const QString &line); + bool isfileExistsMsg(const QString &line); + bool isFileExistsFileName(const QString &line); + bool isExtractionFailedMsg(const QString &line); + bool isCorruptArchiveMsg(const QString &line); + bool isDiskFullMsg(const QString &line); + +private: + QStringList substituteCommentSwitch(const QString &commentfile) const; + QStringList substitutePasswordSwitch(const QString &password, bool headerEnc = false) const; + QString substituteCompressionLevelSwitch(int level) const; + QString substituteCompressionMethodSwitch(const QString &method) const; + QString substituteMultiVolumeSwitch(uint volumeSize) const; + + QString m_addProgram; + QString m_deleteProgram; + QString m_extractProgram; + QString m_listProgram; + QString m_moveProgram; + QString m_testProgram; + + QStringList m_addSwitch; + QStringList m_commentSwitch; + QString m_deleteSwitch; + QStringList m_extractSwitch; + QStringList m_extractSwitchNoPreserve; + QStringList m_listSwitch; + QString m_moveSwitch; + QStringList m_testSwitch; + + QStringList m_passwordSwitch; + QStringList m_passwordSwitchHeaderEnc; + QString m_compressionLevelSwitch; + QHash m_compressionMethodSwitch; + QString m_multiVolumeSwitch; + + QStringList m_passwordPromptPatterns; + QStringList m_wrongPasswordPatterns; + QStringList m_testPassedPatterns; + QStringList m_fileExistsPatterns; + QStringList m_fileExistsFileName; + QStringList m_extractionFailedPatterns; + QStringList m_corruptArchivePatterns; + QStringList m_diskFullPatterns; + + QStringList m_fileExistsInput; + QStringList m_multiVolumeSuffix; + + bool m_captureProgress; + + QMimeType m_mimeType; + KPluginMetaData m_metaData; +}; +} + +#endif /* CLIPROPERTIES_H */ diff --git a/kerfuffle/compressionoptionswidget.cpp b/kerfuffle/compressionoptionswidget.cpp index 29b5b491..e3189d16 100644 --- a/kerfuffle/compressionoptionswidget.cpp +++ b/kerfuffle/compressionoptionswidget.cpp @@ -1,238 +1,238 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 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 "compressionoptionswidget.h" #include "ark_debug.h" #include "archiveformat.h" #include "pluginmanager.h" #include #include #include namespace Kerfuffle { CompressionOptionsWidget::CompressionOptionsWidget(QWidget *parent, const CompressionOptions &opts) : QWidget(parent) , m_opts(opts) { setupUi(this); KColorScheme colorScheme(QPalette::Active, KColorScheme::View); pwdWidget->setBackgroundWarningColor(colorScheme.background(KColorScheme::NegativeBackground).color()); pwdWidget->setPasswordStrengthMeterVisible(false); connect(multiVolumeCheckbox, &QCheckBox::stateChanged, this, &CompressionOptionsWidget::slotMultiVolumeChecked); if (m_opts.isVolumeSizeSet()) { multiVolumeCheckbox->setChecked(true); // Convert from kilobytes. volumeSizeSpinbox->setValue(static_cast(m_opts.volumeSize()) / 1024); } } CompressionOptions CompressionOptionsWidget::commpressionOptions() const { CompressionOptions opts; opts.setCompressionLevel(compLevelSlider->value()); if (multiVolumeCheckbox->isChecked()) { opts.setVolumeSize(volumeSize()); } if (!compMethodComboBox->currentText().isEmpty()) { opts.setCompressionMethod(compMethodComboBox->currentText()); } return opts; } int CompressionOptionsWidget::compressionLevel() const { if (compLevelSlider->isEnabled()) { return compLevelSlider->value(); } else { return -1; } } QString CompressionOptionsWidget::compressionMethod() const { return compMethodComboBox->currentText(); } ulong CompressionOptionsWidget::volumeSize() const { if (collapsibleMultiVolume->isEnabled() && multiVolumeCheckbox->isChecked()) { // Convert to kilobytes. return volumeSizeSpinbox->value() * 1024; } else { return 0; } } void CompressionOptionsWidget::setEncryptionVisible(bool visible) { collapsibleEncryption->setVisible(visible); } QString CompressionOptionsWidget::password() const { return pwdWidget->password(); } void CompressionOptionsWidget::updateWidgets() { const KPluginMetaData metadata = PluginManager().preferredPluginFor(m_mimetype)->metaData(); const ArchiveFormat archiveFormat = ArchiveFormat::fromMetadata(m_mimetype, metadata); Q_ASSERT(archiveFormat.isValid()); if (archiveFormat.encryptionType() != Archive::Unencrypted) { collapsibleEncryption->setEnabled(true); collapsibleEncryption->setToolTip(QString()); pwdWidget->setEnabled(true); if (archiveFormat.encryptionType() == Archive::HeaderEncrypted) { encryptHeaderCheckBox->setEnabled(true); encryptHeaderCheckBox->setToolTip(QString()); } else { encryptHeaderCheckBox->setEnabled(false); // Show the tooltip only if the encryption is still enabled. // This is needed because if the new filter is e.g. tar, the whole encryption group gets disabled. if (collapsibleEncryption->isEnabled() && collapsibleEncryption->isExpanded()) { encryptHeaderCheckBox->setToolTip(i18n("Protection of the list of files is not possible with the %1 format.", m_mimetype.comment())); } else { encryptHeaderCheckBox->setToolTip(QString()); } } } else { collapsibleEncryption->setEnabled(false); collapsibleEncryption->setToolTip(i18n("Protection of the archive with password is not possible with the %1 format.", m_mimetype.comment())); pwdWidget->setEnabled(false); encryptHeaderCheckBox->setToolTip(QString()); } collapsibleCompression->setEnabled(true); if (archiveFormat.maxCompressionLevel() == 0) { compLevelSlider->setEnabled(false); lblCompLevel1->setEnabled(false); lblCompLevel2->setEnabled(false); lblCompLevel3->setEnabled(false); compLevelSlider->setToolTip(i18n("It is not possible to set compression level for the %1 format.", m_mimetype.comment())); } else { compLevelSlider->setEnabled(true); lblCompLevel1->setEnabled(true); lblCompLevel2->setEnabled(true); lblCompLevel3->setEnabled(true); compLevelSlider->setToolTip(QString()); compLevelSlider->setMinimum(archiveFormat.minCompressionLevel()); compLevelSlider->setMaximum(archiveFormat.maxCompressionLevel()); if (m_opts.isCompressionLevelSet()) { compLevelSlider->setValue(m_opts.compressionLevel()); } else { compLevelSlider->setValue(archiveFormat.defaultCompressionLevel()); } } if (archiveFormat.compressionMethods().isEmpty()) { lblCompMethod->setEnabled(false); compMethodComboBox->setEnabled(false); compMethodComboBox->setToolTip(i18n("It is not possible to set compression method for the %1 format.", m_mimetype.comment())); compMethodComboBox->clear(); } else { lblCompMethod->setEnabled(true); compMethodComboBox->setEnabled(true); compMethodComboBox->setToolTip(QString()); compMethodComboBox->clear(); - compMethodComboBox->insertItems(0, archiveFormat.compressionMethods()); + compMethodComboBox->insertItems(0, archiveFormat.compressionMethods().keys()); if (!m_opts.compressionMethod().isEmpty() && compMethodComboBox->findText(m_opts.compressionMethod()) > -1) { compMethodComboBox->setCurrentText(m_opts.compressionMethod()); } else { compMethodComboBox->setCurrentText(archiveFormat.defaultCompressionMethod()); } } collapsibleCompression->setEnabled(compLevelSlider->isEnabled() || compMethodComboBox->isEnabled()); if (archiveFormat.supportsMultiVolume()) { collapsibleMultiVolume->setEnabled(true); collapsibleMultiVolume->setToolTip(QString()); } else { collapsibleMultiVolume->setEnabled(false); collapsibleMultiVolume->setToolTip(i18n("The %1 format does not support multi-volume archives.", m_mimetype.comment())); } } void CompressionOptionsWidget::setMimeType(const QMimeType &mimeType) { m_mimetype = mimeType; updateWidgets(); } bool CompressionOptionsWidget::isEncryptionAvailable() const { return collapsibleEncryption->isEnabled(); } bool CompressionOptionsWidget::isEncryptionEnabled() const { return isEncryptionAvailable() && collapsibleEncryption->isExpanded(); } bool CompressionOptionsWidget::isHeaderEncryptionAvailable() const { return isEncryptionEnabled() && encryptHeaderCheckBox->isEnabled(); } bool CompressionOptionsWidget::isHeaderEncryptionEnabled() const { return isHeaderEncryptionAvailable() && encryptHeaderCheckBox->isChecked(); } KNewPasswordWidget::PasswordStatus CompressionOptionsWidget::passwordStatus() const { return pwdWidget->passwordStatus(); } void CompressionOptionsWidget::slotMultiVolumeChecked(int state) { if (state == Qt::Checked) { lblVolumeSize->setEnabled(true); volumeSizeSpinbox->setEnabled(true); } else { lblVolumeSize->setEnabled(false); volumeSizeSpinbox->setEnabled(false); } } } diff --git a/plugins/cli7zplugin/cliplugin.cpp b/plugins/cli7zplugin/cliplugin.cpp index 8ca99a15..4d5d6cba 100644 --- a/plugins/cli7zplugin/cliplugin.cpp +++ b/plugins/cli7zplugin/cliplugin.cpp @@ -1,334 +1,288 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/cliinterface.h" #include "kerfuffle/kerfuffle_export.h" #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_cli7z.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList & args) : CliInterface(parent, args) , m_archiveType(ArchiveType7z) , m_parseState(ParseStateTitle) , m_linesComment(0) , m_isFirstInformationEntry(true) { qCDebug(ARK) << "Loaded cli_7z plugin"; + + setupCliProperties(); } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateTitle; m_comment.clear(); m_numberOfVolumes = 0; } -ParameterList CliPlugin::parameterList() const +void CliPlugin::setupCliProperties() { - static ParameterList p; - - if (p.isEmpty()) { - //p[CaptureProgress] = true; - p[ListProgram] = p[ExtractProgram] = p[DeleteProgram] = p[MoveProgram] = p[AddProgram] = p[TestProgram] = QStringList() << QStringLiteral("7z"); - p[ListArgs] = QStringList() << QStringLiteral("l") - << QStringLiteral("-slt") - << QStringLiteral("$PasswordSwitch") - << QStringLiteral("$Archive"); - p[ExtractArgs] = QStringList() << QStringLiteral("$PreservePathSwitch") - << QStringLiteral("$PasswordSwitch") - << QStringLiteral("$Archive") - << QStringLiteral("$Files"); - p[PreservePathSwitch] = QStringList() << QStringLiteral("x") - << QStringLiteral("e"); - p[PasswordSwitch] = QStringList() << QStringLiteral("-p$Password"); - p[PasswordHeaderSwitch] = QStringList { QStringLiteral("-p$Password"), QStringLiteral("-mhe=on") }; - p[WrongPasswordPatterns] = QStringList() << QStringLiteral("Wrong password"); - p[CompressionLevelSwitch] = QStringLiteral("-mx=$CompressionLevel"); - p[AddArgs] = QStringList() << QStringLiteral("a") - << QStringLiteral("-l") - << QStringLiteral("$Archive") - << QStringLiteral("$PasswordSwitch") - << QStringLiteral("$CompressionLevelSwitch") - << QStringLiteral("$CompressionMethodSwitch") - << QStringLiteral("$MultiVolumeSwitch") - << QStringLiteral("$Files"); - p[MoveArgs] = QStringList() << QStringLiteral("rn") - << QStringLiteral("$PasswordSwitch") - << QStringLiteral("$Archive") - << QStringLiteral("$PathPairs"); - p[DeleteArgs] = QStringList() << QStringLiteral("d") - << QStringLiteral("$PasswordSwitch") - << QStringLiteral("$Archive") - << QStringLiteral("$Files"); - p[TestArgs] = QStringList() << QStringLiteral("t") - << QStringLiteral("$Archive") - << QStringLiteral("$PasswordSwitch"); - p[TestPassedPattern] = QStringLiteral("^Everything is Ok$"); - - p[FileExistsExpression] = QStringList() - << QStringLiteral("^\\(Y\\)es / \\(N\\)o / \\(A\\)lways / \\(S\\)kip all / A\\(u\\)to rename all / \\(Q\\)uit\\? $") - << QStringLiteral("^\\? \\(Y\\)es / \\(N\\)o / \\(A\\)lways / \\(S\\)kip all / A\\(u\\)to rename all / \\(Q\\)uit\\? $"); - p[FileExistsFileName] = QStringList() << QStringLiteral("^file \\./(.*)$") - << QStringLiteral("^ Path: \\./(.*)$"); - p[FileExistsInput] = QStringList() << QStringLiteral("Y") //overwrite - << QStringLiteral("N") //skip - << QStringLiteral("A") //overwrite all - << QStringLiteral("S") //autoskip - << QStringLiteral("Q"); //cancel - p[PasswordPromptPattern] = QStringLiteral("Enter password \\(will not be echoed\\)"); - p[ExtractionFailedPatterns] = QStringList() << QStringLiteral("ERROR: E_FAIL") << QStringLiteral("Open ERROR: Can not open the file as \\[7z\\] archive"); - p[CorruptArchivePatterns] = QStringList() << QStringLiteral("Unexpected end of archive") - << QStringLiteral("Headers Error"); - p[DiskFullPatterns] = QStringList() << QStringLiteral("No space left on device"); - p[MultiVolumeSwitch] = QStringLiteral("-v$VolumeSizek"); - p[MultiVolumeSuffix] = QStringList() << QStringLiteral("$Suffix.001"); - QMap compMethodMap; - compMethodMap[QStringLiteral("zip")] = QStringLiteral("-mm=$CompressionMethod"); - compMethodMap[QStringLiteral("7z")] = QStringLiteral("-m0=$CompressionMethod"); - p[CompressionMethodSwitch] = compMethodMap; - } - - return p; + qCDebug(ARK) << "Setting up parameters..."; + + m_cliProps->setProperty("captureProgress", false); + + m_cliProps->setProperty("addProgram", QStringLiteral("7z")); + m_cliProps->setProperty("addSwitch", QStringList{QStringLiteral("a"), + QStringLiteral("-l")}); + + m_cliProps->setProperty("deleteProgram", QStringLiteral("7z")); + m_cliProps->setProperty("deleteSwitch", QStringLiteral("d")); + + m_cliProps->setProperty("extractProgram", QStringLiteral("7z")); + m_cliProps->setProperty("extractSwitch", QStringList{QStringLiteral("x")}); + m_cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("e")}); + + m_cliProps->setProperty("listProgram", QStringLiteral("7z")); + m_cliProps->setProperty("listSwitch", QStringList{QStringLiteral("l"), + QStringLiteral("-slt")}); + + m_cliProps->setProperty("moveProgram", QStringLiteral("7z")); + m_cliProps->setProperty("moveSwitch", QStringLiteral("rn")); + + m_cliProps->setProperty("testProgram", QStringLiteral("7z")); + m_cliProps->setProperty("testSwitch", QStringLiteral("t")); + + m_cliProps->setProperty("passwordSwitch", QStringList{QStringLiteral("-p$Password")}); + m_cliProps->setProperty("passwordSwitchHeaderEnc", QStringList{QStringLiteral("-p$Password"), + QStringLiteral("-mhe=on")}); + m_cliProps->setProperty("compressionLevelSwitch", QStringLiteral("-mx=$CompressionLevel")); + m_cliProps->setProperty("compressionMethodSwitch", QHash{{QStringLiteral("application/x-7z-compressed"), QStringLiteral("-m0=$CompressionMethod")}, + {QStringLiteral("application/zip"), QStringLiteral("-mm=$CompressionMethod")}}); + m_cliProps->setProperty("multiVolumeSwitch", QStringLiteral("-v$VolumeSizek")); + + + m_cliProps->setProperty("passwordPromptPatterns", QStringList{QStringLiteral("Enter password \\(will not be echoed\\)")}); + m_cliProps->setProperty("wrongPasswordPatterns", QStringList{QStringLiteral("Wrong password")}); + m_cliProps->setProperty("testPassedPatterns", QStringList{QStringLiteral("^Everything is Ok$")}); + m_cliProps->setProperty("fileExistsPatterns", QStringList{QStringLiteral("^\\(Y\\)es / \\(N\\)o / \\(A\\)lways / \\(S\\)kip all / A\\(u\\)to rename all / \\(Q\\)uit\\? $"), + QStringLiteral("^\\? \\(Y\\)es / \\(N\\)o / \\(A\\)lways / \\(S\\)kip all / A\\(u\\)to rename all / \\(Q\\)uit\\? $")}); + m_cliProps->setProperty("fileExistsFileName", QStringList{QStringLiteral("^file \\./(.*)$"), + QStringLiteral("^ Path: \\./(.*)$")}); + m_cliProps->setProperty("fileExistsInput", QStringList{QStringLiteral("Y"), //Overwrite + QStringLiteral("N"), //Skip + QStringLiteral("A"), //Overwrite all + QStringLiteral("S"), //Autoskip + QStringLiteral("Q")}); //Cancel + m_cliProps->setProperty("extractionFailedPatterns", QStringList{QStringLiteral("ERROR: E_FAIL"), + QStringLiteral("Open ERROR: Can not open the file as \\[7z\\] archive")}); + m_cliProps->setProperty("corruptArchivePatterns", QStringList{QStringLiteral("Unexpected end of archive"), + QStringLiteral("Headers Error")}); + m_cliProps->setProperty("diskFullPatterns", QStringList{QStringLiteral("No space left on device")}); + m_cliProps->setProperty("multiVolumeSuffix", QStringList{QStringLiteral("$Suffix.001")}); } bool CliPlugin::readListLine(const QString& line) { static const QLatin1String archiveInfoDelimiter1("--"); // 7z 9.13+ static const QLatin1String archiveInfoDelimiter2("----"); // 7z 9.04 static const QLatin1String entryInfoDelimiter("----------"); const QRegularExpression rxComment(QStringLiteral("Comment = .+$")); if (m_parseState == ParseStateTitle) { const QRegularExpression rxVersionLine(QStringLiteral("^p7zip Version ([\\d\\.]+) .*$")); QRegularExpressionMatch matchVersion = rxVersionLine.match(line); if (matchVersion.hasMatch()) { m_parseState = ParseStateHeader; const QString p7zipVersion = matchVersion.captured(1); qCDebug(ARK) << "p7zip version" << p7zipVersion << "detected"; } } else if (m_parseState == ParseStateHeader) { if (line.startsWith(QStringLiteral("Listing archive:"))) { qCDebug(ARK) << "Archive name: " << line.right(line.size() - 16).trimmed(); } else if ((line == archiveInfoDelimiter1) || (line == archiveInfoDelimiter2)) { m_parseState = ParseStateArchiveInformation; } else if (line.contains(QStringLiteral("Error: "))) { qCWarning(ARK) << line.mid(7); } } else if (m_parseState == ParseStateArchiveInformation) { if (line == entryInfoDelimiter) { m_parseState = ParseStateEntryInformation; } else if (line.startsWith(QStringLiteral("Type = "))) { const QString type = line.mid(7).trimmed(); qCDebug(ARK) << "Archive type: " << type; if (type == QLatin1String("7z")) { m_archiveType = ArchiveType7z; } else if (type == QLatin1String("bzip2")) { m_archiveType = ArchiveTypeBZip2; } else if (type == QLatin1String("gzip")) { m_archiveType = ArchiveTypeGZip; } else if (type == QLatin1String("xz")) { m_archiveType = ArchiveTypeXz; } else if (type == QLatin1String("tar")) { m_archiveType = ArchiveTypeTar; } else if (type == QLatin1String("zip")) { m_archiveType = ArchiveTypeZip; } else if (type == QLatin1String("Rar")) { m_archiveType = ArchiveTypeRar; } else if (type == QLatin1String("Split")) { setMultiVolume(true); } else { // Should not happen qCWarning(ARK) << "Unsupported archive type"; return false; } } else if (line.startsWith(QStringLiteral("Volumes = "))) { m_numberOfVolumes = line.section(QLatin1Char('='), 1).trimmed().toInt(); } else if (line.startsWith(QStringLiteral("Method = "))) { QStringList methods = line.section(QLatin1Char('='), 1).trimmed().split(QLatin1Char(' '), QString::SkipEmptyParts); // LZMA methods are output with some trailing numbers by 7z representing dictionary/block sizes. // We are not interested in these, so remove them. QMutableListIterator i(methods); while (i.hasNext()) { QString m = i.next(); if (m.startsWith(QLatin1String("LZMA2"))) { m = m.left(5); } else if (m.startsWith(QLatin1String("LZMA"))) { m = m.left(4); } i.setValue(m); } emit compressionMethodFound(methods); } else if (rxComment.match(line).hasMatch()) { m_parseState = ParseStateComment; m_comment.append(line.section(QLatin1Char('='), 1) + QLatin1Char('\n')); } } else if (m_parseState == ParseStateComment) { if (line == entryInfoDelimiter) { m_parseState = ParseStateEntryInformation; if (!m_comment.trimmed().isEmpty()) { m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } } else if (m_parseState == ParseStateEntryInformation) { if (m_isFirstInformationEntry) { m_isFirstInformationEntry = false; m_currentArchiveEntry = new Archive::Entry(this); m_currentArchiveEntry->compressedSizeIsSet = false; } if (line.startsWith(QStringLiteral("Path = "))) { const QString entryFilename = QDir::fromNativeSeparators(line.mid(7).trimmed()); m_currentArchiveEntry->setProperty("fullPath", entryFilename); } else if (line.startsWith(QStringLiteral("Size = "))) { m_currentArchiveEntry->setProperty("size", line.mid(7).trimmed()); } else if (line.startsWith(QStringLiteral("Packed Size = "))) { // #236696: 7z files only show a single Packed Size value // corresponding to the whole archive. if (m_archiveType != ArchiveType7z) { m_currentArchiveEntry->compressedSizeIsSet = true; m_currentArchiveEntry->setProperty("compressedSize", line.mid(14).trimmed()); } } else if (line.startsWith(QStringLiteral("Modified = "))) { m_currentArchiveEntry->setProperty("timestamp", QDateTime::fromString(line.mid(11).trimmed(), QStringLiteral("yyyy-MM-dd hh:mm:ss"))); } else if (line.startsWith(QStringLiteral("Attributes = "))) { const QString attributes = line.mid(13).trimmed(); const bool isDirectory = attributes.startsWith(QLatin1Char('D')); m_currentArchiveEntry->setProperty("isDirectory", isDirectory); if (isDirectory) { const QString directoryName = m_currentArchiveEntry->fullPath(); if (!directoryName.endsWith(QLatin1Char('/'))) { const bool isPasswordProtected = (line.at(12) == QLatin1Char('+')); m_currentArchiveEntry->setProperty("fullPath", QString(directoryName + QLatin1Char('/'))); m_currentArchiveEntry->setProperty("isPasswordProtected", isPasswordProtected); } } m_currentArchiveEntry->setProperty("permissions", attributes.mid(1)); } else if (line.startsWith(QStringLiteral("CRC = "))) { m_currentArchiveEntry->setProperty("CRC", line.mid(6).trimmed()); } else if (line.startsWith(QStringLiteral("Method = "))) { m_currentArchiveEntry->setProperty("method", line.mid(9).trimmed()); // For zip archives we need to check method for each entry. if (m_archiveType == ArchiveTypeZip) { QString method = line.mid(9).trimmed(); if (method == QLatin1String("xz")) { method = QStringLiteral("XZ"); } if (!m_compressionMethods.contains(method)) { m_compressionMethods.append(method); emit compressionMethodFound(m_compressionMethods); } } } else if (line.startsWith(QStringLiteral("Encrypted = ")) && line.size() >= 13) { m_currentArchiveEntry->setProperty("isPasswordProtected", line.at(12) == QLatin1Char('+')); } else if (line.startsWith(QStringLiteral("Block = ")) || line.startsWith(QStringLiteral("Version = "))) { m_isFirstInformationEntry = true; if (!m_currentArchiveEntry->fullPath().isEmpty()) { emit entry(m_currentArchiveEntry); } else { delete m_currentArchiveEntry; } m_currentArchiveEntry = Q_NULLPTR; } } return true; } -QStringList CliPlugin::passwordHeaderSwitch(const QString& password) const -{ - if (password.isEmpty()) { - return QStringList(); - } - - Q_ASSERT(m_param.contains(PasswordHeaderSwitch)); - - QStringList passwordHeaderSwitch = m_param.value(PasswordHeaderSwitch).toStringList(); - Q_ASSERT(!passwordHeaderSwitch.isEmpty() && passwordHeaderSwitch.size() == 2); - - passwordHeaderSwitch[0].replace(QLatin1String("$Password"), password); - - return passwordHeaderSwitch; -} - -QString CliPlugin::compressionMethodSwitch(const QString &method) const -{ - if (method.isEmpty()) { - return QString(); - } - - Q_ASSERT(!filename().isEmpty()); - Q_ASSERT(m_param.contains(CompressionMethodSwitch)); - - QMap switches = m_param.value(CompressionMethodSwitch).toMap(); - Q_ASSERT(!switches.isEmpty()); - - QString compMethodSwitch; - - compMethodSwitch = switches[QFileInfo(filename()).suffix().toLower()].toString(); - compMethodSwitch.replace(QLatin1String("$CompressionMethod"), method); - - return compMethodSwitch; -} - #include "cliplugin.moc" diff --git a/plugins/cli7zplugin/cliplugin.h b/plugins/cli7zplugin/cliplugin.h index c2b89201..08ea8a8d 100644 --- a/plugins/cli7zplugin/cliplugin.h +++ b/plugins/cli7zplugin/cliplugin.h @@ -1,74 +1,69 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2010 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "kerfuffle/cliinterface.h" #include "kerfuffle/archiveentry.h" class CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList & args); virtual ~CliPlugin(); virtual void resetParsing() Q_DECL_OVERRIDE; - virtual Kerfuffle::ParameterList parameterList() const Q_DECL_OVERRIDE; virtual bool readListLine(const QString &line) Q_DECL_OVERRIDE; - /** - * @return The password header-switch with the given @p password. - */ - virtual QStringList passwordHeaderSwitch(const QString& password) const Q_DECL_OVERRIDE; - virtual QString compressionMethodSwitch(const QString &method) const Q_DECL_OVERRIDE; - private: enum ArchiveType { ArchiveType7z = 0, ArchiveTypeBZip2, ArchiveTypeGZip, ArchiveTypeXz, ArchiveTypeTar, ArchiveTypeZip, ArchiveTypeRar } m_archiveType; enum ParseState { ParseStateTitle = 0, ParseStateHeader, ParseStateArchiveInformation, ParseStateComment, ParseStateEntryInformation } m_parseState; + void setupCliProperties(); + int m_linesComment; Kerfuffle::Archive::Entry *m_currentArchiveEntry; bool m_isFirstInformationEntry; QStringList m_compressionMethods; }; #endif // CLIPLUGIN_H diff --git a/plugins/cli7zplugin/kerfuffle_cli7z.json.cmake b/plugins/cli7zplugin/kerfuffle_cli7z.json.cmake index 2e34a9d4..546dfecb 100644 --- a/plugins/cli7zplugin/kerfuffle_cli7z.json.cmake +++ b/plugins/cli7zplugin/kerfuffle_cli7z.json.cmake @@ -1,83 +1,83 @@ { "KPlugin": { "Id": "kerfuffle_cli7z", "MimeTypes": [ "@SUPPORTED_MIMETYPES@" ], "Name": "7zip archive plugin", "Name[ca@valencia]": "Connector per arxius 7zip", "Name[ca]": "Connector per arxius 7zip", "Name[cs]": "Modul pro archiv 7zip", "Name[de]": "7zip-Archiv-Modul", "Name[es]": "Complemento de archivo 7zip", "Name[et]": "7zip arhiivi plugin", "Name[fi]": "7zip-pakkaustuki", - "Name[fr]": "Module externe d'archive « 7zip »", + "Name[fr]": "Module externe d'archive « 7zip »", "Name[gl]": "Complemento de arquivo de 7zip", "Name[he]": "תוסף ארכיוני 7zip", "Name[it]": "Estensione per archivi 7zip", "Name[nb]": "Programtillegg for 7zip-arkiv", "Name[nl]": "7zip-archiefplug-in", "Name[nn]": "7zip-arkivtillegg", "Name[pl]": "Wtyczka archiwów 7zip", "Name[pt]": "'Plugin' de pacotes 7zip", "Name[pt_BR]": "Plugin de arquivos 7zip", "Name[ru]": "Поддержка архивов 7zip", "Name[sk]": "Modul 7zip archívu", "Name[sl]": "Vstavek za arhive 7zip", "Name[sr@ijekavian]": "Прикључак 7зип архива", "Name[sr@ijekavianlatin]": "Priključak 7zip arhiva", "Name[sr@latin]": "Priključak 7zip arhiva", "Name[sr]": "Прикључак 7зип архива", "Name[sv]": "Insticksprogram för 7zip-arkiv", "Name[uk]": "Додаток для архівів 7zip", "Name[x-test]": "xx7zip archive pluginxx", "Name[zh_CN]": "7zip 归档插件", "ServiceTypes": [ "Kerfuffle/Plugin" ], "Version": "@KDE_APPLICATIONS_VERSION@" }, "X-KDE-Kerfuffle-ReadOnlyExecutables": [ "7z" ], "X-KDE-Kerfuffle-ReadWrite": true, "X-KDE-Kerfuffle-ReadWriteExecutables": [ "7z" ], "X-KDE-Priority": 180, "application/x-7z-compressed": { "CompressionLevelDefault": 5, "CompressionLevelMax": 9, "CompressionLevelMin": 0, "CompressionMethodDefault": "LZMA2", - "CompressionMethods": [ - "BZip2", - "Copy", - "Deflate", - "LZMA", - "LZMA2", - "PPMd" - ], + "CompressionMethods": { + "BZip2" : "BZip2", + "Copy" : "Copy", + "Deflate" : "Deflate", + "LZMA" : "LZMA", + "LZMA2" : "LZMA2", + "PPMd" : "PPMd" + }, "HeaderEncryption": true, "SupportsMultiVolume": true, "SupportsTesting": true }, "application/zip": { "CompressionLevelDefault": 5, "CompressionLevelMax": 9, "CompressionLevelMin": 0, "CompressionMethodDefault": "Deflate", - "CompressionMethods": [ - "BZip2", - "Copy", - "Deflate", - "Deflate64", - "LZMA", - "PPMd" - ], + "CompressionMethods": { + "BZip2" : "BZip2", + "Copy" : "Copy", + "Deflate" : "Deflate", + "Deflate64" : "Deflate64", + "LZMA" : "LZMA", + "PPMd": "PPMd" + }, "Encryption": true, "SupportsMultiVolume": true, "SupportsTesting": true } -} \ No newline at end of file +} diff --git a/plugins/clirarplugin/cliplugin.cpp b/plugins/clirarplugin/cliplugin.cpp index eb29459d..407ec23d 100644 --- a/plugins/clirarplugin/cliplugin.cpp +++ b/plugins/clirarplugin/cliplugin.cpp @@ -1,570 +1,546 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2010-2011,2014 Raphael Kubo da Costa * Copyright (C) 2015-2016 Ragnar Thomsen * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/archiveentry.h" #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_clirar.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList& args) : CliInterface(parent, args) , m_parseState(ParseStateTitle) , m_isUnrar5(false) , m_isPasswordProtected(false) , m_isSolid(false) , m_remainingIgnoreLines(1) //The first line of UNRAR output is empty. , m_linesComment(0) { qCDebug(ARK) << "Loaded cli_rar plugin"; // Empty lines are needed for parsing output of unrar. setListEmptyLines(true); + + setupCliProperties(); } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateTitle; m_remainingIgnoreLines = 1; m_unrarVersion.clear(); m_comment.clear(); m_numberOfVolumes = 0; } -ParameterList CliPlugin::parameterList() const +void CliPlugin::setupCliProperties() { - static ParameterList p; - - if (p.isEmpty()) { - p[CaptureProgress] = true; - p[ListProgram] = p[ExtractProgram] = p[TestProgram] = QStringList() << QStringLiteral("unrar"); - p[DeleteProgram] = p[MoveProgram] = p[AddProgram] = QStringList() << QStringLiteral("rar"); - - p[ListArgs] = QStringList() << QStringLiteral("vt") - << QStringLiteral("-v") - << QStringLiteral("$PasswordSwitch") - << QStringLiteral("$Archive"); - p[ExtractArgs] = QStringList() << QStringLiteral("-kb") - << QStringLiteral("-p-") - << QStringLiteral("$PreservePathSwitch") - << QStringLiteral("$PasswordSwitch") - << QStringLiteral("$Archive") - << QStringLiteral("$Files"); - p[PreservePathSwitch] = QStringList() << QStringLiteral("x") - << QStringLiteral("e"); - p[PasswordSwitch] = QStringList() << QStringLiteral("-p$Password"); - p[PasswordHeaderSwitch] = QStringList() << QStringLiteral("-hp$Password"); - p[CompressionLevelSwitch] = QStringLiteral("-m$CompressionLevel"); - p[MultiVolumeSwitch] = QStringLiteral("-v$VolumeSizek"); - p[DeleteArgs] = QStringList() << QStringLiteral("d") - << QStringLiteral("$PasswordSwitch") - << QStringLiteral("$Archive") - << QStringLiteral("$Files"); - p[FileExistsExpression] = QStringList() - << QStringLiteral("^\\[Y\\]es, \\[N\\]o, \\[A\\]ll, n\\[E\\]ver, \\[R\\]ename, \\[Q\\]uit $"); - p[FileExistsFileName] = QStringList() << QStringLiteral("^(.+) already exists. Overwrite it") // unrar 3 & 4 - << QStringLiteral("^Would you like to replace the existing file (.+)$"); // unrar 5 - p[FileExistsInput] = QStringList() << QStringLiteral("Y") //overwrite - << QStringLiteral("N") //skip - << QStringLiteral("A") //overwrite all - << QStringLiteral("E") //autoskip - << QStringLiteral("Q"); //cancel - p[AddArgs] = QStringList() << QStringLiteral("a") - << QStringLiteral("$Archive") - << QStringLiteral("$PasswordSwitch") - << QStringLiteral("$CompressionLevelSwitch") - << QStringLiteral("$CompressionMethodSwitch") - << QStringLiteral("$MultiVolumeSwitch") - << QStringLiteral("$Files"); - p[MoveArgs] = QStringList() << QStringLiteral("rn") - << QStringLiteral("$PasswordSwitch") - << QStringLiteral("$Archive") - << QStringLiteral("$PathPairs"); - p[PasswordPromptPattern] = QLatin1String("Enter password \\(will not be echoed\\) for"); - p[WrongPasswordPatterns] = QStringList() << QStringLiteral("password incorrect") << QStringLiteral("wrong password"); - p[ExtractionFailedPatterns] = QStringList() << QStringLiteral("CRC failed") - << QStringLiteral("Cannot find volume"); - p[CorruptArchivePatterns] = QStringList() << QStringLiteral("Unexpected end of archive") - << QStringLiteral("the file header is corrupt"); - p[DiskFullPatterns] = QStringList() << QStringLiteral("No space left on device"); - p[CommentArgs] = QStringList() << QStringLiteral("c") - << QStringLiteral("$CommentSwitch") - << QStringLiteral("$Archive"); - p[CommentSwitch] = QStringLiteral("-z$CommentFile"); - p[TestArgs] = QStringList() << QStringLiteral("t") - << QStringLiteral("$Archive") - << QStringLiteral("$PasswordSwitch"); - p[TestPassedPattern] = QStringLiteral("^All OK$"); - // rar will sometimes create multi-volume archives where first volume is - // called name.part1.rar and other times name.part01.rar. - p[MultiVolumeSuffix] = QStringList() << QStringLiteral("part01.$Suffix") - << QStringLiteral("part1.$Suffix"); - p[CompressionMethodSwitch] = QStringLiteral("-ma$CompressionMethod"); - } - - return p; + qCDebug(ARK) << "Setting up parameters..."; + + m_cliProps->setProperty("captureProgress", true); + + m_cliProps->setProperty("addProgram", QStringLiteral("rar")); + m_cliProps->setProperty("addSwitch", QStringList({QStringLiteral("a")})); + + m_cliProps->setProperty("deleteProgram", QStringLiteral("rar")); + m_cliProps->setProperty("deleteSwitch", QStringLiteral("d")); + + m_cliProps->setProperty("extractProgram", QStringLiteral("unrar")); + m_cliProps->setProperty("extractSwitch", QStringList{QStringLiteral("x"), + QStringLiteral("-kb"), + QStringLiteral("-p-")}); + m_cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("e"), + QStringLiteral("-kb"), + QStringLiteral("-p-")}); + + m_cliProps->setProperty("listProgram", QStringLiteral("unrar")); + m_cliProps->setProperty("listSwitch", QStringList{QStringLiteral("vt"), + QStringLiteral("-v")}); + + m_cliProps->setProperty("moveProgram", QStringLiteral("rar")); + m_cliProps->setProperty("moveSwitch", QStringLiteral("rn")); + + m_cliProps->setProperty("testProgram", QStringLiteral("unrar")); + m_cliProps->setProperty("testSwitch", QStringLiteral("t")); + + m_cliProps->setProperty("commentSwitch", QStringList{QStringLiteral("c"), + QStringLiteral("-z$CommentFile")}); + + m_cliProps->setProperty("passwordSwitch", QStringList{QStringLiteral("-p$Password")}); + m_cliProps->setProperty("passwordSwitchHeaderEnc", QStringList{QStringLiteral("-hp$Password")}); + + m_cliProps->setProperty("compressionLevelSwitch", QStringLiteral("-m$CompressionLevel")); + m_cliProps->setProperty("compressionMethodSwitch", QHash{{QStringLiteral("application/vnd.rar"), QStringLiteral("-ma$CompressionMethod")}, + {QStringLiteral("application/x-rar"), QStringLiteral("-ma$CompressionMethod")}}); + m_cliProps->setProperty("multiVolumeSwitch", QStringLiteral("-v$VolumeSizek")); + + + m_cliProps->setProperty("passwordPromptPatterns", QStringList{QStringLiteral("Enter password \\(will not be echoed\\) for")}); + m_cliProps->setProperty("wrongPasswordPatterns", QStringList{QStringLiteral("password incorrect"), + QStringLiteral("wrong password")}); + m_cliProps->setProperty("testPassedPatterns", QStringList{QStringLiteral("^All OK$")}); + m_cliProps->setProperty("fileExistsPatterns", QStringList{QStringLiteral("^\\[Y\\]es, \\[N\\]o, \\[A\\]ll, n\\[E\\]ver, \\[R\\]ename, \\[Q\\]uit $")}); + m_cliProps->setProperty("fileExistsFileName", QStringList{QStringLiteral("^(.+) already exists. Overwrite it"), // unrar 3 & 4 + QStringLiteral("^Would you like to replace the existing file (.+)$")}); // unrar 5 + m_cliProps->setProperty("fileExistsInput", QStringList{QStringLiteral("Y"), //Overwrite + QStringLiteral("N"), //Skip + QStringLiteral("A"), //Overwrite all + QStringLiteral("E"), //Autoskip + QStringLiteral("Q")}); //Cancel + m_cliProps->setProperty("extractionFailedPatterns", QStringList{QStringLiteral("CRC failed"), + QStringLiteral("Cannot find volume")}); + m_cliProps->setProperty("corruptArchivePatterns", QStringList{QStringLiteral("Unexpected end of archive"), + QStringLiteral("the file header is corrupt")}); + m_cliProps->setProperty("diskFullPatterns", QStringList{QStringLiteral("No space left on device")}); + + // rar will sometimes create multi-volume archives where first volume is + // called name.part1.rar and other times name.part01.rar. + m_cliProps->setProperty("multiVolumeSuffix", QStringList{QStringLiteral("part01.$Suffix"), + QStringLiteral("part1.$Suffix")}); } bool CliPlugin::readListLine(const QString &line) { // Ignore number of lines corresponding to m_remainingIgnoreLines. if (m_remainingIgnoreLines > 0) { --m_remainingIgnoreLines; return true; } // Parse the title line, which contains the version of unrar. if (m_parseState == ParseStateTitle) { QRegularExpression rxVersionLine(QStringLiteral("^UNRAR (\\d+\\.\\d+)( beta \\d)? .*$")); QRegularExpressionMatch matchVersion = rxVersionLine.match(line); if (matchVersion.hasMatch()) { m_parseState = ParseStateComment; m_unrarVersion = matchVersion.captured(1); qCDebug(ARK) << "UNRAR version" << m_unrarVersion << "detected"; if (m_unrarVersion.toFloat() >= 5) { m_isUnrar5 = true; qCDebug(ARK) << "Using UNRAR 5 parser"; } else { qCDebug(ARK) << "Using UNRAR 4 parser"; } } else { // If the second line doesn't contain an UNRAR title, something // is wrong. qCCritical(ARK) << "Failed to detect UNRAR output."; return false; } // Or see what version of unrar we are dealing with and call specific // handler functions. } else if (m_isUnrar5) { return handleUnrar5Line(line); } else { return handleUnrar4Line(line); } return true; } bool CliPlugin::handleUnrar5Line(const QString &line) { // Parses the comment field. if (m_parseState == ParseStateComment) { // RegExp matching end of comment field. // FIXME: Comment itself could also contain the Archive path string here. QRegularExpression rxCommentEnd(QStringLiteral("^Archive: .+$")); if (rxCommentEnd.match(line).hasMatch()) { m_parseState = ParseStateHeader; m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; if (!m_comment.isEmpty()) { qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } return true; } // Parses the header, which is whatever is between the comment field // and the entries. else if (m_parseState == ParseStateHeader) { // "Details: " indicates end of header. if (line.startsWith(QStringLiteral("Details: "))) { ignoreLines(1, ParseStateEntryDetails); if (line.contains(QLatin1String("volume"))) { m_numberOfVolumes++; if (!isMultiVolume()) { setMultiVolume(true); qCDebug(ARK) << "Multi-volume archive detected"; } } if (line.contains(QLatin1String("solid")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } if (line.contains(QLatin1String("RAR 4"))) { emit compressionMethodFound(QStringList{QStringLiteral("RAR4")}); } else if (line.contains(QLatin1String("RAR 5"))) { emit compressionMethodFound(QStringList{QStringLiteral("RAR5")}); } } return true; } // Parses the entry details for each entry. else if (m_parseState == ParseStateEntryDetails) { // For multi-volume archives there is a header between the entries in // each volume. if (line.startsWith(QLatin1String("Archive: "))) { m_parseState = ParseStateHeader; return true; // Empty line indicates end of entry. } else if (line.trimmed().isEmpty() && !m_unrar5Details.isEmpty()) { handleUnrar5Entry(); } else { // All detail lines should contain a colon. if (!line.contains(QLatin1Char(':'))) { qCWarning(ARK) << "Unrecognized line:" << line; return true; } // The details are on separate lines, so we store them in the QHash // m_unrar5Details. m_unrar5Details.insert(line.section(QLatin1Char(':'), 0, 0).trimmed().toLower(), line.section(QLatin1Char(':'), 1).trimmed()); } return true; } return true; } void CliPlugin::handleUnrar5Entry() { Archive::Entry *e = new Archive::Entry(this); QString compressionRatio = m_unrar5Details.value(QStringLiteral("ratio")); compressionRatio.chop(1); // Remove the '%' e->setProperty("ratio", compressionRatio); QString time = m_unrar5Details.value(QStringLiteral("mtime")); QDateTime ts = QDateTime::fromString(time, QStringLiteral("yyyy-MM-dd HH:mm:ss,zzz")); e->setProperty("timestamp", ts); bool isDirectory = (m_unrar5Details.value(QStringLiteral("type")) == QLatin1String("Directory")); e->setProperty("isDirectory", isDirectory); if (isDirectory && !m_unrar5Details.value(QStringLiteral("name")).endsWith(QLatin1Char('/'))) { m_unrar5Details[QStringLiteral("name")] += QLatin1Char('/'); } QString compression = m_unrar5Details.value(QStringLiteral("compression")); int optionPos = compression.indexOf(QLatin1Char('-')); if (optionPos != -1) { e->setProperty("method", compression.mid(optionPos)); e->setProperty("version", compression.left(optionPos).trimmed()); } else { // No method specified. e->setProperty("method", QStringLiteral("")); e->setProperty("version", compression); } m_isPasswordProtected = m_unrar5Details.value(QStringLiteral("flags")).contains(QStringLiteral("encrypted")); e->setProperty("isPasswordProtected", m_isPasswordProtected); e->setProperty("fullPath", m_unrar5Details.value(QStringLiteral("name"))); e->setProperty("size", m_unrar5Details.value(QStringLiteral("size"))); e->setProperty("compressedSize", m_unrar5Details.value(QStringLiteral("packed size"))); e->setProperty("permissions", m_unrar5Details.value(QStringLiteral("attributes"))); e->setProperty("CRC", m_unrar5Details.value(QStringLiteral("crc32"))); if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { e->setProperty("link", m_unrar5Details.value(QStringLiteral("target"))); } m_unrar5Details.clear(); emit entry(e); } bool CliPlugin::handleUnrar4Line(const QString &line) { // Parses the comment field. if (m_parseState == ParseStateComment) { // RegExp matching end of comment field. // FIXME: Comment itself could also contain the Archive path string here. QRegularExpression rxCommentEnd(QStringLiteral("^(Solid archive|Archive|Volume) .+$")); // unrar 4 outputs the following string when opening v5 RAR archives. if (line == QLatin1String("Unsupported archive format. Please update RAR to a newer version.")) { emit error(i18n("Your unrar executable is version %1, which is too old to handle this archive. Please update to a more recent version.", m_unrarVersion)); return false; } // unrar 3 reports a non-RAR archive when opening v5 RAR archives. if (line.endsWith(QLatin1String(" is not RAR archive"))) { emit error(i18n("Unrar reported a non-RAR archive. The installed unrar version (%1) is old. Try updating your unrar.", m_unrarVersion)); return false; } // If we reach this point, then we can be sure that it's not a RAR5 // archive, so assume RAR4. emit compressionMethodFound(QStringList{QStringLiteral("RAR4")}); if (rxCommentEnd.match(line).hasMatch()) { if (line.startsWith(QLatin1String("Volume "))) { m_numberOfVolumes++; if (!isMultiVolume()) { setMultiVolume(true); qCDebug(ARK) << "Multi-volume archive detected"; } } if (line.startsWith(QLatin1String("Solid archive")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } m_parseState = ParseStateHeader; m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; if (!m_comment.isEmpty()) { qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } return true; } // Parses the header, which is whatever is between the comment field // and the entries. else if (m_parseState == ParseStateHeader) { // Horizontal line indicates end of header. if (line.startsWith(QStringLiteral("--------------------"))) { m_parseState = ParseStateEntryFileName; } else if (line.startsWith(QLatin1String("Volume "))) { m_numberOfVolumes++; } return true; } // Parses the entry name, which is on the first line of each entry. else if (m_parseState == ParseStateEntryFileName) { // Ignore empty lines. if (line.trimmed().isEmpty()) { return true; } // Three types of subHeaders can be displayed for unrar 3 and 4. // STM has 4 lines, RR has 3, and CMT has lines corresponding to // length of comment field +3. We ignore the subheaders. QRegularExpression rxSubHeader(QStringLiteral("^Data header type: (CMT|STM|RR)$")); QRegularExpressionMatch matchSubHeader = rxSubHeader.match(line); if (matchSubHeader.hasMatch()) { qCDebug(ARK) << "SubHeader of type" << matchSubHeader.captured(1) << "found"; if (matchSubHeader.captured(1) == QLatin1String("STM")) { ignoreLines(4, ParseStateEntryFileName); } else if (matchSubHeader.captured(1) == QLatin1String("CMT")) { ignoreLines(m_linesComment + 3, ParseStateEntryFileName); } else if (matchSubHeader.captured(1) == QLatin1String("RR")) { ignoreLines(3, ParseStateEntryFileName); } return true; } // The entries list ends with a horizontal line, followed by a // single summary line or, for multi-volume archives, another header. if (line.startsWith(QStringLiteral("-----------------"))) { m_parseState = ParseStateHeader; return true; // Encrypted files are marked with an asterisk. } else if (line.startsWith(QLatin1Char('*'))) { m_isPasswordProtected = true; m_unrar4Details.append(QString(line.trimmed()).remove(0, 1)); //Remove the asterisk // Entry names always start at the second position, so a line not // starting with a space is not an entry name. } else if (!line.startsWith(QLatin1Char(' '))) { qCWarning(ARK) << "Unrecognized line:" << line; return true; // If we reach this, then we can assume the line is an entry name, so // save it, and move on to the rest of the entry details. } else { m_unrar4Details.append(line.trimmed()); } m_parseState = ParseStateEntryDetails; return true; } // Parses the remainder of the entry details for each entry. else if (m_parseState == ParseStateEntryDetails) { // If the line following an entry name is empty, we did something // wrong. Q_ASSERT(!line.trimmed().isEmpty()); // If we reach a horizontal line, then the previous line was not an // entry name, so go back to header. if (line.startsWith(QStringLiteral("-----------------"))) { m_parseState = ParseStateHeader; return true; } // In unrar 3 and 4 the details are on a single line, so we // pass a QStringList containing the details. We need to store // it due to symlinks (see below). m_unrar4Details.append(line.split(QLatin1Char(' '), QString::SkipEmptyParts)); // The details line contains 9 fields, so m_unrar4Details // should now contain 9 + the filename = 10 strings. If not, this is // not an archive entry. if (m_unrar4Details.size() != 10) { m_parseState = ParseStateHeader; return true; } // When unrar 3 and 4 list a symlink, they output an extra line // containing the link target. The extra line is output after // the line we ignore, so we first need to ignore one line. if (m_unrar4Details.at(6).startsWith(QLatin1Char('l'))) { ignoreLines(1, ParseStateLinkTarget); return true; } else { handleUnrar4Entry(); } // Unrar 3 & 4 show a third line for each entry, which contains // three details: Host OS, Solid, and Old. We can ignore this // line. ignoreLines(1, ParseStateEntryFileName); return true; } // Parses a symlink target. else if (m_parseState == ParseStateLinkTarget) { m_unrar4Details.append(QString(line).remove(QStringLiteral("-->")).trimmed()); handleUnrar4Entry(); m_parseState = ParseStateEntryFileName; return true; } return true; } void CliPlugin::handleUnrar4Entry() { Archive::Entry *e = new Archive::Entry(this); QDateTime ts = QDateTime::fromString(QString(m_unrar4Details.at(4) + QLatin1Char(' ') + m_unrar4Details.at(5)), QStringLiteral("dd-MM-yy hh:mm")); // Unrar 3 & 4 output dates with a 2-digit year but QDateTime takes it as // 19??. Let's take 1950 as cut-off; similar to KDateTime. if (ts.date().year() < 1950) { ts = ts.addYears(100); } e->setProperty("timestamp", ts); bool isDirectory = ((m_unrar4Details.at(6).at(0) == QLatin1Char('d')) || (m_unrar4Details.at(6).at(1) == QLatin1Char('D'))); e->setProperty("isDirectory", isDirectory); if (isDirectory && !m_unrar4Details.at(0).endsWith(QLatin1Char('/'))) { m_unrar4Details[0] += QLatin1Char('/'); } // Unrar reports the ratio as ((compressed size * 100) / size); // we consider ratio as (100 * ((size - compressed size) / size)). // If the archive is a multivolume archive, a string indicating // whether the archive's position in the volume is displayed // instead of the compression ratio. QString compressionRatio = m_unrar4Details.at(3); if ((compressionRatio == QStringLiteral("<--")) || (compressionRatio == QStringLiteral("<->")) || (compressionRatio == QStringLiteral("-->"))) { compressionRatio = QLatin1Char('0'); } else { compressionRatio.chop(1); // Remove the '%' } e->setProperty("ratio", compressionRatio); // TODO: // - Permissions differ depending on the system the entry was added // to the archive. e->setProperty("fullPath", m_unrar4Details.at(0)); e->setProperty("size", m_unrar4Details.at(1)); e->setProperty("compressedSize", m_unrar4Details.at(2)); e->setProperty("permissions", m_unrar4Details.at(6)); e->setProperty("CRC", m_unrar4Details.at(7)); e->setProperty("method", m_unrar4Details.at(8)); e->setProperty("version", m_unrar4Details.at(9)); e->setProperty("isPasswordProtected", m_isPasswordProtected); if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { e->setProperty("link", m_unrar4Details.at(10)); } m_unrar4Details.clear(); emit entry(e); } void CliPlugin::ignoreLines(int lines, ParseState nextState) { m_remainingIgnoreLines = lines; m_parseState = nextState; } -QString CliPlugin::compressionMethodSwitch(const QString &method) const -{ - if (method.isEmpty()) { - return QString(); - } - - Q_ASSERT(m_param.contains(CompressionMethodSwitch)); - QString compMethodSwitch = m_param.value(CompressionMethodSwitch).toString(); - Q_ASSERT(!compMethodSwitch.isEmpty()); - compMethodSwitch.replace(QLatin1String("$CompressionMethod"), method); - - // This is needed for because the user-visible strings are different from the - // ones needed by the switch (e.g. RAR4 vs 4). - compMethodSwitch.remove(QLatin1String("RAR")); - - return compMethodSwitch; -} - #include "cliplugin.moc" diff --git a/plugins/clirarplugin/cliplugin.h b/plugins/clirarplugin/cliplugin.h index bb11f9a6..072d5078 100644 --- a/plugins/clirarplugin/cliplugin.h +++ b/plugins/clirarplugin/cliplugin.h @@ -1,73 +1,72 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2010 Raphael Kubo da Costa * Copyright (C) 2015-2016 Ragnar Thomsen * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "kerfuffle/cliinterface.h" class CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList &args); virtual ~CliPlugin(); virtual void resetParsing() Q_DECL_OVERRIDE; - virtual Kerfuffle::ParameterList parameterList() const Q_DECL_OVERRIDE; virtual bool readListLine(const QString &line) Q_DECL_OVERRIDE; - virtual QString compressionMethodSwitch(const QString &method) const Q_DECL_OVERRIDE; - private: enum ParseState { ParseStateTitle = 0, ParseStateComment, ParseStateHeader, ParseStateEntryFileName, ParseStateEntryDetails, ParseStateLinkTarget } m_parseState; + void setupCliProperties(); + bool handleUnrar5Line(const QString &line); void handleUnrar5Entry(); bool handleUnrar4Line(const QString &line); void handleUnrar4Entry(); void ignoreLines(int lines, ParseState nextState); QStringList m_unrar4Details; QHash m_unrar5Details; QString m_unrarVersion; bool m_isUnrar5; bool m_isPasswordProtected; bool m_isSolid; int m_remainingIgnoreLines; int m_linesComment; }; #endif // CLIPLUGIN_H diff --git a/plugins/clirarplugin/kerfuffle_clirar.json.cmake b/plugins/clirarplugin/kerfuffle_clirar.json.cmake index a0b7e6ff..18f9f3b8 100644 --- a/plugins/clirarplugin/kerfuffle_clirar.json.cmake +++ b/plugins/clirarplugin/kerfuffle_clirar.json.cmake @@ -1,78 +1,78 @@ { "KPlugin": { "Id": "kerfuffle_clirar", "MimeTypes": [ "@SUPPORTED_MIMETYPES@" ], "Name": "RAR archive plugin", "Name[ca@valencia]": "Connector per arxius RAR", "Name[ca]": "Connector per arxius RAR", "Name[cs]": "Modul pro archiv RAR", "Name[de]": "RAR-Archiv-Modul", "Name[es]": "Complemento de archivo RAR", "Name[et]": "RAR-arhiivi plugin", "Name[fi]": "RAR-pakkaustuki", - "Name[fr]": "Module externe d'archive « RAR »", + "Name[fr]": "Module externe d'archive « RAR »", "Name[gl]": "Complemento de arquivo RAR", "Name[he]": "תוסף ארכיוני RAR", "Name[it]": "Estensione per archivi RAR", "Name[ja]": "RAR アーカイブ用プラグイン", "Name[nb]": "Programtillegg for RAR-arkiv", "Name[nl]": "RAR-archiefplug-in", "Name[nn]": "RAR-arkivtillegg", "Name[pl]": "Wtyczka archiwów RAR", "Name[pt]": "'Plugin' de pacotes RAR", "Name[pt_BR]": "Plugin de arquivos RAR", "Name[ru]": "Поддержка архивов RAR", "Name[sk]": "Modul RAR archívu", "Name[sl]": "Vstavek za arhive RAR", "Name[sr@ijekavian]": "Прикључак РАР архива", "Name[sr@ijekavianlatin]": "Priključak RAR arhiva", "Name[sr@latin]": "Priključak RAR arhiva", "Name[sr]": "Прикључак РАР архива", "Name[sv]": "Insticksprogram för RAR-arkiv", "Name[uk]": "Додаток для архівів RAR", "Name[x-test]": "xxRAR archive pluginxx", "Name[zh_CN]": "RAR 归档插件", "ServiceTypes": [ "Kerfuffle/Plugin" ], "Version": "@KDE_APPLICATIONS_VERSION@" }, "X-KDE-Kerfuffle-ReadOnlyExecutables": [ "unrar" ], "X-KDE-Kerfuffle-ReadWrite": true, "X-KDE-Kerfuffle-ReadWriteExecutables": [ "rar" ], "X-KDE-Priority": 120, "application/vnd.rar": { "CompressionLevelDefault": 3, "CompressionLevelMax": 5, "CompressionLevelMin": 0, "CompressionMethodDefault": "RAR4", - "CompressionMethods": [ - "RAR4", - "RAR5" - ], + "CompressionMethods": { + "RAR4" : "4", + "RAR5" : "5" + }, "HeaderEncryption": true, "SupportsMultiVolume": true, "SupportsTesting": true, "SupportsWriteComment": true }, "application/x-rar": { "CompressionLevelDefault": 3, "CompressionLevelMax": 5, "CompressionLevelMin": 0, "CompressionMethodDefault": "RAR4", - "CompressionMethods": [ - "RAR4", - "RAR5" - ], + "CompressionMethods": { + "RAR4" : "4", + "RAR5" : "5" + }, "HeaderEncryption": true, "SupportsMultiVolume": true, "SupportsTesting": true, "SupportsWriteComment": true } -} \ No newline at end of file +} diff --git a/plugins/cliunarchiverplugin/cliplugin.cpp b/plugins/cliunarchiverplugin/cliplugin.cpp index 7451255b..bdcc2a9f 100644 --- a/plugins/cliunarchiverplugin/cliplugin.cpp +++ b/plugins/cliunarchiverplugin/cliplugin.cpp @@ -1,277 +1,257 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2011 Luke Shumaker * Copyright (C) 2016 Elvis Angelaccio * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "queries.h" #include #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_cliunarchiver.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList &args) : CliInterface(parent, args) { qCDebug(ARK) << "Loaded cli_unarchiver plugin"; + setupCliProperties(); } CliPlugin::~CliPlugin() { } bool CliPlugin::list() { resetParsing(); - cacheParameterList(); m_operationMode = List; - const auto args = substituteListVariables(m_param.value(ListArgs).toStringList(), password()); - return runProcess(m_param.value(ListProgram).toStringList(), args); + return runProcess(m_cliProps->property("listProgram").toString(), m_cliProps->listArgs(filename(), password())); } bool CliPlugin::extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) { ExtractionOptions newOptions = options; // unar has the following limitations: // 1. creates an empty file upon entering a wrong password. // 2. detects that the stdout has been redirected and blocks the stdin. // This prevents Ark from executing unar's overwrite queries. // To prevent both, we always extract to a temporary directory // and then we move the files to the intended destination. qCDebug(ARK) << "Enabling extraction to temporary directory."; newOptions.setAlwaysUseTempDir(true); return CliInterface::extractFiles(files, destinationDirectory, newOptions); } void CliPlugin::resetParsing() { m_jsonOutput.clear(); m_numberOfVolumes = 0; } -ParameterList CliPlugin::parameterList() const +void CliPlugin::setupCliProperties() { - static ParameterList p; - if (p.isEmpty()) { + m_cliProps->setProperty("captureProgress", false); - ///////////////[ COMMON ]///////////// + m_cliProps->setProperty("extractProgram", QStringLiteral("unar")); + m_cliProps->setProperty("extractSwitch", QStringList{QStringLiteral("-D")}); + m_cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("-D")}); - p[CaptureProgress] = false; - // Displayed when running lsar -json with header-encrypted archives. - p[PasswordPromptPattern] = QStringLiteral("This archive requires a password to unpack. Use the -p option to provide one."); + m_cliProps->setProperty("listProgram", QStringLiteral("lsar")); + m_cliProps->setProperty("listSwitch", QStringList{QStringLiteral("-json")}); - ///////////////[ LIST ]///////////// + m_cliProps->setProperty("passwordSwitch", QStringList{QStringLiteral("-password"), + QStringLiteral("$Password")}); - p[ListProgram] = QStringLiteral("lsar"); - p[ListArgs] = QStringList() << QStringLiteral("-json") << QStringLiteral("$Archive") << QStringLiteral("$PasswordSwitch"); + m_cliProps->setProperty("passwordPromptPatterns", QStringList{QStringLiteral("This archive requires a password to unpack. Use the -p option to provide one.")}); - ///////////////[ EXTRACT ]///////////// - - p[ExtractProgram] = QStringLiteral("unar"); - p[ExtractArgs] = QStringList() << QStringLiteral("-D") << QStringLiteral("$Archive") << QStringLiteral("$Files") << QStringLiteral("$PasswordSwitch"); - p[NoTrailingSlashes] = true; - p[PasswordSwitch] = QStringList() << QStringLiteral("-password") << QStringLiteral("$Password"); - - ///////////////[ ERRORS ]///////////// - - p[ExtractionFailedPatterns] = QStringList() - << QStringLiteral("Failed! \\((.+)\\)$") - << QStringLiteral("Segmentation fault$"); - } - return p; + m_cliProps->setProperty("extractionFailedPatterns", QStringList{QStringLiteral("Failed! \\((.+)\\)$"), + QStringLiteral("Segmentation fault$")}); } bool CliPlugin::readListLine(const QString &line) { Q_UNUSED(line) return true; } void CliPlugin::setJsonOutput(const QString &jsonOutput) { m_jsonOutput = jsonOutput; readJsonOutput(); } void CliPlugin::readStdout(bool handleAll) { if (!handleAll) { CliInterface::readStdout(false); return; } // We are ready to read the json output. readJsonOutput(); } -void CliPlugin::cacheParameterList() -{ - m_param = parameterList(); - Q_ASSERT(m_param.contains(ExtractProgram)); - Q_ASSERT(m_param.contains(ListProgram)); -} - bool CliPlugin::handleLine(const QString& line) { // Collect the json output line by line. if (m_operationMode == List) { m_jsonOutput += line + QLatin1Char('\n'); } // TODO: is this check really needed? if (m_operationMode == Copy) { - if (checkForErrorMessage(line, ExtractionFailedPatterns)) { + if (m_cliProps->isExtractionFailedMsg(line)) { qCWarning(ARK) << "Error in extraction:" << line; emit error(i18n("Extraction failed because of an unexpected error.")); return false; } } if (m_operationMode == List) { // This can only be an header-encrypted archive. - if (checkForPasswordPromptMessage(line)) { + if (m_cliProps->isPasswordPrompt(line)) { qCDebug(ARK) << "Detected header-encrypted RAR archive"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); // Process is gone, so we emit finished() manually and we return true. emit finished(false); return true; } setPassword(query.password()); CliPlugin::list(); } } return true; } void CliPlugin::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { qCDebug(ARK) << "Process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { //handle all the remaining data in the process readStdout(true); delete m_process; m_process = Q_NULLPTR; } // #193908 - #222392 // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (!password().isEmpty()) { // lsar -json exits with error code 1 if the archive is header-encrypted and the password is wrong. if (exitCode == 1) { qCWarning(ARK) << "Wrong password, list() aborted"; emit error(i18n("Wrong password.")); emit finished(false); setPassword(QString()); return; } } // lsar -json exits with error code 2 if the archive is header-encrypted and no password is given as argument. // At this point we are asking a password to the user and we are going to list() again after we get one. // This means that we cannot emit finished here. if (exitCode == 2) { return; } emit finished(true); } void CliPlugin::readJsonOutput() { QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(m_jsonOutput.toUtf8(), &error); if (error.error != QJsonParseError::NoError) { qCDebug(ARK) << "Could not parse json output:" << error.errorString(); return; } const QJsonObject json = jsonDoc.object(); const QJsonObject properties = json.value(QStringLiteral("lsarProperties")).toObject(); const QJsonArray volumes = properties.value(QStringLiteral("XADVolumes")).toArray(); if (volumes.count() > 1) { qCDebug(ARK) << "Detected multivolume archive"; m_numberOfVolumes = volumes.count(); setMultiVolume(true); } QString formatName = json.value(QStringLiteral("lsarFormatName")).toString(); if (formatName == QLatin1String("RAR")) { emit compressionMethodFound(QStringList{QStringLiteral("RAR4")}); } else if (formatName == QLatin1String("RAR 5")) { emit compressionMethodFound(QStringList{QStringLiteral("RAR5")}); } const QJsonArray entries = json.value(QStringLiteral("lsarContents")).toArray(); foreach (const QJsonValue& value, entries) { const QJsonObject currentEntryJson = value.toObject(); Archive::Entry *currentEntry = new Archive::Entry(this); QString filename = currentEntryJson.value(QStringLiteral("XADFileName")).toString(); currentEntry->setProperty("isDirectory", !currentEntryJson.value(QStringLiteral("XADIsDirectory")).isUndefined()); if (currentEntry->isDir()) { filename += QLatin1Char('/'); } currentEntry->setProperty("fullPath", filename); // FIXME: archives created from OSX (i.e. with the __MACOSX folder) list each entry twice, the 2nd time with size 0 currentEntry->setProperty("size", currentEntryJson.value(QStringLiteral("XADFileSize"))); currentEntry->setProperty("compressedSize", currentEntryJson.value(QStringLiteral("XADCompressedSize"))); currentEntry->setProperty("timestamp", currentEntryJson.value(QStringLiteral("XADLastModificationDate")).toVariant()); currentEntry->setProperty("size", currentEntryJson.value(QStringLiteral("XADFileSize"))); currentEntry->setProperty("isPasswordProtected", (currentEntryJson.value(QStringLiteral("XADIsEncrypted")).toInt() == 1)); // TODO: missing fields emit entry(currentEntry); } } #include "cliplugin.moc" diff --git a/plugins/cliunarchiverplugin/cliplugin.h b/plugins/cliunarchiverplugin/cliplugin.h index e703d8b1..5e8c8f87 100644 --- a/plugins/cliunarchiverplugin/cliplugin.h +++ b/plugins/cliunarchiverplugin/cliplugin.h @@ -1,66 +1,64 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2011 Luke Shumaker * Copyright (C) 2016 Elvis Angelaccio * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "kerfuffle/archiveentry.h" #include "kerfuffle/cliinterface.h" class CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList &args); virtual ~CliPlugin(); virtual bool list() Q_DECL_OVERRIDE; virtual bool extractFiles(const QVector &files, const QString &destinationDirectory, const Kerfuffle::ExtractionOptions &options) Q_DECL_OVERRIDE; virtual void resetParsing() Q_DECL_OVERRIDE; - virtual Kerfuffle::ParameterList parameterList() const Q_DECL_OVERRIDE; virtual bool readListLine(const QString &line) Q_DECL_OVERRIDE; /** * Fill the lsar's json output all in once (useful for unit testing). */ void setJsonOutput(const QString &jsonOutput); protected slots: void readStdout(bool handleAll = false) Q_DECL_OVERRIDE; protected: - void cacheParameterList() Q_DECL_OVERRIDE; bool handleLine(const QString& line) Q_DECL_OVERRIDE; private slots: void processFinished(int exitCode, QProcess::ExitStatus exitStatus) Q_DECL_OVERRIDE; private: - + void setupCliProperties(); void readJsonOutput(); QString m_jsonOutput; }; #endif // CLIPLUGIN_H diff --git a/plugins/cliunarchiverplugin/kerfuffle_cliunarchiver.json.cmake b/plugins/cliunarchiverplugin/kerfuffle_cliunarchiver.json.cmake index 5f227715..7d09bc8f 100644 --- a/plugins/cliunarchiverplugin/kerfuffle_cliunarchiver.json.cmake +++ b/plugins/cliunarchiverplugin/kerfuffle_cliunarchiver.json.cmake @@ -1,45 +1,51 @@ { "KPlugin": { "Id": "kerfuffle_cliunarchiver", "MimeTypes": [ "@SUPPORTED_MIMETYPES@" ], "Name": "The Unarchiver plugin", "Name[ca@valencia]": "Connector de l'Unarchiver", "Name[ca]": "Connector de l'Unarchiver", "Name[cs]": "Modul pro Unarchiver", "Name[de]": "Unarchiver-Modul", "Name[es]": "El complemento de Unarchiver", "Name[et]": "Unarchiveri plugin", "Name[fi]": "Unarchiver-tuki", "Name[fr]": "Module externe d'extraction d'archive", "Name[gl]": "Complemento de desarquivar", "Name[he]": "תוסף חילוץ אכיונים", "Name[it]": "Estensione The Unarchiver", "Name[nl]": "De plug-in voor uit archief halen", "Name[nn]": "Unarchiver-tillegget", "Name[pl]": "Wtyczka wypakowywacza", "Name[pt]": "O 'plugin' do Unarchiver", "Name[pt_BR]": "Plugin Unarchiver", "Name[sk]": "Plugin Unarchiver", "Name[sl]": "Vstavek Unarchiver", "Name[sr@ijekavian]": "Прикључак Унархивера", "Name[sr@ijekavianlatin]": "Priključak Unarchivera", "Name[sr@latin]": "Priključak Unarchivera", "Name[sr]": "Прикључак Унархивера", "Name[sv]": "Insticksprogram för Unarchiver", "Name[uk]": "Додаток Unarchiver", "Name[x-test]": "xxThe Unarchiver pluginxx", "Name[zh_CN]": "Unarchiver 插件", "ServiceTypes": [ "Kerfuffle/Plugin" ], "Version": "@KDE_APPLICATIONS_VERSION@" }, "X-KDE-Kerfuffle-ReadOnlyExecutables": [ "lsar", "unar" ], "X-KDE-Kerfuffle-ReadWrite": false, - "X-KDE-Priority": 100 -} \ No newline at end of file + "X-KDE-Priority": 100, + "application/vnd.rar": { + "HeaderEncryption": true + }, + "application/x-rar": { + "HeaderEncryption": true + } +} diff --git a/plugins/clizipplugin/cliplugin.cpp b/plugins/clizipplugin/cliplugin.cpp index 91585100..1a8b9bc6 100644 --- a/plugins/clizipplugin/cliplugin.cpp +++ b/plugins/clizipplugin/cliplugin.cpp @@ -1,328 +1,302 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/archiveentry.h" #include "kerfuffle/cliinterface.h" #include "kerfuffle/kerfuffle_export.h" #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_clizip.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList & args) : CliInterface(parent, args) , m_parseState(ParseStateHeader) , m_linesComment(0) { qCDebug(ARK) << "Loaded cli_zip plugin"; + setupCliProperties(); } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateHeader; m_tempComment.clear(); m_comment.clear(); } // #208091: infozip applies special meanings to some characters, so we // need to escape them with backslashes.see match.c in // infozip's source code QString CliPlugin::escapeFileName(const QString &fileName) const { const QString escapedCharacters(QStringLiteral("[]*?^-\\!")); QString quoted; const int len = fileName.length(); const QLatin1Char backslash('\\'); quoted.reserve(len * 2); for (int i = 0; i < len; ++i) { if (escapedCharacters.contains(fileName.at(i))) { quoted.append(backslash); } quoted.append(fileName.at(i)); } return quoted; } -ParameterList CliPlugin::parameterList() const +void CliPlugin::setupCliProperties() { - static ParameterList p; - - if (p.isEmpty()) { - p[CaptureProgress] = false; - p[ListProgram] = QStringList() << QStringLiteral("zipinfo"); - p[ExtractProgram] = p[TestProgram] = QStringList() << QStringLiteral("unzip"); - p[DeleteProgram] = p[AddProgram] = QStringList() << QStringLiteral("zip"); - - p[ListArgs] = QStringList() << QStringLiteral("-l") - << QStringLiteral("-T") - << QStringLiteral("-z") - << QStringLiteral("$Archive"); - p[ExtractArgs] = QStringList() << QStringLiteral("$PreservePathSwitch") - << QStringLiteral("$PasswordSwitch") - << QStringLiteral("$Archive") - << QStringLiteral("$Files"); - p[PreservePathSwitch] = QStringList() << QStringLiteral("") - << QStringLiteral("-j"); - p[PasswordSwitch] = QStringList() << QStringLiteral("-P$Password"); - p[CompressionLevelSwitch] = QStringLiteral("-$CompressionLevel"); - p[DeleteArgs] = QStringList() << QStringLiteral("-d") - << QStringLiteral("$Archive") - << QStringLiteral("$Files"); - - p[FileExistsExpression] = QStringList() - << QStringLiteral("^replace (.+)\\? \\[y\\]es, \\[n\\]o, \\[A\\]ll, \\[N\\]one, \\[r\\]ename: $"); - p[FileExistsFileName] = QStringList() << p[FileExistsExpression].toString(); - p[FileExistsInput] = QStringList() << QStringLiteral("y") //overwrite - << QStringLiteral("n") //skip - << QStringLiteral("A") //overwrite all - << QStringLiteral("N"); //autoskip - - p[AddArgs] = QStringList() << QStringLiteral("-r") - << QStringLiteral("$Archive") - << QStringLiteral("$PasswordSwitch") - << QStringLiteral("$CompressionLevelSwitch") - << QStringLiteral("$CompressionMethodSwitch") - << QStringLiteral("$Files"); - - p[PasswordPromptPattern] = QStringLiteral(" password: "); - p[WrongPasswordPatterns] = QStringList() << QStringLiteral("incorrect password"); - p[ExtractionFailedPatterns] = QStringList() << QStringLiteral("unsupported compression method"); - p[CorruptArchivePatterns] = QStringList() << QStringLiteral("End-of-central-directory signature not found"); - p[DiskFullPatterns] = QStringList() << QStringLiteral("write error \\(disk full\\?\\)") - << QStringLiteral("No space left on device"); - p[TestArgs] = QStringList() << QStringLiteral("-t") - << QStringLiteral("$Archive") - << QStringLiteral("$PasswordSwitch"); - p[TestPassedPattern] = QStringLiteral("^No errors detected in compressed data of "); - p[CompressionMethodSwitch] = QStringLiteral("-Z$CompressionMethod"); - } - return p; + qCDebug(ARK) << "Setting up parameters..."; + + m_cliProps->setProperty("captureProgress", false); + + m_cliProps->setProperty("addProgram", QStringLiteral("zip")); + m_cliProps->setProperty("addSwitch", QStringList({QStringLiteral("-r")})); + + m_cliProps->setProperty("deleteProgram", QStringLiteral("zip")); + m_cliProps->setProperty("deleteSwitch", QStringLiteral("-d")); + + m_cliProps->setProperty("extractProgram", QStringLiteral("unzip")); + m_cliProps->setProperty("extractSwitchNoPreserve", QStringList{QStringLiteral("-j")}); + + m_cliProps->setProperty("listProgram", QStringLiteral("zipinfo")); + m_cliProps->setProperty("listSwitch", QStringList{QStringLiteral("-l"), + QStringLiteral("-T"), + QStringLiteral("-z")}); + + m_cliProps->setProperty("testProgram", QStringLiteral("unzip")); + m_cliProps->setProperty("testSwitch", QStringLiteral("-t")); + + m_cliProps->setProperty("passwordSwitch", QStringList{QStringLiteral("-P$Password")}); + + m_cliProps->setProperty("compressionLevelSwitch", QStringLiteral("-$CompressionLevel")); + m_cliProps->setProperty("compressionMethodSwitch", QHash{{QStringLiteral("application/zip"), QStringLiteral("-Z$CompressionMethod")}, + {QStringLiteral("application/x-java-archive"), QStringLiteral("-Z$CompressionMethod")}}); + m_cliProps->setProperty("multiVolumeSwitch", QStringLiteral("-v$VolumeSizek")); + + m_cliProps->setProperty("passwordPromptPatterns", QStringList{QStringLiteral(" password: ")}); + m_cliProps->setProperty("wrongPasswordPatterns", QStringList{QStringLiteral("incorrect password")}); + m_cliProps->setProperty("testPassedPatterns", QStringList{QStringLiteral("^No errors detected in compressed data of ")}); + m_cliProps->setProperty("fileExistsPatterns", QStringList{QStringLiteral("^replace (.+)\\? \\[y\\]es, \\[n\\]o, \\[A\\]ll, \\[N\\]one, \\[r\\]ename: $")}); + m_cliProps->setProperty("fileExistsFileName", QStringList{QStringLiteral("^replace (.+)\\? \\[y\\]es, \\[n\\]o, \\[A\\]ll, \\[N\\]one, \\[r\\]ename: $")}); + m_cliProps->setProperty("fileExistsInput", QStringList{QStringLiteral("y"), //Overwrite + QStringLiteral("n"), //Skip + QStringLiteral("A"), //Overwrite all + QStringLiteral("N")}); //Autoskip + m_cliProps->setProperty("extractionFailedPatterns", QStringList{QStringLiteral("unsupported compression method")}); + m_cliProps->setProperty("corruptArchivePatterns", QStringList{QStringLiteral("End-of-central-directory signature not found")}); + m_cliProps->setProperty("diskFullPatterns", QStringList{QStringLiteral("write error \\(disk full\\?\\)"), + QStringLiteral("No space left on device")}); } bool CliPlugin::readListLine(const QString &line) { static const QRegularExpression entryPattern(QStringLiteral( "^(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\d{8}).(\\d{6})\\s+(.+)$") ); // RegExp to identify the line preceding comments. const QRegularExpression commentPattern(QStringLiteral("^Archive: .*$")); // RegExp to identify the line following comments. const QRegularExpression commentEndPattern(QStringLiteral("^Zip file size: .*$")); switch (m_parseState) { case ParseStateHeader: if (commentPattern.match(line).hasMatch()) { m_parseState = ParseStateComment; } else if (commentEndPattern.match(line).hasMatch()){ m_parseState = ParseStateEntry; } break; case ParseStateComment: if (commentEndPattern.match(line).hasMatch()) { m_parseState = ParseStateEntry; if (!m_tempComment.trimmed().isEmpty()) { m_comment = m_tempComment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_tempComment.append(line + QLatin1Char('\n')); } case ParseStateEntry: QRegularExpressionMatch rxMatch = entryPattern.match(line); if (rxMatch.hasMatch()) { Archive::Entry *e = new Archive::Entry(this); e->setProperty("permissions", rxMatch.captured(1)); // #280354: infozip may not show the right attributes for a given directory, so an entry // ending with '/' is actually more reliable than 'd' bein in the attributes. e->setProperty("isDirectory", rxMatch.captured(10).endsWith(QLatin1Char('/'))); e->setProperty("size", rxMatch.captured(4)); QString status = rxMatch.captured(5); if (status[0].isUpper()) { e->setProperty("isPasswordProtected", true); } e->setProperty("compressedSize", rxMatch.captured(6).toInt()); e->setProperty("method", rxMatch.captured(7)); QString method = convertCompressionMethod(rxMatch.captured(7)); if (!m_compressionMethods.contains(method)) { m_compressionMethods.append(method); emit compressionMethodFound(m_compressionMethods); } const QDateTime ts(QDate::fromString(rxMatch.captured(8), QStringLiteral("yyyyMMdd")), QTime::fromString(rxMatch.captured(9), QStringLiteral("hhmmss"))); e->setProperty("timestamp", ts); e->setProperty("fullPath", rxMatch.captured(10)); emit entry(e); } break; } return true; } bool CliPlugin::moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { qCDebug(ARK) << "Moving" << files.count() << "file(s) to destination:" << destination; m_oldWorkingDir = QDir::currentPath(); m_tempExtractDir = new QTemporaryDir(); m_tempAddDir = new QTemporaryDir(); QDir::setCurrent(m_tempExtractDir->path()); m_passedFiles = files; m_passedDestination = destination; m_passedOptions = options; m_subOperation = Extract; connect(this, &CliPlugin::finished, this, &CliPlugin::continueMoving); return extractFiles(files, QDir::currentPath(), ExtractionOptions()); } int CliPlugin::moveRequiredSignals() const { return 4; } void CliPlugin::continueMoving(bool result) { if (!result) { finishMoving(false); return; } switch (m_subOperation) { case Extract: m_subOperation = Delete; if (!deleteFiles(m_passedFiles)) { finishMoving(false); } break; case Delete: m_subOperation = Add; if (!setMovingAddedFiles() || !addFiles(m_tempAddedFiles, m_passedDestination, m_passedOptions)) { finishMoving(false); } break; case Add: finishMoving(true); break; default: Q_ASSERT(false); } } bool CliPlugin::setMovingAddedFiles() { m_passedFiles = entriesWithoutChildren(m_passedFiles); // If there are more files being moved than 1, we have destination as a destination folder, // otherwise it's new entry full path. if (m_passedFiles.count() > 1) { return setAddedFiles(); } QDir::setCurrent(m_tempAddDir->path()); const Archive::Entry *file = m_passedFiles.at(0); const QString oldPath = m_tempExtractDir->path() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + m_passedDestination->name(); if (!QFile::rename(oldPath, newPath)) { return false; } m_tempAddedFiles << new Archive::Entry(Q_NULLPTR, m_passedDestination->name()); // We have to exclude file name from destination path in order to pass it to addFiles method. const QString destinationPath = m_passedDestination->fullPath(); if (!destinationPath.endsWith(QLatin1Char('/')) || destinationPath.count(QLatin1Char('/')) > 1) { int destinationLength = destinationPath.count(); bool iteratedChar = false; do { destinationLength--; if (destinationPath.at(destinationLength) != QLatin1Char('/')) { iteratedChar = true; } } while (destinationLength > 0 && !(iteratedChar && destinationPath.at(destinationLength) == QLatin1Char('/'))); m_passedDestination->setProperty("fullPath", destinationPath.left(destinationLength + 1)); } else { // ...unless the destination path is already a single folder, e.g. "dir/". // In this case we're going to add to the root, so we just need to set a null destination. m_passedDestination = Q_NULLPTR; } return true; } void CliPlugin::finishMoving(bool result) { disconnect(this, &CliPlugin::finished, this, &CliPlugin::continueMoving); emit progress(1.0); emit finished(result); cleanUp(); } -QString CliPlugin::compressionMethodSwitch(const QString &method) const -{ - if (method.isEmpty()) { - return QString(); - } - - Q_ASSERT(m_param.contains(CompressionMethodSwitch)); - QString compMethodSwitch = m_param.value(CompressionMethodSwitch).toString(); - Q_ASSERT(!compMethodSwitch.isEmpty()); - - // We use capitalization of methods in UI, but CLI requires lowercase. - compMethodSwitch.replace(QLatin1String("$CompressionMethod"), method.toLower()); - - return compMethodSwitch; -} - QString CliPlugin::convertCompressionMethod(const QString &method) { if (method == QLatin1String("stor")) { return QStringLiteral("Store"); } else if (method.startsWith(QLatin1String("def"))) { return QStringLiteral("Deflate"); } else if (method == QLatin1String("bzp2")) { return QStringLiteral("BZip2"); } return method; } #include "cliplugin.moc" diff --git a/plugins/clizipplugin/cliplugin.h b/plugins/clizipplugin/cliplugin.h index 4a789ee4..beca40e4 100644 --- a/plugins/clizipplugin/cliplugin.h +++ b/plugins/clizipplugin/cliplugin.h @@ -1,68 +1,66 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "kerfuffle/cliinterface.h" #include using namespace Kerfuffle; class KERFUFFLE_EXPORT CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList &args); virtual ~CliPlugin(); virtual void resetParsing() Q_DECL_OVERRIDE; virtual QString escapeFileName(const QString &fileName) const Q_DECL_OVERRIDE; - virtual Kerfuffle::ParameterList parameterList() const Q_DECL_OVERRIDE; virtual bool readListLine(const QString &line) Q_DECL_OVERRIDE; virtual bool moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual int moveRequiredSignals() const Q_DECL_OVERRIDE; - virtual QString compressionMethodSwitch(const QString &method) const Q_DECL_OVERRIDE; - private slots: void continueMoving(bool result); private: + void setupCliProperties(); bool setMovingAddedFiles(); void finishMoving(bool result); QString convertCompressionMethod(const QString &method); enum ParseState { ParseStateHeader = 0, ParseStateComment, ParseStateEntry } m_parseState; int m_linesComment; QString m_tempComment; QStringList m_compressionMethods; }; #endif // CLIPLUGIN_H diff --git a/plugins/clizipplugin/kerfuffle_clizip.json.cmake b/plugins/clizipplugin/kerfuffle_clizip.json.cmake index d4a852d1..0fce0a77 100644 --- a/plugins/clizipplugin/kerfuffle_clizip.json.cmake +++ b/plugins/clizipplugin/kerfuffle_clizip.json.cmake @@ -1,70 +1,70 @@ { "KPlugin": { "Id": "kerfuffle_clizip", "MimeTypes": [ "@SUPPORTED_MIMETYPES@" ], "Name": "ZIP archive plugin", "Name[ca@valencia]": "Connector per arxius ZIP", "Name[ca]": "Connector per arxius ZIP", "Name[cs]": "Modul pro archiv ZIP", "Name[de]": "ZIP-Archiv-Modul", "Name[es]": "Complemento de archivo ZIP", "Name[et]": "ZIP-arhiivi plugin", "Name[fi]": "ZIP-pakkaustuki", - "Name[fr]": "Module externe d'archive « zip »", + "Name[fr]": "Module externe d'archive « zip »", "Name[gl]": "Complemento de arquivo ZIP", "Name[he]": "תוסף ארכיוני ZIP", "Name[it]": "Estensione per archivi ZIP", "Name[nb]": "Programtillegg for ZIP-arkiv", "Name[nl]": "ZIP-archiefplug-in", "Name[nn]": "ZIP-arkivtillegg", "Name[pl]": "Wtyczka archiwów ZIP", "Name[pt]": "'Plugin' de pacotes ZIP", "Name[pt_BR]": "Plugin de arquivos ZIP", "Name[ru]": "Поддержка архивов ZIP", "Name[sk]": "Modul ZIP archívu", "Name[sl]": "Vstavek za arhive ZIP", "Name[sr@ijekavian]": "Прикључак ЗИП архива", "Name[sr@ijekavianlatin]": "Priključak ZIP arhiva", "Name[sr@latin]": "Priključak ZIP arhiva", "Name[sr]": "Прикључак ЗИП архива", "Name[sv]": "Insticksprogram för ZIP-arkiv", "Name[uk]": "Додаток для архівів ZIP", "Name[x-test]": "xxZIP archive pluginxx", "Name[zh_CN]": "ZIP 归档插件", "ServiceTypes": [ "Kerfuffle/Plugin" ], "Version": "@KDE_APPLICATIONS_VERSION@" }, "X-KDE-Kerfuffle-ReadOnlyExecutables": [ "zipinfo", "unzip" ], "X-KDE-Kerfuffle-ReadWrite": true, "X-KDE-Kerfuffle-ReadWriteExecutables": [ "zip" ], "X-KDE-Priority": 160, "application/x-java-archive": { "CompressionLevelDefault": 6, "CompressionLevelMax": 9, "CompressionLevelMin": 0, "Encryption": true, "SupportsTesting": true }, "application/zip": { "CompressionLevelDefault": 6, "CompressionLevelMax": 9, "CompressionLevelMin": 0, - "CompressionMethodDefault": "deflate", - "CompressionMethods": [ - "BZip2", - "Deflate", - "Store" - ], + "CompressionMethodDefault": "Deflate", + "CompressionMethods": { + "BZip2" : "bzip2", + "Deflate" : "deflate", + "Store" : "store" + }, "Encryption": true, "SupportsTesting": true } -} \ No newline at end of file +}