diff --git a/autotests/kerfuffle/data/archive-deepsinglehierarchy.json b/autotests/kerfuffle/data/archive-deepsinglehierarchy.json index d673c7c0..9aa8c0c1 100644 --- a/autotests/kerfuffle/data/archive-deepsinglehierarchy.json +++ b/autotests/kerfuffle/data/archive-deepsinglehierarchy.json @@ -1,20 +1,20 @@ [ { - "FileName": "aDir/", - "IsDirectory": true + "fileName": "aDir/", + "isDirectory": true }, { - "FileName": "aDir/b.txt" + "fileName": "aDir/b.txt" }, { - "FileName": "aDir/aDirInside/", - "IsDirectory": true + "fileName": "aDir/aDirInside/", + "isDirectory": true }, { - "FileName": "aDir/aDirInside/anotherDir/", - "IsDirectory": true + "fileName": "aDir/aDirInside/anotherDir/", + "isDirectory": true }, { - "FileName": "aDir/aDirInside/anotherDir/file.txt" + "fileName": "aDir/aDirInside/anotherDir/file.txt" } ] diff --git a/autotests/kerfuffle/data/archive-emptysinglefolder.json b/autotests/kerfuffle/data/archive-emptysinglefolder.json index 98f110b6..e6c95a20 100644 --- a/autotests/kerfuffle/data/archive-emptysinglefolder.json +++ b/autotests/kerfuffle/data/archive-emptysinglefolder.json @@ -1,6 +1,6 @@ [ { - "FileName": "aDir/", - "IsDirectory": true + "fileName": "aDir/", + "isDirectory": true } ] diff --git a/autotests/kerfuffle/data/archive-multiplefolders.json b/autotests/kerfuffle/data/archive-multiplefolders.json index 92e9224c..47b0caf0 100644 --- a/autotests/kerfuffle/data/archive-multiplefolders.json +++ b/autotests/kerfuffle/data/archive-multiplefolders.json @@ -1,16 +1,16 @@ [ { - "FileName": "aDir/", - "IsDirectory": true + "fileName": "aDir/", + "isDirectory": true }, { - "FileName": "aDir/b.txt" + "fileName": "aDir/b.txt" }, { - "FileName": "anotherDir/", - "IsDirectory": true + "fileName": "anotherDir/", + "isDirectory": true }, { - "FileName": "anotherDir/file.txt" + "fileName": "anotherDir/file.txt" } ] diff --git a/autotests/kerfuffle/data/archive-nodir-manyfiles.json b/autotests/kerfuffle/data/archive-nodir-manyfiles.json index 945c6168..9f89f946 100644 --- a/autotests/kerfuffle/data/archive-nodir-manyfiles.json +++ b/autotests/kerfuffle/data/archive-nodir-manyfiles.json @@ -1,8 +1,8 @@ [ { - "FileName": "a.txt" + "fileName": "a.txt" }, { - "FileName": "file.txt" + "fileName": "file.txt" } ] diff --git a/autotests/kerfuffle/data/archive-onetopfolder.json b/autotests/kerfuffle/data/archive-onetopfolder.json index ec1e2ef5..ca025e9f 100644 --- a/autotests/kerfuffle/data/archive-onetopfolder.json +++ b/autotests/kerfuffle/data/archive-onetopfolder.json @@ -1,9 +1,9 @@ [ { - "FileName": "aDir/", - "IsDirectory": true + "fileName": "aDir/", + "isDirectory": true }, { - "FileName": "aDir/b.txt" + "fileName": "aDir/b.txt" } ] diff --git a/autotests/kerfuffle/data/archive-password.json b/autotests/kerfuffle/data/archive-password.json index a6abd9bf..73fd1c1c 100644 --- a/autotests/kerfuffle/data/archive-password.json +++ b/autotests/kerfuffle/data/archive-password.json @@ -1,13 +1,13 @@ [ { - "FileName": "foo.txt", - "IsPasswordProtected": true + "fileName": "foo.txt", + "isPasswordProtected": true }, { - "FileName": "bar.txt" + "fileName": "bar.txt" }, { - "FileName": "aDirectory/", - "IsDirectory": true + "fileName": "aDirectory/", + "isDirectory": true } ] diff --git a/autotests/kerfuffle/data/archive-singlefile.json b/autotests/kerfuffle/data/archive-singlefile.json index 21d49f77..8573c439 100644 --- a/autotests/kerfuffle/data/archive-singlefile.json +++ b/autotests/kerfuffle/data/archive-singlefile.json @@ -1,5 +1,5 @@ [ { - "FileName": "a.txt" + "fileName": "a.txt" } ] \ No newline at end of file diff --git a/autotests/kerfuffle/data/archive-unorderedsinglefolder.json b/autotests/kerfuffle/data/archive-unorderedsinglefolder.json index 28db1cff..60ce54dd 100644 --- a/autotests/kerfuffle/data/archive-unorderedsinglefolder.json +++ b/autotests/kerfuffle/data/archive-unorderedsinglefolder.json @@ -1,16 +1,16 @@ [ { - "FileName": "aDir/anotherDir/bar.txt" + "fileName": "aDir/anotherDir/bar.txt" }, { - "FileName": "aDir/foo.txt" + "fileName": "aDir/foo.txt" }, { - "FileName": "aDir/anotherDir/", - "IsDirectory": true + "fileName": "aDir/anotherDir/", + "isDirectory": true }, { - "FileName": "aDir/", - "IsDirectory": true + "fileName": "aDir/", + "isDirectory": true } ] diff --git a/autotests/kerfuffle/data/archive001.json b/autotests/kerfuffle/data/archive001.json index 01a78601..3efc5583 100644 --- a/autotests/kerfuffle/data/archive001.json +++ b/autotests/kerfuffle/data/archive001.json @@ -1,15 +1,15 @@ [ { - "FileName": "a.txt" + "fileName": "a.txt" }, { - "FileName": "aDir/", - "IsDirectory": true + "fileName": "aDir/", + "isDirectory": true }, { - "FileName": "aDir/b.txt" + "fileName": "aDir/b.txt" }, { - "FileName": "c.txt" + "fileName": "c.txt" } ] diff --git a/autotests/kerfuffle/data/archive002.json b/autotests/kerfuffle/data/archive002.json index 5aa50b69..c4019196 100644 --- a/autotests/kerfuffle/data/archive002.json +++ b/autotests/kerfuffle/data/archive002.json @@ -1,18 +1,18 @@ [ { - "FileName": "a.txt", - "Size": 5 + "fileName": "a.txt", + "size": 5 }, { - "FileName": "aDir/", - "IsDirectory": true + "fileName": "aDir/", + "isDirectory": true }, { - "FileName": "aDir/b.txt", - "Size": 954 + "fileName": "aDir/b.txt", + "size": 954 }, { - "FileName": "c.txt", - "Size": 45000 + "fileName": "c.txt", + "size": 45000 } ] diff --git a/autotests/kerfuffle/jobstest.cpp b/autotests/kerfuffle/jobstest.cpp index 8effddcd..d9dd0c93 100644 --- a/autotests/kerfuffle/jobstest.cpp +++ b/autotests/kerfuffle/jobstest.cpp @@ -1,367 +1,367 @@ /* * 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 using namespace Kerfuffle; class JobsTest : public QObject { Q_OBJECT public: JobsTest(); protected Q_SLOTS: void init(); - void slotNewEntry(const ArchiveEntry& entry); + void slotNewEntry(Archive::Entry *entry); private Q_SLOTS: // ListJob-related tests void testListJob_data(); void testListJob(); // 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); - QList listEntries(JSONArchiveInterface *iface); + QList listEntries(JSONArchiveInterface *iface); void startAndWaitForResult(KJob *job); - QList m_entries; + QList m_entries; QEventLoop m_eventLoop; }; QTEST_GUILESS_MAIN(JobsTest) JobsTest::JobsTest() : QObject(Q_NULLPTR) , m_eventLoop(this) { - qRegisterMetaType("ArchiveEntry"); } void JobsTest::init() { m_entries.clear(); } -void JobsTest::slotNewEntry(const ArchiveEntry& entry) +void JobsTest::slotNewEntry(Archive::Entry *entry) { m_entries.append(entry); } JSONArchiveInterface *JobsTest::createArchiveInterface(const QString& filePath) { JSONArchiveInterface *iface = new JSONArchiveInterface(this, {filePath}); if (!iface->open()) { qDebug() << "Could not open" << filePath; return Q_NULLPTR; } return iface; } -QList JobsTest::listEntries(JSONArchiveInterface *iface) +QList JobsTest::listEntries(JSONArchiveInterface *iface) { m_entries.clear(); ListJob *listJob = new ListJob(iface); connect(listJob, &Job::newEntry, this, &JobsTest::slotNewEntry); startAndWaitForResult(listJob); return m_entries; } void JobsTest::startAndWaitForResult(KJob *job) { connect(job, &KJob::result, &m_eventLoop, &QEventLoop::quit); job->start(); m_eventLoop.exec(); } void JobsTest::testListJob_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::testListJob() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); ListJob *listJob = new ListJob(iface); listJob->setAutoDelete(false); startAndWaitForResult(listJob); QFETCH(qlonglong, expectedExtractedFilesSize); QCOMPARE(listJob->extractedFilesSize(), expectedExtractedFilesSize); QFETCH(bool, isPasswordProtected); QCOMPARE(listJob->isPasswordProtected(), isPasswordProtected); QFETCH(bool, isSingleFolder); QCOMPARE(listJob->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)[FileName].toString(), expectedEntryNames.at(i)); + QCOMPARE(archiveEntries.at(i)->property("fileName").toString(), expectedEntryNames.at(i)); } listJob->deleteLater(); } void JobsTest::testExtractJobAccessors() { JSONArchiveInterface *iface = createArchiveInterface(QFINDTESTDATA("data/archive001.json")); ExtractJob *job = new ExtractJob(QVariantList(), QStringLiteral("/tmp/some-dir"), ExtractionOptions(), iface); ExtractionOptions defaultOptions; defaultOptions[QStringLiteral("PreservePaths")] = false; QCOMPARE(job->destinationDirectory(), QLatin1String("/tmp/some-dir")); QCOMPARE(job->extractionOptions(), defaultOptions); job->setAutoDelete(false); startAndWaitForResult(job); QCOMPARE(job->destinationDirectory(), QLatin1String("/tmp/some-dir")); QCOMPARE(job->extractionOptions(), defaultOptions); delete job; ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; options[QStringLiteral("foo")] = QLatin1String("bar"); options[QStringLiteral("pi")] = 3.14f; job = new ExtractJob(QVariantList(), QStringLiteral("/root"), options, iface); QCOMPARE(job->destinationDirectory(), QLatin1String("/root")); QCOMPARE(job->extractionOptions(), options); job->setAutoDelete(false); startAndWaitForResult(job); QCOMPARE(job->destinationDirectory(), QLatin1String("/root")); QCOMPARE(job->extractionOptions(), options); delete job; } void JobsTest::testTempExtractJob() { JSONArchiveInterface *iface = createArchiveInterface(QFINDTESTDATA("data/archive-malicious.json")); PreviewJob *job = new PreviewJob(QStringLiteral("anotherDir/../../file.txt"), false, iface); QVERIFY(job->validatedFilePath().endsWith(QLatin1String("anotherDir/file.txt"))); QVERIFY(job->extractionOptions()[QStringLiteral("PreservePaths")].toBool()); job->setAutoDelete(false); startAndWaitForResult(job); QVERIFY(job->validatedFilePath().endsWith(QLatin1String("anotherDir/file.txt"))); QVERIFY(job->extractionOptions()[QStringLiteral("PreservePaths")].toBool()); delete job; } void JobsTest::testRemoveEntries_data() { QTest::addColumn("jsonArchive"); QTest::addColumn("entries"); QTest::addColumn("entriesToDelete"); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} << QVariantList {QStringLiteral("c.txt")}; QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} << QVariantList {QStringLiteral("a.txt"), QStringLiteral("c.txt")}; // Error test: if we delete non-existent entries, the archive must not change. QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} << QVariantList {QStringLiteral("foo.txt")}; } void JobsTest::testRemoveEntries() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); QFETCH(QStringList, entries); QFETCH(QVariantList, entriesToDelete); QStringList expectedRemainingEntries; Q_FOREACH (const QString& entry, entries) { if (!entriesToDelete.contains(entry)) { 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)[FileName].toString(), expectedRemainingEntries.at(i)); + QCOMPARE(remainingEntries.at(i)->property("fileName").toString(), expectedRemainingEntries.at(i)); } iface->deleteLater(); } void JobsTest::testAddEntries_data() { QTest::addColumn("jsonArchive"); QTest::addColumn("originalEntries"); QTest::addColumn("entriesToAdd"); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} << QStringList {QStringLiteral("foo.txt")}; QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} << QStringList {QStringLiteral("foo.txt"), QStringLiteral("bar.txt")}; // Error test: if we add an already existent entry, the archive must not change. QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} << QStringList {QStringLiteral("c.txt")}; } void JobsTest::testAddEntries() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); QFETCH(QStringList, originalEntries); auto currentEntries = listEntries(iface); QCOMPARE(currentEntries.size(), originalEntries.size()); QFETCH(QStringList, entriesToAdd); AddJob *addJob = new AddJob(entriesToAdd, CompressionOptions(), iface); startAndWaitForResult(addJob); currentEntries = listEntries(iface); int expectedEntriesCount = originalEntries.size(); Q_FOREACH (const QString& entry, entriesToAdd) { if (!originalEntries.contains(entry)) { expectedEntriesCount++; } } QCOMPARE(currentEntries.size(), expectedEntriesCount); iface->deleteLater(); } #include "jobstest.moc" diff --git a/autotests/kerfuffle/jsonarchiveinterface.cpp b/autotests/kerfuffle/jsonarchiveinterface.cpp index c135dbb7..07b6d1ca 100644 --- a/autotests/kerfuffle/jsonarchiveinterface.cpp +++ b/autotests/kerfuffle/jsonarchiveinterface.cpp @@ -1,115 +1,116 @@ /* * Copyright (c) 2010-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "jsonarchiveinterface.h" #include +#include "kerfuffle/archiveentry.h" JSONArchiveInterface::JSONArchiveInterface(QObject *parent, const QVariantList& args) : Kerfuffle::ReadWriteArchiveInterface(parent, args) { } JSONArchiveInterface::~JSONArchiveInterface() { } bool JSONArchiveInterface::list() { JSONParser::JSONArchive::const_iterator it = m_archive.constBegin(); for (; it != m_archive.constEnd(); ++it) { emit entry(*it); } return true; } bool JSONArchiveInterface::open() { QFile file(filename()); if (!file.exists()) { return false; } if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return false; } m_archive = JSONParser::parse(&file); return !m_archive.isEmpty(); } bool JSONArchiveInterface::addFiles(const QStringList& files, const Kerfuffle::CompressionOptions& options) { Q_UNUSED(options) foreach (const QString& file, files) { if (m_archive.contains(file)) { return false; } - Kerfuffle::ArchiveEntry e; - e[Kerfuffle::FileName] = file; + Kerfuffle::Archive::Entry *e = new Kerfuffle::Archive::Entry(NULL); + e->setProperty("fileName", file); m_archive[file] = e; } return true; } bool JSONArchiveInterface::copyFiles(const QList& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) { Q_UNUSED(files) Q_UNUSED(destinationDirectory) Q_UNUSED(options) return true; } bool JSONArchiveInterface::deleteFiles(const QList& files) { foreach (const QVariant& file, files) { const QString fileName = file.toString(); if (m_archive.contains(fileName)) { m_archive.remove(fileName); emit entryRemoved(fileName); } } return true; } bool JSONArchiveInterface::addComment(const QString& comment) { Q_UNUSED(comment) return true; } bool JSONArchiveInterface::testArchive() { return true; } diff --git a/autotests/kerfuffle/jsonparser.cpp b/autotests/kerfuffle/jsonparser.cpp index 8719b115..f9e5751d 100644 --- a/autotests/kerfuffle/jsonparser.cpp +++ b/autotests/kerfuffle/jsonparser.cpp @@ -1,113 +1,83 @@ /* * 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. */ #include "jsonparser.h" #include "kerfuffle/archiveinterface.h" +#include "kerfuffle/archiveentry.h" #include #include #include -typedef QMap ArchiveProperties; - -static ArchiveProperties archiveProperties() -{ - static ArchiveProperties properties; - - if (!properties.isEmpty()) { - return properties; - } - - properties[QStringLiteral("FileName")] = Kerfuffle::FileName; - properties[QStringLiteral("InternalID")] = Kerfuffle::InternalID; - properties[QStringLiteral("Permissions")] = Kerfuffle::Permissions; - properties[QStringLiteral("Owner")] = Kerfuffle::Owner; - properties[QStringLiteral("Group")] = Kerfuffle::Group; - properties[QStringLiteral("Size")] = Kerfuffle::Size; - properties[QStringLiteral("CompressedSize")] = Kerfuffle::CompressedSize; - properties[QStringLiteral("Link")] = Kerfuffle::Link; - properties[QStringLiteral("Ratio")] = Kerfuffle::Ratio; - properties[QStringLiteral("CRC")] = Kerfuffle::CRC; - properties[QStringLiteral("Method")] = Kerfuffle::Method; - properties[QStringLiteral("Version")] = Kerfuffle::Version; - properties[QStringLiteral("Timestamp")] = Kerfuffle::Timestamp; - properties[QStringLiteral("IsDirectory")] = Kerfuffle::IsDirectory; - properties[QStringLiteral("Comment")] = Kerfuffle::Comment; - properties[QStringLiteral("IsPasswordProtected")] = Kerfuffle::IsPasswordProtected; - - return properties; -} - JSONParser::JSONParser() { } JSONParser::~JSONParser() { } JSONParser::JSONArchive JSONParser::parse(QIODevice *json) { QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(json->readAll(), &error); if (error.error != QJsonParseError::NoError) { qDebug() << "Parse error: " << error.errorString(); return JSONParser::JSONArchive(); } return createJSONArchive(jsonDoc.toVariant()); } JSONParser::JSONArchive JSONParser::createJSONArchive(const QVariant &json) { - static const ArchiveProperties properties = archiveProperties(); - JSONParser::JSONArchive archive; foreach (const QVariant &entry, json.toList()) { const QVariantMap entryMap = entry.toMap(); - if (!entryMap.contains(QStringLiteral("FileName"))) { + if (!entryMap.contains(QStringLiteral("fileName"))) { continue; } - Kerfuffle::ArchiveEntry archiveEntry; + Kerfuffle::Archive::Entry *e = new Kerfuffle::Archive::Entry(Q_NULLPTR); QVariantMap::const_iterator entryIterator = entryMap.constBegin(); for (; entryIterator != entryMap.constEnd(); ++entryIterator) { - if (properties.contains(entryIterator.key())) { - archiveEntry[properties[entryIterator.key()]] = entryIterator.value(); + const char *key = entryIterator.key().toStdString().c_str(); + if (e->property(key).isValid()) { + e->setProperty(key, entryIterator.value()); } else { qDebug() << entryIterator.key() << "is not a valid entry key"; } } - const QString fileName = entryMap[QStringLiteral("FileName")].toString(); - archive[fileName] = archiveEntry; + const QString fileName = entryMap[QStringLiteral("fileName")].toString(); + archive[fileName] = e; } return archive; } diff --git a/autotests/kerfuffle/jsonparser.h b/autotests/kerfuffle/jsonparser.h index d52368c5..8f02ff81 100644 --- a/autotests/kerfuffle/jsonparser.h +++ b/autotests/kerfuffle/jsonparser.h @@ -1,80 +1,80 @@ /* * 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 JSONPARSER_H #define JSONPARSER_H #include "kerfuffle/archive_kerfuffle.h" #include #include /** * Simple parser which reads JSON files and creates @c ArchiveEntry objects * from it. * * The JSON file is expected to follow a specific format that describes an * archive read by Kerfuffle. * * The format consists of a list of dictionaries whose keys are values from the * EntryMetaDataType enum. The only required key for each entry is FileName; * other values which are omitted for each entry are assumed to be 0 or false. * * Example file: * @code * [ * { "FileName": "foo", "IsPasswordProtected": true }, * { "FileName": "aDir/", "IsDirectory": true } * ] * @endcode * * @author Raphael Kubo da Costa */ class JSONParser { public: - typedef QMap JSONArchive; + typedef QMap JSONArchive; ~JSONParser(); static JSONArchive parse(QIODevice *json); private: JSONParser(); /** * Parses each entry in the QVariant obtained from parsing a JSON file and * creates a @c JSONArchive from them. * * If an entry does not have a "FileName" key, it is ignored. Keys which do * not correspond to a value in the EntryMetaDataType enum are ignored. * * @return A new @c JSONArchive corresponding to the parsed JSON file. If a * parsing error occurs, it is empty. */ static JSONArchive createJSONArchive(const QVariant &json); }; #endif // JSONPARSER_H diff --git a/autotests/plugins/cli7zplugin/cli7ztest.cpp b/autotests/plugins/cli7zplugin/cli7ztest.cpp index 77dad77f..4c9ecab2 100644 --- a/autotests/plugins/cli7zplugin/cli7ztest.cpp +++ b/autotests/plugins/cli7zplugin/cli7ztest.cpp @@ -1,382 +1,380 @@ /* * 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 #include #include #include #include QTEST_GUILESS_MAIN(Cli7zTest) using namespace Kerfuffle; void Cli7zTest::initTestCase() { - qRegisterMetaType(); - 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); Archive *archive = Archive::create(archivePath, m_plugin, this); 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->isSingleFolderArchive(), 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"); // 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 15.14 tests QTest::newRow("normal-file-1514") << QFINDTESTDATA("data/archive-with-symlink-1514.txt") << 10 << 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 << 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 << 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 << 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 << 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 << 3 << QStringLiteral("testarchive/dir1/file1.txt") << false << true << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); } void Cli7zTest::testList() { CliPlugin *plugin = new CliPlugin(this, {QStringLiteral("dummy.7z")}); QSignalSpy signalSpy(plugin, SIGNAL(entry(ArchiveEntry))); QFETCH(QString, outputTextFile); QFETCH(int, expectedEntriesCount); QFile outputText(outputTextFile); QVERIFY(outputText.open(QIODevice::ReadOnly)); QTextStream outputStream(&outputText); while (!outputStream.atEnd()) { const QString line(outputStream.readLine()); QVERIFY(plugin->readListLine(line)); } QCOMPARE(signalSpy.count(), expectedEntriesCount); QFETCH(int, someEntryIndex); QVERIFY(someEntryIndex < signalSpy.count()); - ArchiveEntry entry = qvariant_cast(signalSpy.at(someEntryIndex).at(0)); + Archive::Entry *entry = signalSpy.at(someEntryIndex).at(0).value(); QFETCH(QString, expectedName); - QCOMPARE(entry[FileName].toString(), expectedName); + QCOMPARE(entry->property("fileName").toString(), expectedName); QFETCH(bool, isDirectory); - QCOMPARE(entry[IsDirectory].toBool(), isDirectory); + QCOMPARE(entry->isDir(), isDirectory); QFETCH(bool, isPasswordProtected); - QCOMPARE(entry[IsPasswordProtected].toBool(), isPasswordProtected); + QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected); QFETCH(qulonglong, expectedSize); - QCOMPARE(entry[Size].toULongLong(), expectedSize); + QCOMPARE(entry->property("size").toULongLong(), expectedSize); QFETCH(QString, expectedTimestamp); - QCOMPARE(entry[Timestamp].toString(), 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() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList listArgs = { QStringLiteral("l"), QStringLiteral("-slt"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$Archive") }; QFETCH(QString, password); const auto replacedArgs = plugin->substituteListVariables(listArgs, 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("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.7z") << QString() << false << 5 << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("-mx=5") }; QTest::newRow("encrypted") << QStringLiteral("/tmp/foo.7z") << QStringLiteral("1234") << false << 5 << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("-p1234"), QStringLiteral("-mx=5") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.7z") << QStringLiteral("1234") << true << 5 << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("-p1234"), QStringLiteral("-mhe=on"), QStringLiteral("-mx=5") }; } void Cli7zTest::testAddArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList addArgs = { QStringLiteral("a"), QStringLiteral("$Archive"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$CompressionLevelSwitch"), QStringLiteral("$Files") }; QFETCH(QString, password); QFETCH(bool, encryptHeader); QFETCH(int, compressionLevel); QStringList replacedArgs = plugin->substituteAddVariables(addArgs, {}, password, encryptHeader, compressionLevel); 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") << QVariantList { QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) } << true << QStringLiteral("1234") << QStringList { QStringLiteral("x"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("preserve paths, unencrypted") << QStringLiteral("/tmp/foo.7z") << QVariantList { QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) } << true << QString() << QStringList { QStringLiteral("x"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, encrypted") << QStringLiteral("/tmp/foo.7z") << QVariantList { QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) } << false << QStringLiteral("1234") << QStringList { QStringLiteral("e"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, unencrypted") << QStringLiteral("/tmp/foo.7z") << QVariantList { QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) } << false << QString() << QStringList { QStringLiteral("e"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; } void Cli7zTest::testExtractArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList extractArgs = { QStringLiteral("$PreservePathSwitch"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$Archive"), QStringLiteral("$Files") }; QFETCH(QVariantList, files); QFETCH(bool, preservePaths); QFETCH(QString, password); QStringList replacedArgs = plugin->substituteCopyVariables(extractArgs, files, preservePaths, password); QVERIFY(replacedArgs.size() >= extractArgs.size()); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } diff --git a/autotests/plugins/cli7zplugin/cli7ztest.h b/autotests/plugins/cli7zplugin/cli7ztest.h index c44ac46a..1e55abfe 100644 --- a/autotests/plugins/cli7zplugin/cli7ztest.h +++ b/autotests/plugins/cli7zplugin/cli7ztest.h @@ -1,58 +1,56 @@ /* * 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 CLI7ZTEST_H #define CLI7ZTEST_H #include "cliplugin.h" #include "pluginmanager.h" using namespace Kerfuffle; class Cli7zTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testArchive_data(); void testArchive(); void testList_data(); void testList(); void testListArgs_data(); void testListArgs(); void testAddArgs_data(); void testAddArgs(); void testExtractArgs_data(); void testExtractArgs(); private: PluginManager m_pluginManger; Plugin *m_plugin; }; -Q_DECLARE_METATYPE(ArchiveEntry) - #endif diff --git a/autotests/plugins/clirarplugin/clirartest.cpp b/autotests/plugins/clirarplugin/clirartest.cpp index b0693925..47b78c04 100644 --- a/autotests/plugins/clirarplugin/clirartest.cpp +++ b/autotests/plugins/clirarplugin/clirartest.cpp @@ -1,425 +1,424 @@ /* * 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 "kerfuffle/archiveentry.h" #include #include #include #include #include QTEST_GUILESS_MAIN(CliRarTest) using namespace Kerfuffle; void CliRarTest::initTestCase() { - qRegisterMetaType(); - 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); Archive *archive = Archive::create(archivePath, m_plugin, this); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not load the clirar plugin. Skipping test.", SkipSingle); } QFETCH(QString, expectedFileName); QCOMPARE(QFileInfo(archive->fileName()).fileName(), expectedFileName); QFETCH(bool, isReadOnly); QCOMPARE(archive->isReadOnly(), isReadOnly); QFETCH(bool, isSingleFolder); QCOMPARE(archive->isSingleFolderArchive(), isSingleFolder); QFETCH(Archive::EncryptionType, expectedEncryptionType); QCOMPARE(archive->encryptionType(), expectedEncryptionType); QFETCH(QString, expectedSubfolderName); QCOMPARE(archive->subfolderName(), expectedSubfolderName); } void CliRarTest::testList_data() { QTest::addColumn("outputTextFile"); QTest::addColumn("expectedEntriesCount"); // Index of some entry to be tested. QTest::addColumn("someEntryIndex"); // Entry metadata. QTest::addColumn("expectedName"); QTest::addColumn("isDirectory"); QTest::addColumn("isPasswordProtected"); QTest::addColumn("symlinkTarget"); QTest::addColumn("expectedSize"); QTest::addColumn("expectedCompressedSize"); QTest::addColumn("expectedTimestamp"); // Unrar 5 tests QTest::newRow("normal-file-unrar5") << QFINDTESTDATA("data/archive-with-symlink-unrar5.txt") << 8 << 2 << QStringLiteral("rartest/file2.txt") << false << false << QString() << (qulonglong) 14 << (qulonglong) 23 << QStringLiteral("2016-03-21T08:57:36"); QTest::newRow("symlink-unrar5") << QFINDTESTDATA("data/archive-with-symlink-unrar5.txt") << 8 << 3 << QStringLiteral("rartest/linktofile1.txt") << false << false << QStringLiteral("file1.txt") << (qulonglong) 9 << (qulonglong) 9 << QStringLiteral("2016-03-21T08:58:16"); QTest::newRow("encrypted-unrar5") << QFINDTESTDATA("data/archive-encrypted-unrar5.txt") << 7 << 2 << QStringLiteral("rartest/file2.txt") << false << true << QString() << (qulonglong) 14 << (qulonglong) 32 << QStringLiteral("2016-03-21T17:03:36"); QTest::newRow("recovery-record-unrar5") << QFINDTESTDATA("data/archive-recovery-record-unrar5.txt") << 3 << 0 << QStringLiteral("file1.txt") << false << false << QString() << (qulonglong) 32 << (qulonglong) 33 << QStringLiteral("2015-07-26T19:04:38"); QTest::newRow("corrupt-archive-unrar5") << QFINDTESTDATA("data/archive-corrupt-file-header-unrar5.txt") << 8 << 6 << QStringLiteral("dir1/") << true << false << QString() << (qulonglong) 0 << (qulonglong) 0 << QStringLiteral("2015-05-14T01:45:24"); // Unrar 4 tests QTest::newRow("normal-file-unrar4") << QFINDTESTDATA("data/archive-with-symlink-unrar4.txt") << 8 << 2 << QStringLiteral("rartest/file2.txt") << false << false << QString() << (qulonglong) 14 << (qulonglong) 23 << QStringLiteral("2016-03-21T08:57:00"); QTest::newRow("symlink-unrar4") << QFINDTESTDATA("data/archive-with-symlink-unrar4.txt") << 8 << 3 << QStringLiteral("rartest/linktofile1.txt") << false << false << QStringLiteral("file1.txt") << (qulonglong) 9 << (qulonglong) 9 << QStringLiteral("2016-03-21T08:58:00"); QTest::newRow("encrypted-unrar4") << QFINDTESTDATA("data/archive-encrypted-unrar4.txt") << 7 << 2 << QStringLiteral("rartest/file2.txt") << false << true << QString() << (qulonglong) 14 << (qulonglong) 32 << QStringLiteral("2016-03-21T17:03:00"); QTest::newRow("recovery-record-unrar4") << QFINDTESTDATA("data/archive-recovery-record-unrar4.txt") << 3 << 0 << QStringLiteral("file1.txt") << false << false << QString() << (qulonglong) 32 << (qulonglong) 33 << QStringLiteral("2015-07-26T19:04:00"); QTest::newRow("corrupt-archive-unrar4") << QFINDTESTDATA("data/archive-corrupt-file-header-unrar4.txt") << 8 << 6 << QStringLiteral("dir1/") << true << false << QString() << (qulonglong) 0 << (qulonglong) 0 << QStringLiteral("2015-05-14T01:45:00"); /* * Check that the plugin will not crash when reading corrupted archives, which * have lines such as "Unexpected end of archive" or "??? - the file header is * corrupt" instead of a file name and the header string after it. * * See bug 262857 and commit 2042997013432cdc6974f5b26d39893a21e21011. */ QTest::newRow("corrupt-archive-unrar3") << QFINDTESTDATA("data/archive-corrupt-file-header-unrar3.txt") << 1 << 0 << QStringLiteral("some-file.ext") << false << false << QString() << (qulonglong) 732522496 << (qulonglong) 14851208 << QStringLiteral("2010-10-29T20:47:00"); } void CliRarTest::testList() { CliPlugin *rarPlugin = new CliPlugin(this, {QStringLiteral("dummy.rar")}); QSignalSpy signalSpy(rarPlugin, SIGNAL(entry(ArchiveEntry))); QFETCH(QString, outputTextFile); QFETCH(int, expectedEntriesCount); QFile outputText(outputTextFile); QVERIFY(outputText.open(QIODevice::ReadOnly)); QTextStream outputStream(&outputText); while (!outputStream.atEnd()) { const QString line(outputStream.readLine()); QVERIFY(rarPlugin->readListLine(line)); } QCOMPARE(signalSpy.count(), expectedEntriesCount); QFETCH(int, someEntryIndex); QVERIFY(someEntryIndex < signalSpy.count()); - ArchiveEntry entry = qvariant_cast(signalSpy.at(someEntryIndex).at(0)); + Archive::Entry *entry = signalSpy.at(someEntryIndex).at(0).value(); QFETCH(QString, expectedName); - QCOMPARE(entry[FileName].toString(), expectedName); + QCOMPARE(entry->property("fileName").toString(), expectedName); QFETCH(bool, isDirectory); - QCOMPARE(entry[IsDirectory].toBool(), isDirectory); + QCOMPARE(entry->isDir(), isDirectory); QFETCH(bool, isPasswordProtected); - QCOMPARE(entry[IsPasswordProtected].toBool(), isPasswordProtected); + QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected); QFETCH(QString, symlinkTarget); - QCOMPARE(entry[Link].toString(), symlinkTarget); + QCOMPARE(entry->property("link").toString(), symlinkTarget); QFETCH(qulonglong, expectedSize); - QCOMPARE(entry[Size].toULongLong(), expectedSize); + QCOMPARE(entry->property("size").toULongLong(), expectedSize); QFETCH(qulonglong, expectedCompressedSize); - QCOMPARE(entry[CompressedSize].toULongLong(), expectedCompressedSize); + QCOMPARE(entry->property("compressedSize").toULongLong(), expectedCompressedSize); QFETCH(QString, expectedTimestamp); - QCOMPARE(entry[Timestamp].toString(), 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() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList listArgs = { QStringLiteral("vt"), QStringLiteral("-v"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$Archive") }; QFETCH(QString, password); const auto replacedArgs = plugin->substituteListVariables(listArgs, 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("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.rar") << QString() << false << 3 << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("-m3") }; QTest::newRow("encrypted") << QStringLiteral("/tmp/foo.rar") << QStringLiteral("1234") << false << 3 << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("-p1234"), QStringLiteral("-m3") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.rar") << QStringLiteral("1234") << true << 3 << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("-hp1234"), QStringLiteral("-m3") }; } void CliRarTest::testAddArgs() { QFETCH(QString, archiveName); CliPlugin *rarPlugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(rarPlugin); const QStringList addArgs = { QStringLiteral("a"), QStringLiteral("$Archive"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$CompressionLevelSwitch"), QStringLiteral("$Files") }; QFETCH(QString, password); QFETCH(bool, encryptHeader); QFETCH(int, compressionLevel); QStringList replacedArgs = rarPlugin->substituteAddVariables(addArgs, {}, password, encryptHeader, compressionLevel); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); rarPlugin->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") << QVariantList { QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) } << true << QStringLiteral("1234") << QStringList { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("x"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("preserve paths, unencrypted") << QStringLiteral("/tmp/foo.rar") << QVariantList { QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) } << true << QString() << QStringList { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("x"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, encrypted") << QStringLiteral("/tmp/foo.rar") << QVariantList { QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) } << false << QStringLiteral("1234") << QStringList { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("e"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, unencrypted") << QStringLiteral("/tmp/foo.rar") << QVariantList { QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) } << false << QString() << QStringList { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("e"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; } void CliRarTest::testExtractArgs() { QFETCH(QString, archiveName); CliPlugin *rarPlugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(rarPlugin); const QStringList extractArgs = { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("$PreservePathSwitch"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$Archive"), QStringLiteral("$Files") }; QFETCH(QVariantList, files); QFETCH(bool, preservePaths); QFETCH(QString, password); QStringList replacedArgs = rarPlugin->substituteCopyVariables(extractArgs, files, preservePaths, password); QVERIFY(replacedArgs.size() >= extractArgs.size()); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); rarPlugin->deleteLater(); } diff --git a/autotests/plugins/clirarplugin/clirartest.h b/autotests/plugins/clirarplugin/clirartest.h index 7e545d36..8e538319 100644 --- a/autotests/plugins/clirarplugin/clirartest.h +++ b/autotests/plugins/clirarplugin/clirartest.h @@ -1,59 +1,57 @@ /* * Copyright (c) 2011 Raphael Kubo da Costa * Copyright (c) 2015,2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CLIRARTEST_H #define CLIRARTEST_H #include "cliplugin.h" #include "pluginmanager.h" using namespace Kerfuffle; class CliRarTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testArchive_data(); void testArchive(); void testList_data(); void testList(); void testListArgs_data(); void testListArgs(); void testAddArgs_data(); void testAddArgs(); void testExtractArgs_data(); void testExtractArgs(); private: PluginManager m_pluginManger; Plugin *m_plugin; }; -Q_DECLARE_METATYPE(ArchiveEntry) - #endif diff --git a/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp b/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp index 9bc3672c..a913b1ad 100644 --- a/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp +++ b/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp @@ -1,384 +1,383 @@ /* * 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 "kerfuffle/archiveentry.h" #include #include #include #include #include #include QTEST_GUILESS_MAIN(CliUnarchiverTest) using namespace Kerfuffle; void CliUnarchiverTest::initTestCase() { - qRegisterMetaType(); - 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); Archive *archive = Archive::create(archivePath, m_plugin, this); 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->isSingleFolderArchive(), 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-21 16:57:20 +0100"); 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-21 16:56:28 +0100"); 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-21 16:56:28 +0100"); 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-14 03:10:10 +0200"); } void CliUnarchiverTest::testList() { CliPlugin *unarPlugin = new CliPlugin(this, {QStringLiteral("dummy.rar")}); QSignalSpy signalSpy(unarPlugin, SIGNAL(entry(ArchiveEntry))); QFETCH(QString, jsonFilePath); QFETCH(int, expectedEntriesCount); QFile jsonFile(jsonFilePath); QVERIFY(jsonFile.open(QIODevice::ReadOnly)); QTextStream stream(&jsonFile); unarPlugin->setJsonOutput(stream.readAll()); QCOMPARE(signalSpy.count(), expectedEntriesCount); QFETCH(int, someEntryIndex); QVERIFY(someEntryIndex < signalSpy.count()); - ArchiveEntry entry = qvariant_cast(signalSpy.at(someEntryIndex).at(0)); + Archive::Entry *entry = signalSpy.at(someEntryIndex).at(0).value(); QFETCH(QString, expectedName); - QCOMPARE(entry[FileName].toString(), expectedName); + QCOMPARE(entry->property("fileName").toString(), expectedName); QFETCH(bool, isDirectory); - QCOMPARE(entry[IsDirectory].toBool(), isDirectory); + QCOMPARE(entry->isDir(), isDirectory); QFETCH(bool, isPasswordProtected); - QCOMPARE(entry[IsPasswordProtected].toBool(), isPasswordProtected); + QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected); QFETCH(qulonglong, expectedSize); - QCOMPARE(entry[Size].toULongLong(), expectedSize); + QCOMPARE(entry->property("size").toULongLong(), expectedSize); QFETCH(qulonglong, expectedCompressedSize); - QCOMPARE(entry[CompressedSize].toULongLong(), expectedCompressedSize); + QCOMPARE(entry->property("compressedSize").toULongLong(), expectedCompressedSize); QFETCH(QString, expectedTimestamp); - QCOMPARE(entry[Timestamp].toString(), expectedTimestamp); + QCOMPARE(entry->property("timestamp").toString(), expectedTimestamp); unarPlugin->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") }; } void CliUnarchiverTest::testListArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList listArgs = { QStringLiteral("-json"), QStringLiteral("$Archive"), QStringLiteral("$PasswordSwitch") }; QFETCH(QString, password); const auto replacedArgs = plugin->substituteListVariables(listArgs, 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 options; options[QStringLiteral("AlwaysUseTmpDir")] = true; ExtractionOptions optionsPreservePaths = options; optionsPreservePaths[QStringLiteral("PreservePaths")] = true; ExtractionOptions dragAndDropOptions = optionsPreservePaths; dragAndDropOptions[QStringLiteral("DragAndDrop")] = true; QTest::newRow("extract the whole multiple_toplevel_entries.rar") << QFINDTESTDATA("data/multiple_toplevel_entries.rar") << QVariantList() << optionsPreservePaths << 12; QTest::newRow("extract selected entries from a rar, without paths") << QFINDTESTDATA("data/one_toplevel_folder.rar") << QVariantList { QVariant::fromValue(fileRootNodePair(QStringLiteral("A/test2.txt"), QStringLiteral("A"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B"))) } << options << 2; QTest::newRow("extract selected entries from a rar, preserve paths") << QFINDTESTDATA("data/one_toplevel_folder.rar") << QVariantList { QVariant::fromValue(fileRootNodePair(QStringLiteral("A/test2.txt"), QStringLiteral("A"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B"))) } << optionsPreservePaths << 4; QTest::newRow("extract selected entries from a rar, drag-and-drop") << QFINDTESTDATA("data/one_toplevel_folder.rar") << QVariantList { QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/C/"), QStringLiteral("A/B/"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("A/test2.txt"), QStringLiteral("A/"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/C/test1.txt"), QStringLiteral("A/B/"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/C/test2.txt"), QStringLiteral("A/B/"))) } << dragAndDropOptions << 4; QTest::newRow("rar with empty folders") << QFINDTESTDATA("data/empty_folders.rar") << QVariantList() << optionsPreservePaths << 5; } // 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); Archive *archive = Archive::create(archivePath, m_plugin, this); 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(QVariantList, entriesToExtract); QFETCH(ExtractionOptions, extractionOptions); auto extractionJob = archive->copyFiles(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::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") << QVariantList { QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) } << QStringLiteral("1234") << QStringList { QStringLiteral("-D"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), QStringLiteral("-password"), QStringLiteral("1234") }; QTest::newRow("unencrypted, multiple files") << QStringLiteral("/tmp/foo.rar") << QVariantList { QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) } << QString() << QStringList { QStringLiteral("-D"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; } void CliUnarchiverTest::testExtractArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList extractArgs = { QStringLiteral("-D"), QStringLiteral("$Archive"), QStringLiteral("$Files"), QStringLiteral("$PasswordSwitch") }; QFETCH(QVariantList, files); QFETCH(QString, password); QStringList replacedArgs = plugin->substituteCopyVariables(extractArgs, files, false, password); QVERIFY(replacedArgs.size() >= extractArgs.size()); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } diff --git a/autotests/plugins/cliunarchiverplugin/cliunarchivertest.h b/autotests/plugins/cliunarchiverplugin/cliunarchivertest.h index 50ceb4a9..8b36d2f9 100644 --- a/autotests/plugins/cliunarchiverplugin/cliunarchivertest.h +++ b/autotests/plugins/cliunarchiverplugin/cliunarchivertest.h @@ -1,54 +1,52 @@ /* * 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 CLIUNARCHIVERTEST_H #define CLIUNARCHIVERTEST_H #include "cliplugin.h" #include "pluginmanager.h" using namespace Kerfuffle; class CliUnarchiverTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testArchive_data(); void testArchive(); void testList_data(); void testList(); void testListArgs_data(); void testListArgs(); void testExtraction_data(); void testExtraction(); void testExtractArgs_data(); void testExtractArgs(); private: PluginManager m_pluginManger; Plugin *m_plugin; }; -Q_DECLARE_METATYPE(ArchiveEntry) - #endif diff --git a/kerfuffle/CMakeLists.txt b/kerfuffle/CMakeLists.txt index 4eaabaa9..938f6e89 100644 --- a/kerfuffle/CMakeLists.txt +++ b/kerfuffle/CMakeLists.txt @@ -1,64 +1,66 @@ ########### next target ############### set(kerfuffle_SRCS archiveformat.cpp archive_kerfuffle.cpp archiveinterface.cpp extractionsettingspage.cpp previewsettingspage.cpp settingspage.cpp jobs.cpp createdialog.cpp extractiondialog.cpp propertiesdialog.cpp queries.cpp addtoarchive.cpp cliinterface.cpp mimetypes.cpp plugin.cpp pluginmanager.cpp + archiveentry.cpp + archiveentry.h ) kconfig_add_kcfg_files(kerfuffle_SRCS settings.kcfgc) ki18n_wrap_ui(kerfuffle_SRCS createdialog.ui extractiondialog.ui extractionsettings.ui previewsettings.ui propertiesdialog.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 fbb5a29b..47cdb16c 100644 --- a/kerfuffle/archive_kerfuffle.cpp +++ b/kerfuffle/archive_kerfuffle.cpp @@ -1,464 +1,463 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008 Harald Hvaal * Copyright (c) 2009-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "archive_kerfuffle.h" +#include "archiveentry.h" #include "ark_debug.h" #include "archiveinterface.h" #include "jobs.h" #include "mimetypes.h" #include "pluginmanager.h" #include #include #include #include #include #include #include #include namespace Kerfuffle { QDebug operator<<(QDebug d, const fileRootNodePair &pair) { d.nospace() << "fileRootNodePair(" << pair.file << "," << pair.rootNode << ")"; return d.space(); } 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; - qRegisterMetaType("ArchiveEntry"); - 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; 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())}; 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); } 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_hasBeenListed(false) , m_isReadOnly(isReadOnly) , m_isSingleFolderArchive(false) , m_extractedFilesSize(0) , m_error(NoError) , m_encryptionType(Unencrypted) , m_numberOfFiles(0) { qCDebug(ARK) << "Created archive instance"; Q_ASSERT(archiveInterface); archiveInterface->setParent(this); QMetaType::registerComparators(); QMetaType::registerDebugStreamOperator(); connect(m_iface, &ReadOnlyArchiveInterface::entry, this, &Archive::onNewEntry); } Archive::~Archive() { } QString Archive::completeBaseName() const { QString base = QFileInfo(fileName()).completeBaseName(); // Special case for compressed tar archives. if (base.right(4).toUpper() == QLatin1String(".TAR")) { base.chop(4); } 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::isReadOnly() const { return isValid() ? (m_iface->isReadOnly() || m_isReadOnly) : false; } bool Archive::isSingleFolderArchive() { if (!isValid()) { return false; } listIfNotListed(); return m_isSingleFolderArchive; } bool Archive::hasComment() const { return isValid() ? !comment().isEmpty() : false; } Archive::EncryptionType Archive::encryptionType() { if (!isValid()) { return Unencrypted; } listIfNotListed(); return m_encryptionType; } qulonglong Archive::numberOfFiles() { if (!isValid()) { return 0; } listIfNotListed(); return m_numberOfFiles; } qulonglong Archive::unpackedSize() { if (!isValid()) { return 0; } listIfNotListed(); return m_extractedFilesSize; } qulonglong Archive::packedSize() const { return isValid() ? QFileInfo(fileName()).size() : 0; } QString Archive::subfolderName() { if (!isValid()) { return QString(); } listIfNotListed(); return m_subfolderName; } -void Archive::onNewEntry(const ArchiveEntry &entry) +void Archive::onNewEntry(const Archive::Entry *entry) { - if (!entry[IsDirectory].toBool()) { + if (!entry->isDir()) { m_numberOfFiles++; } } bool Archive::isValid() const { return m_iface && (m_error == NoError); } ArchiveError Archive::error() const { return m_error; } KJob* Archive::open() { return 0; } KJob* Archive::create() { return 0; } ListJob* Archive::list() { if (!isValid() || !QFileInfo::exists(fileName())) { return Q_NULLPTR; } qCDebug(ARK) << "Going to list files"; ListJob *job = new ListJob(m_iface); //if this job has not been listed before, we grab the opportunity to //collect some information about the archive if (!m_hasBeenListed) { connect(job, &ListJob::result, this, &Archive::onListFinished); } return job; } DeleteJob* Archive::deleteFiles(const QList & files) { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to delete files" << files; if (m_iface->isReadOnly()) { return 0; } DeleteJob *newJob = new DeleteJob(files, static_cast(m_iface)); return newJob; } AddJob* Archive::addFiles(const QStringList & files, const CompressionOptions& options) { if (!isValid()) { return Q_NULLPTR; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral("PasswordProtectedHint")] = true; } qCDebug(ARK) << "Going to add files" << files << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); AddJob *newJob = new AddJob(files, newOptions, static_cast(m_iface)); connect(newJob, &AddJob::result, this, &Archive::onAddFinished); return newJob; } ExtractJob* Archive::copyFiles(const QList& files, const QString& destinationDir, const ExtractionOptions& options) { if (!isValid()) { return Q_NULLPTR; } ExtractionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral( "PasswordProtectedHint" )] = true; } ExtractJob *newJob = new ExtractJob(files, destinationDir, newOptions, m_iface); return newJob; } PreviewJob *Archive::preview(const QString &file) { if (!isValid()) { return Q_NULLPTR; } PreviewJob *job = new PreviewJob(file, (encryptionType() != Unencrypted), m_iface); return job; } OpenJob *Archive::open(const QString &file) { if (!isValid()) { return Q_NULLPTR; } OpenJob *job = new OpenJob(file, (encryptionType() != Unencrypted), m_iface); return job; } OpenWithJob *Archive::openWith(const QString &file) { if (!isValid()) { return Q_NULLPTR; } OpenWithJob *job = new OpenWithJob(file, (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_isSingleFolderArchive && !job->error()) { m_isSingleFolderArchive = false; } } void Archive::onListFinished(KJob* job) { ListJob *ljob = qobject_cast(job); m_extractedFilesSize = ljob->extractedFilesSize(); m_isSingleFolderArchive = ljob->isSingleFolderArchive(); m_subfolderName = ljob->subfolderName(); if (m_subfolderName.isEmpty()) { m_subfolderName = completeBaseName(); } if (ljob->isPasswordProtected()) { // If we already know the password, it means that the archive is header-encrypted. m_encryptionType = m_iface->password().isEmpty() ? Encrypted : HeaderEncrypted; } m_hasBeenListed = true; } void Archive::listIfNotListed() { if (!m_hasBeenListed) { ListJob *job = list(); if (!job) { return; } connect(job, &ListJob::userQuery, this, &Archive::onUserQuery); QEventLoop loop(this); connect(job, &KJob::result, &loop, &QEventLoop::quit); job->start(); loop.exec(); // krazy:exclude=crashy } } void Archive::onUserQuery(Query* query) { query->execute(); } } // namespace Kerfuffle diff --git a/kerfuffle/archive_kerfuffle.h b/kerfuffle/archive_kerfuffle.h index 3cdacf3d..112d6384 100644 --- a/kerfuffle/archive_kerfuffle.h +++ b/kerfuffle/archive_kerfuffle.h @@ -1,264 +1,234 @@ /* * 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 #include #include #include #include class KJob; namespace Kerfuffle { class ListJob; class ExtractJob; class DeleteJob; class AddJob; class CommentJob; class TestJob; class OpenJob; class OpenWithJob; class Plugin; class PreviewJob; class Query; class ReadOnlyArchiveInterface; -/** - * Meta data related to one entry in a compressed archive. - * - * When creating a plugin, information about every single entry in - * an archive is contained in an ArchiveEntry, and metadata - * is set with the entries in this enum. - * - * Please notice that not all archive formats support all the properties - * below, so set those that are available. - */ -enum EntryMetaDataType { - FileName = 0, /**< The entry's file name */ - InternalID, /**< The entry's ID for Ark's internal manipulation */ - Permissions, /**< The entry's permissions */ - Owner, /**< The user the entry belongs to */ - Group, /**< The user group the entry belongs to */ - Size, /**< The entry's original size */ - CompressedSize, /**< The compressed size for the entry */ - Link, /**< The entry is a symbolic link */ - Ratio, /**< The compression ratio for the entry */ - CRC, /**< The entry's CRC */ - Method, /**< The compression method used on the entry */ - Version, /**< The archiver version needed to extract the entry */ - Timestamp, /**< The timestamp for the current entry */ - IsDirectory, /**< The entry is a directory */ - Comment, - IsPasswordProtected, /**< The entry is password-protected */ - Custom = 1048576 -}; - enum ArchiveError { NoError = 0, NoPlugin, FailedPlugin }; -typedef QHash ArchiveEntry; - /** These are the extra options for doing the compression. Naming convention is CamelCase with either Global, or the compression type (such as Zip, Rar, etc), followed by the property name used */ typedef QHash CompressionOptions; typedef QHash ExtractionOptions; /** * Stores a filename and rootnode pair. This is used to cut an individual * rootnode from the path of each file, e.g. when drag'n'drop extracting a * selection of files. */ struct fileRootNodePair { QString file; QString rootNode; fileRootNodePair() {} fileRootNodePair(const QString &f) : file(f) {} fileRootNodePair(const QString &f, const QString &n) : file(f), rootNode(n) {} // Required to compare QVariants with this type. bool operator==(const fileRootNodePair &right) const { if (file == right.file) return true; else return false; } bool operator<(const fileRootNodePair &) const { return false; } }; QDebug operator<<(QDebug d, const fileRootNodePair &pair); class KERFUFFLE_EXPORT Archive : public QObject { Q_OBJECT Q_ENUMS(EncryptionType) /** * 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 isReadOnly READ isReadOnly CONSTANT) Q_PROPERTY(bool isSingleFolderArchive READ isSingleFolderArchive) Q_PROPERTY(EncryptionType encryptionType READ encryptionType) Q_PROPERTY(qulonglong numberOfFiles READ numberOfFiles) Q_PROPERTY(qulonglong unpackedSize READ unpackedSize) Q_PROPERTY(qulonglong packedSize READ packedSize) Q_PROPERTY(QString subfolderName READ subfolderName) public: enum EncryptionType { Unencrypted, Encrypted, HeaderEncrypted }; + class Entry; + QString completeBaseName() const; QString fileName() const; QString comment() const; QMimeType mimeType(); bool isReadOnly() const; bool isSingleFolderArchive(); bool hasComment() const; EncryptionType encryptionType(); qulonglong numberOfFiles(); qulonglong unpackedSize(); qulonglong packedSize() const; QString subfolderName(); 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); ~Archive(); ArchiveError error() const; bool isValid() const; KJob* open(); KJob* create(); /** * @return A ListJob if the archive already exists. A null pointer otherwise. */ ListJob* list(); DeleteJob* deleteFiles(const QList & files); 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. * * TODO: find a way to actually add files to specific locations in * the archive * (not supported yet) GlobalPathInArchive - a path relative to the * archive root where the files will be added under * */ AddJob* addFiles(const QStringList & files, const CompressionOptions& options = CompressionOptions()); ExtractJob* copyFiles(const QList &files, const QString &destinationDir, const ExtractionOptions &options = ExtractionOptions()); PreviewJob* preview(const QString &file); OpenJob* open(const QString &file); OpenWithJob* openWith(const QString &file); /** * @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 onListFinished(KJob*); void onAddFinished(KJob*); void onUserQuery(Kerfuffle::Query*); - void onNewEntry(const ArchiveEntry &entry); + void onNewEntry(const Archive::Entry *entry); private: Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent = 0); Archive(ArchiveError errorCode, QObject *parent = 0); void listIfNotListed(); ReadOnlyArchiveInterface *m_iface; bool m_hasBeenListed; bool m_isReadOnly; bool m_isSingleFolderArchive; QString m_subfolderName; qulonglong m_extractedFilesSize; ArchiveError m_error; EncryptionType m_encryptionType; qulonglong m_numberOfFiles; QMimeType m_mimeType; }; } // namespace Kerfuffle Q_DECLARE_METATYPE(Kerfuffle::Archive::EncryptionType) Q_DECLARE_METATYPE(Kerfuffle::fileRootNodePair) #endif // ARCHIVE_H diff --git a/kerfuffle/archiveentry.cpp b/kerfuffle/archiveentry.cpp new file mode 100644 index 00000000..5a7ef157 --- /dev/null +++ b/kerfuffle/archiveentry.cpp @@ -0,0 +1,156 @@ +// +// Created by mvlabat on 5/27/16. +// + +#include "archiveentry.h" + +namespace Kerfuffle { +Archive::Entry::Entry(Entry *parent) + : compressedSizeIsSet(true) + , m_parent(parent) +{ + clearMetaData(); +} + +Archive::Entry::~Entry() +{ + clear(); +} + +QList Archive::Entry::entries() +{ + Q_ASSERT(isDir()); + return m_entries; +} + +void Archive::Entry::setEntryAt(int index, Entry *value) +{ + Q_ASSERT(isDir()); + m_entries[index] = value; +} + +void Archive::Entry::appendEntry(Entry *entry) +{ + Q_ASSERT(isDir()); + m_entries.append(entry); +} + +void Archive::Entry::removeEntryAt(int index) +{ + Q_ASSERT(isDir()); + delete m_entries.takeAt(index); +} + +Archive::Entry *Archive::Entry::getParent() const +{ + return m_parent; +} + +void Archive::Entry::setParent(Archive::Entry *parent) +{ + m_parent = parent; +} + +int Archive::Entry::row() const +{ + if (getParent()) { + return getParent()->entries().indexOf(const_cast(this)); + } + return 0; +} + +bool Archive::Entry::isDir() const +{ + return m_isDirectory; +} + +void Archive::Entry::processNameAndIcon() +{ + const QStringList pieces = m_fileName.split(QLatin1Char( '/' ), QString::SkipEmptyParts); + m_name = pieces.isEmpty() ? QString() : pieces.last(); + + QMimeDatabase db; + if (isDir()) { + m_icon = QIcon::fromTheme(db.mimeTypeForName(QStringLiteral("inode/directory")).iconName()).pixmap(IconSize(KIconLoader::Small), + IconSize(KIconLoader::Small)); + } else { + m_icon = QIcon::fromTheme(db.mimeTypeForFile(m_fileName).iconName()).pixmap(IconSize(KIconLoader::Small), + IconSize(KIconLoader::Small)); + } +} + +QPixmap Archive::Entry::icon() const +{ + return m_icon; +} + +QString Archive::Entry::name() const +{ + return m_name; +} + +Archive::Entry *Archive::Entry::find(const QString & name) +{ + foreach(Entry *entry, m_entries) { + if (entry && (entry->name() == name)) { + return entry; + } + } + return 0; +} + +Archive::Entry *Archive::Entry::findByPath(const QStringList & pieces, int index) +{ + if (index == pieces.count()) { + return 0; + } + + Entry *next = find(pieces.at(index)); + + if (index == pieces.count() - 1) { + return next; + } + if (next && next->isDir()) { + return next->findByPath(pieces, index + 1); + } + return 0; +} + +void Archive::Entry::clearMetaData() +{ + m_fileName.clear(); + m_permissions.clear(); + m_owner.clear(); + m_group.clear(); + m_size = 0; + m_compressedSize = 0; + m_link.clear(); + m_ratio.clear(); + m_CRC.clear(); + m_method.clear(); + m_version.clear(); + m_timestamp = QDateTime(); + m_isDirectory = false; + m_comment.clear(); + m_isPasswordProtected = false; +} + +void Archive::Entry::returnDirEntries(QList *store) +{ + foreach(Entry *entry, m_entries) { + if (entry->isDir()) { + store->prepend(entry); + entry->returnDirEntries(store); + } + } +} + +void Archive::Entry::clear() +{ + if (isDir()) { + qDeleteAll(m_entries); + m_entries.clear(); + } +} + +} diff --git a/kerfuffle/archiveentry.h b/kerfuffle/archiveentry.h new file mode 100644 index 00000000..d6a7a4fc --- /dev/null +++ b/kerfuffle/archiveentry.h @@ -0,0 +1,100 @@ +// +// Created by mvlabat on 5/27/16. +// + +#ifndef ARK_ENTRY_H +#define ARK_ENTRY_H + +#include "archive_kerfuffle.h" +#include "app/ark_debug.h" + +#include +#include +#include +#include + +#include + +namespace Kerfuffle { +class Archive::Entry : public QObject +{ + Q_OBJECT + + /** + * Meta data related to one entry in a compressed archive. + * + * When creating a plugin, information about every single entry in + * an archive is contained in an ArchiveEntry, and metadata + * is set with the entries in this enum. + * + * Please notice that not all archive formats support all the properties + * below, so set those that are available. + */ + Q_PROPERTY(QString fileName MEMBER m_fileName) + Q_PROPERTY(QString permissions MEMBER m_permissions) + Q_PROPERTY(QString owner MEMBER m_owner) + Q_PROPERTY(QString group MEMBER m_group) + Q_PROPERTY(qulonglong size MEMBER m_size) + Q_PROPERTY(qulonglong compressedSize MEMBER m_compressedSize) + Q_PROPERTY(QString link MEMBER m_link) + Q_PROPERTY(QString ratio MEMBER m_ratio) + Q_PROPERTY(QString CRC MEMBER m_CRC) + Q_PROPERTY(QString method MEMBER m_method) + Q_PROPERTY(QString version MEMBER m_version) + Q_PROPERTY(QDateTime timestamp MEMBER m_timestamp) + Q_PROPERTY(bool isDirectory MEMBER m_isDirectory) + Q_PROPERTY(QString comment MEMBER m_comment) + Q_PROPERTY(bool isPasswordProtected MEMBER m_isPasswordProtected) + +public: + + Entry(Entry *parent); + ~Entry(); + + QList entries(); + void setEntryAt(int index, Entry *value); + void appendEntry(Entry *entry); + void removeEntryAt(int index); + Entry *getParent() const; + void setParent(Entry *parent); + int row() const; + bool isDir() const; + void processNameAndIcon(); + QPixmap icon() const; + QString name() const; + Entry *find(const QString & name); + Entry *findByPath(const QStringList & pieces, int index = 0); + void clearMetaData(); + void returnDirEntries(QList *store); + void clear(); + +public: + bool compressedSizeIsSet; + +private: + QList m_entries; + QPixmap m_icon; + QString m_name; + Entry *m_parent; + + QString m_fileName; + QString m_permissions; + QString m_owner; + QString m_group; + qulonglong m_size; + qulonglong m_compressedSize; + QString m_link; + QString m_ratio; + QString m_CRC; + QString m_method; + QString m_version; + QDateTime m_timestamp; + bool m_isDirectory; + QString m_comment; + bool m_isPasswordProtected; +}; + +} + + +#endif //ARK_ENTRY_H diff --git a/kerfuffle/archiveinterface.h b/kerfuffle/archiveinterface.h index ec205377..0afffa60 100644 --- a/kerfuffle/archiveinterface.h +++ b/kerfuffle/archiveinterface.h @@ -1,156 +1,157 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 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 ARCHIVEINTERFACE_H #define ARCHIVEINTERFACE_H #include "archive_kerfuffle.h" +#include "archive_entry.h" #include "kerfuffle_export.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); 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; /** * 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 copyFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) = 0; bool waitForFinishedSignal(); virtual bool doKill(); virtual bool doSuspend(); virtual bool doResume(); bool isHeaderEncryptionEnabled() const; signals: void cancelled(); void error(const QString &message, const QString &details = QString()); - void entry(const ArchiveEntry &archiveEntry); + void entry(Archive::Entry *archiveEntry); void entryRemoved(const QString &path); void progress(double progress); void info(const QString &info); void finished(bool result); void userQuery(Query *query); void testSuccess(); 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; private: QString m_filename; QString m_password; bool m_waitForFinishedSignal; bool m_isHeaderEncryptionEnabled; bool m_isCorrupt; }; class KERFUFFLE_EXPORT ReadWriteArchiveInterface: public ReadOnlyArchiveInterface { Q_OBJECT public: explicit ReadWriteArchiveInterface(QObject *parent, const QVariantList & args); virtual ~ReadWriteArchiveInterface(); bool isReadOnly() const Q_DECL_OVERRIDE; //see archive.h for a list of what the compressionoptions might //contain virtual bool addFiles(const QStringList & files, const CompressionOptions& options) = 0; virtual bool deleteFiles(const QList & files) = 0; virtual bool addComment(const QString &comment) = 0; }; } // namespace Kerfuffle #endif // ARCHIVEINTERFACE_H diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp index c14e5142..33427f18 100644 --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -1,558 +1,559 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "jobs.h" +#include "archiveentry.h" #include "ark_debug.h" #include #include #include #include #include #include //#define DEBUG_RACECONDITION namespace Kerfuffle { class Job::Private : public QThread { public: Private(Job *job, QObject *parent = 0) : QThread(parent) , q(job) { connect(q, &KJob::result, this, &QThread::quit); } virtual void run() Q_DECL_OVERRIDE; private: Job *q; }; void Job::Private::run() { q->doWork(); if (q->isRunning()) { exec(); } #ifdef DEBUG_RACECONDITION QThread::sleep(2); #endif } Job::Job(ReadOnlyArchiveInterface *interface) : KJob() , m_archiveInterface(interface) , m_isRunning(false) , d(new Private(this)) { static bool onlyOnce = false; if (!onlyOnce) { qRegisterMetaType >("QPair"); onlyOnce = true; } setCapabilities(KJob::Killable); } Job::~Job() { if (d->isRunning()) { d->wait(); } delete d; } ReadOnlyArchiveInterface *Job::archiveInterface() { return m_archiveInterface; } bool Job::isRunning() const { return m_isRunning; } void Job::start() { jobTimer.start(); m_isRunning = true; if (archiveInterface()->waitForFinishedSignal()) { // CLI-based interfaces run a QProcess, no need to use threads. QTimer::singleShot(0, this, &Job::doWork); } else { // Run the job in another thread. d->start(); } } void Job::emitResult() { m_isRunning = false; KJob::emitResult(); } void Job::connectToArchiveInterfaceSignals() { connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &Job::onCancelled); connect(archiveInterface(), &ReadOnlyArchiveInterface::error, this, &Job::onError); connect(archiveInterface(), &ReadOnlyArchiveInterface::entry, this, &Job::onEntry); connect(archiveInterface(), &ReadOnlyArchiveInterface::entryRemoved, this, &Job::onEntryRemoved); connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &Job::onProgress); connect(archiveInterface(), &ReadOnlyArchiveInterface::info, this, &Job::onInfo); connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished, Qt::DirectConnection); connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery); } void Job::onCancelled() { qCDebug(ARK) << "Cancelled emitted"; setError(KJob::KilledJobError); } void Job::onError(const QString & message, const QString & details) { Q_UNUSED(details) qCDebug(ARK) << "Error emitted:" << message; setError(KJob::UserDefinedError); setErrorText(message); } -void Job::onEntry(const ArchiveEntry & archiveEntry) +void Job::onEntry(Archive::Entry *entry) { - emit newEntry(archiveEntry); + emit newEntry(entry); } void Job::onProgress(double value) { setPercent(static_cast(100.0*value)); } void Job::onInfo(const QString& info) { emit infoMessage(this, info); } void Job::onEntryRemoved(const QString & path) { emit entryRemoved(path); } void Job::onFinished(bool result) { qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms"; emitResult(); } void Job::onUserQuery(Query *query) { emit userQuery(query); } bool Job::doKill() { bool ret = archiveInterface()->doKill(); if (!ret) { qCWarning(ARK) << "Killing does not seem to be supported here."; } return ret; } ListJob::ListJob(ReadOnlyArchiveInterface *interface) : Job(interface) , m_isSingleFolderArchive(true) , m_isPasswordProtected(false) , m_extractedFilesSize(0) , m_dirCount(0) , m_filesCount(0) { qCDebug(ARK) << "ListJob started"; connect(this, &ListJob::newEntry, this, &ListJob::onNewEntry); } void ListJob::doWork() { emit description(this, i18n("Loading archive...")); connectToArchiveInterfaceSignals(); bool ret = archiveInterface()->list(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } qlonglong ListJob::extractedFilesSize() const { return m_extractedFilesSize; } bool ListJob::isPasswordProtected() const { return m_isPasswordProtected; } bool ListJob::isSingleFolderArchive() const { if (m_filesCount == 1 && m_dirCount == 0) { return false; } return m_isSingleFolderArchive; } -void ListJob::onNewEntry(const ArchiveEntry& entry) +void ListJob::onNewEntry(const Archive::Entry *entry) { - m_extractedFilesSize += entry[ Size ].toLongLong(); - m_isPasswordProtected |= entry [ IsPasswordProtected ].toBool(); + m_extractedFilesSize += entry->property("size").toLongLong(); + m_isPasswordProtected |= entry->property("isPasswordProtected").toBool(); - if (entry[IsDirectory].toBool()) { + if (entry->isDir()) { m_dirCount++; } else { m_filesCount++; } if (m_isSingleFolderArchive) { // RPM filenames have the ./ prefix, and "." would be detected as the subfolder name, so we remove it. - const QString fileName = entry[FileName].toString().replace(QRegularExpression(QStringLiteral("^\\./")), QString()); + const QString fileName = entry->property("fileName").toString().replace(QRegularExpression(QStringLiteral("^\\./")), QString()); const QString basePath = fileName.split(QLatin1Char('/')).at(0); if (m_basePath.isEmpty()) { m_basePath = basePath; m_subfolderName = basePath; } else { if (m_basePath != basePath) { m_isSingleFolderArchive = false; m_subfolderName.clear(); } } } } QString ListJob::subfolderName() const { if (!isSingleFolderArchive()) { return QString(); } return m_subfolderName; } ExtractJob::ExtractJob(const QVariantList& files, const QString& destinationDir, const ExtractionOptions& options, ReadOnlyArchiveInterface *interface) : Job(interface) , m_files(files) , m_destinationDir(destinationDir) , m_options(options) { qCDebug(ARK) << "ExtractJob created"; setDefaultOptions(); } void ExtractJob::doWork() { QString desc; if (m_files.count() == 0) { desc = i18n("Extracting all files"); } else { desc = i18np("Extracting one file", "Extracting %1 files", m_files.count()); } emit description(this, desc); QFileInfo destDirInfo(m_destinationDir); if (destDirInfo.isDir() && (!destDirInfo.isWritable() || !destDirInfo.isExecutable())) { onError(xi18n("Could not write to destination %1.Check whether you have sufficient permissions.", m_destinationDir), QString()); onFinished(false); return; } connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Starting extraction with selected files:" << m_files << "Destination dir:" << m_destinationDir << "Options:" << m_options; bool ret = archiveInterface()->copyFiles(m_files, m_destinationDir, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void ExtractJob::setDefaultOptions() { ExtractionOptions defaultOptions; defaultOptions[QStringLiteral("PreservePaths")] = false; ExtractionOptions::const_iterator it = defaultOptions.constBegin(); for (; it != defaultOptions.constEnd(); ++it) { if (!m_options.contains(it.key())) { m_options[it.key()] = it.value(); } } } QString ExtractJob::destinationDirectory() const { return m_destinationDir; } ExtractionOptions ExtractJob::extractionOptions() const { return m_options; } TempExtractJob::TempExtractJob(const QString &file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : Job(interface) , m_file(file) , m_passwordProtectedHint(passwordProtectedHint) { } QString TempExtractJob::validatedFilePath() const { QString path = extractionDir() + QLatin1Char('/') + m_file; // Make sure a maliciously crafted archive with parent folders named ".." do // not cause the previewed file path to be located outside the temporary // directory, resulting in a directory traversal issue. path.remove(QStringLiteral("../")); return path; } ExtractionOptions TempExtractJob::extractionOptions() const { ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; if (m_passwordProtectedHint) { options[QStringLiteral("PasswordProtectedHint")] = true; } return options; } void TempExtractJob::doWork() { emit description(this, i18n("Extracting one file")); connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Extracting:" << m_file; bool ret = archiveInterface()->copyFiles({ QVariant::fromValue(fileRootNodePair(m_file)) }, extractionDir(), extractionOptions()); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } PreviewJob::PreviewJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(file, passwordProtectedHint, interface) { qCDebug(ARK) << "PreviewJob started"; } QString PreviewJob::extractionDir() const { return m_tmpExtractDir.path(); } OpenJob::OpenJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(file, passwordProtectedHint, interface) { qCDebug(ARK) << "OpenJob started"; m_tmpExtractDir = new QTemporaryDir(); } QTemporaryDir *OpenJob::tempDir() const { return m_tmpExtractDir; } QString OpenJob::extractionDir() const { return m_tmpExtractDir->path(); } OpenWithJob::OpenWithJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : OpenJob(file, passwordProtectedHint, interface) { qCDebug(ARK) << "OpenWithJob started"; } AddJob::AddJob(const QStringList& files, const CompressionOptions& options , ReadWriteArchiveInterface *interface) : Job(interface) , m_files(files) , m_options(options) { qCDebug(ARK) << "AddJob started"; } void AddJob::doWork() { qCDebug(ARK) << "AddJob: going to add" << m_files.count() << "file(s)"; emit description(this, i18np("Adding a file", "Adding %1 files", m_files.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); const QString globalWorkDir = m_options.value(QStringLiteral("GlobalWorkDir")).toString(); const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir); if (!globalWorkDir.isEmpty()) { qCDebug(ARK) << "GlobalWorkDir is set, changing dir to " << globalWorkDir; m_oldWorkingDir = QDir::currentPath(); QDir::setCurrent(globalWorkDir); } // The file paths must be relative to GlobalWorkDir. QStringList relativeFiles; foreach (const QString& file, m_files) { // #191821: workDir must be used instead of QDir::current() // so that symlinks aren't resolved automatically QString relativePath = workDir.relativeFilePath(file); if (file.endsWith(QLatin1Char('/'))) { relativePath += QLatin1Char('/'); } relativeFiles << relativePath; } connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addFiles(relativeFiles, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void AddJob::onFinished(bool result) { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } Job::onFinished(result); } DeleteJob::DeleteJob(const QVariantList& files, ReadWriteArchiveInterface *interface) : Job(interface) , m_files(files) { } void DeleteJob::doWork() { emit description(this, i18np("Deleting a file from the archive", "Deleting %1 files", m_files.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->deleteFiles(m_files); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface) : Job(interface) , m_comment(comment) { } void CommentJob::doWork() { emit description(this, i18n("Adding comment")); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addComment(m_comment); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } TestJob::TestJob(ReadOnlyArchiveInterface *interface) : Job(interface) { m_testSuccess = false; } void TestJob::doWork() { qCDebug(ARK) << "TestJob started"; emit description(this, i18n("Testing archive")); connectToArchiveInterfaceSignals(); connect(archiveInterface(), &ReadOnlyArchiveInterface::testSuccess, this, &TestJob::onTestSuccess); bool ret = archiveInterface()->testArchive(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void TestJob::onTestSuccess() { m_testSuccess = true; } bool TestJob::testSucceeded() { return m_testSuccess; } } // namespace Kerfuffle diff --git a/kerfuffle/jobs.h b/kerfuffle/jobs.h index 8633b876..67a75d13 100644 --- a/kerfuffle/jobs.h +++ b/kerfuffle/jobs.h @@ -1,288 +1,288 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 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 JOBS_H #define JOBS_H #include "kerfuffle_export.h" #include "archiveinterface.h" #include "archive_kerfuffle.h" #include "queries.h" #include #include #include namespace Kerfuffle { class KERFUFFLE_EXPORT Job : public KJob { Q_OBJECT public: void start(); bool isRunning() const; protected: Job(ReadOnlyArchiveInterface *interface); virtual ~Job(); virtual bool doKill(); virtual void emitResult(); ReadOnlyArchiveInterface *archiveInterface(); void connectToArchiveInterfaceSignals(); public slots: virtual void doWork() = 0; protected slots: virtual void onCancelled(); virtual void onError(const QString &message, const QString &details); virtual void onInfo(const QString &info); - virtual void onEntry(const ArchiveEntry &archiveEntry); + virtual void onEntry(Archive::Entry *entry); virtual void onProgress(double progress); virtual void onEntryRemoved(const QString &path); virtual void onFinished(bool result); virtual void onUserQuery(Query *query); signals: void entryRemoved(const QString & entry); void error(const QString& errorMessage, const QString& details); - void newEntry(const ArchiveEntry &); + void newEntry(Archive::Entry*); void userQuery(Kerfuffle::Query*); private: ReadOnlyArchiveInterface *m_archiveInterface; bool m_isRunning; QElapsedTimer jobTimer; class Private; Private * const d; }; class KERFUFFLE_EXPORT ListJob : public Job { Q_OBJECT public: explicit ListJob(ReadOnlyArchiveInterface *interface); qlonglong extractedFilesSize() const; bool isPasswordProtected() const; bool isSingleFolderArchive() const; QString subfolderName() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: bool m_isSingleFolderArchive; bool m_isPasswordProtected; QString m_subfolderName; QString m_basePath; qlonglong m_extractedFilesSize; qlonglong m_dirCount; qlonglong m_filesCount; private slots: - void onNewEntry(const ArchiveEntry&); + void onNewEntry(const Archive::Entry*); }; class KERFUFFLE_EXPORT ExtractJob : public Job { Q_OBJECT public: ExtractJob(const QVariantList& files, const QString& destinationDir, const ExtractionOptions& options, ReadOnlyArchiveInterface *interface); QString destinationDirectory() const; ExtractionOptions extractionOptions() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: // TODO: Maybe this should be a method if ExtractionOptions were a class? void setDefaultOptions(); QVariantList m_files; QString m_destinationDir; ExtractionOptions m_options; }; /** * Abstract base class for jobs that extract a single file to a temporary dir. * It's not possible to pass extraction options and paths will be always preserved. * The only option that the job needs to know is whether the file is password protected. */ class KERFUFFLE_EXPORT TempExtractJob : public Job { Q_OBJECT public: TempExtractJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); /** * @return The absolute path of the extracted file. * The path is validated in order to prevent directory traversal attacks. */ QString validatedFilePath() const; ExtractionOptions extractionOptions() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: virtual QString extractionDir() const = 0; QString m_file; bool m_passwordProtectedHint; }; /** * This TempExtractJob can be used to preview a file. * The temporary extraction directory will be deleted upon job's completion. */ class KERFUFFLE_EXPORT PreviewJob : public TempExtractJob { Q_OBJECT public: PreviewJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); private: QString extractionDir() const Q_DECL_OVERRIDE; QTemporaryDir m_tmpExtractDir; }; /** * This TempExtractJob can be used to open a file in its dedicated application. * For this reason, the temporary extraction directory will NOT be deleted upon job's completion. */ class KERFUFFLE_EXPORT OpenJob : public TempExtractJob { Q_OBJECT public: OpenJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); /** * @return The temporary dir used for the extraction. * It is safe to delete this pointer in order to remove the directory. */ QTemporaryDir *tempDir() const; private: QString extractionDir() const Q_DECL_OVERRIDE; QTemporaryDir *m_tmpExtractDir; }; class KERFUFFLE_EXPORT OpenWithJob : public OpenJob { Q_OBJECT public: OpenWithJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); }; class KERFUFFLE_EXPORT AddJob : public Job { Q_OBJECT public: AddJob(const QStringList& files, const CompressionOptions& options, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; protected slots: virtual void onFinished(bool result) Q_DECL_OVERRIDE; private: QString m_oldWorkingDir; QStringList m_files; CompressionOptions m_options; }; class KERFUFFLE_EXPORT DeleteJob : public Job { Q_OBJECT public: DeleteJob(const QVariantList& files, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QVariantList m_files; }; class KERFUFFLE_EXPORT CommentJob : public Job { Q_OBJECT public: CommentJob(const QString& comment, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QString m_comment; }; class KERFUFFLE_EXPORT TestJob : public Job { Q_OBJECT public: TestJob(ReadOnlyArchiveInterface *interface); bool testSucceeded(); public slots: virtual void doWork() Q_DECL_OVERRIDE; private slots: virtual void onTestSuccess(); private: bool m_testSuccess; }; } // namespace Kerfuffle #endif // JOBS_H diff --git a/part/archivemodel.cpp b/part/archivemodel.cpp index b8aec0ec..b7c9e128 100644 --- a/part/archivemodel.cpp +++ b/part/archivemodel.cpp @@ -1,1012 +1,889 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2010-2012 Raphael Kubo da Costa * * 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 "archivemodel.h" #include "ark_debug.h" -#include "kerfuffle/archive_kerfuffle.h" +#include "kerfuffle/archiveentry.h" #include "kerfuffle/jobs.h" -#include #include #include -#include #include #include -#include -#include #include #include using namespace Kerfuffle; -class ArchiveDirNode; - //used to speed up the loading of large archives -static ArchiveNode* s_previousMatch = Q_NULLPTR; +static Archive::Entry *s_previousMatch = Q_NULLPTR; Q_GLOBAL_STATIC(QStringList, s_previousPieces) - -// TODO: This class hierarchy needs some love. -// Having a parent take a child class as a parameter in the constructor -// should trigger one's spider-sense (TM). -class ArchiveNode -{ -public: - ArchiveNode(ArchiveDirNode *parent, const ArchiveEntry & entry) - : m_parent(parent) - { - setEntry(entry); - } - - virtual ~ArchiveNode() - { - } - - const ArchiveEntry &entry() const - { - return m_entry; - } - - void setEntry(const ArchiveEntry& entry) - { - m_entry = entry; - - const QStringList pieces = entry[FileName].toString().split(QLatin1Char( '/' ), QString::SkipEmptyParts); - m_name = pieces.isEmpty() ? QString() : pieces.last(); - - QMimeDatabase db; - if (entry[IsDirectory].toBool()) { - m_icon = QIcon::fromTheme(db.mimeTypeForName(QStringLiteral("inode/directory")).iconName()).pixmap(IconSize(KIconLoader::Small), - IconSize(KIconLoader::Small)); - } else { - m_icon = QIcon::fromTheme(db.mimeTypeForFile(m_entry[FileName].toString()).iconName()).pixmap(IconSize(KIconLoader::Small), - IconSize(KIconLoader::Small)); - } - } - - ArchiveDirNode *parent() const - { - return m_parent; - } - - int row() const; - - virtual bool isDir() const - { - return false; - } - - QPixmap icon() const - { - return m_icon; - } - - QString name() const - { - return m_name; - } - -protected: - void setIcon(const QPixmap &icon) - { - m_icon = icon; - } - -private: - ArchiveEntry m_entry; - QPixmap m_icon; - QString m_name; - ArchiveDirNode *m_parent; +/** + * Meta data related to one entry in a compressed archive. + * + * This is used for indexing entry properties as numbers + * and for determining data displaying order in part's view. + */ +enum EntryMetaDataType { + FileName, /**< The entry's file name */ + Size, /**< The entry's original size */ + CompressedSize, /**< The compressed size for the entry */ + Permissions, /**< The entry's permissions */ + Owner, /**< The user the entry belongs to */ + Group, /**< The user group the entry belongs to */ + Ratio, /**< The compression ratio for the entry */ + CRC, /**< The entry's CRC */ + Method, /**< The compression method used on the entry */ + Version, /**< The archiver version needed to extract the entry */ + Timestamp, /**< The timestamp for the current entry */ + Comment, }; - -class ArchiveDirNode: public ArchiveNode -{ -public: - ArchiveDirNode(ArchiveDirNode *parent, const ArchiveEntry & entry) - : ArchiveNode(parent, entry) - { - } - - ~ArchiveDirNode() - { - clear(); - } - - QList entries() - { - return m_entries; - } - - void setEntryAt(int index, ArchiveNode* value) - { - m_entries[index] = value; - } - - void appendEntry(ArchiveNode* entry) - { - m_entries.append(entry); - } - - void removeEntryAt(int index) - { - delete m_entries.takeAt(index); - } - - virtual bool isDir() const - { - return true; - } - - ArchiveNode* find(const QString & name) - { - foreach(ArchiveNode *node, m_entries) { - if (node && (node->name() == name)) { - return node; - } - } - return 0; - } - - ArchiveNode* findByPath(const QStringList & pieces, int index = 0) - { - if (index == pieces.count()) { - return 0; - } - - ArchiveNode *next = find(pieces.at(index)); - - if (index == pieces.count() - 1) { - return next; - } - if (next && next->isDir()) { - return static_cast(next)->findByPath(pieces, index + 1); - } - return 0; - } - - void returnDirNodes(QList *store) - { - foreach(ArchiveNode *node, m_entries) { - if (node->isDir()) { - store->prepend(static_cast(node)); - static_cast(node)->returnDirNodes(store); - } - } - } - - void clear() - { - qDeleteAll(m_entries); - m_entries.clear(); - } - -private: - QList m_entries; -}; +/** + * Mappings between column indexes and entry properties. + */ +static QMap initializePropertiesList() { + QMap propertiesList = QMap(); + propertiesList.insert(FileName, QStringLiteral("fileName")); + propertiesList.insert(Size, QStringLiteral("size")); + propertiesList.insert(CompressedSize, QStringLiteral("compressedSize")); + propertiesList.insert(Permissions, QStringLiteral("permissions")); + propertiesList.insert(Owner, QStringLiteral("owner")); + propertiesList.insert(Group, QStringLiteral("group")); + propertiesList.insert(Ratio, QStringLiteral("ratio")); + propertiesList.insert(CRC, QStringLiteral("CRC")); + propertiesList.insert(Method, QStringLiteral("method")); + propertiesList.insert(Version, QStringLiteral("version")); + propertiesList.insert(Timestamp, QStringLiteral("timestamp")); + propertiesList.insert(Comment, QStringLiteral("comment")); + return propertiesList; +} +static const QMap propertiesList = initializePropertiesList(); /** * Helper functor used by qStableSort. * * It always sorts folders before files. * * @internal */ class ArchiveModelSorter { public: ArchiveModelSorter(int column, Qt::SortOrder order) : m_sortColumn(column) , m_sortOrder(order) { } virtual ~ArchiveModelSorter() { } - inline bool operator()(const QPair &left, const QPair &right) const + inline bool operator()(const QPair &left, const QPair &right) const { if (m_sortOrder == Qt::AscendingOrder) { return lessThan(left, right); } else { return !lessThan(left, right); } } protected: - bool lessThan(const QPair &left, const QPair &right) const + bool lessThan(const QPair &left, const QPair &right) const { - const ArchiveNode * const leftNode = left.first; - const ArchiveNode * const rightNode = right.first; + const Archive::Entry * const leftEntry = left.first; + const Archive::Entry * const rightEntry = right.first; // #234373: sort folders before files - if ((leftNode->isDir()) && (!rightNode->isDir())) { + if ((leftEntry->isDir()) && (!rightEntry->isDir())) { return (m_sortOrder == Qt::AscendingOrder); - } else if ((!leftNode->isDir()) && (rightNode->isDir())) { + } else if ((!leftEntry->isDir()) && (rightEntry->isDir())) { return !(m_sortOrder == Qt::AscendingOrder); } - const QVariant &leftEntry = leftNode->entry()[m_sortColumn]; - const QVariant &rightEntry = rightNode->entry()[m_sortColumn]; + EntryMetaDataType column = static_cast(m_sortColumn); + const QVariant &leftEntryMetaData = leftEntry->property(propertiesList[column].toStdString().c_str()); + const QVariant &rightEntryMetaData = rightEntry->property(propertiesList[column].toStdString().c_str()); switch (m_sortColumn) { case FileName: - return leftNode->name() < rightNode->name(); + return leftEntry->name() < rightEntry->name(); case Size: case CompressedSize: - return leftEntry.toInt() < rightEntry.toInt(); + return leftEntryMetaData.toInt() < rightEntryMetaData.toInt(); default: - return leftEntry.toString() < rightEntry.toString(); + return leftEntryMetaData.toString() < rightEntryMetaData.toString(); } // We should not get here. Q_ASSERT(false); return false; } private: int m_sortColumn; Qt::SortOrder m_sortOrder; }; -int ArchiveNode::row() const -{ - if (parent()) { - return parent()->entries().indexOf(const_cast(this)); - } - return 0; -} - ArchiveModel::ArchiveModel(const QString &dbusPathName, QObject *parent) : QAbstractItemModel(parent) - , m_rootNode(new ArchiveDirNode(0, ArchiveEntry())) + , m_rootEntry(new Archive::Entry(Q_NULLPTR)) , m_dbusPathName(dbusPathName) { + m_rootEntry->setProperty("isDirectory", true); } ArchiveModel::~ArchiveModel() { - delete m_rootNode; - m_rootNode = 0; + delete m_rootEntry; + m_rootEntry = 0; } QVariant ArchiveModel::data(const QModelIndex &index, int role) const { if (index.isValid()) { - ArchiveNode *node = static_cast(index.internalPointer()); + Archive::Entry *entry = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: { //TODO: complete the columns - int columnId = m_showColumns.at(index.column()); - switch (columnId) { + int column = m_showColumns.at(index.column()); + switch (column) { case FileName: - return node->name(); + return entry->name(); case Size: - if (node->isDir()) { + if (entry->isDir()) { int dirs; int files; const int children = childCount(index, dirs, files); return KIO::itemsSummaryString(children, files, dirs, 0, false); - } else if (node->entry().contains(Link)) { + } else if (!entry->property("link").toString().isEmpty()) { return QVariant(); } else { - return KIO::convertSize(node->entry()[ Size ].toULongLong()); + return KIO::convertSize(entry->property("size").toULongLong()); } case CompressedSize: - if (node->isDir() || node->entry().contains(Link)) { + if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { - qulonglong compressedSize = node->entry()[ CompressedSize ].toULongLong(); + qulonglong compressedSize = entry->property("compressedSize").toULongLong(); if (compressedSize != 0) { return KIO::convertSize(compressedSize); } else { return QVariant(); } } - case Ratio: // TODO: Use node->entry()[Ratio] when available - if (node->isDir() || node->entry().contains(Link)) { + case Ratio: // TODO: Use entry->metaData()[Ratio] when available + if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { - qulonglong compressedSize = node->entry()[ CompressedSize ].toULongLong(); - qulonglong size = node->entry()[ Size ].toULongLong(); + qulonglong compressedSize = entry->property("compressedSize").toULongLong(); + qulonglong size = entry->property("size").toULongLong(); if (compressedSize == 0 || size == 0) { return QVariant(); } else { int ratio = int(100 * ((double)size - compressedSize) / size); return QString(QString::number(ratio) + QStringLiteral(" %")); } } case Timestamp: { - const QDateTime timeStamp = node->entry().value(Timestamp).toDateTime(); + const QDateTime timeStamp = entry->property("timestamp").toDateTime(); return QLocale().toString(timeStamp, QLocale::ShortFormat); } default: - return node->entry().value(columnId); + return entry->property(propertiesList[column].toStdString().c_str()); } - break; } case Qt::DecorationRole: if (index.column() == 0) { - return node->icon(); + return entry->icon(); } return QVariant(); case Qt::FontRole: { QFont f; - f.setItalic(node->entry()[ IsPasswordProtected ].toBool()); + f.setItalic(entry->property("isPasswordProtected").toBool()); return f; } default: return QVariant(); } } return QVariant(); } Qt::ItemFlags ArchiveModel::flags(const QModelIndex &index) const { Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); if (index.isValid()) { return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | defaultFlags; } return 0; } QVariant ArchiveModel::headerData(int section, Qt::Orientation, int role) const { if (role == Qt::DisplayRole) { if (section >= m_showColumns.size()) { qCDebug(ARK) << "WEIRD: showColumns.size = " << m_showColumns.size() << " and section = " << section; return QVariant(); } int columnId = m_showColumns.at(section); switch (columnId) { case FileName: return i18nc("Name of a file inside an archive", "Name"); case Size: return i18nc("Uncompressed size of a file inside an archive", "Size"); case CompressedSize: return i18nc("Compressed size of a file inside an archive", "Compressed"); case Ratio: return i18nc("Compression rate of file", "Rate"); case Owner: return i18nc("File's owner username", "Owner"); case Group: return i18nc("File's group", "Group"); case Permissions: return i18nc("File permissions", "Mode"); case CRC: return i18nc("CRC hash code", "CRC"); case Method: return i18nc("Compression method", "Method"); case Version: //TODO: what exactly is a file version? return i18nc("File version", "Version"); case Timestamp: return i18nc("Timestamp", "Date"); case Comment: return i18nc("File comment", "Comment"); default: return i18nc("Unnamed column", "??"); } } return QVariant(); } QModelIndex ArchiveModel::index(int row, int column, const QModelIndex &parent) const { if (hasIndex(row, column, parent)) { - ArchiveDirNode *parentNode = parent.isValid() ? static_cast(parent.internalPointer()) : m_rootNode; + Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : m_rootEntry; - Q_ASSERT(parentNode->isDir()); + Q_ASSERT(parentEntry->isDir()); - ArchiveNode *item = parentNode->entries().value(row, 0); + Archive::Entry *item = parentEntry->entries().value(row, 0); if (item) { return createIndex(row, column, item); } } return QModelIndex(); } QModelIndex ArchiveModel::parent(const QModelIndex &index) const { if (index.isValid()) { - ArchiveNode *item = static_cast(index.internalPointer()); + Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); - if (item->parent() && (item->parent() != m_rootNode)) { - return createIndex(item->parent()->row(), 0, item->parent()); + if (item->getParent() && (item->getParent() != m_rootEntry)) { + return createIndex(item->getParent()->row(), 0, item->getParent()); } } return QModelIndex(); } -ArchiveEntry ArchiveModel::entryForIndex(const QModelIndex &index) +Archive::Entry *ArchiveModel::entryForIndex(const QModelIndex &index) { if (index.isValid()) { - ArchiveNode *item = static_cast(index.internalPointer()); + Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); - return item->entry(); + return item; } - return ArchiveEntry(); + return Q_NULLPTR; } int ArchiveModel::childCount(const QModelIndex &index, int &dirs, int &files) const { if (index.isValid()) { dirs = files = 0; - ArchiveNode *item = static_cast(index.internalPointer()); + Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); if (item->isDir()) { - const QList entries = static_cast(item)->entries(); - foreach(const ArchiveNode *node, entries) { - if (node->isDir()) { + const QList entries = static_cast(item)->entries(); + foreach(const Archive::Entry *entry, entries) { + if (entry->isDir()) { dirs++; } else { files++; } } return entries.count(); } return 0; } return -1; } int ArchiveModel::rowCount(const QModelIndex &parent) const { if (parent.column() <= 0) { - ArchiveNode *parentNode = parent.isValid() ? static_cast(parent.internalPointer()) : m_rootNode; + Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : m_rootEntry; - if (parentNode && parentNode->isDir()) { - return static_cast(parentNode)->entries().count(); + if (parentEntry && parentEntry->isDir()) { + return static_cast(parentEntry)->entries().count(); } } return 0; } int ArchiveModel::columnCount(const QModelIndex &parent) const { return m_showColumns.size(); } void ArchiveModel::sort(int column, Qt::SortOrder order) { if (m_showColumns.size() <= column) { return; } emit layoutAboutToBeChanged(); - QList dirNodes; - m_rootNode->returnDirNodes(&dirNodes); - dirNodes.append(m_rootNode); + QList dirEntries; + m_rootEntry->returnDirEntries(&dirEntries); + dirEntries.append(m_rootEntry); const ArchiveModelSorter modelSorter(m_showColumns.at(column), order); - foreach(ArchiveDirNode* dir, dirNodes) { - QVector < QPair > sorting(dir->entries().count()); + foreach(Archive::Entry *dir, dirEntries) { + QVector < QPair > sorting(dir->entries().count()); for (int i = 0; i < dir->entries().count(); ++i) { - ArchiveNode *item = dir->entries().at(i); + Archive::Entry *item = dir->entries().at(i); sorting[i].first = item; sorting[i].second = i; } qStableSort(sorting.begin(), sorting.end(), modelSorter); QModelIndexList fromIndexes; QModelIndexList toIndexes; for (int r = 0; r < sorting.count(); ++r) { - ArchiveNode *item = sorting.at(r).first; + Archive::Entry *item = sorting.at(r).first; toIndexes.append(createIndex(r, 0, item)); fromIndexes.append(createIndex(sorting.at(r).second, 0, sorting.at(r).first)); dir->setEntryAt(r, sorting.at(r).first); } changePersistentIndexList(fromIndexes, toIndexes); emit dataChanged( - index(0, 0, indexForNode(dir)), - index(dir->entries().size() - 1, 0, indexForNode(dir))); + index(0, 0, indexForEntry(dir)), + index(dir->entries().size() - 1, 0, indexForEntry(dir))); } emit layoutChanged(); } Qt::DropActions ArchiveModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } QStringList ArchiveModel::mimeTypes() const { QStringList types; // MIME types we accept for dragging (eg. Dolphin -> Ark). types << QStringLiteral("text/uri-list") << QStringLiteral("text/plain") << QStringLiteral("text/x-moz-url"); // MIME types we accept for dropping (eg. Ark -> Dolphin). types << QStringLiteral("application/x-kde-ark-dndextract-service") << QStringLiteral("application/x-kde-ark-dndextract-path"); return types; } QMimeData *ArchiveModel::mimeData(const QModelIndexList &indexes) const { Q_UNUSED(indexes) QMimeData *mimeData = new QMimeData; mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-service"), QDBusConnection::sessionBus().baseService().toUtf8()); mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-path"), m_dbusPathName.toUtf8()); return mimeData; } bool ArchiveModel::dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) { Q_UNUSED(action) Q_UNUSED(row) Q_UNUSED(column) Q_UNUSED(parent) if (!data->hasUrls()) { return false; } QStringList paths; foreach(const QUrl &url, data->urls()) { paths << url.toLocalFile(); } //for now, this code is not used because adding files to paths inside the //archive is not supported yet. need a solution for this later. QString path; #if 0 if (parent.isValid()) { QModelIndex droppedOnto = index(row, column, parent); - if (entryForIndex(droppedOnto).value(IsDirectory).toBool()) { + Archive::Entry *entry = entryForIndex(droppedOnto); + if (entry->isDir()) { qCDebug(ARK) << "Using entry"; - path = entryForIndex(droppedOnto).value(FileName).toString(); + path = entry->fileName.toString(); } else { - path = entryForIndex(parent).value(FileName).toString(); + path = entryForIndex(parent)->fileName.toString(); } } qCDebug(ARK) << "Dropped onto " << path; #endif emit droppedFiles(paths, path); return true; } // For a rationale, see bugs #194241, #241967 and #355839 QString ArchiveModel::cleanFileName(const QString& fileName) { // Skip entries with filename "/" or "//" or "." // "." is present in ISO files QRegularExpression pattern(QStringLiteral("/+|\\.")); QRegularExpressionMatch match; if (fileName.contains(pattern, &match) && match.captured() == fileName) { qCDebug(ARK) << "Skipping entry with filename" << fileName; return QString(); } else if (fileName.startsWith(QLatin1String("./"))) { return fileName.mid(2); } return fileName; } -ArchiveDirNode* ArchiveModel::parentFor(const ArchiveEntry& entry) +Archive::Entry *ArchiveModel::parentFor(const Archive::Entry *entry) { - QStringList pieces = entry[ FileName ].toString().split(QLatin1Char( '/' ), QString::SkipEmptyParts); + QStringList pieces = entry->property("fileName").toString().split(QLatin1Char( '/' ), QString::SkipEmptyParts); if (pieces.isEmpty()) { return Q_NULLPTR; } pieces.removeLast(); if (s_previousMatch) { //the number of path elements must be the same for the shortcut //to work if (s_previousPieces->count() == pieces.count()) { bool equal = true; //make sure all the pieces match up for (int i = 0; i < s_previousPieces->count(); ++i) { if (s_previousPieces->at(i) != pieces.at(i)) { equal = false; break; } } //if match return it if (equal) { - return static_cast(s_previousMatch); + return s_previousMatch; } } } - ArchiveDirNode *parent = m_rootNode; + Archive::Entry *parent = m_rootEntry; foreach(const QString &piece, pieces) { - ArchiveNode *node = parent->find(piece); - if (!node) { - ArchiveEntry e; - e[ FileName ] = (parent == m_rootNode) ? - piece : parent->entry()[ FileName ].toString() + QLatin1Char( '/' ) + piece; - e[ InternalID ] = e.value(FileName); - e[ IsDirectory ] = true; - node = new ArchiveDirNode(parent, e); - insertNode(node); + Archive::Entry *entry = parent->find(piece); + if (!entry) { + // Directory entry will be traversed later (that happens for some archive formats, 7z for instance). + // We have to create one before, in order to construct tree from its children, + // and then delete the existing one (see ArchiveModel::newEntry). + entry = new Archive::Entry(parent); + + entry->setProperty("fileName", (parent == m_rootEntry) + ? piece + : parent->property("fileName").toString() + QLatin1Char( '/' ) + piece); + entry->setProperty("isDirectory", true); + entry->processNameAndIcon(); + insertEntry(entry); } - if (!node->isDir()) { - ArchiveEntry e(node->entry()); - node = new ArchiveDirNode(parent, e); - //Maybe we have both a file and a directory of the same name - // We avoid removing previous entries unless necessary - insertNode(node); + if (!entry->isDir()) { + Archive::Entry *e = new Archive::Entry(parent); + copyEntryMetaData(e, entry); + e->processNameAndIcon(); + // Maybe we have both a file and a directory of the same name. + // We avoid removing previous entries unless necessary. + insertEntry(e); } - parent = static_cast(node); + parent = entry; } s_previousMatch = parent; *s_previousPieces = pieces; return parent; } -QModelIndex ArchiveModel::indexForNode(ArchiveNode *node) + +QModelIndex ArchiveModel::indexForEntry(Archive::Entry *entry) { - Q_ASSERT(node); - if (node != m_rootNode) { - Q_ASSERT(node->parent()); - Q_ASSERT(node->parent()->isDir()); - return createIndex(node->row(), 0, node); + Q_ASSERT(entry); + if (entry != m_rootEntry) { + Q_ASSERT(entry->getParent()); + Q_ASSERT(entry->getParent()->isDir()); + return createIndex(entry->row(), 0, entry); } return QModelIndex(); } void ArchiveModel::slotEntryRemoved(const QString & path) { const QString entryFileName(cleanFileName(path)); if (entryFileName.isEmpty()) { return; } - ArchiveNode *entry = m_rootNode->findByPath(entryFileName.split(QLatin1Char( '/' ), QString::SkipEmptyParts)); + Archive::Entry *entry = m_rootEntry->findByPath(entryFileName.split(QLatin1Char( '/' ), QString::SkipEmptyParts)); if (entry) { - ArchiveDirNode *parent = entry->parent(); - QModelIndex index = indexForNode(entry); + Archive::Entry *parent = entry->getParent(); + QModelIndex index = indexForEntry(entry); Q_UNUSED(index); - beginRemoveRows(indexForNode(parent), entry->row(), entry->row()); + beginRemoveRows(indexForEntry(parent), entry->row(), entry->row()); - //delete parent->entries()[ entry->row() ]; - //parent->entries()[ entry->row() ] = 0; + //delete parent->entries()[ metaData->row() ]; + //parent->entries()[ metaData->row() ] = 0; parent->removeEntryAt(entry->row()); endRemoveRows(); } } void ArchiveModel::slotUserQuery(Kerfuffle::Query *query) { query->execute(); } -void ArchiveModel::slotNewEntryFromSetArchive(const ArchiveEntry& entry) +void ArchiveModel::slotNewEntryFromSetArchive(Archive::Entry *entry) { // we cache all entries that appear when opening a new archive // so we can all them together once it's done, this is a huge // performance improvement because we save from doing lots of // begin/endInsertRows m_newArchiveEntries.push_back(entry); } -void ArchiveModel::slotNewEntry(const ArchiveEntry& entry) +void ArchiveModel::slotNewEntry(Archive::Entry *entry) { newEntry(entry, NotifyViews); } -void ArchiveModel::newEntry(const ArchiveEntry& receivedEntry, InsertBehaviour behaviour) +void ArchiveModel::newEntry(Archive::Entry *receivedEntry, InsertBehaviour behaviour) { - if (receivedEntry[FileName].toString().isEmpty()) { + if (receivedEntry->property("fileName").toString().isEmpty()) { qCDebug(ARK) << "Weird, received empty entry (no filename) - skipping"; return; } //if there are no addidional columns registered, then have a look at the //entry and populate some if (m_showColumns.isEmpty()) { - //these are the columns we are interested in showing in the display - static const QList columnsForDisplay = - QList() - << FileName - << Size - << CompressedSize - << Permissions - << Owner - << Group - << Ratio - << CRC - << Method - << Version - << Timestamp - << Comment; - QList toInsert; - foreach(int column, columnsForDisplay) { - if (receivedEntry.contains(column)) { - toInsert << column; + QMap::const_iterator i = propertiesList.begin(); + while (i != propertiesList.end()) { + if (!receivedEntry->property(i.value().toStdString().c_str()).toString().isEmpty()) { + if (i.key() != CompressedSize || receivedEntry->compressedSizeIsSet) { + toInsert << i.key(); + } } + ++i; } beginInsertColumns(QModelIndex(), 0, toInsert.size() - 1); m_showColumns << toInsert; endInsertColumns(); qCDebug(ARK) << "Showing columns: " << m_showColumns; } - //make a copy - ArchiveEntry entry = receivedEntry; - //#194241: Filenames such as "./file" should be displayed as "file" //#241967: Entries called "/" should be ignored //#355839: Entries called "//" should be ignored - QString entryFileName = cleanFileName(entry[FileName].toString()); + QString entryFileName = cleanFileName(receivedEntry->property("fileName").toString()); if (entryFileName.isEmpty()) { // The entry contains only "." or "./" return; } - entry[FileName] = entryFileName; + receivedEntry->setProperty("fileName", entryFileName); - /// 1. Skip already created nodes - if (m_rootNode) { - ArchiveNode *existing = m_rootNode->findByPath(entry[ FileName ].toString().split(QLatin1Char( '/' ))); + /// 1. Skip already created entries + if (m_rootEntry) { + Archive::Entry *existing = m_rootEntry->findByPath(entryFileName.split(QLatin1Char( '/' ))); if (existing) { - qCDebug(ARK) << "Refreshing entry for" << entry[FileName].toString(); + qCDebug(ARK) << "Refreshing entry for" << entryFileName; + existing->setProperty("fileName", entryFileName); // Multi-volume files are repeated at least in RAR archives. // In that case, we need to sum the compressed size for each volume - qulonglong currentCompressedSize = existing->entry()[CompressedSize].toULongLong(); - entry[CompressedSize] = currentCompressedSize + entry[CompressedSize].toULongLong(); - - //TODO: benchmark whether it's a bad idea to reset the entry here. - existing->setEntry(entry); + qulonglong currentCompressedSize = existing->property("compressedSize").toULongLong(); + existing->setProperty("compressedSize", currentCompressedSize + receivedEntry->property("compressedSize").toULongLong()); + existing->processNameAndIcon(); return; } } - /// 2. Find Parent Node, creating missing ArchiveDirNodes in the process - ArchiveDirNode *parent = parentFor(entry); + /// 2. Find Parent Entry, creating missing direcotry ArchiveEntries in the process + Archive::Entry *parent = parentFor(receivedEntry); - /// 3. Create an ArchiveNode - const QStringList path = entry[FileName].toString().split(QLatin1Char('/'), QString::SkipEmptyParts); + /// 3. Create an Archive::Entry + const QStringList path = entryFileName.split(QLatin1Char('/'), QString::SkipEmptyParts); const QString name = path.last(); - ArchiveNode *node = parent->find(name); - if (node) { - node->setEntry(entry); + Archive::Entry *entry = parent->find(name); + if (entry) { + copyEntryMetaData(entry, receivedEntry); + entry->setProperty("fileName", entryFileName); + entry->processNameAndIcon(); + delete receivedEntry; } else { - if (entry[ FileName ].toString().endsWith(QLatin1Char( '/' )) || (entry.contains(IsDirectory) && entry[ IsDirectory ].toBool())) { - node = new ArchiveDirNode(parent, entry); - } else { - node = new ArchiveNode(parent, entry); - } - insertNode(node, behaviour); + receivedEntry->setParent(parent); + receivedEntry->processNameAndIcon(); + insertEntry(receivedEntry, behaviour); } } void ArchiveModel::slotLoadingFinished(KJob *job) { int i = 0; - foreach(const ArchiveEntry &entry, m_newArchiveEntries) { + foreach(Archive::Entry *entry, m_newArchiveEntries) { newEntry(entry, DoNotNotifyViews); i++; } beginResetModel(); endResetModel(); m_newArchiveEntries.clear(); qCDebug(ARK) << "Added" << i << "entries to model"; emit loadingFinished(job); } -void ArchiveModel::insertNode(ArchiveNode *node, InsertBehaviour behaviour) +void ArchiveModel::copyEntryMetaData(Archive::Entry *destinationEntry, const Archive::Entry *sourceEntry) { - Q_ASSERT(node); - ArchiveDirNode *parent = node->parent(); + destinationEntry->setProperty("fileName", sourceEntry->property("fileName")); + destinationEntry->setProperty("permissions", sourceEntry->property("permissions")); + destinationEntry->setProperty("owner", sourceEntry->property("owner")); + destinationEntry->setProperty("group", sourceEntry->property("group")); + destinationEntry->setProperty("size", sourceEntry->property("size")); + destinationEntry->setProperty("compressedSize", sourceEntry->property("compressedSize")); + destinationEntry->setProperty("link", sourceEntry->property("link")); + destinationEntry->setProperty("ratio", sourceEntry->property("ratio")); + destinationEntry->setProperty("CRC", sourceEntry->property("CRC")); + destinationEntry->setProperty("method", sourceEntry->property("method")); + destinationEntry->setProperty("version", sourceEntry->property("version")); + destinationEntry->setProperty("timestamp", sourceEntry->property("timestamp").toDateTime()); + destinationEntry->setProperty("isDirectory", sourceEntry->property("isDirectory")); + destinationEntry->setProperty("comment", sourceEntry->property("comment")); + destinationEntry->setProperty("isPasswordProtected", sourceEntry->property("isPasswordProtected")); +} + +void ArchiveModel::insertEntry(Archive::Entry *entry, InsertBehaviour behaviour) +{ + Q_ASSERT(entry); + Archive::Entry *parent = entry->getParent(); Q_ASSERT(parent); if (behaviour == NotifyViews) { - beginInsertRows(indexForNode(parent), parent->entries().count(), parent->entries().count()); + beginInsertRows(indexForEntry(parent), parent->entries().count(), parent->entries().count()); } - parent->appendEntry(node); + parent->appendEntry(entry); if (behaviour == NotifyViews) { endInsertRows(); } } Kerfuffle::Archive* ArchiveModel::archive() const { return m_archive.data(); } KJob* ArchiveModel::setArchive(Kerfuffle::Archive *archive) { m_archive.reset(archive); - m_rootNode->clear(); + m_rootEntry->clear(); s_previousMatch = Q_NULLPTR; s_previousPieces->clear(); Kerfuffle::ListJob *job = Q_NULLPTR; m_newArchiveEntries.clear(); if (m_archive) { job = m_archive->list(); // TODO: call "open" or "create"? if (job) { connect(job, &Kerfuffle::ListJob::newEntry, this, &ArchiveModel::slotNewEntryFromSetArchive); connect(job, &Kerfuffle::ListJob::result, this, &ArchiveModel::slotLoadingFinished); connect(job, &Kerfuffle::ListJob::userQuery, this, &ArchiveModel::slotUserQuery); emit loadingStarted(); // TODO: make sure if it's ok to not have calls to beginRemoveColumns here m_showColumns.clear(); } } beginResetModel(); endResetModel(); return job; } ExtractJob* ArchiveModel::extractFile(const QVariant& fileName, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { QList files; files << QVariant::fromValue(fileRootNodePair(fileName.toString())); return extractFiles(files, destinationDir, options); } ExtractJob* ArchiveModel::extractFiles(const QList& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { Q_ASSERT(m_archive); ExtractJob *newJob = m_archive->copyFiles(files, destinationDir, options); connect(newJob, &ExtractJob::userQuery, this, &ArchiveModel::slotUserQuery); return newJob; } Kerfuffle::PreviewJob *ArchiveModel::preview(const QString& file) const { Q_ASSERT(m_archive); PreviewJob *job = m_archive->preview(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } OpenJob *ArchiveModel::open(const QString& file) const { Q_ASSERT(m_archive); OpenJob *job = m_archive->open(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } OpenWithJob *ArchiveModel::openWith(const QString& file) const { Q_ASSERT(m_archive); OpenWithJob *job = m_archive->openWith(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } AddJob* ArchiveModel::addFiles(const QStringList & filenames, const CompressionOptions& options) { if (!m_archive) { return Q_NULLPTR; } if (!m_archive->isReadOnly()) { AddJob *job = m_archive->addFiles(filenames, options); connect(job, &AddJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &AddJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return Q_NULLPTR; } DeleteJob* ArchiveModel::deleteFiles(const QList & files) { Q_ASSERT(m_archive); if (!m_archive->isReadOnly()) { DeleteJob *job = m_archive->deleteFiles(files); connect(job, &DeleteJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved); connect(job, &DeleteJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs); connect(job, &DeleteJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return Q_NULLPTR; } void ArchiveModel::encryptArchive(const QString &password, bool encryptHeader) { if (!m_archive) { return; } m_archive->encrypt(password, encryptHeader); } void ArchiveModel::slotCleanupEmptyDirs() { QList queue; QList nodesToDelete; //add root nodes for (int i = 0; i < rowCount(); ++i) { queue.append(QPersistentModelIndex(index(i, 0))); } //breadth-first traverse while (!queue.isEmpty()) { QPersistentModelIndex node = queue.takeFirst(); - ArchiveEntry entry = entryForIndex(node); + Archive::Entry *entry = entryForIndex(node); if (!hasChildren(node)) { - if (!entry.contains(InternalID)) { + if (!entry->property("fileName").toString().isEmpty()) { nodesToDelete << node; } } else { for (int i = 0; i < rowCount(node); ++i) { queue.append(QPersistentModelIndex(index(i, 0, node))); } } } foreach(const QPersistentModelIndex& node, nodesToDelete) { - ArchiveNode *rawNode = static_cast(node.internalPointer()); - qCDebug(ARK) << "Delete with parent entries " << rawNode->parent()->entries() << " and row " << rawNode->row(); - beginRemoveRows(parent(node), rawNode->row(), rawNode->row()); - rawNode->parent()->removeEntryAt(rawNode->row()); + Archive::Entry *rawEntry = static_cast(node.internalPointer()); + qCDebug(ARK) << "Delete with parent entries " << rawEntry->getParent()->entries() << " and row " << rawEntry->row(); + beginRemoveRows(parent(node), rawEntry->row(), rawEntry->row()); + rawEntry->getParent()->removeEntryAt(rawEntry->row()); endRemoveRows(); } } - - diff --git a/part/archivemodel.h b/part/archivemodel.h index 9a2b551e..a9227311 100644 --- a/part/archivemodel.h +++ b/part/archivemodel.h @@ -1,135 +1,133 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * * 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 ARCHIVEMODEL_H #define ARCHIVEMODEL_H #include #include #include #include "kerfuffle/archive_kerfuffle.h" -using Kerfuffle::ArchiveEntry; +using Kerfuffle::Archive; namespace Kerfuffle { class Query; } -class ArchiveNode; -class ArchiveDirNode; - class ArchiveModel: public QAbstractItemModel { Q_OBJECT public: explicit ArchiveModel(const QString &dbusPathName, QObject *parent = 0); ~ArchiveModel(); QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; virtual void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) Q_DECL_OVERRIDE; //drag and drop related Qt::DropActions supportedDropActions() const Q_DECL_OVERRIDE; QStringList mimeTypes() const Q_DECL_OVERRIDE; - QMimeData * mimeData(const QModelIndexList & indexes) const Q_DECL_OVERRIDE; + QMimeData *mimeData(const QModelIndexList & indexes) const Q_DECL_OVERRIDE; bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) Q_DECL_OVERRIDE; KJob* setArchive(Kerfuffle::Archive *archive); Kerfuffle::Archive *archive() const; - Kerfuffle::ArchiveEntry entryForIndex(const QModelIndex &index); + Archive::Entry *entryForIndex(const QModelIndex &index); int childCount(const QModelIndex &index, int &dirs, int &files) const; Kerfuffle::ExtractJob* extractFile(const QVariant& fileName, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; Kerfuffle::ExtractJob* extractFiles(const QList& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; Kerfuffle::PreviewJob* preview(const QString& file) const; Kerfuffle::OpenJob* open(const QString& file) const; Kerfuffle::OpenWithJob* openWith(const QString& file) const; Kerfuffle::AddJob* addFiles(const QStringList & paths, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::DeleteJob* deleteFiles(const QList & files); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ void encryptArchive(const QString &password, bool encryptHeader); signals: void loadingStarted(); void loadingFinished(KJob *); void extractionFinished(bool success); void error(const QString& error, const QString& details); void droppedFiles(const QStringList& files, const QString& path = QString()); private slots: - void slotNewEntryFromSetArchive(const ArchiveEntry& entry); - void slotNewEntry(const ArchiveEntry& entry); + void slotNewEntryFromSetArchive(Archive::Entry *entry); + void slotNewEntry(Archive::Entry *entry); void slotLoadingFinished(KJob *job); void slotEntryRemoved(const QString & path); void slotUserQuery(Kerfuffle::Query *query); void slotCleanupEmptyDirs(); private: /** * Strips file names that start with './'. * * For more information, see bug 194241. * * @param fileName The file name that will be stripped. * * @return @p fileName without the leading './' */ QString cleanFileName(const QString& fileName); - ArchiveDirNode* parentFor(const Kerfuffle::ArchiveEntry& entry); - QModelIndex indexForNode(ArchiveNode *node); + Archive::Entry *parentFor(const Kerfuffle::Archive::Entry *entry); + QModelIndex indexForEntry(Archive::Entry *entry); static bool compareAscending(const QModelIndex& a, const QModelIndex& b); static bool compareDescending(const QModelIndex& a, const QModelIndex& b); /** * Insert the node @p node into the model, ensuring all views are notified * of the change. */ enum InsertBehaviour { NotifyViews, DoNotNotifyViews }; - void insertNode(ArchiveNode *node, InsertBehaviour behaviour = NotifyViews); - void newEntry(const Kerfuffle::ArchiveEntry& entry, InsertBehaviour behaviour); + void copyEntryMetaData(Archive::Entry *destinationEntry, const Archive::Entry *sourceEntry); + void insertEntry(Archive::Entry *entry, InsertBehaviour behaviour = NotifyViews); + void newEntry(Kerfuffle::Archive::Entry *receivedEntry, InsertBehaviour behaviour); - QList m_newArchiveEntries; // holds entries from opening a new archive until it's totally open + QList m_newArchiveEntries; // holds entries from opening a new archive until it's totally open QList m_showColumns; QScopedPointer m_archive; - ArchiveDirNode *m_rootNode; + Archive::Entry *m_rootEntry; QString m_dbusPathName; }; #endif // ARCHIVEMODEL_H diff --git a/part/infopanel.cpp b/part/infopanel.cpp index d93a0b36..55a8a560 100644 --- a/part/infopanel.cpp +++ b/part/infopanel.cpp @@ -1,211 +1,206 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * * 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 "infopanel.h" -#include "ark_debug.h" -#include "kerfuffle/archive_kerfuffle.h" +#include "kerfuffle/archiveentry.h" -#include -#include #include -#include #include -#include #include using namespace Kerfuffle; static QPixmap getDesktopIconForName(const QString& name) { return QIcon::fromTheme(name).pixmap(IconSize(KIconLoader::Desktop), IconSize(KIconLoader::Desktop)); } InfoPanel::InfoPanel(ArchiveModel *model, QWidget *parent) : QFrame(parent), m_model(model) { setupUi(this); // Make the file name font bigger than the rest QFont fnt = fileName->font(); if (fnt.pointSize() > -1) { fnt.setPointSize(fnt.pointSize() + 1); } else { fnt.setPixelSize(fnt.pixelSize() + 3); } fileName->setFont(fnt); updateWithDefaults(); } InfoPanel::~InfoPanel() { } void InfoPanel::updateWithDefaults() { iconLabel->setPixmap(getDesktopIconForName(QStringLiteral("utilities-file-archiver"))); const QString currentFileName = prettyFileName(); if (currentFileName.isEmpty()) { fileName->setText(i18n("No archive loaded")); } else { fileName->setText(currentFileName); } additionalInfo->setText(QString()); hideMetaData(); } QString InfoPanel::prettyFileName() const { if (m_prettyFileName.isEmpty()) { if (m_model->archive()) { QFileInfo fileInfo(m_model->archive()->fileName()); return fileInfo.fileName(); } } return m_prettyFileName; } void InfoPanel::setPrettyFileName(const QString& fileName) { m_prettyFileName = fileName; } void InfoPanel::setIndex(const QModelIndex& index) { if (!index.isValid()) { updateWithDefaults(); } else { - const ArchiveEntry& entry = m_model->entryForIndex(index); + const Archive::Entry *entry = m_model->entryForIndex(index); QMimeDatabase db; QMimeType mimeType; - if (entry[ IsDirectory ].toBool()) { + if (entry->isDir()) { mimeType = db.mimeTypeForName(QStringLiteral("inode/directory")); } else { - mimeType = db.mimeTypeForFile(entry[ FileName ].toString(), QMimeDatabase::MatchExtension); + mimeType = db.mimeTypeForFile(entry->property("fileName").toString(), QMimeDatabase::MatchExtension); } iconLabel->setPixmap(getDesktopIconForName(mimeType.iconName())); - if (entry[ IsDirectory ].toBool()) { + if (entry->isDir()) { int dirs; int files; const int children = m_model->childCount(index, dirs, files); additionalInfo->setText(KIO::itemsSummaryString(children, files, dirs, 0, false)); - } else if (entry.contains(Link)) { + } else if (!entry->property("link").toString().isEmpty()) { additionalInfo->setText(i18n("Symbolic Link")); } else { - if (entry.contains(Size)) { - additionalInfo->setText(KIO::convertSize(entry[ Size ].toULongLong())); + if (entry->property("size") != 0) { + additionalInfo->setText(KIO::convertSize(entry->property("size").toULongLong())); } else { additionalInfo->setText(i18n("Unknown size")); } } - const QStringList nameParts = entry[ FileName ].toString().split(QLatin1Char( '/' ), QString::SkipEmptyParts); - const QString name = (nameParts.count() > 0) ? nameParts.last() : entry[ FileName ].toString(); + const QStringList nameParts = entry->property("fileName").toString().split(QLatin1Char( '/' ), QString::SkipEmptyParts); + const QString name = (nameParts.count() > 0) ? nameParts.last() : entry->property("fileName").toString(); fileName->setText(name); showMetaDataFor(index); } } void InfoPanel::setIndexes(const QModelIndexList &list) { if (list.size() == 0) { setIndex(QModelIndex()); } else if (list.size() == 1) { setIndex(list[ 0 ]); } else { iconLabel->setPixmap(getDesktopIconForName(QStringLiteral("utilities-file-archiver"))); fileName->setText(i18np("One file selected", "%1 files selected", list.size())); quint64 totalSize = 0; foreach(const QModelIndex& index, list) { - const ArchiveEntry& entry = m_model->entryForIndex(index); - totalSize += entry[ Size ].toULongLong(); + const Archive::Entry *entry = m_model->entryForIndex(index); + totalSize += entry->property("size").toULongLong(); } additionalInfo->setText(KIO::convertSize(totalSize)); hideMetaData(); } } void InfoPanel::showMetaData() { m_separator->show(); m_metaDataWidget->show(); } void InfoPanel::hideMetaData() { m_separator->hide(); m_metaDataWidget->hide(); } void InfoPanel::showMetaDataFor(const QModelIndex &index) { showMetaData(); - const ArchiveEntry& entry = m_model->entryForIndex(index); + const Archive::Entry *entry = m_model->entryForIndex(index); QMimeDatabase db; QMimeType mimeType; - if (entry[ IsDirectory ].toBool()) { + if (entry->isDir()) { mimeType = db.mimeTypeForName(QStringLiteral("inode/directory")); } else { - mimeType = db.mimeTypeForFile(entry[FileName].toString(), QMimeDatabase::MatchExtension); + mimeType = db.mimeTypeForFile(entry->property("fileName").toString(), QMimeDatabase::MatchExtension); } m_typeLabel->setText(i18n("Type: %1", mimeType.comment())); - if (entry.contains(Owner)) { + if (!entry->property("owner").toString().isEmpty()) { m_ownerLabel->show(); - m_ownerLabel->setText(i18n("Owner: %1", entry[Owner].toString())); + m_ownerLabel->setText(i18n("Owner: %1", entry->property("owner").toString())); } else { m_ownerLabel->hide(); } - if (entry.contains(Group)) { + if (!entry->property("group").toString().isEmpty()) { m_groupLabel->show(); - m_groupLabel->setText(i18n("Group: %1", entry[Group].toString())); + m_groupLabel->setText(i18n("Group: %1", entry->property("group").toString())); } else { m_groupLabel->hide(); } - if (entry.contains(Link)) { + if (!entry->property("link").toString().isEmpty()) { m_targetLabel->show(); - m_targetLabel->setText(i18n("Target: %1", entry[Link].toString())); + m_targetLabel->setText(i18n("Target: %1", entry->property("link").toString())); } else { m_targetLabel->hide(); } - if (entry.contains(IsPasswordProtected) && entry[ IsPasswordProtected ].toBool()) { + if (entry->property("isPasswordProtected").toBool()) { m_passwordLabel->show(); m_passwordLabel->setText(i18n("Password protected: Yes")); } else { m_passwordLabel->hide(); } } diff --git a/part/part.cpp b/part/part.cpp index abd6e8fd..fb486f0b 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1,1371 +1,1370 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2009-2012 Raphael Kubo da Costa * * 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 "part.h" #include "ark_debug.h" #include "archiveformat.h" #include "archivemodel.h" #include "archiveview.h" #include "arkviewer.h" #include "dnddbusinterfaceadaptor.h" #include "infopanel.h" #include "jobtracker.h" -#include "kerfuffle/archive_kerfuffle.h" +#include "kerfuffle/archiveentry.h" #include "kerfuffle/extractiondialog.h" #include "kerfuffle/extractionsettingspage.h" #include "kerfuffle/jobs.h" #include "kerfuffle/settings.h" #include "kerfuffle/previewsettingspage.h" #include "kerfuffle/propertiesdialog.h" #include "pluginmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY(Factory, registerPlugin();) namespace Ark { static quint32 s_instanceCounter = 1; Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList& args) : KParts::ReadWritePart(parent), m_splitter(Q_NULLPTR), m_busy(false), m_jobTracker(Q_NULLPTR) { Q_UNUSED(args) setComponentData(*createAboutData(), false); new DndExtractAdaptor(this); const QString pathName = QStringLiteral("/DndExtract/%1").arg(s_instanceCounter++); if (!QDBusConnection::sessionBus().registerObject(pathName, this)) { qCCritical(ARK) << "Could not register a D-Bus object for drag'n'drop"; } // m_vlayout is needed for later insertion of QMessageWidget QWidget *mainWidget = new QWidget; m_vlayout = new QVBoxLayout; m_model = new ArchiveModel(pathName, this); m_splitter = new QSplitter(Qt::Horizontal, parentWidget); m_view = new ArchiveView; m_infoPanel = new InfoPanel(m_model); // Add widgets for the comment field. m_commentView = new QPlainTextEdit(); m_commentView->setReadOnly(true); m_commentView->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_commentBox = new QGroupBox(i18n("Comment")); m_commentBox->hide(); QVBoxLayout *vbox = new QVBoxLayout; vbox->addWidget(m_commentView); m_commentBox->setLayout(vbox); m_commentMsgWidget = new KMessageWidget(); m_commentMsgWidget->setText(i18n("Comment has been modified.")); m_commentMsgWidget->setMessageType(KMessageWidget::Information); m_commentMsgWidget->setCloseButtonVisible(false); m_commentMsgWidget->hide(); QAction *saveAction = new QAction(i18n("Save"), m_commentMsgWidget); m_commentMsgWidget->addAction(saveAction); connect(saveAction, &QAction::triggered, this, &Part::slotAddComment); m_commentBox->layout()->addWidget(m_commentMsgWidget); connect(m_commentView, &QPlainTextEdit::textChanged, this, &Part::slotCommentChanged); setWidget(mainWidget); mainWidget->setLayout(m_vlayout); // Configure the QVBoxLayout and add widgets m_vlayout->setContentsMargins(0,0,0,0); m_vlayout->addWidget(m_splitter); // Vertical QSplitter for the file view and comment field. m_commentSplitter = new QSplitter(Qt::Vertical, parentWidget); m_commentSplitter->setOpaqueResize(false); m_commentSplitter->addWidget(m_view); m_commentSplitter->addWidget(m_commentBox); m_commentSplitter->setCollapsible(0, false); // Horizontal QSplitter for the file view and infopanel. m_splitter->addWidget(m_commentSplitter); m_splitter->addWidget(m_infoPanel); // Read settings from config file and show/hide infoPanel. if (!ArkSettings::showInfoPanel()) { m_infoPanel->hide(); } else { m_splitter->setSizes(ArkSettings::splitterSizes()); } setupView(); setupActions(); connect(m_model, &ArchiveModel::loadingStarted, this, &Part::slotLoadingStarted); connect(m_model, &ArchiveModel::loadingFinished, this, &Part::slotLoadingFinished); connect(m_model, &ArchiveModel::droppedFiles, this, static_cast(&Part::slotAddFiles)); connect(m_model, &ArchiveModel::error, this, &Part::slotError); connect(this, &Part::busy, this, &Part::setBusyGui); connect(this, &Part::ready, this, &Part::setReadyGui); connect(this, static_cast(&KParts::ReadOnlyPart::completed), this, &Part::setFileNameFromArchive); m_statusBarExtension = new KParts::StatusBarExtension(this); setXMLFile(QStringLiteral("ark_part.rc")); } Part::~Part() { qDeleteAll(m_tmpOpenDirList); // Only save splitterSizes if infopanel is visible, // because we don't want to store zero size for infopanel. if (m_showInfoPanelAction->isChecked()) { ArkSettings::setSplitterSizes(m_splitter->sizes()); } ArkSettings::setShowInfoPanel(m_showInfoPanelAction->isChecked()); ArkSettings::self()->save(); m_extractArchiveAction->menu()->deleteLater(); m_extractAction->menu()->deleteLater(); } void Part::slotCommentChanged() { if (m_commentMsgWidget->isHidden() && m_commentView->toPlainText() != m_model->archive()->comment()) { m_commentMsgWidget->animatedShow(); } else if (m_commentMsgWidget->isVisible() && m_commentView->toPlainText() == m_model->archive()->comment()) { m_commentMsgWidget->hide(); } } KAboutData *Part::createAboutData() { return new KAboutData(QStringLiteral("ark"), i18n("ArkPart"), QStringLiteral("3.0")); } void Part::registerJob(KJob* job) { if (!m_jobTracker) { m_jobTracker = new JobTracker(widget()); m_statusBarExtension->addStatusBarItem(m_jobTracker->widget(0), 0, true); m_jobTracker->widget(job)->show(); } m_jobTracker->registerJob(job); emit busy(); connect(job, &KJob::result, this, &Part::ready); } // TODO: KIO::mostLocalHere is used here to resolve some KIO URLs to local // paths (e.g. desktop:/), but more work is needed to support extraction // to non-local destinations. See bugs #189322 and #204323. void Part::extractSelectedFilesTo(const QString& localPath) { if (!m_model) { return; } const QUrl url = QUrl::fromUserInput(localPath, QString()); KIO::StatJob* statJob = nullptr; // Try to resolve the URL to a local path. if (!url.isLocalFile() && !url.scheme().isEmpty()) { statJob = KIO::mostLocalUrl(url); if (!statJob->exec() || statJob->error() != 0) { return; } } const QString destination = statJob ? statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : localPath; delete statJob; // The URL could not be resolved to a local path. if (!url.isLocalFile() && destination.isEmpty()) { qCWarning(ARK) << "Ark cannot extract to non-local destination:" << localPath; KMessageBox::sorry(widget(), xi18nc("@info", "Ark can only extract to local destinations.")); return; } qCDebug(ARK) << "Extract to" << destination; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; options[QStringLiteral("RemoveRootNode")] = true; options[QStringLiteral("DragAndDrop")] = true; // Create and start the ExtractJob. ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())), destination, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } void Part::setupView() { m_view->setContextMenuPolicy(Qt::CustomContextMenu); m_view->setModel(m_model); m_view->setSortingEnabled(true); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::updateActions); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::selectionChanged); connect(m_view, &QTreeView::activated, this, &Part::slotActivated); connect(m_view, &QWidget::customContextMenuRequested, this, &Part::slotShowContextMenu); connect(m_model, &QAbstractItemModel::columnsInserted, this, &Part::adjustColumns); } void Part::slotActivated(QModelIndex) { // The activated signal is emitted when items are selected with the mouse, // so do nothing if CTRL or SHIFT key is pressed. if (QGuiApplication::keyboardModifiers() != Qt::ShiftModifier && QGuiApplication::keyboardModifiers() != Qt::ControlModifier) { ArkSettings::defaultOpenAction() == ArkSettings::EnumDefaultOpenAction::Preview ? slotOpenEntry(Preview) : slotOpenEntry(OpenFile); } } void Part::setupActions() { // We use a QSignalMapper for the preview, open and openwith actions. This // way we can connect all three actions to the same slot slotOpenEntry and // pass the OpenFileMode as argument to the slot. m_signalMapper = new QSignalMapper; m_showInfoPanelAction = new KToggleAction(i18nc("@action:inmenu", "Show information panel"), this); actionCollection()->addAction(QStringLiteral( "show-infopanel" ), m_showInfoPanelAction); m_showInfoPanelAction->setChecked(ArkSettings::showInfoPanel()); connect(m_showInfoPanelAction, &QAction::triggered, this, &Part::slotToggleInfoPanel); m_saveAsAction = actionCollection()->addAction(KStandardAction::SaveAs, QStringLiteral("ark_file_save_as"), this, SLOT(slotSaveAs())); m_openFileAction = actionCollection()->addAction(QStringLiteral("openfile")); m_openFileAction->setText(i18nc("open a file with external program", "&Open")); m_openFileAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with the associated application")); connect(m_openFileAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_openFileAction, OpenFile); m_openFileWithAction = actionCollection()->addAction(QStringLiteral("openfilewith")); m_openFileWithAction->setText(i18nc("open a file with external program", "Open &With...")); m_openFileWithAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileWithAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with an external program")); connect(m_openFileWithAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_openFileWithAction, OpenFileWith); m_previewAction = actionCollection()->addAction(QStringLiteral("preview")); m_previewAction->setText(i18nc("to preview a file inside an archive", "Pre&view")); m_previewAction->setIcon(QIcon::fromTheme(QStringLiteral("document-preview-archive"))); m_previewAction->setToolTip(i18nc("@info:tooltip", "Click to preview the selected file")); actionCollection()->setDefaultShortcut(m_previewAction, Qt::CTRL + Qt::Key_P); connect(m_previewAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_previewAction, Preview); m_extractArchiveAction = actionCollection()->addAction(QStringLiteral("extract_all")); m_extractArchiveAction->setText(i18nc("@action:inmenu", "E&xtract All")); m_extractArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); m_extractArchiveAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose how to extract all the files in the archive")); actionCollection()->setDefaultShortcut(m_extractArchiveAction, Qt::CTRL + Qt::SHIFT + Qt::Key_E); connect(m_extractArchiveAction, &QAction::triggered, this, &Part::slotExtractArchive); m_extractAction = actionCollection()->addAction(QStringLiteral("extract")); m_extractAction->setText(i18nc("@action:inmenu", "&Extract")); m_extractAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); actionCollection()->setDefaultShortcut(m_extractAction, Qt::CTRL + Qt::Key_E); m_extractAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose to extract either all files or just the selected ones")); connect(m_extractAction, &QAction::triggered, this, &Part::slotShowExtractionDialog); m_addFilesAction = actionCollection()->addAction(QStringLiteral("add")); m_addFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert"))); m_addFilesAction->setText(i18n("Add &File...")); m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive")); connect(m_addFilesAction, SIGNAL(triggered(bool)), this, SLOT(slotAddFiles())); m_addDirAction = actionCollection()->addAction(QStringLiteral("add-dir")); m_addDirAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert-directory"))); m_addDirAction->setText(i18n("Add Fo&lder...")); m_addDirAction->setToolTip(i18nc("@info:tooltip", "Click to add a folder to the archive")); connect(m_addDirAction, &QAction::triggered, this, &Part::slotAddDir); m_deleteFilesAction = actionCollection()->addAction(QStringLiteral("delete")); m_deleteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-remove"))); m_deleteFilesAction->setText(i18n("De&lete")); actionCollection()->setDefaultShortcut(m_deleteFilesAction, Qt::Key_Delete); m_deleteFilesAction->setToolTip(i18nc("@info:tooltip", "Click to delete the selected files")); connect(m_deleteFilesAction, &QAction::triggered, this, &Part::slotDeleteFiles); m_propertiesAction = actionCollection()->addAction(QStringLiteral("properties")); m_propertiesAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); m_propertiesAction->setText(i18nc("@action:inmenu", "&Properties")); actionCollection()->setDefaultShortcut(m_propertiesAction, Qt::ALT + Qt::Key_Return); m_propertiesAction->setToolTip(i18nc("@info:tooltip", "Click to see properties for archive")); connect(m_propertiesAction, &QAction::triggered, this, &Part::slotShowProperties); m_editCommentAction = actionCollection()->addAction(QStringLiteral("edit_comment")); m_editCommentAction->setIcon(QIcon::fromTheme(QStringLiteral("document-edit"))); actionCollection()->setDefaultShortcut(m_editCommentAction, Qt::ALT + Qt::Key_C); m_editCommentAction->setToolTip(i18nc("@info:tooltip", "Click to add or edit comment")); connect(m_editCommentAction, &QAction::triggered, this, &Part::slotShowComment); m_testArchiveAction = actionCollection()->addAction(QStringLiteral("test_archive")); m_testArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("checkmark"))); m_testArchiveAction->setText(i18nc("@action:inmenu", "&Test Integrity")); actionCollection()->setDefaultShortcut(m_testArchiveAction, Qt::ALT + Qt::Key_T); m_testArchiveAction->setToolTip(i18nc("@info:tooltip", "Click to test the archive for integrity")); connect(m_testArchiveAction, &QAction::triggered, this, &Part::slotTestArchive); connect(m_signalMapper, static_cast(&QSignalMapper::mapped), this, &Part::slotOpenEntry); updateActions(); updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); } void Part::updateActions() { bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); - bool isDirectory = m_model->entryForIndex(m_view->selectionModel()->currentIndex())[IsDirectory].toBool(); + const Archive::Entry *entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); int selectedEntriesCount = m_view->selectionModel()->selectedRows().count(); // Figure out if entry size is larger than preview size limit. const int maxPreviewSize = ArkSettings::previewFileSizeLimit() * 1024 * 1024; const bool limit = ArkSettings::limitPreviewFileSize(); - const qlonglong size = m_model->entryForIndex(m_view->selectionModel()->currentIndex())[Size].toLongLong(); - bool isPreviewable = (!limit || (limit && size < maxPreviewSize)); + bool isPreviewable = (!limit || (limit && entry != Q_NULLPTR && entry->property("size").toLongLong() < maxPreviewSize)); m_previewAction->setEnabled(!isBusy() && isPreviewable && - !isDirectory && + !entry->isDir() && (selectedEntriesCount == 1)); m_extractArchiveAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_extractAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_saveAsAction->setEnabled(!isBusy() && m_model->rowCount() > 0); m_addFilesAction->setEnabled(!isBusy() && isWritable); m_addDirAction->setEnabled(!isBusy() && isWritable); m_deleteFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0)); m_openFileAction->setEnabled(!isBusy() && isPreviewable && - !isDirectory && + !entry->isDir() && (selectedEntriesCount == 1)); m_openFileWithAction->setEnabled(!isBusy() && isPreviewable && - !isDirectory && + !entry->isDir() && (selectedEntriesCount == 1)); m_propertiesAction->setEnabled(!isBusy() && m_model->archive()); m_commentView->setEnabled(!isBusy()); m_commentMsgWidget->setEnabled(!isBusy()); m_editCommentAction->setEnabled(false); m_testArchiveAction->setEnabled(false); if (m_model->archive()) { const KPluginMetaData metadata = PluginManager().preferredPluginFor(m_model->archive()->mimeType())->metaData(); bool supportsWriteComment = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsWriteComment(); m_editCommentAction->setEnabled(!isBusy() && supportsWriteComment); m_commentView->setReadOnly(!supportsWriteComment); m_editCommentAction->setText(m_model->archive()->hasComment() ? i18nc("@action:inmenu mutually exclusive with Add &Comment", "Edit &Comment") : i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment")); bool supportsTesting = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsTesting(); m_testArchiveAction->setEnabled(!isBusy() && supportsTesting); } else { m_commentView->setReadOnly(true); m_editCommentAction->setText(i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment")); } } void Part::slotShowComment() { if (!m_commentBox->isVisible()) { m_commentBox->show(); m_commentSplitter->setSizes(QList() << m_view->height() * 0.6 << 1); } m_commentView->setFocus(); } void Part::slotAddComment() { CommentJob *job = m_model->archive()->addComment(m_commentView->toPlainText()); if (!job) { return; } registerJob(job); job->start(); m_commentMsgWidget->hide(); if (m_commentView->toPlainText().isEmpty()) { m_commentBox->hide(); } } void Part::slotTestArchive() { TestJob *job = m_model->archive()->testArchive(); if (!job) { return; } registerJob(job); connect(job, &KJob::result, this, &Part::slotTestingDone); job->start(); } void Part::slotTestingDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else if (static_cast(job)->testSucceeded()) { KMessageBox::information(widget(), i18n("The archive passed the integrity test."), i18n("Test Results")); } else { KMessageBox::error(widget(), i18n("The archive failed the integrity test."), i18n("Test Results")); } } void Part::updateQuickExtractMenu(QAction *extractAction) { if (!extractAction) { return; } QMenu *menu = extractAction->menu(); if (!menu) { menu = new QMenu(); extractAction->setMenu(menu); connect(menu, &QMenu::triggered, this, &Part::slotQuickExtractFiles); // Remember to keep this action's properties as similar to // extractAction's as possible (except where it does not make // sense, such as the text or the shortcut). QAction *extractTo = menu->addAction(i18n("Extract To...")); extractTo->setIcon(extractAction->icon()); extractTo->setToolTip(extractAction->toolTip()); if (extractAction == m_extractArchiveAction) { connect(extractTo, &QAction::triggered, this, &Part::slotExtractArchive); } else { connect(extractTo, &QAction::triggered, this, &Part::slotShowExtractionDialog); } menu->addSeparator(); QAction *header = menu->addAction(i18n("Quick Extract To...")); header->setEnabled(false); header->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); } while (menu->actions().size() > 3) { menu->removeAction(menu->actions().last()); } const KConfigGroup conf(KSharedConfig::openConfig(), "ExtractDialog"); const QStringList dirHistory = conf.readPathEntry("DirHistory", QStringList()); for (int i = 0; i < qMin(10, dirHistory.size()); ++i) { const QString dir = QUrl(dirHistory.value(i)).toString(QUrl::RemoveScheme | QUrl::NormalizePathSegments | QUrl::PreferLocalFile); if (QDir(dir).exists()) { QAction *newAction = menu->addAction(dir); newAction->setData(dir); } } } void Part::slotQuickExtractFiles(QAction *triggeredAction) { // #190507: triggeredAction->data.isNull() means it's the "Extract to..." // action, and we do not want it to run here if (!triggeredAction->data().isNull()) { const QString userDestination = triggeredAction->data().toString(); qCDebug(ARK) << "Extract to user dest" << userDestination; QString finalDestinationDirectory; const QString detectedSubfolder = detectSubfolder(); qCDebug(ARK) << "Detected subfolder" << detectedSubfolder; if (!isSingleFolderArchive()) { finalDestinationDirectory = userDestination + QDir::separator() + detectedSubfolder; QDir(userDestination).mkdir(detectedSubfolder); } else { finalDestinationDirectory = userDestination; } qCDebug(ARK) << "Extract to final dest" << finalDestinationDirectory; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; QList files = filesAndRootNodesForIndexes(m_view->selectionModel()->selectedRows()); ExtractJob *job = m_model->extractFiles(files, finalDestinationDirectory, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } } void Part::selectionChanged() { m_infoPanel->setIndexes(m_view->selectionModel()->selectedRows()); } bool Part::openFile() { qCDebug(ARK) << "Attempting to open archive" << localFilePath(); if (!isLocalFileValid()) { return false; } const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")]; QScopedPointer archive(Kerfuffle::Archive::create(localFilePath(), fixedMimeType, m_model)); Q_ASSERT(archive); if (archive->error() == NoPlugin) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open %1. No suitable plugin found." "Ark does not seem to support this file type.", QFileInfo(localFilePath()).fileName())); return false; } if (archive->error() == FailedPlugin) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open %1. Failed to load a suitable plugin." "Make sure any executables needed to handle the archive type are installed.", QFileInfo(localFilePath()).fileName())); return false; } Q_ASSERT(archive->isValid()); // Plugin loaded successfully. KJob *job = m_model->setArchive(archive.take()); if (job) { registerJob(job); job->start(); } else { updateActions(); } m_infoPanel->setIndex(QModelIndex()); if (arguments().metaData()[QStringLiteral("showExtractDialog")] == QLatin1String("true")) { QTimer::singleShot(0, this, &Part::slotShowExtractionDialog); } const QString password = arguments().metaData()[QStringLiteral("encryptionPassword")]; if (!password.isEmpty()) { m_model->encryptArchive(password, arguments().metaData()[QStringLiteral("encryptHeader")] == QLatin1String("true")); } return true; } bool Part::saveFile() { return true; } bool Part::isBusy() const { return m_busy; } KConfigSkeleton *Part::config() const { return ArkSettings::self(); } QList Part::settingsPages(QWidget *parent) const { QList pages; pages.append(new ExtractionSettingsPage(parent, i18nc("@title:tab", "Extraction Settings"), QStringLiteral("archive-extract"))); pages.append(new PreviewSettingsPage(parent, i18nc("@title:tab", "Preview Settings"), QStringLiteral("document-preview-archive"))); return pages; } bool Part::isLocalFileValid() { const QString localFile = localFilePath(); const QFileInfo localFileInfo(localFile); const bool creatingNewArchive = arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true"); if (localFileInfo.isDir()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "%1 is a directory.", localFile)); return false; } if (creatingNewArchive) { if (localFileInfo.exists()) { if (!confirmAndDelete(localFile)) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Could not overwrite %1. Check whether you have write permission.", localFile)); return false; } } displayMsgWidget(KMessageWidget::Information, xi18nc("@info", "The archive %1 will be created as soon as you add a file.", localFile)); } else { if (!localFileInfo.exists()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 was not found.", localFile)); return false; } if (!localFileInfo.isReadable()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 could not be loaded, as it was not possible to read from it.", localFile)); return false; } } return true; } bool Part::confirmAndDelete(const QString &targetFile) { QFileInfo targetInfo(targetFile); const auto buttonCode = KMessageBox::warningYesNo(widget(), xi18nc("@info", "The archive %1 already exists. Do you wish to overwrite it?", targetInfo.fileName()), i18nc("@title:window", "File Exists"), KGuiItem(i18nc("@action:button", "Overwrite")), KStandardGuiItem::cancel()); if (buttonCode != KMessageBox::Yes || !targetInfo.isWritable()) { return false; } qCDebug(ARK) << "Removing file" << targetFile; return QFile(targetFile).remove(); } void Part::slotLoadingStarted() { } void Part::slotLoadingFinished(KJob *job) { if (job->error()) { if (arguments().metaData()[QStringLiteral("createNewArchive")] != QLatin1String("true")) { if (job->error() != KJob::KilledJobError) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Loading the archive %1 failed with the following error:%2", localFilePath(), job->errorText())); } // The file failed to open, so reset the open archive, info panel and caption. m_model->setArchive(Q_NULLPTR); m_infoPanel->setPrettyFileName(QString()); m_infoPanel->updateWithDefaults(); emit setWindowCaption(QString()); } } m_view->sortByColumn(0, Qt::AscendingOrder); // #303708: expand the first level only when there is just one root folder. // Typical use case: an archive with source files. if (m_view->model()->rowCount() == 1) { m_view->expandToDepth(0); } // After loading all files, resize the columns to fit all fields m_view->header()->resizeSections(QHeaderView::ResizeToContents); updateActions(); if (!m_model->archive()) { return; } if (!m_model->archive()->comment().isEmpty()) { m_commentView->setPlainText(m_model->archive()->comment()); slotShowComment(); } else { m_commentView->clear(); m_commentBox->hide(); } if (m_model->rowCount() == 0) { qCWarning(ARK) << "No entry listed by the plugin"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "The archive is empty or Ark could not open its content.")); } else if (m_model->rowCount() == 1) { if (m_model->archive()->mimeType().inherits(QStringLiteral("application/x-cd-image")) && - m_model->entryForIndex(m_model->index(0, 0))[FileName].toString() == QLatin1String("README.TXT")) { + m_model->entryForIndex(m_model->index(0, 0))->property("fileName").toString() == QLatin1String("README.TXT")) { qCWarning(ARK) << "Detected ISO image with UDF filesystem"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "Ark does not currently support ISO files with UDF filesystem.")); } } } void Part::setReadyGui() { QApplication::restoreOverrideCursor(); m_busy = false; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->hide(); } m_view->setEnabled(true); updateActions(); } void Part::setBusyGui() { QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); m_busy = true; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->show(); } m_view->setEnabled(false); updateActions(); } void Part::setFileNameFromArchive() { const QString prettyName = url().fileName(); m_infoPanel->setPrettyFileName(prettyName); m_infoPanel->updateWithDefaults(); emit setWindowCaption(prettyName); } void Part::slotOpenEntry(int mode) { qCDebug(ARK) << "Opening with mode" << mode; QModelIndex index = m_view->selectionModel()->currentIndex(); - const ArchiveEntry& entry = m_model->entryForIndex(index); + const Archive::Entry *entry = m_model->entryForIndex(index); // Don't open directories. - if (entry[IsDirectory].toBool()) { + if (entry->isDir()) { return; } // We don't support opening symlinks. - if (entry[Link].toBool()) { + if (!entry->property("link").toString().isEmpty()) { displayMsgWidget(KMessageWidget::Information, i18n("Ark cannot open symlinks.")); return; } // Extract the entry. - if (!entry.isEmpty()) { + if (!entry->property("fileName").toString().isEmpty()) { m_openFileMode = static_cast(mode); KJob *job = Q_NULLPTR; if (m_openFileMode == Preview) { - job = m_model->preview(entry[InternalID].toString()); + job = m_model->preview(entry->property("fileName").toString()); connect(job, &KJob::result, this, &Part::slotPreviewExtractedEntry); } else { - const QString file = entry[InternalID].toString(); + const QString file = entry->property("fileName").toString(); job = (m_openFileMode == OpenFile) ? m_model->open(file) : m_model->openWith(file); connect(job, &KJob::result, this, &Part::slotOpenExtractedEntry); } registerJob(job); job->start(); } } void Part::slotOpenExtractedEntry(KJob *job) { if (!job->error()) { OpenJob *openJob = qobject_cast(job); Q_ASSERT(openJob); // Since the user could modify the file (unlike the Preview case), // we'll need to manually delete the temp dir in the Part destructor. m_tmpOpenDirList << openJob->tempDir(); const QString fullName = openJob->validatedFilePath(); bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); // If archive is readonly set temporarily extracted file to readonly as // well so user will be notified if trying to modify and save the file. if (!isWritable) { QFile::setPermissions(fullName, QFileDevice::ReadOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther); } if (isWritable) { m_fileWatcher = new QFileSystemWatcher; connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &Part::slotWatchedFileModified); m_fileWatcher->addPath(fullName); } if (qobject_cast(job)) { const QList urls = {QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile)}; KRun::displayOpenWithDialog(urls, widget()); } else { KRun::runUrl(QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile), QMimeDatabase().mimeTypeForFile(fullName).name(), widget()); } } else if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotPreviewExtractedEntry(KJob *job) { if (!job->error()) { PreviewJob *previewJob = qobject_cast(job); Q_ASSERT(previewJob); ArkViewer::view(previewJob->validatedFilePath()); } else if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotWatchedFileModified(const QString& file) { qCDebug(ARK) << "Watched file modified:" << file; // Find the relative path of the file within the archive. QString relPath = file; foreach (QTemporaryDir *tmpDir, m_tmpOpenDirList) { relPath.remove(tmpDir->path()); //Remove tmpDir. } relPath = relPath.mid(1); //Remove leading slash. if (relPath.contains(QLatin1Char('/'))) { relPath = relPath.section(QLatin1Char('/'), 0, -2); //Remove filename. } else { // File is in the root of the archive, no path. relPath = QString(); } // Set up a string for display in KMessageBox. QString prettyFilename; if (relPath.isEmpty()) { prettyFilename = file.section(QLatin1Char('/'), -1); } else { prettyFilename = relPath + QLatin1Char('/') + file.section(QLatin1Char('/'), -1); } if (KMessageBox::questionYesNo(widget(), xi18n("The file %1 was modified. Do you want to update the archive?", prettyFilename), i18nc("@title:window", "File Modified")) == KMessageBox::Yes) { QStringList list = QStringList() << file; qCDebug(ARK) << "Updating file" << file << "with path" << relPath; slotAddFiles(list, relPath); } // This is needed because some apps, such as Kate, delete and recreate // files when saving. m_fileWatcher->addPath(file); } void Part::slotError(const QString& errorMessage, const QString& details) { if (details.isEmpty()) { KMessageBox::error(widget(), errorMessage); } else { KMessageBox::detailedError(widget(), errorMessage, details); } } bool Part::isSingleFolderArchive() const { return m_model->archive()->isSingleFolderArchive(); } QString Part::detectSubfolder() const { if (!m_model) { return QString(); } return m_model->archive()->subfolderName(); } void Part::slotExtractArchive() { if (m_view->selectionModel()->selectedRows().count() > 0) { m_view->selectionModel()->clear(); } slotShowExtractionDialog(); } void Part::slotShowExtractionDialog() { if (!m_model) { return; } QPointer dialog(new Kerfuffle::ExtractionDialog); dialog.data()->setModal(true); if (m_view->selectionModel()->selectedRows().count() > 0) { dialog.data()->setShowSelectedFiles(true); } dialog.data()->setSingleFolderArchive(isSingleFolderArchive()); dialog.data()->setSubfolder(detectSubfolder()); dialog.data()->setCurrentUrl(QUrl::fromLocalFile(QFileInfo(m_model->archive()->fileName()).absolutePath())); dialog.data()->show(); dialog.data()->restoreWindowSize(); if (dialog.data()->exec()) { updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); QVariantList files; // If the user has chosen to extract only selected entries, fetch these // from the QTreeView. if (!dialog.data()->extractAllFiles()) { files = filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); } qCDebug(ARK) << "Selected " << files; Kerfuffle::ExtractionOptions options; if (dialog.data()->preservePaths()) { options[QStringLiteral("PreservePaths")] = true; } options[QStringLiteral("FollowExtractionDialogSettings")] = true; const QString destinationDirectory = dialog.data()->destinationDirectory().toDisplayString(QUrl::PreferLocalFile); ExtractJob *job = m_model->extractFiles(files, destinationDirectory, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } delete dialog.data(); } QModelIndexList Part::addChildren(const QModelIndexList &list) const { Q_ASSERT(m_model); QModelIndexList ret = list; // Iterate over indexes in list and add all children. for (int i = 0; i < ret.size(); ++i) { QModelIndex index = ret.at(i); for (int j = 0; j < m_model->rowCount(index); ++j) { QModelIndex child = m_model->index(j, 0, index); if (!ret.contains(child)) { ret << child; } } } return ret; } QList Part::filesForIndexes(const QModelIndexList& list) const { QVariantList ret; foreach(const QModelIndex& index, list) { - const ArchiveEntry& entry = m_model->entryForIndex(index); - ret << entry[InternalID].toString(); + const Archive::Entry *entry = m_model->entryForIndex(index); + ret << entry->property("fileName").toString(); } return ret; } QList Part::filesAndRootNodesForIndexes(const QModelIndexList& list) const { QVariantList fileList; foreach (const QModelIndex& index, list) { // Find the topmost unselected parent. This is done by iterating up // through the directory hierarchy and see if each parent is included // in the selection OR if the parent is already part of list. // The latter is needed for unselected folders which are subfolders of // a selected parent folder. QModelIndex selectionRoot = index.parent(); while (m_view->selectionModel()->isSelected(selectionRoot) || list.contains(selectionRoot)) { selectionRoot = selectionRoot.parent(); } // Fetch the root node for the unselected parent. - const QString rootInternalID = - m_model->entryForIndex(selectionRoot).value(InternalID).toString(); + const QString rootFileName = + m_model->entryForIndex(selectionRoot)->property("fileName").toString(); // Append index with root node to fileList. QModelIndexList alist = QModelIndexList() << index; foreach (const QVariant &file, filesForIndexes(alist)) { - QVariant v = QVariant::fromValue(fileRootNodePair(file.toString(), rootInternalID)); + QVariant v = QVariant::fromValue(fileRootNodePair(file.toString(), rootFileName)); if (!fileList.contains(v)) { fileList.append(v); } } } return fileList; } void Part::slotExtractionDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else { ExtractJob *extractJob = qobject_cast(job); Q_ASSERT(extractJob); const bool followExtractionDialogSettings = extractJob->extractionOptions().value(QStringLiteral("FollowExtractionDialogSettings"), false).toBool(); if (!followExtractionDialogSettings) { return; } if (ArkSettings::openDestinationFolderAfterExtraction()) { qCDebug(ARK) << "Shall open" << extractJob->destinationDirectory(); QUrl destinationDirectory = QUrl::fromLocalFile(extractJob->destinationDirectory()).adjusted(QUrl::NormalizePathSegments); qCDebug(ARK) << "Shall open URL" << destinationDirectory; KRun::runUrl(destinationDirectory, QStringLiteral("inode/directory"), widget()); } if (ArkSettings::closeAfterExtraction()) { emit quit(); } } } void Part::adjustColumns() { m_view->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); } void Part::slotAddFiles(const QStringList& filesToAdd, const QString& path) { if (filesToAdd.isEmpty()) { return; } qCDebug(ARK) << "Adding " << filesToAdd << " to " << path; // Add a trailing slash to directories. QStringList cleanFilesToAdd(filesToAdd); for (int i = 0; i < cleanFilesToAdd.size(); ++i) { QString& file = cleanFilesToAdd[i]; if (QFileInfo(file).isDir()) { if (!file.endsWith(QLatin1Char( '/' ))) { file += QLatin1Char( '/' ); } } } // GlobalWorkDir is used by AddJob and should contain the part of the // absolute path of files to be added that should NOT be included in the // directory structure within the archive. // Example: We add file "/home/user/somedir/somefile.txt" and want the file // to have the relative path within the archive "somedir/somefile.txt". // GlobalWorkDir is then: "/home/user" QString globalWorkDir = cleanFilesToAdd.first(); // path represents the path of the file within the archive. This needs to // be removed from globalWorkDir, otherwise the files will be added to the // root of the archive. In the example above, path would be "somedir/". if (!path.isEmpty()) { globalWorkDir.remove(path); } // Remove trailing slash (needed when adding dirs). if (globalWorkDir.right(1) == QLatin1String("/")) { globalWorkDir.chop(1); } // Now take the absolute path of the parent directory. globalWorkDir = QFileInfo(globalWorkDir).dir().absolutePath(); qCDebug(ARK) << "Detected GlobalWorkDir to be " << globalWorkDir; CompressionOptions options; options[QStringLiteral("GlobalWorkDir")] = globalWorkDir; if (arguments().metaData().contains(QStringLiteral("compressionLevel"))) { options[QStringLiteral("CompressionLevel")] = arguments().metaData()[QStringLiteral("compressionLevel")]; } AddJob *job = m_model->addFiles(cleanFilesToAdd, options); if (!job) { return; } connect(job, &KJob::result, this, &Part::slotAddFilesDone); registerJob(job); job->start(); } void Part::slotAddFiles() { // #264819: passing widget() as the parent will not work as expected. // KFileDialog will create a KFileWidget, which runs an internal // event loop to stat the given directory. This, in turn, leads to // events being delivered to widget(), which is a QSplitter, which // in turn reimplements childEvent() and will end up calling // QWidget::show() on the KFileDialog (thus showing it in a // non-modal state). // When KFileDialog::exec() is called, the widget is already shown // and nothing happens. const QStringList filesToAdd = QFileDialog::getOpenFileNames(widget(), i18nc("@title:window", "Add Files")); slotAddFiles(filesToAdd); } void Part::slotAddDir() { const QString dirToAdd = QFileDialog::getExistingDirectory(widget(), i18nc("@title:window", "Add Folder")); if (!dirToAdd.isEmpty()) { slotAddFiles(QStringList() << dirToAdd); } } void Part::slotAddFilesDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } } void Part::slotDeleteFilesDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } } void Part::slotDeleteFiles() { const int selectionsCount = m_view->selectionModel()->selectedRows().count(); const auto reallyDelete = KMessageBox::questionYesNo(widget(), i18ncp("@info", "Deleting this file is not undoable. Are you sure you want to do this?", "Deleting these files is not undoable. Are you sure you want to do this?", selectionsCount), i18ncp("@title:window", "Delete File", "Delete Files", selectionsCount), KStandardGuiItem::del(), KStandardGuiItem::no(), QString(), KMessageBox::Dangerous | KMessageBox::Notify); if (reallyDelete == KMessageBox::No) { return; } DeleteJob *job = m_model->deleteFiles(filesForIndexes(addChildren(m_view->selectionModel()->selectedRows()))); connect(job, &KJob::result, this, &Part::slotDeleteFilesDone); registerJob(job); job->start(); } void Part::slotShowProperties() { QPointer dialog(new Kerfuffle::PropertiesDialog(0, m_model->archive())); dialog.data()->show(); } void Part::slotToggleInfoPanel(bool visible) { if (visible) { m_splitter->setSizes(ArkSettings::splitterSizes()); m_infoPanel->show(); } else { // We need to save the splitterSizes before hiding, otherwise // Ark won't remember resizing done by the user. ArkSettings::setSplitterSizes(m_splitter->sizes()); m_infoPanel->hide(); } } void Part::slotSaveAs() { QUrl saveUrl = QFileDialog::getSaveFileUrl(widget(), i18nc("@title:window", "Save Archive As"), url()); if ((saveUrl.isValid()) && (!saveUrl.isEmpty())) { auto statJob = KIO::stat(saveUrl, KIO::StatJob::DestinationSide, 0); KJobWidgets::setWindow(statJob, widget()); if (statJob->exec()) { int overwrite = KMessageBox::warningContinueCancel(widget(), xi18nc("@info", "An archive named %1 already exists. Are you sure you want to overwrite it?", saveUrl.fileName()), QString(), KStandardGuiItem::overwrite()); if (overwrite != KMessageBox::Continue) { return; } } QUrl srcUrl = QUrl::fromLocalFile(localFilePath()); if (!QFile::exists(localFilePath())) { if (url().isLocalFile()) { KMessageBox::error(widget(), xi18nc("@info", "The archive %1 cannot be copied to the specified location. The archive does not exist anymore.", localFilePath())); return; } else { srcUrl = url(); } } KIO::Job *copyJob = KIO::file_copy(srcUrl, saveUrl, -1, KIO::Overwrite); KJobWidgets::setWindow(copyJob, widget()); copyJob->exec(); if (copyJob->error()) { KMessageBox::error(widget(), xi18nc("@info", "The archive could not be saved as %1. Try saving it to another location.", saveUrl.path())); } } } void Part::slotShowContextMenu() { if (!factory()) { return; } QMenu *popup = static_cast(factory()->container(QStringLiteral("context_menu"), this)); popup->popup(QCursor::pos()); } void Part::displayMsgWidget(KMessageWidget::MessageType type, const QString& msg) { KMessageWidget *msgWidget = new KMessageWidget(); msgWidget->setText(msg); msgWidget->setMessageType(type); m_vlayout->insertWidget(0, msgWidget); msgWidget->animatedShow(); } } // namespace Ark #include "part.moc" diff --git a/plugins/cli7zplugin/cliplugin.cpp b/plugins/cli7zplugin/cliplugin.cpp index 788890eb..cbb794a9 100644 --- a/plugins/cli7zplugin/cliplugin.cpp +++ b/plugins/cli7zplugin/cliplugin.cpp @@ -1,258 +1,265 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * 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"; } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateTitle; m_comment.clear(); } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { //p[CaptureProgress] = true; p[ListProgram] = p[ExtractProgram] = p[DeleteProgram] = 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("$Archive") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$CompressionLevelSwitch") << QStringLiteral("$Files"); p[DeleteArgs] = QStringList() << QStringLiteral("d") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive") << QStringLiteral("$Files"); p[TestArgs] = QStringList() << QStringLiteral("t") << QStringLiteral("$Archive"); 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"); p[CorruptArchivePatterns] = QStringList() << QStringLiteral("Unexpected end of archive") << QStringLiteral("Headers Error"); p[DiskFullPatterns] = QStringList() << QStringLiteral("No space left on device"); } return p; } 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 { // Should not happen qCWarning(ARK) << "Unsupported archive type"; return false; } } 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(Q_NULLPTR); + m_currentArchiveEntry->compressedSizeIsSet = false; + } if (line.startsWith(QStringLiteral("Path = "))) { const QString entryFilename = QDir::fromNativeSeparators(line.mid(7).trimmed()); - m_currentArchiveEntry.clear(); - m_currentArchiveEntry[FileName] = entryFilename; - m_currentArchiveEntry[InternalID] = entryFilename; + m_currentArchiveEntry->setProperty("fileName", entryFilename); } else if (line.startsWith(QStringLiteral("Size = "))) { - m_currentArchiveEntry[ Size ] = line.mid(7).trimmed(); + 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[CompressedSize] = line.mid(14).trimmed(); + m_currentArchiveEntry->compressedSizeIsSet = true; + m_currentArchiveEntry->setProperty("compressedSize", line.mid(14).trimmed()); } } else if (line.startsWith(QStringLiteral("Modified = "))) { - m_currentArchiveEntry[ Timestamp ] = - QDateTime::fromString(line.mid(11).trimmed(), - QStringLiteral("yyyy-MM-dd hh:mm:ss")); + 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[ IsDirectory ] = isDirectory; + m_currentArchiveEntry->setProperty("isDirectory", isDirectory); if (isDirectory) { const QString directoryName = - m_currentArchiveEntry[FileName].toString(); + m_currentArchiveEntry->property("fileName").toString(); if (!directoryName.endsWith(QLatin1Char('/'))) { const bool isPasswordProtected = (line.at(12) == QLatin1Char('+')); - m_currentArchiveEntry[FileName] = - m_currentArchiveEntry[InternalID] = QString(directoryName + QLatin1Char('/')); - m_currentArchiveEntry[ IsPasswordProtected ] = - isPasswordProtected; + m_currentArchiveEntry->setProperty("fileName", QString(directoryName + QLatin1Char('/'))); + m_currentArchiveEntry->setProperty("isPasswordProtected", isPasswordProtected); } } - m_currentArchiveEntry[ Permissions ] = attributes.mid(1); + m_currentArchiveEntry->setProperty("permissions", attributes.mid(1)); } else if (line.startsWith(QStringLiteral("CRC = "))) { - m_currentArchiveEntry[ CRC ] = line.mid(6).trimmed(); + m_currentArchiveEntry->setProperty("CRC", line.mid(6).trimmed()); } else if (line.startsWith(QStringLiteral("Method = "))) { - m_currentArchiveEntry[ Method ] = line.mid(9).trimmed(); + m_currentArchiveEntry->setProperty("method", line.mid(9).trimmed()); } else if (line.startsWith(QStringLiteral("Encrypted = ")) && line.size() >= 13) { - m_currentArchiveEntry[ IsPasswordProtected ] = (line.at(12) == QLatin1Char('+')); + m_currentArchiveEntry->setProperty("isPasswordProtected", line.at(12) == QLatin1Char('+')); } else if (line.startsWith(QStringLiteral("Block = ")) || line.startsWith(QStringLiteral("Version = "))) { - if (m_currentArchiveEntry.contains(FileName)) { + m_isFirstInformationEntry = true; + if (!m_currentArchiveEntry->property("fileName").toString().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; } #include "cliplugin.moc" diff --git a/plugins/cli7zplugin/cliplugin.h b/plugins/cli7zplugin/cliplugin.h index 9a332bf4..806c6475 100644 --- a/plugins/cli7zplugin/cliplugin.h +++ b/plugins/cli7zplugin/cliplugin.h @@ -1,69 +1,71 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2010 Raphael Kubo da Costa * * 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; private: enum ArchiveType { ArchiveType7z = 0, ArchiveTypeBZip2, ArchiveTypeGZip, ArchiveTypeXz, ArchiveTypeTar, ArchiveTypeZip, ArchiveTypeRar } m_archiveType; enum ParseState { ParseStateTitle = 0, ParseStateHeader, ParseStateArchiveInformation, ParseStateComment, ParseStateEntryInformation } m_parseState; int m_linesComment; - Kerfuffle::ArchiveEntry m_currentArchiveEntry; + Kerfuffle::Archive::Entry *m_currentArchiveEntry; + bool m_isFirstInformationEntry; }; #endif // CLIPLUGIN_H diff --git a/plugins/cliplugin-example/cliplugin.cpp b/plugins/cliplugin-example/cliplugin.cpp index b0701e41..fa21fbb6 100644 --- a/plugins/cliplugin-example/cliplugin.cpp +++ b/plugins/cliplugin-example/cliplugin.cpp @@ -1,150 +1,149 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Claudio Bantaloukas * Copyright (C) 2007 Henrique Pinto * * 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/kerfuffle_export.h" #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_cli.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList &args) : CliInterface(parent, args), m_isFirstLine(true), m_incontent(false), m_isPasswordProtected(false) { qCDebug(ARK) << "Loaded cli-example plugin"; } CliPlugin::~CliPlugin() { } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { p[CaptureProgress] = true; p[ListProgram] = p[ExtractProgram] = p[DeleteProgram] = p[AddProgram] = QLatin1String("rar"); p[ListArgs] = QStringList() << QLatin1String("v") << QLatin1String("-c-") << QLatin1String("$Archive"); p[ExtractArgs] = QStringList() << QLatin1String("-p-") << QLatin1String("$PreservePathSwitch") << QLatin1String("$PasswordSwitch") << QLatin1String("$Archive") << QLatin1String("$Files"); p[PreservePathSwitch] = QStringList() << QLatin1String("x") << QLatin1String("e"); p[PasswordSwitch] = QStringList() << QLatin1String("-p$Password"); p[DeleteArgs] = QStringList() << QLatin1String("d") << QLatin1String("$Archive") << QLatin1String("$Files"); p[FileExistsExpression] = QLatin1String("^(.+) already exists. Overwrite it"); p[FileExistsInput] = QStringList() << QLatin1String("Y") //overwrite << QLatin1String("N") //skip << QLatin1String("A") //overwrite all << QLatin1String("E") //autoskip << QLatin1String("Q") //cancel ; p[AddArgs] = QStringList() << QLatin1String("a") << QLatin1String("$Archive") << QLatin1String("$Files"); p[WrongPasswordPatterns] = QStringList() << QLatin1String("password incorrect"); p[ExtractionFailedPatterns] = QStringList() << QLatin1String("CRC failed"); } return p; } bool CliPlugin::readListLine(const QString &line) { const QString m_headerString = QLatin1String("-----------------------------------------"); // skip the heading if (!m_incontent) { if (line.startsWith(m_headerString)) { m_incontent = true; } return true; } // catch final line if (line.startsWith(m_headerString)) { m_incontent = false; return true; } // rar gives one line for the filename and a line after it with some file properties if (m_isFirstLine) { - m_internalId = line.trimmed(); + m_entryFilename = line.trimmed(); //m_entryFilename.chop(1); // handle newline - if (!m_internalId.isEmpty() && m_internalId.at(0) == QLatin1Char('*')) { + if (!m_entryFilename.isEmpty() && m_entryFilename.at(0) == QLatin1Char('*')) { m_isPasswordProtected = true; - m_internalId.remove(0, 1); // and the spaces in front + m_entryFilename.remove(0, 1); // and the spaces in front } else m_isPasswordProtected = false; m_isFirstLine = false; return true; } QStringList fileprops = line.split(QLatin1Char(' '), QString::SkipEmptyParts); - m_internalId = QDir::fromNativeSeparators(m_internalId); + m_entryFilename = QDir::fromNativeSeparators(m_entryFilename); bool isDirectory = (bool)(fileprops[ 5 ].contains(QLatin1Char('d'), Qt::CaseInsensitive)); QDateTime ts(QDate::fromString(fileprops[ 3 ], QLatin1String("dd-MM-yy")), QTime::fromString(fileprops[ 4 ], QLatin1String("hh:mm"))); // rar output date with 2 digit year but QDate takes is as 19?? // let's take 1950 is cut-off; similar to KDateTime if (ts.date().year() < 1950) { ts = ts.addYears(100); } - m_entryFilename = m_internalId; - if (isDirectory && !m_internalId.endsWith(QLatin1Char('/'))) { + if (isDirectory && !m_entryFilename.endsWith(QLatin1Char('/'))) { m_entryFilename += QLatin1Char('/'); } qCDebug(ARK) << m_entryFilename << " : " << fileprops; - ArchiveEntry e; - e[ FileName ] = m_entryFilename; - e[ InternalID ] = m_internalId; - e[ Size ] = fileprops[ 0 ]; - e[ CompressedSize] = fileprops[ 1 ]; - e[ Ratio ] = fileprops[ 2 ]; - e[ Timestamp ] = ts; - e[ IsDirectory ] = isDirectory; - e[ Permissions ] = fileprops[ 5 ].remove(0, 1); - e[ CRC ] = fileprops[ 6 ]; - e[ Method ] = fileprops[ 7 ]; - e[ Version ] = fileprops[ 8 ]; - e[ IsPasswordProtected] = m_isPasswordProtected; + Archive::Entry *e = new Archive::Entry(Q_NULLPTR); + e->setProperty("fileName", m_entryFilename); + e->setProperty("size", fileprops[ 0 ]); + e->setProperty("compressedSize", fileprops[ 1 ]); + e->setProperty("ratio", fileprops[ 2 ]); + e->setProperty("timestamp", ts); + e->setProperty("isDirectory", isDirectory); + e->setProperty("permissions", fileprops[ 5 ].remove(0, 1)); + e->setProperty("CRC", fileprops[ 6 ]); + e->setProperty("method", fileprops[ 7 ]); + e->setProperty("version", fileprops[ 8 ]); + e->setProperty("ssPasswordProtected", m_isPasswordProtected); qCDebug(ARK) << "Added entry: " << e; emit entry(e); m_isFirstLine = true; return true; } #include "cliplugin.moc" diff --git a/plugins/cliplugin-example/cliplugin.h b/plugins/cliplugin-example/cliplugin.h index 21e935d6..cd4770df 100644 --- a/plugins/cliplugin-example/cliplugin.h +++ b/plugins/cliplugin-example/cliplugin.h @@ -1,45 +1,45 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Claudio Bantaloukas * Copyright (C) 2007 Henrique Pinto * * 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" using namespace Kerfuffle; class CliPlugin: public CliInterface { public: explicit CliPlugin(QObject *parent = 0, const QVariantList &args = QVariantList()); virtual ~CliPlugin(); virtual ParameterList parameterList() const; bool readListLine(const QString &line); private: bool m_isFirstLine, m_incontent, m_isPasswordProtected; - QString m_entryFilename, m_internalId; + QString m_entryFilename; }; #endif // CLIPLUGIN_H diff --git a/plugins/clirarplugin/cliplugin.cpp b/plugins/clirarplugin/cliplugin.cpp index ab31ffbf..545bed0f 100644 --- a/plugins/clirarplugin/cliplugin.cpp +++ b/plugins/clirarplugin/cliplugin.cpp @@ -1,517 +1,514 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2010-2011,2014 Raphael Kubo da Costa * * 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/kerfuffle_export.h" +#include "kerfuffle/archiveentry.h" #include -#include #include -using namespace Kerfuffle; + 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_isMultiVolume(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); } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateTitle; m_remainingIgnoreLines = 1; m_comment.clear(); } // #272281: the proprietary unrar program does not like trailing '/'s // in directories passed to it when extracting only part of // the files in an archive. QString CliPlugin::escapeFileName(const QString &fileName) const { if (fileName.endsWith(QLatin1Char('/'))) { return fileName.left(fileName.length() - 1); } return fileName; } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { p[CaptureProgress] = true; p[ListProgram] = p[ExtractProgram] = p[TestProgram] = QStringList() << QStringLiteral( "unrar" ); p[DeleteProgram] = 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[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( "$Files" ); 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"); p[TestPassedPattern] = QStringLiteral("^All OK$"); } return p; } 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; QString unrarVersion = matchVersion.captured(1); qCDebug(ARK) << "UNRAR version" << unrarVersion << "detected"; if (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) { handleUnrar5Line(line); } else { handleUnrar4Line(line); } return true; } void 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; } // 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_isMultiVolume) { m_isMultiVolume = true; qCDebug(ARK) << "Multi-volume archive detected"; } if (line.contains(QLatin1String("solid")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } } return; } // 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; // 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; } // 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; } } void CliPlugin::handleUnrar5Entry() { - ArchiveEntry e; + Archive::Entry *e = new Archive::Entry(Q_NULLPTR); QString compressionRatio = m_unrar5Details.value(QStringLiteral("ratio")); compressionRatio.chop(1); // Remove the '%' - e[Ratio] = compressionRatio; + 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[Timestamp] = ts; + e->setProperty("timestamp", ts); bool isDirectory = (m_unrar5Details.value(QStringLiteral("type")) == QLatin1String("Directory")); - e[IsDirectory] = isDirectory; + 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[Method] = compression.mid(optionPos); - e[Version] = compression.left(optionPos).trimmed(); + e->setProperty("method", compression.mid(optionPos)); + e->setProperty("version", compression.left(optionPos).trimmed()); } else { // No method specified. - e[Method].clear(); - e[Version] = compression; + e->setProperty("method", QStringLiteral("")); + e->setProperty("version", compression); } m_isPasswordProtected = m_unrar5Details.value(QStringLiteral("flags")).contains(QStringLiteral("encrypted")); - e[IsPasswordProtected] = m_isPasswordProtected; + e->setProperty("isPasswordProtected", m_isPasswordProtected); - e[FileName] = m_unrar5Details.value(QStringLiteral("name")); - e[InternalID] = m_unrar5Details.value(QStringLiteral("name")); - e[Size] = m_unrar5Details.value(QStringLiteral("size")); - e[CompressedSize] = m_unrar5Details.value(QStringLiteral("packed size")); - e[Permissions] = m_unrar5Details.value(QStringLiteral("attributes")); - e[CRC] = m_unrar5Details.value(QStringLiteral("crc32")); + e->setProperty("fileName", 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[Permissions].toString().startsWith(QLatin1Char('l'))) { - e[Link] = m_unrar5Details.value(QStringLiteral("target")); + if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { + e->setProperty("link", m_unrar5Details.value(QStringLiteral("target"))); } m_unrar5Details.clear(); emit entry(e); } void 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) .+$")); if (rxCommentEnd.match(line).hasMatch()) { if (line.startsWith(QLatin1String("Volume")) && !m_isMultiVolume) { m_isMultiVolume = 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; } // 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; } return; } // 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; } // 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; } // 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; // 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; // 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; } // 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; } // 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; } // 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; } 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; } // Parses a symlink target. else if (m_parseState == ParseStateLinkTarget) { m_unrar4Details.append(QString(line).remove(QStringLiteral("-->")).trimmed()); handleUnrar4Entry(); m_parseState = ParseStateEntryFileName; return; } } void CliPlugin::handleUnrar4Entry() { - ArchiveEntry e; + Archive::Entry *e = new Archive::Entry(NULL); 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[Timestamp] = ts; + e->setProperty("timestamp", ts); bool isDirectory = ((m_unrar4Details.at(6).at(0) == QLatin1Char('d')) || (m_unrar4Details.at(6).at(1) == QLatin1Char('D'))); - e[IsDirectory] = isDirectory; + 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[Ratio] = compressionRatio; + e->setProperty("ratio", compressionRatio); // TODO: // - Permissions differ depending on the system the entry was added // to the archive. - e[FileName] = m_unrar4Details.at(0); - e[InternalID] = m_unrar4Details.at(0); - e[Size] = m_unrar4Details.at(1); - e[CompressedSize] = m_unrar4Details.at(2); - e[Permissions] = m_unrar4Details.at(6); - e[CRC] = m_unrar4Details.at(7); - e[Method] = m_unrar4Details.at(8); - e[Version] = m_unrar4Details.at(9); - e[IsPasswordProtected] = m_isPasswordProtected; - - if (e[Permissions].toString().startsWith(QLatin1Char('l'))) { - e[Link] = m_unrar4Details.at(10); + e->setProperty("fileName", 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; } #include "cliplugin.moc" diff --git a/plugins/cliunarchiverplugin/cliplugin.cpp b/plugins/cliunarchiverplugin/cliplugin.cpp index 71ac4a02..7e3e4ff9 100644 --- a/plugins/cliunarchiverplugin/cliplugin.cpp +++ b/plugins/cliunarchiverplugin/cliplugin.cpp @@ -1,216 +1,215 @@ /* * 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 "ark_debug.h" #include "kerfuffle/kerfuffle_export.h" #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"; } CliPlugin::~CliPlugin() { } bool CliPlugin::list() { resetParsing(); cacheParameterList(); m_operationMode = List; const auto args = substituteListVariables(m_param.value(ListArgs).toStringList(), password()); if (!runProcess(m_param.value(ListProgram).toStringList(), args)) { return false; } if (!password().isEmpty()) { // lsar -json exits with error code 1 if the archive is header-encrypted and the password is wrong. if (m_exitCode == 1) { qCWarning(ARK) << "Wrong password, list() aborted"; emit error(i18n("Wrong password.")); emit finished(false); killProcess(); setPassword(QString()); return false; } // lsar -json exits with error code 2 if the archive is header-encrypted and no password is given as argument. // At this point we have already asked a password to the user, so we can just list() again. if (m_exitCode == 2) { return CliPlugin::list(); } } return true; } bool CliPlugin::copyFiles(const QList &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[QStringLiteral("AlwaysUseTmpDir")] = true; return CliInterface::copyFiles(files, destinationDirectory, newOptions); } void CliPlugin::resetParsing() { m_jsonOutput.clear(); } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { ///////////////[ COMMON ]///////////// 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."); ///////////////[ LIST ]///////////// p[ListProgram] = QStringLiteral("lsar"); p[ListArgs] = QStringList() << QStringLiteral("-json") << QStringLiteral("$Archive") << QStringLiteral("$PasswordSwitch"); ///////////////[ 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; } 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)); } void CliPlugin::handleLine(const QString& line) { // Collect the json output line by line. if (m_operationMode == List) { m_jsonOutput += line + QLatin1Char('\n'); } CliInterface::handleLine(line); } 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 QJsonArray entries = json.value(QStringLiteral("lsarContents")).toArray(); foreach (const QJsonValue& value, entries) { const QJsonObject currentEntry = value.toObject(); - m_currentEntry.clear(); + m_currentEntry->clearMetaData(); QString filename = currentEntry.value(QStringLiteral("XADFileName")).toString(); - m_currentEntry[IsDirectory] = !currentEntry.value(QStringLiteral("XADIsDirectory")).isUndefined(); - if (m_currentEntry[IsDirectory].toBool()) { + m_currentEntry->setProperty("isDirectory", !currentEntry.value(QStringLiteral("XADIsDirectory")).isUndefined()); + if (m_currentEntry->isDir()) { filename += QLatin1Char('/'); } - m_currentEntry[FileName] = filename; - m_currentEntry[InternalID] = filename; + m_currentEntry->setProperty("fileName", filename); // FIXME: archives created from OSX (i.e. with the __MACOSX folder) list each entry twice, the 2nd time with size 0 - m_currentEntry[Size] = currentEntry.value(QStringLiteral("XADFileSize")); - m_currentEntry[CompressedSize] = currentEntry.value(QStringLiteral("XADCompressedSize")); - m_currentEntry[Timestamp] = currentEntry.value(QStringLiteral("XADLastModificationDate")).toVariant(); - m_currentEntry[Size] = currentEntry.value(QStringLiteral("XADFileSize")); - m_currentEntry[IsPasswordProtected] = (currentEntry.value(QStringLiteral("XADIsEncrypted")).toInt() == 1); + m_currentEntry->setProperty("size", currentEntry.value(QStringLiteral("XADFileSize"))); + m_currentEntry->setProperty("compressedSize", currentEntry.value(QStringLiteral("XADCompressedSize"))); + m_currentEntry->setProperty("timestamp", currentEntry.value(QStringLiteral("XADLastModificationDate")).toVariant()); + m_currentEntry->setProperty("size", currentEntry.value(QStringLiteral("XADFileSize"))); + m_currentEntry->setProperty("isPasswordProtected", (currentEntry.value(QStringLiteral("XADIsEncrypted")).toInt() == 1)); // TODO: missing fields emit entry(m_currentEntry); } } #include "cliplugin.moc" diff --git a/plugins/cliunarchiverplugin/cliplugin.h b/plugins/cliunarchiverplugin/cliplugin.h index ac7b5d66..0923c487 100644 --- a/plugins/cliunarchiverplugin/cliplugin.h +++ b/plugins/cliunarchiverplugin/cliplugin.h @@ -1,63 +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 copyFiles(const QList &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; void handleLine(const QString& line) Q_DECL_OVERRIDE; private: void readJsonOutput(); - Kerfuffle::ArchiveEntry m_currentEntry; + Kerfuffle::Archive::Entry *m_currentEntry; QString m_jsonOutput; }; #endif // CLIPLUGIN_H diff --git a/plugins/clizipplugin/cliplugin.cpp b/plugins/clizipplugin/cliplugin.cpp index ea2f6d23..bf61f94e 100644 --- a/plugins/clizipplugin/cliplugin.cpp +++ b/plugins/clizipplugin/cliplugin.cpp @@ -1,192 +1,193 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * 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 "kerfuffle/archiveentry.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"; } 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 { 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("$Files"); p[PasswordPromptPattern] = QStringLiteral(" password: "); p[WrongPasswordPatterns] = QStringList() << QStringLiteral("incorrect password"); //p[ExtractionFailedPatterns] = QStringList() << "CRC failed"; 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"); p[TestPassedPattern] = QStringLiteral("^No errors detected in compressed data of "); } return p; } 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()) { - ArchiveEntry e; - e[Permissions] = rxMatch.captured(1); + Archive::Entry *e = new Archive::Entry(Q_NULLPTR); + 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[IsDirectory] = rxMatch.captured(10).endsWith(QLatin1Char('/')); + e->setProperty("isDirectory", rxMatch.captured(10).endsWith(QLatin1Char('/'))); - e[Size] = rxMatch.captured(4); + e->setProperty("size", rxMatch.captured(4)); QString status = rxMatch.captured(5); if (status[0].isUpper()) { - e[IsPasswordProtected] = true; + e->setProperty("isPasswordProtected", true); } - e[CompressedSize] = rxMatch.captured(6).toInt(); + e->setProperty("compressedSize", rxMatch.captured(6).toInt()); const QDateTime ts(QDate::fromString(rxMatch.captured(8), QStringLiteral("yyyyMMdd")), QTime::fromString(rxMatch.captured(9), QStringLiteral("hhmmss"))); - e[Timestamp] = ts; + e->setProperty("timestamp", ts); - e[FileName] = e[InternalID] = rxMatch.captured(10); + e->setProperty("fileName", rxMatch.captured(10)); emit entry(e); } break; } return true; } #include "cliplugin.moc" diff --git a/plugins/libarchive/libarchiveplugin.cpp b/plugins/libarchive/libarchiveplugin.cpp index 934e5a22..ad7685cc 100644 --- a/plugins/libarchive/libarchiveplugin.cpp +++ b/plugins/libarchive/libarchiveplugin.cpp @@ -1,521 +1,522 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2010 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "libarchiveplugin.h" #include "ark_debug.h" #include "kerfuffle/kerfuffle_export.h" +#include "kerfuffle/archiveentry.h" #include "kerfuffle/queries.h" #include #include #include #include #include #include #include LibarchivePlugin::LibarchivePlugin(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args) , m_archiveReadDisk(archive_read_disk_new()) , m_abortOperation(false) , m_cachedArchiveEntryCount(0) , m_emitNoEntries(false) , m_extractedFilesSize(0) { qCDebug(ARK) << "Initializing libarchive plugin"; archive_read_disk_set_standard_lookup(m_archiveReadDisk.data()); } LibarchivePlugin::~LibarchivePlugin() { } bool LibarchivePlugin::list() { qCDebug(ARK) << "Listing archive contents"; ArchiveRead arch_reader(archive_read_new()); if (!(arch_reader.data())) { return false; } if (archive_read_support_filter_all(arch_reader.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(arch_reader.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(arch_reader.data(), QFile::encodeName(filename()), 10240) != ARCHIVE_OK) { emit error(i18nc("@info", "Could not open the archive.")); return false; } qDebug(ARK) << "Detected compression filter:" << archive_filter_name(arch_reader.data(), 0); m_cachedArchiveEntryCount = 0; m_extractedFilesSize = 0; struct archive_entry *aentry; int result = ARCHIVE_RETRY; bool firstEntry = true; while (!m_abortOperation && (result = archive_read_next_header(arch_reader.data(), &aentry)) == ARCHIVE_OK) { if (firstEntry) { qDebug(ARK) << "Detected format for first entry:" << archive_format_name(arch_reader.data()); firstEntry = false; } if (!m_emitNoEntries) { emitEntryFromArchiveEntry(aentry); } m_extractedFilesSize += (qlonglong)archive_entry_size(aentry); m_cachedArchiveEntryCount++; archive_read_data_skip(arch_reader.data()); } m_abortOperation = false; if (result != ARCHIVE_EOF) { const QString errorString = QLatin1String(archive_error_string(arch_reader.data())); // FIXME: what about the other archive_error_string() calls? Do they also happen to return empty strings? emit error(errorString.isEmpty() ? i18nc("@info", "Could not read until the end of the archive") : errorString); return false; } return archive_read_close(arch_reader.data()) == ARCHIVE_OK; } bool LibarchivePlugin::addFiles(const QStringList &files, const CompressionOptions &options) { Q_UNUSED(files) Q_UNUSED(options) return false; } bool LibarchivePlugin::deleteFiles(const QList &files) { Q_UNUSED(files) return false; } bool LibarchivePlugin::addComment(const QString& comment) { Q_UNUSED(comment) return false; } bool LibarchivePlugin::testArchive() { return false; } bool LibarchivePlugin::doKill() { m_abortOperation = true; return true; } bool LibarchivePlugin::copyFiles(const QVariantList& files, const QString& destinationDirectory, const ExtractionOptions& options) { qCDebug(ARK) << "Changing current directory to " << destinationDirectory; QDir::setCurrent(destinationDirectory); const bool extractAll = files.isEmpty(); const bool preservePaths = options.value(QStringLiteral( "PreservePaths" )).toBool(); bool removeRootNode = options.value(QStringLiteral("RemoveRootNode"), QVariant()).toBool(); // To avoid traversing the entire archive when extracting a limited set of // entries, we maintain a list of remaining entries and stop when it's // empty. QVariantList remainingFiles = files; ArchiveRead arch(archive_read_new()); if (!(arch.data())) { return false; } if (archive_read_support_filter_all(arch.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(arch.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(arch.data(), QFile::encodeName(filename()), 10240) != ARCHIVE_OK) { // This error might be shown outside of a running Ark part (e.g. from a batch job). emit error(xi18nc("@info", "Could not open the archive %1." "Check whether you have sufficient permissions.", filename())); return false; } ArchiveWrite writer(archive_write_disk_new()); if (!writer.data()) { return false; } archive_write_disk_set_options(writer.data(), extractionFlags()); int entryNr = 0; int totalCount = 0; if (extractAll) { if (!m_cachedArchiveEntryCount) { emit progress(0); //TODO: once information progress has been implemented, send //feedback here that the archive is being read qCDebug(ARK) << "For getting progress information, the archive will be listed once"; m_emitNoEntries = true; list(); m_emitNoEntries = false; } totalCount = m_cachedArchiveEntryCount; } else { totalCount = files.size(); } qCDebug(ARK) << "Going to extract" << totalCount << "entries"; // Initialize variables. bool overwriteAll = false; // Whether to overwrite all files bool skipAll = false; // Whether to skip all files bool dontPromptErrors = false; // Whether to prompt for errors m_currentExtractedFilesSize = 0; int no_entries = 0; struct archive_entry *entry; QString fileBeingRenamed; // Iterate through all entries in archive. while (!m_abortOperation && (archive_read_next_header(arch.data(), &entry) == ARCHIVE_OK)) { if (!extractAll && remainingFiles.isEmpty()) { break; } fileBeingRenamed.clear(); int index = -1; // Retry with renamed entry, fire an overwrite query again // if the new entry also exists. retry: const bool entryIsDir = S_ISDIR(archive_entry_mode(entry)); // Skip directories if not preserving paths. if (!preservePaths && entryIsDir) { archive_read_data_skip(arch.data()); continue; } // entryName is the name inside the archive, full path QString entryName = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry))); // For now we just can't handle absolute filenames in a tar archive. // TODO: find out what to do here!! if (entryName.startsWith(QLatin1Char( '/' ))) { emit error(i18n("This archive contains archive entries with absolute paths, " "which are not supported by Ark.")); return false; } // Should the entry be extracted? if (extractAll || remainingFiles.contains(QVariant::fromValue(fileRootNodePair(entryName))) || entryName == fileBeingRenamed) { // Find the index of entry. if (entryName != fileBeingRenamed) { index = files.indexOf(QVariant::fromValue(fileRootNodePair(entryName))); } if (!extractAll && index == -1) { // If entry is not found in files, skip entry. continue; } // entryFI is the fileinfo pointing to where the file will be // written from the archive. QFileInfo entryFI(entryName); //qCDebug(ARK) << "setting path to " << archive_entry_pathname( entry ); const QString fileWithoutPath(entryFI.fileName()); // If we DON'T preserve paths, we cut the path and set the entryFI // fileinfo to the one without the path. if (!preservePaths) { // Empty filenames (ie dirs) should have been skipped already, // so asserting. Q_ASSERT(!fileWithoutPath.isEmpty()); archive_entry_copy_pathname(entry, QFile::encodeName(fileWithoutPath).constData()); entryFI = QFileInfo(fileWithoutPath); // OR, if the file has a rootNode attached, remove it from file path. } else if (!extractAll && removeRootNode && entryName != fileBeingRenamed && !files.at(index).value().rootNode.isEmpty()) { //qCDebug(ARK) << "Removing" << files.at(index).value().rootNode << "from" << entryName; const QString truncatedFilename(entryName.remove(0, files.at(index).value().rootNode.size())); archive_entry_copy_pathname(entry, QFile::encodeName(truncatedFilename).constData()); entryFI = QFileInfo(truncatedFilename); } // Check if the file about to be written already exists. if (!entryIsDir && entryFI.exists()) { if (skipAll) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); continue; } else if (!overwriteAll && !skipAll) { Kerfuffle::OverwriteQuery query(entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); break; } else if (query.responseSkip()) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); continue; } else if (query.responseAutoSkip()) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); skipAll = true; continue; } else if (query.responseRename()) { const QString newName(query.newFilename()); fileBeingRenamed = newName; archive_entry_copy_pathname(entry, QFile::encodeName(newName).constData()); goto retry; } else if (query.responseOverwriteAll()) { overwriteAll = true; } } } // If there is an already existing directory. if (entryIsDir && entryFI.exists()) { if (entryFI.isWritable()) { qCWarning(ARK) << "Warning, existing, but writable dir"; } else { qCWarning(ARK) << "Warning, existing, but non-writable dir. skipping"; archive_entry_clear(entry); archive_read_data_skip(arch.data()); continue; } } // Write the entry header and check return value. const int returnCode = archive_write_header(writer.data(), entry); switch (returnCode) { case ARCHIVE_OK: // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(entryName, arch.data(), writer.data(), (extractAll && m_extractedFilesSize)); break; case ARCHIVE_FAILED: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); // If they user previously decided to ignore future errors, // don't bother prompting again. if (!dontPromptErrors) { // Ask the user if he wants to continue extraction despite an error for this entry. Kerfuffle::ContinueExtractionQuery query(QLatin1String(archive_error_string(writer.data())), entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } dontPromptErrors = query.dontAskAgain(); } break; case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); emit error(xi18nc("@info", "Extraction failed at:%1", entryName)); return false; default: qCDebug(ARK) << "archive_write_header() returned" << returnCode << "which will be ignored."; break; } // If we only partially extract the archive and the number of // archive entries is available we use a simple progress based on // number of items extracted. if (!extractAll && m_cachedArchiveEntryCount) { ++entryNr; emit progress(float(entryNr) / totalCount); } archive_entry_clear(entry); no_entries++; remainingFiles.removeOne(QVariant::fromValue(fileRootNodePair(entryName))); } else { // Archive entry not among selected files, skip it. archive_read_data_skip(arch.data()); } } // While entries left to read in archive. m_abortOperation = false; qCDebug(ARK) << "Extracted" << no_entries << "entries"; return archive_read_close(arch.data()) == ARCHIVE_OK; } void LibarchivePlugin::emitEntryFromArchiveEntry(struct archive_entry *aentry) { - ArchiveEntry e; + Archive::Entry *e = new Archive::Entry(NULL); #ifdef _MSC_VER - e[FileName] = QDir::fromNativeSeparators(QString::fromUtf16((ushort*)archive_entry_pathname_w(aentry))); + e->fileName = QDir::fromNativeSeparators(QString::fromUtf16((ushort*)archive_entry_pathname_w(aentry))); #else - e[FileName] = QDir::fromNativeSeparators(QString::fromWCharArray(archive_entry_pathname_w(aentry))); + e->setProperty("fileName", QDir::fromNativeSeparators(QString::fromWCharArray(archive_entry_pathname_w(aentry)))); #endif - e[InternalID] = e[FileName]; const QString owner = QString::fromLatin1(archive_entry_uname(aentry)); if (!owner.isEmpty()) { - e[Owner] = owner; + e->setProperty("owner", owner); } const QString group = QString::fromLatin1(archive_entry_gname(aentry)); if (!group.isEmpty()) { - e[Group] = group; + e->setProperty("group", group); } - e[Size] = (qlonglong)archive_entry_size(aentry); - e[IsDirectory] = S_ISDIR(archive_entry_mode(aentry)); + e->compressedSizeIsSet = false; + e->setProperty("size", (qlonglong)archive_entry_size(aentry)); + e->setProperty("isDirectory", S_ISDIR(archive_entry_mode(aentry))); if (archive_entry_symlink(aentry)) { - e[Link] = QLatin1String( archive_entry_symlink(aentry) ); + e->setProperty("link", QLatin1String( archive_entry_symlink(aentry) )); } - e[Timestamp] = QDateTime::fromTime_t(archive_entry_mtime(aentry)); + e->setProperty("timestamp", QDateTime::fromTime_t(archive_entry_mtime(aentry))); emit entry(e); } int LibarchivePlugin::extractionFlags() const { int result = ARCHIVE_EXTRACT_TIME; result |= ARCHIVE_EXTRACT_SECURE_NODOTDOT; // TODO: Don't use arksettings here /*if ( ArkSettings::preservePerms() ) { result &= ARCHIVE_EXTRACT_PERM; } if ( !ArkSettings::extractOverwrite() ) { result &= ARCHIVE_EXTRACT_NO_OVERWRITE; }*/ return result; } void LibarchivePlugin::copyData(const QString& filename, struct archive *dest, bool partialprogress) { char buff[10240]; ssize_t readBytes; QFile file(filename); if (!file.open(QIODevice::ReadOnly)) { return; } readBytes = file.read(buff, sizeof(buff)); while (readBytes > 0) { archive_write_data(dest, buff, readBytes); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while writing" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = file.read(buff, sizeof(buff)); } file.close(); } void LibarchivePlugin::copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress) { char buff[10240]; ssize_t readBytes; readBytes = archive_read_data(source, buff, sizeof(buff)); while (readBytes > 0) { archive_write_data(dest, buff, readBytes); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while extracting" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = archive_read_data(source, buff, sizeof(buff)); } } #include "libarchiveplugin.moc" diff --git a/plugins/libsinglefileplugin/singlefileplugin.cpp b/plugins/libsinglefileplugin/singlefileplugin.cpp index c09a72e2..2c1eb045 100644 --- a/plugins/libsinglefileplugin/singlefileplugin.cpp +++ b/plugins/libsinglefileplugin/singlefileplugin.cpp @@ -1,171 +1,164 @@ /* * Copyright (c) 2009 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "singlefileplugin.h" #include "ark_debug.h" -#include "kerfuffle/kerfuffle_export.h" +#include "kerfuffle/archiveentry.h" #include "kerfuffle/queries.h" -#include #include #include -#include #include #include LibSingleFileInterface::LibSingleFileInterface(QObject *parent, const QVariantList & args) : Kerfuffle::ReadOnlyArchiveInterface(parent, args) { qCDebug(ARK) << "Loaded singlefile plugin"; } LibSingleFileInterface::~LibSingleFileInterface() { } bool LibSingleFileInterface::copyFiles(const QList& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) { Q_UNUSED(files) Q_UNUSED(options) QString outputFileName = destinationDirectory; if (!destinationDirectory.endsWith(QLatin1Char('/'))) { outputFileName += QLatin1Char('/'); } outputFileName += uncompressedFileName(); outputFileName = overwriteFileName(outputFileName); if (outputFileName.isEmpty()) { return true; } qCDebug(ARK) << "Extracting to" << outputFileName; QFile outputFile(outputFileName); if (!outputFile.open(QIODevice::WriteOnly)) { qCCritical(ARK) << "Failed to open output file" << outputFile.errorString(); emit error(xi18nc("@info", "Ark could not extract %1.", outputFile.fileName())); return false; } KCompressionDevice *device = new KCompressionDevice(filename(), KFilterDev::compressionTypeForMimeType(m_mimeType)); if (!device) { qCCritical(ARK) << "Could not create KCompressionDevice"; emit error(xi18nc("@info", "Ark could not open %1 for extraction.", filename())); return false; } device->open(QIODevice::ReadOnly); qint64 bytesRead; QByteArray dataChunk(1024*16, '\0'); // 16Kb while (true) { bytesRead = device->read(dataChunk.data(), dataChunk.size()); if (bytesRead == -1) { emit error(xi18nc("@info", "There was an error while reading %1 during extraction.", filename())); break; } else if (bytesRead == 0) { break; } outputFile.write(dataChunk.data(), bytesRead); } delete device; return true; } bool LibSingleFileInterface::list() { qCDebug(ARK) << "Listing archive contents"; - const QString filename = uncompressedFileName(); - - Kerfuffle::ArchiveEntry e; - - e[Kerfuffle::FileName] = filename; - e[Kerfuffle::InternalID] = filename; - + Kerfuffle::Archive::Entry *e = new Kerfuffle::Archive::Entry(Q_NULLPTR); + e->setProperty("fileName", uncompressedFileName()); emit entry(e); return true; } QString LibSingleFileInterface::overwriteFileName(QString& filename) { QString newFileName(filename); while (QFile::exists(newFileName)) { Kerfuffle::OverwriteQuery query(newFileName); query.setMultiMode(false); emit userQuery(&query); query.waitForResponse(); if ((query.responseCancelled()) || (query.responseSkip())) { return QString(); } else if (query.responseOverwrite()) { break; } else if (query.responseRename()) { newFileName = query.newFilename(); } } return newFileName; } const QString LibSingleFileInterface::uncompressedFileName() const { QString uncompressedName(QFileInfo(filename()).fileName()); // Bug 252701: For .svgz just remove the terminal "z". if (uncompressedName.endsWith(QLatin1String(".svgz"), Qt::CaseInsensitive)) { uncompressedName.chop(1); return uncompressedName; } foreach(const QString & extension, m_possibleExtensions) { qCDebug(ARK) << extension; if (uncompressedName.endsWith(extension, Qt::CaseInsensitive)) { uncompressedName.chop(extension.size()); return uncompressedName; } } return uncompressedName + QStringLiteral( ".uncompressed" ); } bool LibSingleFileInterface::testArchive() { return false; }