diff --git a/autotests/kerfuffle/jobstest.cpp b/autotests/kerfuffle/jobstest.cpp index 1b79eac9..6392d8e0 100644 --- a/autotests/kerfuffle/jobstest.cpp +++ b/autotests/kerfuffle/jobstest.cpp @@ -1,450 +1,450 @@ /* * 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(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); void startAndWaitForResult(KJob *job); QList m_entries; QEventLoop m_eventLoop; }; QTEST_GUILESS_MAIN(JobsTest) JobsTest::JobsTest() : QObject(Q_NULLPTR) , m_eventLoop(this) { } void JobsTest::init() { m_entries.clear(); } void JobsTest::slotNewEntry(Archive::Entry *entry) { m_entries.append(entry); } JSONArchiveInterface *JobsTest::createArchiveInterface(const QString& filePath) { JSONArchiveInterface *iface = new JSONArchiveInterface(this, {filePath}); if (!iface->open()) { qDebug() << "Could not open" << filePath; return Q_NULLPTR; } return 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)->property("fullPath").toString(), expectedEntryNames.at(i)); + QCOMPARE(archiveEntries.at(i)->fullPath(), expectedEntryNames.at(i)); } listJob->deleteLater(); } void JobsTest::testExtractJobAccessors() { JSONArchiveInterface *iface = createArchiveInterface(QFINDTESTDATA("data/archive001.json")); ExtractJob *job = new ExtractJob(QList(), 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(QList(), 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(new Archive::Entry(this, 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") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QList {new Archive::Entry(this, QStringLiteral("c.txt"))}; QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) }; // Error test: if we delete non-existent entries, the archive must not change. QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QList {new Archive::Entry(this, QStringLiteral("foo.txt"))}; } void JobsTest::testRemoveEntries() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); QFETCH(QList, entries); QFETCH(QList, entriesToDelete); QStringList fullPathsToDelete = iface->entryFullPaths(entriesToDelete); QList expectedRemainingEntries; Q_FOREACH (Archive::Entry *entry, entries) { - if (!fullPathsToDelete.contains(entry->property("fullPath").toString())) { + if (!fullPathsToDelete.contains(entry->fullPath())) { expectedRemainingEntries.append(entry); } } DeleteJob *deleteJob = new DeleteJob(entriesToDelete, iface); startAndWaitForResult(deleteJob); auto remainingEntries = listEntries(iface); QCOMPARE(remainingEntries.size(), expectedRemainingEntries.size()); for (int i = 0; i < remainingEntries.size(); i++) { QCOMPARE(*remainingEntries.at(i), *expectedRemainingEntries.at(i)); } iface->deleteLater(); } void JobsTest::testAddEntries_data() { QTest::addColumn("jsonArchive"); QTest::addColumn>("originalEntries"); QTest::addColumn>("entriesToAdd"); QTest::addColumn("destinationEntry"); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QList { new Archive::Entry(this, QStringLiteral("foo.txt")) } << new Archive::Entry(this); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QList { new Archive::Entry(this, QStringLiteral("foo.txt")), new Archive::Entry(this, QStringLiteral("bar.txt")) } << new Archive::Entry(this); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QList { new Archive::Entry(this, QStringLiteral("foo.txt")), new Archive::Entry(this, QStringLiteral("bar.txt")) } << new Archive::Entry(this, QStringLiteral("aDir/")); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QList {new Archive::Entry(this, QStringLiteral("c.txt"))} << new Archive::Entry(this, QStringLiteral("aDir/")); // Error test: if we add an already existent entry, the archive must not change. QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("aDir/")), new Archive::Entry(this, QStringLiteral("aDir/b.txt")), new Archive::Entry(this, QStringLiteral("c.txt")) } << QList {new Archive::Entry(this, QStringLiteral("c.txt"))} << new Archive::Entry(this); } void JobsTest::testAddEntries() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); QFETCH(QList, originalEntries); QStringList originalFullPaths = QStringList(); Q_FOREACH (const Archive::Entry *entry, originalEntries) { - originalFullPaths.append(entry->property("fullPath").toString()); + originalFullPaths.append(entry->fullPath()); } auto currentEntries = listEntries(iface); QCOMPARE(currentEntries.size(), originalEntries.size()); QFETCH(QList, entriesToAdd); QFETCH(Archive::Entry*, destinationEntry); AddJob *addJob = new AddJob(entriesToAdd, destinationEntry, CompressionOptions(), iface); startAndWaitForResult(addJob); QStringList expectedAddedFullPaths = QStringList(); - const QString destinationPath = destinationEntry->property("fullPath").toString(); + const QString destinationPath = destinationEntry->fullPath(); int expectedEntriesCount = originalEntries.size(); Q_FOREACH (const Archive::Entry *entry, entriesToAdd) { - const QString fullPath = destinationPath + entry->property("fullPath").toString(); + const QString fullPath = destinationPath + entry->fullPath(); if (!originalFullPaths.contains(fullPath)) { expectedEntriesCount++; - expectedAddedFullPaths << destinationPath + entry->property("fullPath").toString(); + expectedAddedFullPaths << destinationPath + entry->fullPath(); } } currentEntries = listEntries(iface); QCOMPARE(currentEntries.size(), expectedEntriesCount); QStringList currentFullPaths = QStringList(); Q_FOREACH (const Archive::Entry* entry, currentEntries) { - currentFullPaths << entry->property("fullPath").toString(); + currentFullPaths << entry->fullPath(); } Q_FOREACH (const QString fullPath, expectedAddedFullPaths) { QVERIFY(currentFullPaths.contains(fullPath)); } iface->deleteLater(); } #include "jobstest.moc" diff --git a/autotests/kerfuffle/jsonarchiveinterface.cpp b/autotests/kerfuffle/jsonarchiveinterface.cpp index 443f9e22..5e5672ee 100644 --- a/autotests/kerfuffle/jsonarchiveinterface.cpp +++ b/autotests/kerfuffle/jsonarchiveinterface.cpp @@ -1,126 +1,126 @@ /* * 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 QList& files, const Kerfuffle::Archive::Entry *destination, const Kerfuffle::CompressionOptions& options) { Q_UNUSED(options) foreach (const Kerfuffle::Archive::Entry *entry, files) { - const QString &path = destination->property("fullPath").toString() + entry->property("fullPath").toString(); + const QString &path = destination->fullPath() + entry->fullPath(); if (m_archive.contains(path)) { return false; } Kerfuffle::Archive::Entry *e = new Kerfuffle::Archive::Entry(Q_NULLPTR); e->setProperty("fullPath", path); m_archive[path] = e; } return true; } bool JSONArchiveInterface::moveFiles(const QList& files, Kerfuffle::Archive::Entry *destination, const Kerfuffle::ExtractionOptions& options) { Q_UNUSED(files) Q_UNUSED(destination) Q_UNUSED(options) 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 Kerfuffle::Archive::Entry *file, files) { - const QString &fileName = file->property("fullPath").toString(); + const QString &fileName = file->fullPath(); 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/plugins/cli7zplugin/cli7ztest.cpp b/autotests/plugins/cli7zplugin/cli7ztest.cpp index d1aaed47..b60a2350 100644 --- a/autotests/plugins/cli7zplugin/cli7ztest.cpp +++ b/autotests/plugins/cli7zplugin/cli7ztest.cpp @@ -1,488 +1,488 @@ /* * 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 #include QTEST_GUILESS_MAIN(Cli7zTest) using namespace Kerfuffle; void Cli7zTest::initTestCase() { m_plugin = new Plugin(this); foreach (Plugin *plugin, m_pluginManger.availablePlugins()) { if (plugin->metaData().pluginId() == QStringLiteral("kerfuffle_cli7z")) { m_plugin = plugin; return; } } } void Cli7zTest::testArchive_data() { QTest::addColumn("archivePath"); QTest::addColumn("expectedFileName"); QTest::addColumn("isReadOnly"); QTest::addColumn("isSingleFolder"); QTest::addColumn("expectedEncryptionType"); QTest::addColumn("expectedSubfolderName"); QString archivePath = QFINDTESTDATA("data/one_toplevel_folder.7z"); QTest::newRow("archive with one top-level folder") << archivePath << QFileInfo(archivePath).fileName() << false << true << Archive::Unencrypted << QStringLiteral("A"); } void Cli7zTest::testArchive() { if (!m_plugin->isValid()) { QSKIP("cli7z plugin not available. Skipping test.", SkipSingle); } QFETCH(QString, archivePath); 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() { qRegisterMetaType("Archive::Entry*"); CliPlugin *plugin = new CliPlugin(this, {QStringLiteral("dummy.7z")}); QSignalSpy signalSpy(plugin, &CliPlugin::entry); 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()); Archive::Entry *entry = signalSpy.at(someEntryIndex).at(0).value(); QFETCH(QString, expectedName); - QCOMPARE(entry->property("fullPath").toString(), expectedName); + QCOMPARE(entry->fullPath(), expectedName); QFETCH(bool, isDirectory); QCOMPARE(entry->isDir(), isDirectory); QFETCH(bool, isPasswordProtected); QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected); QFETCH(qulonglong, expectedSize); QCOMPARE(entry->property("size").toULongLong(), expectedSize); QFETCH(QString, expectedTimestamp); QCOMPARE(entry->property("timestamp").toString(), expectedTimestamp); plugin->deleteLater(); } void Cli7zTest::testListArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.7z") << QString() << QStringList { QStringLiteral("l"), QStringLiteral("-slt"), QStringLiteral("/tmp/foo.7z") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.7z") << QStringLiteral("1234") << QStringList { QStringLiteral("l"), QStringLiteral("-slt"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.7z") }; } void Cli7zTest::testListArgs() { 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") << QList { new Archive::Entry(this, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << true << QStringLiteral("1234") << QStringList { QStringLiteral("x"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("preserve paths, unencrypted") << QStringLiteral("/tmp/foo.7z") << QList { new Archive::Entry(this, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), new Archive::Entry(this, 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") << QList { new Archive::Entry(this, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << false << QStringLiteral("1234") << QStringList { QStringLiteral("e"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, unencrypted") << QStringLiteral("/tmp/foo.7z") << QList { new Archive::Entry(this, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), new Archive::Entry(this, 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(QList, 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(); } void Cli7zTest::testAdd_data() { QTest::addColumn("archiveName"); QTest::addColumn>("files"); QTest::addColumn("destination"); QTest::newRow("without destination") << QStringLiteral("test.7z") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("b.txt")), } << new Archive::Entry(this); QTest::newRow("with destination, files") << QStringLiteral("test.7z") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("b.txt")), } << new Archive::Entry(this, QStringLiteral("dir/")); QTest::newRow("with destination, directory") << QStringLiteral("test.7z") << QList { new Archive::Entry(this, QStringLiteral("dir/")), } << new Archive::Entry(this, QStringLiteral("dir/")); } void Cli7zTest::testAdd() { QTemporaryDir temporaryDir; QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(temporaryDir.path() + QLatin1Char('/') + archiveName)}); QVERIFY(plugin); QFETCH(QList, files); QFETCH(Archive::Entry*, destination); CompressionOptions options = CompressionOptions(); options.insert(QStringLiteral("GlobalWorkDir"), QFINDTESTDATA("data")); AddJob *addJob = new AddJob(files, destination, options, plugin); TestHelper::startAndWaitForResult(addJob); QList resultedEntries = TestHelper::getEntryList(plugin); TestHelper::verifyAddedEntriesWithDestination(files, destination, resultedEntries); plugin->deleteLater(); } void Cli7zTest::testMove_data() { QTest::addColumn("archiveName"); QTest::addColumn>("files"); QTest::addColumn("destination"); QTest::newRow("replace a file") << QStringLiteral("test.7z") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), } << new Archive::Entry(this, QStringLiteral("empty_dir/a.txt")); QTest::newRow("replace several files") << QStringLiteral("test.7z") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("b.txt")), } << new Archive::Entry(this, QStringLiteral("empty_dir/")); QTest::newRow("replace a directory") << QStringLiteral("test.7z") << QList { new Archive::Entry(this, QStringLiteral("dir/")), } << new Archive::Entry(this, QStringLiteral("empty_dir/dir/")); } void Cli7zTest::testMove() { QTemporaryDir temporaryDir; QFETCH(QString, archiveName); const QString archivePath = temporaryDir.path() + QLatin1Char('/') + archiveName; Q_ASSERT(QFile::copy(QFINDTESTDATA(QStringLiteral("data/") + archiveName), archivePath)); CliPlugin *plugin = new CliPlugin(this, {QVariant(archivePath)}); QVERIFY(plugin); QFETCH(QList, files); QFETCH(Archive::Entry*, destination); CompressionOptions options = CompressionOptions(); options.insert(QStringLiteral("GlobalWorkDir"), QFINDTESTDATA("data")); MoveJob *moveJob = new MoveJob(files, destination, options, plugin); TestHelper::startAndWaitForResult(moveJob); QList resultedEntries = TestHelper::getEntryList(plugin); TestHelper::verifyMovedEntriesWithDestination(files, destination, resultedEntries); plugin->deleteLater(); } diff --git a/autotests/plugins/clirarplugin/clirartest.cpp b/autotests/plugins/clirarplugin/clirartest.cpp index 8b8a2ffe..43a35592 100644 --- a/autotests/plugins/clirarplugin/clirartest.cpp +++ b/autotests/plugins/clirarplugin/clirartest.cpp @@ -1,531 +1,531 @@ /* * 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() { 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() { qRegisterMetaType("Archive::Entry*"); CliPlugin *rarPlugin = new CliPlugin(this, {QStringLiteral("dummy.rar")}); QSignalSpy signalSpy(rarPlugin, &CliPlugin::entry); 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()); Archive::Entry *entry = signalSpy.at(someEntryIndex).at(0).value(); QFETCH(QString, expectedName); - QCOMPARE(entry->property("fullPath").toString(), expectedName); + QCOMPARE(entry->fullPath(), expectedName); QFETCH(bool, isDirectory); QCOMPARE(entry->isDir(), isDirectory); QFETCH(bool, isPasswordProtected); QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected); QFETCH(QString, symlinkTarget); QCOMPARE(entry->property("link").toString(), symlinkTarget); QFETCH(qulonglong, expectedSize); QCOMPARE(entry->property("size").toULongLong(), expectedSize); QFETCH(qulonglong, expectedCompressedSize); QCOMPARE(entry->property("compressedSize").toULongLong(), expectedCompressedSize); QFETCH(QString, expectedTimestamp); QCOMPARE(entry->property("timestamp").toString(), expectedTimestamp); rarPlugin->deleteLater(); } void CliRarTest::testListArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.rar") << QString() << QStringList { QStringLiteral("vt"), QStringLiteral("-v"), QStringLiteral("/tmp/foo.rar") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.rar") << QStringLiteral("1234") << QStringList { QStringLiteral("vt"), QStringLiteral("-v"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.rar") }; } void CliRarTest::testListArgs() { 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") << QList { new Archive::Entry(this, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), new Archive::Entry(this, 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") << QList { new Archive::Entry(this, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), new Archive::Entry(this, 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") << QList { new Archive::Entry(this, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), new Archive::Entry(this, 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") << QList { new Archive::Entry(this, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), new Archive::Entry(this, 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(QList, 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(); } void CliRarTest::testAdd_data() { QTest::addColumn("archiveName"); QTest::addColumn>("files"); QTest::addColumn("destination"); QTest::newRow("without destination") << QStringLiteral("test.rar") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("b.txt")), } << new Archive::Entry(this); QTest::newRow("with destination, files") << QStringLiteral("test.rar") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("b.txt")), } << new Archive::Entry(this, QStringLiteral("dir/")); QTest::newRow("with destination, directory") << QStringLiteral("test.rar") << QList { new Archive::Entry(this, QStringLiteral("dir/")), } << new Archive::Entry(this, QStringLiteral("dir/")); } void CliRarTest::testAdd() { QTemporaryDir temporaryDir; QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(temporaryDir.path() + QLatin1Char('/') + archiveName)}); QVERIFY(plugin); QFETCH(QList, files); QFETCH(Archive::Entry*, destination); CompressionOptions options = CompressionOptions(); options.insert(QStringLiteral("GlobalWorkDir"), QFINDTESTDATA("data")); AddJob *addJob = new AddJob(files, destination, options, plugin); TestHelper::startAndWaitForResult(addJob); QList resultedEntries = TestHelper::getEntryList(plugin); TestHelper::verifyAddedEntriesWithDestination(files, destination, resultedEntries); plugin->deleteLater(); } void CliRarTest::testMove_data() { QTest::addColumn("archiveName"); QTest::addColumn>("files"); QTest::addColumn("destination"); QTest::newRow("replace a file") << QStringLiteral("test.rar") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), } << new Archive::Entry(this, QStringLiteral("empty_dir/a.txt")); QTest::newRow("replace several files") << QStringLiteral("test.rar") << QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("b.txt")), } << new Archive::Entry(this, QStringLiteral("empty_dir/")); QTest::newRow("replace a directory") << QStringLiteral("test.rar") << QList { new Archive::Entry(this, QStringLiteral("dir/")), } << new Archive::Entry(this, QStringLiteral("empty_dir/dir/")); } void CliRarTest::testMove() { QTemporaryDir temporaryDir; QFETCH(QString, archiveName); const QString archivePath = temporaryDir.path() + QLatin1Char('/') + archiveName; Q_ASSERT(QFile::copy(QFINDTESTDATA(QStringLiteral("data/") + archiveName), archivePath)); CliPlugin *plugin = new CliPlugin(this, {QVariant(archivePath)}); QVERIFY(plugin); QFETCH(QList, files); QFETCH(Archive::Entry*, destination); CompressionOptions options = CompressionOptions(); options.insert(QStringLiteral("GlobalWorkDir"), QFINDTESTDATA("data")); MoveJob *moveJob = new MoveJob(files, destination, options, plugin); TestHelper::startAndWaitForResult(moveJob); QList resultedEntries = TestHelper::getEntryList(plugin); TestHelper::verifyMovedEntriesWithDestination(files, destination, resultedEntries); plugin->deleteLater(); } diff --git a/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp b/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp index 1c7675dd..d89c0b4b 100644 --- a/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp +++ b/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp @@ -1,384 +1,384 @@ /* * 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() { 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-21T16:57:20+01:00"); QTest::newRow("archive with multiple top-level entries") << QFINDTESTDATA("data/multiple_toplevel_entries.json") << 12 << 4 << QStringLiteral("data/A/B/test1.txt") << false << false << (qulonglong) 7 << (qulonglong) 7 << QStringLiteral("2015-12-21T16:56:28+01:00"); QTest::newRow("archive with encrypted entries") << QFINDTESTDATA("data/encrypted_entries.json") << 9 << 5 << QStringLiteral("A/test1.txt") << false << true << (qulonglong) 7 << (qulonglong) 32 << QStringLiteral("2015-12-21T16:56:28+01:00"); QTest::newRow("huge archive") << QFINDTESTDATA("data/huge_archive.json") << 250 << 8 << QStringLiteral("PsycOPacK/Base Dictionnaries/att800") << false << false << (qulonglong) 593687 << (qulonglong) 225219 << QStringLiteral("2011-08-14T03:10:10+02:00"); } void CliUnarchiverTest::testList() { qRegisterMetaType("Archive::Entry*"); CliPlugin *unarPlugin = new CliPlugin(this, {QStringLiteral("dummy.rar")}); QSignalSpy signalSpy(unarPlugin, &CliPlugin::entry); 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()); Archive::Entry *entry = signalSpy.at(someEntryIndex).at(0).value(); QFETCH(QString, expectedName); - QCOMPARE(entry->property("fullPath").toString(), expectedName); + QCOMPARE(entry->fullPath(), expectedName); QFETCH(bool, isDirectory); QCOMPARE(entry->isDir(), isDirectory); QFETCH(bool, isPasswordProtected); QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected); QFETCH(qulonglong, expectedSize); QCOMPARE(entry->property("size").toULongLong(), expectedSize); QFETCH(qulonglong, expectedCompressedSize); QCOMPARE(entry->property("compressedSize").toULongLong(), expectedCompressedSize); QFETCH(QString, expectedTimestamp); QCOMPARE(entry->property("timestamp").toString(), expectedTimestamp); unarPlugin->deleteLater(); } 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") << QList() << optionsPreservePaths << 12; QTest::newRow("extract selected entries from a rar, without paths") << QFINDTESTDATA("data/one_toplevel_folder.rar") << QList { new Archive::Entry(this, QStringLiteral("A/test2.txt"), QStringLiteral("A")), new Archive::Entry(this, 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") << QList { new Archive::Entry(this, QStringLiteral("A/test2.txt"), QStringLiteral("A")), new Archive::Entry(this, 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") << QList { new Archive::Entry(this, QStringLiteral("A/B/C/"), QStringLiteral("A/B/")), new Archive::Entry(this, QStringLiteral("A/test2.txt"), QStringLiteral("A/")), new Archive::Entry(this, QStringLiteral("A/B/C/test1.txt"), QStringLiteral("A/B/")), new Archive::Entry(this, QStringLiteral("A/B/C/test2.txt"), QStringLiteral("A/B/")) } << dragAndDropOptions << 4; QTest::newRow("rar with empty folders") << QFINDTESTDATA("data/empty_folders.rar") << QList() << 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(QList, 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") << QList { new Archive::Entry(this, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), new Archive::Entry(this, 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") << QList { new Archive::Entry(this, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << QString() << QStringList { QStringLiteral("-D"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; } void CliUnarchiverTest::testExtractArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList extractArgs = { QStringLiteral("-D"), QStringLiteral("$Archive"), QStringLiteral("$Files"), QStringLiteral("$PasswordSwitch") }; QFETCH(QList, 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/testhelper/testhelper.cpp b/autotests/testhelper/testhelper.cpp index 83845f87..1c41139f 100644 --- a/autotests/testhelper/testhelper.cpp +++ b/autotests/testhelper/testhelper.cpp @@ -1,61 +1,61 @@ // // Created by mvlabat on 7/21/16. // #include "testhelper.h" QEventLoop TestHelper::m_eventLoop; void TestHelper::startAndWaitForResult(KJob *job) { QObject::connect(job, &KJob::result, &m_eventLoop, &QEventLoop::quit); job->start(); m_eventLoop.exec(); } QList TestHelper::getEntryList(ReadOnlyArchiveInterface *iface) { QList list = QList(); ListJob *listJob = new ListJob(iface); QObject::connect(listJob, &Job::newEntry, [&list](Archive::Entry* entry) { list << entry; }); startAndWaitForResult(listJob); return list; } QStringList TestHelper::getExpectedEntryPaths(const QList &entryList, const Archive::Entry *destination) { QStringList expectedPaths = QStringList(); if (entryList.count() > 1) { foreach (const Archive::Entry *entry, entryList) { - expectedPaths << destination->property("fullPath").toString() + entry->property("fullPath").toString(); + expectedPaths << destination->fullPath() + entry->fullPath(); } } else { - expectedPaths << destination->property("fullPath").toString(); + expectedPaths << destination->fullPath(); } return expectedPaths; } void TestHelper::verifyAddedEntriesWithDestination(const QList &argumentEntries, const Archive::Entry *destination, const QList &newEntries) { QStringList expectedPaths = getExpectedEntryPaths(argumentEntries, destination); QStringList actualPaths = ReadOnlyArchiveInterface::entryFullPaths(newEntries); foreach (const QString &path, expectedPaths) { QVERIFY2(actualPaths.contains(path), (QStringLiteral("No ") + path + QStringLiteral(" inside the archive")).toStdString().c_str()); } } void TestHelper::verifyMovedEntriesWithDestination(const QList &argumentEntries, const Archive::Entry *destination, const QList &newEntries) { QStringList oldPaths = ReadOnlyArchiveInterface::entryFullPaths(argumentEntries); QStringList expectedPaths = getExpectedEntryPaths(argumentEntries, destination); QStringList actualPaths = ReadOnlyArchiveInterface::entryFullPaths(newEntries); foreach (const QString &path, expectedPaths) { QVERIFY2(actualPaths.contains(path), (QStringLiteral("No ") + path + QStringLiteral(" inside the archive")).toStdString().c_str()); QVERIFY2(!oldPaths.contains(path), (QStringLiteral("Entry ") + path + QStringLiteral(" is still inside the archive, when it shouldn't be")).toStdString().c_str()); } } diff --git a/kerfuffle/addtoarchive.cpp b/kerfuffle/addtoarchive.cpp index 435ee207..3df09af4 100644 --- a/kerfuffle/addtoarchive.cpp +++ b/kerfuffle/addtoarchive.cpp @@ -1,271 +1,271 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Harald Hvaal * 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 "addtoarchive.h" #include "ark_debug.h" #include "archive_kerfuffle.h" #include "createdialog.h" #include "jobs.h" #include #include #include #include #include #include #include #include #include #include #include namespace Kerfuffle { AddToArchive::AddToArchive(QObject *parent) : KJob(parent), m_changeToFirstPath(false) { } AddToArchive::~AddToArchive() { } void AddToArchive::setAutoFilenameSuffix(const QString& suffix) { m_autoFilenameSuffix = suffix; } void AddToArchive::setChangeToFirstPath(bool value) { m_changeToFirstPath = value; } void AddToArchive::setFilename(const QUrl &path) { m_filename = path.toDisplayString(QUrl::PreferLocalFile); } void AddToArchive::setMimeType(const QString & mimeType) { m_mimeType = mimeType; } void AddToArchive::setPassword(const QString &password) { m_password = password; } void AddToArchive::setHeaderEncryptionEnabled(bool enabled) { m_enableHeaderEncryption = enabled; } bool AddToArchive::showAddDialog() { qCDebug(ARK) << "Opening add dialog"; QPointer dialog = new Kerfuffle::CreateDialog( Q_NULLPTR, // parent i18n("Compress to Archive"), // caption QUrl::fromLocalFile(m_firstPath)); // startDir bool ret = dialog.data()->exec(); if (ret) { qCDebug(ARK) << "CreateDialog returned URL:" << dialog.data()->selectedUrl().toString(); qCDebug(ARK) << "CreateDialog returned mime:" << dialog.data()->currentMimeType().name(); setFilename(dialog.data()->selectedUrl()); setMimeType(dialog.data()->currentMimeType().name()); setPassword(dialog.data()->password()); setHeaderEncryptionEnabled(dialog.data()->isHeaderEncryptionEnabled()); } delete dialog.data(); return ret; } bool AddToArchive::addInput(const QUrl &url) { Archive::Entry *entry = new Archive::Entry(); entry->setFullPath(url.toDisplayString(QUrl::PreferLocalFile)); m_entries << entry; if (m_firstPath.isEmpty()) { QString firstEntry = url.toDisplayString(QUrl::PreferLocalFile); m_firstPath = QFileInfo(firstEntry).dir().absolutePath(); } return true; } void AddToArchive::start() { qCDebug(ARK) << "Starting job"; QTimer::singleShot(0, this, &AddToArchive::slotStartJob); } void AddToArchive::slotStartJob() { Kerfuffle::CompressionOptions options; if (m_entries.isEmpty()) { KMessageBox::error(NULL, i18n("No input files were given.")); emitResult(); return; } Kerfuffle::Archive *archive; if (!m_filename.isEmpty()) { archive = Kerfuffle::Archive::create(m_filename, m_mimeType, this); qCDebug(ARK) << "Set filename to " << m_filename; } else { if (m_autoFilenameSuffix.isEmpty()) { KMessageBox::error(Q_NULLPTR, xi18n("You need to either supply a filename for the archive or a suffix (such as rar, tar.gz) with the --autofilename argument.")); emitResult(); return; } if (m_firstPath.isEmpty()) { qCWarning(ARK) << "Weird, this should not happen. no firstpath defined. aborting"; emitResult(); return; } const QString base = detectBaseName(m_entries); QString finalName = base + QLatin1Char( '.' ) + m_autoFilenameSuffix; //if file already exists, append a number to the base until it doesn't //exist int appendNumber = 0; while (QFileInfo::exists(finalName)) { ++appendNumber; finalName = base + QLatin1Char( '_' ) + QString::number(appendNumber) + QLatin1Char( '.' ) + m_autoFilenameSuffix; } qCDebug(ARK) << "Autoset filename to "<< finalName; archive = Kerfuffle::Archive::create(finalName, m_mimeType, this); } Q_ASSERT(archive); if (!archive->isValid()) { if (archive->error() == NoPlugin) { KMessageBox::error(Q_NULLPTR, i18n("Failed to create the new archive. No suitable plugin found.")); emitResult(); return; } if (archive->error() == FailedPlugin) { KMessageBox::error(Q_NULLPTR, i18n("Failed to create the new archive. Could not load a suitable plugin.")); emitResult(); return; } } else if (archive->isReadOnly()) { KMessageBox::error(Q_NULLPTR, i18n("It is not possible to create archives of this type.")); emitResult(); return; } if (!m_password.isEmpty()) { archive->encrypt(m_password, m_enableHeaderEncryption); } if (m_changeToFirstPath) { if (m_firstPath.isEmpty()) { qCWarning(ARK) << "Weird, this should not happen. no firstpath defined. aborting"; emitResult(); return; } const QDir stripDir(m_firstPath); foreach (Archive::Entry *entry, m_entries) { - entry->setFullPath(stripDir.absoluteFilePath(entry->property("fullPath").toString())); + entry->setFullPath(stripDir.absoluteFilePath(entry->fullPath())); } options[QStringLiteral( "GlobalWorkDir" )] = stripDir.path(); qCDebug(ARK) << "Setting GlobalWorkDir to " << stripDir.path(); } Kerfuffle::AddJob *job = archive->addFiles(m_entries, new Archive::Entry(this), options); KIO::getJobTracker()->registerJob(job); connect(job, &Kerfuffle::AddJob::result, this, &AddToArchive::slotFinished); job->start(); } void AddToArchive::slotFinished(KJob *job) { qCDebug(ARK) << "AddToArchive job finished"; if (job->error() && !job->errorText().isEmpty()) { KMessageBox::error(Q_NULLPTR, job->errorText()); } emitResult(); } QString AddToArchive::detectBaseName(const QList &entries) const { - QFileInfo fileInfo = QFileInfo(entries.first()->property("fullPath").toString()); + QFileInfo fileInfo = QFileInfo(entries.first()->fullPath()); QDir parentDir = fileInfo.dir(); QString base = parentDir.absolutePath() + QLatin1Char('/'); if (entries.size() > 1) { if (!parentDir.isRoot()) { // Use directory name for the new archive. base += parentDir.dirName(); } } else { // Strip filename of its extension, but only if present (see #362690). if (!QMimeDatabase().mimeTypeForFile(fileInfo.fileName(), QMimeDatabase::MatchExtension).isDefault()) { base += fileInfo.completeBaseName(); } else { base += fileInfo.fileName(); } } // Special case for compressed tar archives. if (base.right(4).toUpper() == QLatin1String(".TAR")) { base.chop(4); } if (base.endsWith(QLatin1Char('/'))) { base.chop(1); } return base; } } diff --git a/kerfuffle/archiveentry.cpp b/kerfuffle/archiveentry.cpp index 6c03fa6f..4346bee7 100644 --- a/kerfuffle/archiveentry.cpp +++ b/kerfuffle/archiveentry.cpp @@ -1,168 +1,177 @@ // // Created by mvlabat on 5/27/16. // #include "archiveentry.h" namespace Kerfuffle { Archive::Entry::Entry(QObject *parent, QString fullPath, QString rootNode) : QObject(parent) , rootNode(rootNode) , compressedSizeIsSet(true) , m_parent(qobject_cast(parent)) , m_size(0) , m_compressedSize(0) , m_isDirectory(false) , m_isPasswordProtected(false) { if (!fullPath.isEmpty()) setFullPath(fullPath); } Archive::Entry::~Entry() { clear(); } QVector Archive::Entry::entries() { Q_ASSERT(isDir()); return m_entries; } const QVector Archive::Entry::entries() const { Q_ASSERT(isDir()); return m_entries; } void Archive::Entry::setEntryAt(int index, Entry *value) { Q_ASSERT(isDir()); Q_ASSERT(index < m_entries.count()); 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()); Q_ASSERT(index < m_entries.count()); delete m_entries.takeAt(index); } Archive::Entry *Archive::Entry::getParent() const { return m_parent; } void Archive::Entry::setParent(Archive::Entry *parent) { m_parent = parent; } void Archive::Entry::setFullPath(const QString &fullPath) { m_fullPath = fullPath; + m_fullPathWithoutTrailingSlash = fullPath; + if (m_fullPathWithoutTrailingSlash.right(1) == QLatin1String("/")) { + m_fullPathWithoutTrailingSlash.chop(1); + } const QStringList pieces = m_fullPath.split(QLatin1Char( '/' ), QString::SkipEmptyParts); m_name = pieces.isEmpty() ? QString() : pieces.last(); } -void Archive::Entry::setIsDirectory(const bool isDirectory) +QString Archive::Entry::fullPath(bool withoutTrailingSlash) const { - m_isDirectory = isDirectory; + return (withoutTrailingSlash) ? m_fullPathWithoutTrailingSlash : m_fullPath; } -int Archive::Entry::row() const +QString Archive::Entry::name() const { - if (getParent()) { - return getParent()->entries().indexOf(const_cast(this)); - } - return 0; + return m_name; +} + +void Archive::Entry::setIsDirectory(const bool isDirectory) +{ + m_isDirectory = isDirectory; } bool Archive::Entry::isDir() const { return m_isDirectory; } -QString Archive::Entry::name() const +int Archive::Entry::row() const { - return m_name; + if (getParent()) { + return getParent()->entries().indexOf(const_cast(this)); + } + return 0; } 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::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(); } } bool Archive::Entry::operator==(const Archive::Entry &right) const { return m_fullPath == right.m_fullPath; } QDebug operator<<(QDebug d, const Kerfuffle::Archive::Entry &entry) { d.nospace() << "Entry(" << entry.property("fullPath"); if (!entry.rootNode.isEmpty()) { d.nospace() << "," << entry.rootNode; } d.nospace() << ")"; return d.space(); } QDebug operator<<(QDebug d, const Kerfuffle::Archive::Entry *entry) { d.nospace() << "Entry(" << entry->property("fullPath"); if (!entry->rootNode.isEmpty()) { d.nospace() << "," << entry->rootNode; } d.nospace() << ")"; return d.space(); } } diff --git a/kerfuffle/archiveentry.h b/kerfuffle/archiveentry.h index 35617804..c9343ee6 100644 --- a/kerfuffle/archiveentry.h +++ b/kerfuffle/archiveentry.h @@ -1,107 +1,109 @@ // // 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 fullPath MEMBER m_fullPath WRITE setFullPath) Q_PROPERTY(QString name READ name) 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 WRITE setIsDirectory) Q_PROPERTY(QString comment MEMBER m_comment) Q_PROPERTY(bool isPasswordProtected MEMBER m_isPasswordProtected) public: explicit Entry(QObject *parent = Q_NULLPTR, QString fullPath = QString(), QString rootNode = QString()); ~Entry(); QVector entries(); const QVector entries() const; void setEntryAt(int index, Entry *value); void appendEntry(Entry *entry); void removeEntryAt(int index); Entry *getParent() const; void setParent(Entry *parent); void setFullPath(const QString &fullPath); + QString fullPath(bool withoutTrailingSlash = false) const; + QString name() const; void setIsDirectory(const bool isDirectory); - int row() const; bool isDir() const; - QString name() const; + int row() const; Entry *find(const QString & name); Entry *findByPath(const QStringList & pieces, int index = 0); void returnDirEntries(QList *store); void clear(); bool operator==(const Archive::Entry &right) const; public: QString rootNode; bool compressedSizeIsSet; private: QVector m_entries; QString m_name; Entry *m_parent; QString m_fullPath; + QString m_fullPathWithoutTrailingSlash; 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; }; QDebug KERFUFFLE_EXPORT operator<<(QDebug d, const Kerfuffle::Archive::Entry &entry); QDebug KERFUFFLE_EXPORT operator<<(QDebug d, const Kerfuffle::Archive::Entry *entry); } Q_DECLARE_METATYPE(Kerfuffle::Archive::Entry*) #endif //ARK_ENTRY_H diff --git a/kerfuffle/archiveinterface.cpp b/kerfuffle/archiveinterface.cpp index 78afc334..ca9dbd03 100644 --- a/kerfuffle/archiveinterface.cpp +++ b/kerfuffle/archiveinterface.cpp @@ -1,170 +1,170 @@ /* * 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 "archiveinterface.h" #include "ark_debug.h" #include #include #include #include namespace Kerfuffle { ReadOnlyArchiveInterface::ReadOnlyArchiveInterface(QObject *parent, const QVariantList & args) : QObject(parent) , m_waitForFinishedSignal(false) , m_isHeaderEncryptionEnabled(false) , m_isCorrupt(false) { qCDebug(ARK) << "Created read-only interface for" << args.first().toString(); m_filename = args.first().toString(); } ReadOnlyArchiveInterface::~ReadOnlyArchiveInterface() { } QString ReadOnlyArchiveInterface::filename() const { return m_filename; } QString ReadOnlyArchiveInterface::comment() const { return m_comment; } bool ReadOnlyArchiveInterface::isReadOnly() const { return true; } bool ReadOnlyArchiveInterface::open() { return true; } void ReadOnlyArchiveInterface::setPassword(const QString &password) { m_password = password; } void ReadOnlyArchiveInterface::setHeaderEncryptionEnabled(bool enabled) { m_isHeaderEncryptionEnabled = enabled; } QString ReadOnlyArchiveInterface::password() const { return m_password; } bool ReadOnlyArchiveInterface::doKill() { //default implementation return false; } bool ReadOnlyArchiveInterface::doSuspend() { //default implementation return false; } bool ReadOnlyArchiveInterface::doResume() { //default implementation return false; } void ReadOnlyArchiveInterface::setCorrupt(bool isCorrupt) { m_isCorrupt = isCorrupt; } bool ReadOnlyArchiveInterface::isCorrupt() const { return m_isCorrupt; } ReadWriteArchiveInterface::ReadWriteArchiveInterface(QObject *parent, const QVariantList & args) : ReadOnlyArchiveInterface(parent, args) { qCDebug(ARK) << "Created read-write interface for" << args.first().toString(); } ReadWriteArchiveInterface::~ReadWriteArchiveInterface() { } bool ReadOnlyArchiveInterface::waitForFinishedSignal() { return m_waitForFinishedSignal; } int ReadOnlyArchiveInterface::moveRequiredSignals() const { return 1; } void ReadOnlyArchiveInterface::setWaitForFinishedSignal(bool value) { m_waitForFinishedSignal = value; } QStringList ReadOnlyArchiveInterface::entryFullPaths(const QList &entries) { QStringList filesList; foreach (const Archive::Entry *file, entries) { - filesList << file->property("fullPath").toString(); + filesList << file->fullPath(); } return filesList; } bool ReadOnlyArchiveInterface::isHeaderEncryptionEnabled() const { return m_isHeaderEncryptionEnabled; } bool ReadWriteArchiveInterface::isReadOnly() const { // We set corrupt archives to read-only to avoid add/delete actions, that // are likely to fail anyway. if (isCorrupt()) { return true; } QFileInfo fileInfo(filename()); if (fileInfo.exists()) { return !fileInfo.isWritable(); } else { return !fileInfo.dir().exists(); // TODO: Should also check if we can create a file in that directory } } } // namespace Kerfuffle diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp index 1c00fa6b..54e58214 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,1371 +1,1358 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "cliinterface.h" #include "ark_debug.h" #include "queries.h" #ifdef Q_OS_WIN # include #else # include # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Kerfuffle { CliInterface::CliInterface(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args), m_process(0), m_listEmptyLines(false), m_abortingOperation(false), m_extractTempDir(Q_NULLPTR), m_commentTempFile(Q_NULLPTR) { //because this interface uses the event loop setWaitForFinishedSignal(true); if (QMetaType::type("QProcess::ExitStatus") == 0) { qRegisterMetaType("QProcess::ExitStatus"); } } void CliInterface::cacheParameterList() { m_param = parameterList(); Q_ASSERT(m_param.contains(ExtractProgram)); Q_ASSERT(m_param.contains(ListProgram)); Q_ASSERT(m_param.contains(PreservePathSwitch)); Q_ASSERT(m_param.contains(FileExistsExpression)); Q_ASSERT(m_param.contains(FileExistsInput)); } CliInterface::~CliInterface() { Q_ASSERT(!m_process); delete m_commentTempFile; } void CliInterface::setListEmptyLines(bool emptyLines) { m_listEmptyLines = emptyLines; } bool CliInterface::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; } return true; } bool CliInterface::copyFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) { qCDebug(ARK) << Q_FUNC_INFO << "to" << destinationDirectory; cacheParameterList(); m_operationMode = Copy; m_compressionOptions = options; m_copiedFiles = files; m_extractDestDir = destinationDirectory; const QStringList extractArgs = m_param.value(ExtractArgs).toStringList(); if (extractArgs.contains(QStringLiteral("$PasswordSwitch")) && options.value(QStringLiteral("PasswordProtectedHint")).toBool() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } // Populate the argument list. const QStringList args = substituteCopyVariables(extractArgs, files, options.value(QStringLiteral("PreservePaths")).toBool(), password()); QUrl destDir = QUrl(destinationDirectory); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); bool useTmpExtractDir = options.value(QStringLiteral("DragAndDrop")).toBool() || options.value(QStringLiteral("AlwaysUseTmpDir")).toBool(); if (useTmpExtractDir) { Q_ASSERT(!m_extractTempDir); m_extractTempDir = new QTemporaryDir(QApplication::applicationName() + QLatin1Char('-')); qCDebug(ARK) << "Using temporary extraction dir:" << m_extractTempDir->path(); if (!m_extractTempDir->isValid()) { qCDebug(ARK) << "Creation of temporary directory failed."; emit finished(false); return false; } m_oldWorkingDir = QDir::currentPath(); destDir = QUrl(m_extractTempDir->path()); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); } if (!runProcess(m_param.value(ExtractProgram).toStringList(), args)) { return false; } return true; } bool CliInterface::addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) { cacheParameterList(); m_operationMode = Add; const QStringList addArgs = m_param.value(AddArgs).toStringList(); QList filesToPass = QList(); // If destination path is specified, we have recreate its structure inside the temp directory // and then place symlinks of targeted files there. const QString destinationPath = (destination == Q_NULLPTR) ? QString() - : destination->property("fullPath").toString(); + : destination->fullPath(); if (!destinationPath.isEmpty()) { m_extractTempDir = new QTemporaryDir(); const QString absoluteDestinationPath = m_extractTempDir->path() + QLatin1Char('/') + destinationPath; QDir qDir; qDir.mkpath(absoluteDestinationPath); QObject *preservedParent = Q_NULLPTR; foreach (Archive::Entry *file, files) { // The entries may have parent. We have to save and apply it to our new entry in order to prevent memory // leaks. if (preservedParent == Q_NULLPTR) { preservedParent = file->parent(); } - const QString filePath = QDir::currentPath() + QLatin1Char('/') + file->property("fullPath").toString(); - QString newFilePath = absoluteDestinationPath + file->property("fullPath").toString(); - // Symlink function can't accept the second argument as a path with trailing slash. - if (newFilePath[newFilePath.count() - 1] == QLatin1Char('/')) { - newFilePath.remove(newFilePath.count() - 1, 1); + const QString filePath = QDir::currentPath() + QLatin1Char('/') + file->fullPath(true); + const QString newFilePath = absoluteDestinationPath + file->fullPath(true); + if (QFile::link(filePath, newFilePath)) { + qCDebug(ARK) << "Symlink's created:" << filePath << newFilePath; } - if (!QFile::link(filePath, newFilePath)) { + else { qCDebug(ARK) << "Can't create symlink" << filePath << newFilePath; delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; return false; } - else { - qCDebug(ARK) << "Symlink's created:" << filePath << newFilePath; - } } qCDebug(ARK) << "Changing working dir again to " << m_extractTempDir->path(); QDir::setCurrent(m_extractTempDir->path()); filesToPass.push_back(new Archive::Entry(preservedParent, destinationPath.split(QLatin1Char('/'), QString::SkipEmptyParts).at(0))); } else { filesToPass = files; } if (addArgs.contains(QStringLiteral("$PasswordSwitch")) && options.value(QStringLiteral("PasswordProtectedHint")).toBool() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } int compLevel = options.value(QStringLiteral("CompressionLevel"), -1).toInt(); const auto args = substituteAddVariables(m_param.value(AddArgs).toStringList(), filesToPass, password(), isHeaderEncryptionEnabled(), compLevel); return runProcess(m_param.value(AddProgram).toStringList(), args); } bool CliInterface::moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { cacheParameterList(); m_operationMode = Move; m_removedFiles = files; const auto moveArgs = m_param.value(MoveArgs).toStringList(); const auto args = substituteMoveVariables(moveArgs, files, destination, password()); return runProcess(m_param.value(MoveProgram).toStringList(), args); } bool CliInterface::deleteFiles(const QList &files) { cacheParameterList(); m_operationMode = Delete; m_removedFiles = files; const auto deleteArgs = m_param.value(DeleteArgs).toStringList(); const auto args = substituteDeleteVariables(deleteArgs, files, password()); return runProcess(m_param.value(DeleteProgram).toStringList(), args); } bool CliInterface::testArchive() { resetParsing(); cacheParameterList(); m_operationMode = Test; const auto args = substituteTestVariables(m_param.value(TestArgs).toStringList()); return runProcess(m_param.value(TestProgram).toStringList(), args); } bool CliInterface::runProcess(const QStringList& programNames, const QStringList& arguments) { Q_ASSERT(!m_process); QString programPath; for (int i = 0; i < programNames.count(); i++) { programPath = QStandardPaths::findExecutable(programNames.at(i)); if (!programPath.isEmpty()) break; } if (programPath.isEmpty()) { const QString names = programNames.join(QStringLiteral(", ")); emit error(xi18ncp("@info", "Failed to locate program %2 on disk.", "Failed to locate programs %2 on disk.", programNames.count(), names)); emit finished(false); return false; } qCDebug(ARK) << "Executing" << programPath << arguments << "within directory" << QDir::currentPath(); #ifdef Q_OS_WIN m_process = new KProcess; #else m_process = new KPtyProcess; m_process->setPtyChannels(KPtyProcess::StdinChannel); #endif m_process->setOutputChannelMode(KProcess::MergedChannels); m_process->setNextOpenMode(QIODevice::ReadWrite | QIODevice::Unbuffered | QIODevice::Text); m_process->setProgram(programPath, arguments); connect(m_process, SIGNAL(readyReadStandardOutput()), SLOT(readStdout()), Qt::DirectConnection); if (m_operationMode == Copy) { // Extraction jobs need a dedicated post-processing function. connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::copyProcessFinished, Qt::DirectConnection); } else { connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::processFinished, Qt::DirectConnection); } m_stdOutData.clear(); m_process->start(); return true; } void CliInterface::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { m_exitCode = exitCode; qCDebug(ARK) << "Process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { //handle all the remaining data in the process readStdout(true); delete m_process; m_process = Q_NULLPTR; } // #193908 - #222392 // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_operationMode == Delete || m_operationMode == Move) { QStringList removedFullPaths = entryFullPaths(m_removedFiles); foreach (const QString &fullPath, removedFullPaths) { emit entryRemoved(fullPath); } } if (m_operationMode == Add) { if (m_extractTempDir) { delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; } list(); } else if (m_operationMode == List && isCorrupt()) { Kerfuffle::LoadCorruptQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (!query.responseYes()) { emit cancelled(); emit finished(false); } else { emit progress(1.0); emit finished(true); } } else { emit progress(1.0); emit finished(true); } } void CliInterface::copyProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_ASSERT(m_operationMode == Copy); m_exitCode = exitCode; qCDebug(ARK) << "Extraction process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { // Handle all the remaining data in the process. readStdout(true); delete m_process; m_process = Q_NULLPTR; } if (m_compressionOptions.value(QStringLiteral("AlwaysUseTmpDir")).toBool()) { // unar exits with code 1 if extraction fails. // This happens at least with wrong passwords or not enough space in the destination folder. if (m_exitCode == 1) { if (password().isEmpty()) { qCWarning(ARK) << "Extraction aborted, destination folder might not have enough space."; emit error(i18n("Extraction failed. Make sure that enough space is available.")); } else { qCWarning(ARK) << "Extraction aborted, either the password is wrong or the destination folder doesn't have enough space."; emit error(i18n("Extraction failed. Make sure you provided the correct password and that enough space is available.")); setPassword(QString()); } copyProcessCleanup(); emit finished(false); return; } if (!m_compressionOptions.value(QStringLiteral("DragAndDrop")).toBool()) { if (!moveToDestination(QDir::current(), QDir(m_extractDestDir), m_compressionOptions[QStringLiteral("PreservePaths")].toBool())) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_copiedFiles.size())); copyProcessCleanup(); emit finished(false); return; } copyProcessCleanup(); } } if (m_compressionOptions.value(QStringLiteral("DragAndDrop")).toBool()) { if (!moveDroppedFilesToDest(m_copiedFiles, m_extractDestDir)) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_copiedFiles.size())); copyProcessCleanup(); emit finished(false); return; } copyProcessCleanup(); } emit progress(1.0); emit finished(true); } bool CliInterface::moveDroppedFilesToDest(const QList &files, const QString &finalDest) { // Move extracted files from a QTemporaryDir to the final destination. QDir finalDestDir(finalDest); qCDebug(ARK) << "Setting final dir to" << finalDest; bool overwriteAll = false; bool skipAll = false; foreach (const Archive::Entry *file, files) { - QFileInfo relEntry(file->property("fullPath").toString().remove(file->rootNode)); - QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + file->property("fullPath").toString()); + QFileInfo relEntry(file->fullPath().remove(file->rootNode)); + QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + file->fullPath()); QFileInfo absDestEntry(finalDestDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absSourceEntry.isDir()) { // For directories, just create the path. if (!finalDestDir.mkpath(relEntry.filePath())) { qCWarning(ARK) << "Failed to create directory" << relEntry.filePath() << "in final destination."; } } else { // If destination file exists, prompt the user. if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; if (!skipAll && !overwriteAll) { Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); emit userQuery(&query); query.waitForResponse(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } } // Create any parent directories. if (!finalDestDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } // Move files to the final destination. if (!QFile(absSourceEntry.absoluteFilePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << absSourceEntry.filePath() << "to final destination."; return false; } } } return true; } bool CliInterface::isEmptyDir(const QDir &dir) { QDir d = dir; d.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); return d.count() == 0; } void CliInterface::copyProcessCleanup() { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } if (m_extractTempDir) { delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; } } bool CliInterface::moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths) { qCDebug(ARK) << "Moving extracted files from temp dir" << tempDir.path() << "to final destination" << destDir.path(); bool overwriteAll = false; bool skipAll = false; QDirIterator dirIt(tempDir.path(), QDir::AllEntries | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (dirIt.hasNext()) { dirIt.next(); // We skip directories if: // 1. We are not preserving paths // 2. The dir is not empty. Only empty directories need to be explicitly moved. // The non-empty ones are created by QDir::mkpath() below. if (dirIt.fileInfo().isDir()) { if (!preservePaths || !isEmptyDir(QDir(dirIt.filePath()))) { continue; } } QFileInfo relEntry; if (preservePaths) { relEntry = QFileInfo(dirIt.filePath().remove(tempDir.path() + QLatin1Char('/'))); } else { relEntry = QFileInfo(dirIt.fileName()); } QFileInfo absDestEntry(destDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); emit userQuery(&query); query.waitForResponse(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } if (preservePaths) { // Create any parent directories. if (!destDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } } // Move file to the final destination. if (!QFile(dirIt.filePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << dirIt.filePath() << "to final destination."; return false; } } return true; } QStringList CliInterface::substituteListVariables(const QStringList &listArgs, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, listArgs) { if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } // Simple argument (e.g. -slt in 7z), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteCopyVariables(const QStringList &extractArgs, const QList &entries, bool preservePaths, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, extractArgs) { qCDebug(ARK) << "Processing argument" << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PreservePathSwitch")) { args << preservePathSwitch(preservePaths); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } if (arg == QLatin1String("$Files")) { args << copyFilesList(entries); continue; } // Simple argument (e.g. -kb in unrar), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteAddVariables(const QStringList &addArgs, const QList &entries, const QString &password, bool encryptHeader, int compLevel) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, addArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << (encryptHeader ? passwordHeaderSwitch(password) : passwordSwitch(password)); continue; } if (arg == QLatin1String("$CompressionLevelSwitch")) { args << compressionLevelSwitch(compLevel); continue; } if (arg == QLatin1String("$Files")) { args << entryFullPaths(entries); continue; } // Simple argument (e.g. a in 7z), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteMoveVariables(const QStringList &moveArgs, const QList &entries, const Archive::Entry *destination, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, moveArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } if (arg == QLatin1String("$PathPairs")) { args << entryPathDestinationPairs(entries, destination); continue; } // Simple argument (e.g. a in 7z), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteDeleteVariables(const QStringList &deleteArgs, const QList &entries, const QString &password) { cacheParameterList(); QStringList args; foreach (const QString& arg, deleteArgs) { qCDebug(ARK) << "Processing argument" << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } if (arg == QLatin1String("$Files")) { foreach (const Archive::Entry *e, entries) { - args << escapeFileName(e->property("fullPath").toString()); + args << escapeFileName(e->fullPath(true)); } continue; } // Simple argument (e.g. d in rar), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteCommentVariables(const QStringList &commentArgs, const QString &commentFile) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, commentArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$CommentSwitch")) { QString commentSwitch = m_param.value(CommentSwitch).toString(); commentSwitch.replace(QStringLiteral("$CommentFile"), commentFile); args << commentSwitch; continue; } args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteTestVariables(const QStringList &testArgs) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, testArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QString CliInterface::preservePathSwitch(bool preservePaths) const { Q_ASSERT(m_param.contains(PreservePathSwitch)); const QStringList theSwitch = m_param.value(PreservePathSwitch).toStringList(); Q_ASSERT(theSwitch.size() == 2); return (preservePaths ? theSwitch.at(0) : theSwitch.at(1)); } QStringList CliInterface::passwordHeaderSwitch(const QString& password) const { if (password.isEmpty()) { return QStringList(); } Q_ASSERT(m_param.contains(PasswordHeaderSwitch)); QStringList passwordHeaderSwitch = m_param.value(PasswordHeaderSwitch).toStringList(); Q_ASSERT(!passwordHeaderSwitch.isEmpty() && passwordHeaderSwitch.size() <= 2); if (passwordHeaderSwitch.size() == 1) { passwordHeaderSwitch[0].replace(QLatin1String("$Password"), password); } else { passwordHeaderSwitch[1] = password; } return passwordHeaderSwitch; } QStringList CliInterface::passwordSwitch(const QString& password) const { if (password.isEmpty()) { return QStringList(); } Q_ASSERT(m_param.contains(PasswordSwitch)); QStringList passwordSwitch = m_param.value(PasswordSwitch).toStringList(); Q_ASSERT(!passwordSwitch.isEmpty() && passwordSwitch.size() <= 2); if (passwordSwitch.size() == 1) { passwordSwitch[0].replace(QLatin1String("$Password"), password); } else { passwordSwitch[1] = password; } return passwordSwitch; } QString CliInterface::compressionLevelSwitch(int level) const { if (level < 0 || level > 9) { return QString(); } Q_ASSERT(m_param.contains(CompressionLevelSwitch)); QString compLevelSwitch = m_param.value(CompressionLevelSwitch).toString(); Q_ASSERT(!compLevelSwitch.isEmpty()); compLevelSwitch.replace(QLatin1String("$CompressionLevel"), QString::number(level)); return compLevelSwitch; } QStringList CliInterface::copyFilesList(const QList &entries) const { QStringList filesList; foreach (const Archive::Entry *e, entries) { - filesList << escapeFileName(e->property("fullPath").toString()); + filesList << escapeFileName(e->fullPath(true)); } return filesList; } void CliInterface::killProcess(bool emitFinished) { // TODO: Would be good to unit test #304764/#304178. if (!m_process) { return; } m_abortingOperation = !emitFinished; // Give some time for the application to finish gracefully if (!m_process->waitForFinished(5)) { m_process->kill(); // It takes a few hundred ms for the process to be killed. m_process->waitForFinished(1000); } m_abortingOperation = false; } bool CliInterface::passwordQuery() { Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); // There is no process running, so finished() must be emitted manually. emit finished(false); return false; } setPassword(query.password()); return true; } void CliInterface::readStdout(bool handleAll) { //when hacking this function, please remember the following: //- standard output comes in unpredictable chunks, this is why //you can never know if the last part of the output is a complete line or not //- console applications are not really consistent about what //characters they send out (newline, backspace, carriage return, //etc), so keep in mind that this function is supposed to handle //all those special cases and be the lowest common denominator if (m_abortingOperation) return; Q_ASSERT(m_process); if (!m_process->bytesAvailable()) { //if process has no more data, we can just bail out return; } QByteArray dd = m_process->readAllStandardOutput(); m_stdOutData += dd; QList lines = m_stdOutData.split('\n'); //The reason for this check is that archivers often do not end //queries (such as file exists, wrong password) on a new line, but //freeze waiting for input. So we check for errors on the last line in //all cases. // TODO: QLatin1String() might not be the best choice here. // The call to handleLine() at the end of the method uses // QString::fromLocal8Bit(), for example. // TODO: The same check methods are called in handleLine(), this // is suboptimal. bool wrongPasswordMessage = checkForErrorMessage(QLatin1String( lines.last() ), WrongPasswordPatterns); bool foundErrorMessage = (wrongPasswordMessage || checkForErrorMessage(QLatin1String(lines.last()), DiskFullPatterns) || checkForErrorMessage(QLatin1String(lines.last()), ExtractionFailedPatterns) || checkForPasswordPromptMessage(QLatin1String(lines.last())) || checkForErrorMessage(QLatin1String(lines.last()), FileExistsExpression)); if (foundErrorMessage) { handleAll = true; } if (wrongPasswordMessage) { setPassword(QString()); } //this is complex, here's an explanation: //if there is no newline, then there is no guaranteed full line to //handle in the output. The exception is that it is supposed to handle //all the data, OR if there's been an error message found in the //partial data. if (lines.size() == 1 && !handleAll) { return; } if (handleAll) { m_stdOutData.clear(); } else { //because the last line might be incomplete we leave it for now //note, this last line may be an empty string if the stdoutdata ends //with a newline m_stdOutData = lines.takeLast(); } foreach(const QByteArray& line, lines) { if (!line.isEmpty() || (m_listEmptyLines && m_operationMode == List)) { handleLine(QString::fromLocal8Bit(line)); } } } void CliInterface::handleLine(const QString& line) { // TODO: This should be implemented by each plugin; the way progress is // shown by each CLI application is subject to a lot of variation. if ((m_operationMode == Copy || m_operationMode == Add) && m_param.contains(CaptureProgress) && m_param.value(CaptureProgress).toBool()) { //read the percentage int pos = line.indexOf(QLatin1Char( '%' )); if (pos > 1) { int percentage = line.midRef(pos - 2, 2).toInt(); emit progress(float(percentage) / 100); return; } } if (m_operationMode == Copy) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); killProcess(); return; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return; } if (checkForErrorMessage(line, DiskFullPatterns)) { qCWarning(ARK) << "Found disk full message:" << line; emit error(i18nc("@info", "Extraction failed because the disk is full.")); killProcess(); return; } if (checkForErrorMessage(line, WrongPasswordPatterns)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18nc("@info", "Extraction failed: Incorrect password")); killProcess(); return; } if (checkForErrorMessage(line, ExtractionFailedPatterns)) { qCWarning(ARK) << "Error in extraction:" << line; emit error(i18n("Extraction failed because of an unexpected error.")); killProcess(); return; } if (handleFileExistsMessage(line)) { return; } } if (m_operationMode == List) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); killProcess(); return; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return; } if (checkForErrorMessage(line, WrongPasswordPatterns)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18n("Incorrect password.")); killProcess(); return; } if (checkForErrorMessage(line, ExtractionFailedPatterns)) { qCWarning(ARK) << "Error in extraction!!"; emit error(i18n("Extraction failed because of an unexpected error.")); killProcess(); return; } if (checkForErrorMessage(line, CorruptArchivePatterns)) { qCWarning(ARK) << "Archive corrupt"; setCorrupt(true); return; } if (handleFileExistsMessage(line)) { return; } readListLine(line); return; } if (m_operationMode == Test) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; emit error(i18n("Ark does not currently support testing password-protected archives.")); killProcess(); return; } if (checkForTestSuccessMessage(line)) { qCDebug(ARK) << "Test successful"; emit testSuccess(); return; } } } bool CliInterface::checkForPasswordPromptMessage(const QString& line) { const QString passwordPromptPattern(m_param.value(PasswordPromptPattern).toString()); if (passwordPromptPattern.isEmpty()) return false; if (m_passwordPromptPattern.pattern().isEmpty()) { m_passwordPromptPattern.setPattern(m_param.value(PasswordPromptPattern).toString()); } if (m_passwordPromptPattern.match(line).hasMatch()) { return true; } return false; } bool CliInterface::handleFileExistsMessage(const QString& line) { // Check for a filename and store it. foreach (const QString &pattern, m_param.value(FileExistsFileName).toStringList()) { const QRegularExpression rxFileNamePattern(pattern); const QRegularExpressionMatch rxMatch = rxFileNamePattern.match(line); if (rxMatch.hasMatch()) { m_storedFileName = rxMatch.captured(1); qCWarning(ARK) << "Detected existing file:" << m_storedFileName; } } if (!checkForErrorMessage(line, FileExistsExpression)) { return false; } Kerfuffle::OverwriteQuery query(QDir::current().path() + QLatin1Char( '/' ) + m_storedFileName); query.setNoRenameMode(true); emit userQuery(&query); qCDebug(ARK) << "Waiting response"; query.waitForResponse(); qCDebug(ARK) << "Finished response"; QString responseToProcess; const QStringList choices = m_param.value(FileExistsInput).toStringList(); if (query.responseOverwrite()) { responseToProcess = choices.at(0); } else if (query.responseSkip()) { responseToProcess = choices.at(1); } else if (query.responseOverwriteAll()) { responseToProcess = choices.at(2); } else if (query.responseAutoSkip()) { responseToProcess = choices.at(3); } else if (query.responseCancelled()) { if (choices.count() < 5) { // If the program has no way to cancel the extraction, we resort to killing it return doKill(); } responseToProcess = choices.at(4); } Q_ASSERT(!responseToProcess.isEmpty()); responseToProcess += QLatin1Char( '\n' ); writeToProcess(responseToProcess.toLocal8Bit()); return true; } bool CliInterface::checkForErrorMessage(const QString& line, int parameterIndex) { QList patterns; if (m_patternCache.contains(parameterIndex)) { patterns = m_patternCache.value(parameterIndex); } else { if (!m_param.contains(parameterIndex)) { return false; } foreach(const QString& rawPattern, m_param.value(parameterIndex).toStringList()) { patterns << QRegularExpression(rawPattern); } m_patternCache[parameterIndex] = patterns; } foreach(const QRegularExpression& pattern, patterns) { if (pattern.match(line).hasMatch()) { return true; } } return false; } bool CliInterface::checkForTestSuccessMessage(const QString& line) { const QRegularExpression rx(m_param.value(TestPassedPattern).toString()); const QRegularExpressionMatch rxMatch = rx.match(line); if (rxMatch.hasMatch()) { return true; } return false; } bool CliInterface::doKill() { if (m_process) { killProcess(false); return true; } return false; } bool CliInterface::doSuspend() { return false; } bool CliInterface::doResume() { return false; } QString CliInterface::escapeFileName(const QString& fileName) const { return fileName; } QStringList CliInterface::entryPathDestinationPairs(const QList &entries, const Archive::Entry *destination) { QStringList pairList; if (entries.count() > 1) { foreach (const Archive::Entry *file, entries) { - pairList << file->property("fullPath").toString() << destination->property("fullPath").toString() + file->name(); + pairList << file->fullPath() << destination->fullPath() + file->name(); } } else { - // Get rid of trailing slashes for RAR compatibility. - QString oldPath = entries.at(0)->property("fullPath").toString(); - if (oldPath.right(1) == QLatin1String("/")) { - oldPath.chop(1); - } - QString newPath = destination->property("fullPath").toString(); - if (newPath.right(1) == QLatin1String("/")) { - newPath.chop(1); - } - pairList << oldPath << newPath; + pairList << entries.at(0)->fullPath(true) << destination->fullPath(true); } return pairList; } void CliInterface::writeToProcess(const QByteArray& data) { Q_ASSERT(m_process); Q_ASSERT(!data.isNull()); qCDebug(ARK) << "Writing" << data << "to the process"; #ifdef Q_OS_WIN m_process->write(data); #else m_process->pty()->write(data); #endif } bool CliInterface::addComment(const QString &comment) { cacheParameterList(); m_operationMode = Comment; m_commentTempFile = new QTemporaryFile; if (!m_commentTempFile->open()) { qCWarning(ARK) << "Failed to create temporary file for comment"; emit finished(false); return false; } QTextStream stream(m_commentTempFile); stream << comment << endl; m_commentTempFile->close(); const auto args = substituteCommentVariables(m_param.value(CommentArgs).toStringList(), m_commentTempFile->fileName()); if (!runProcess(m_param.value(AddProgram).toStringList(), args)) { return false; } m_comment = comment; return true; } } diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp index 635ad30a..85598168 100644 --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -1,601 +1,601 @@ /* * 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() { qDeleteAll(m_archiveEntries); m_archiveEntries.clear(); 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(Archive::Entry *entry) { 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 Archive::Entry *entry) { m_extractedFilesSize += entry->property("size").toLongLong(); m_isPasswordProtected |= entry->property("isPasswordProtected").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 fullPath = entry->property("fullPath").toString().replace(QRegularExpression(QStringLiteral("^\\./")), QString()); + const QString fullPath = entry->fullPath().replace(QRegularExpression(QStringLiteral("^\\./")), QString()); const QString basePath = fullPath.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 QList &entries, const QString &destinationDir, const ExtractionOptions &options, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destinationDir(destinationDir) , m_options(options) { qCDebug(ARK) << "ExtractJob created"; setDefaultOptions(); } void ExtractJob::doWork() { QString desc; if (m_entries.count() == 0) { desc = i18n("Extracting all files"); } else { desc = i18np("Extracting one file", "Extracting %1 files", m_entries.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_entries << "Destination dir:" << m_destinationDir << "Options:" << m_options; bool ret = archiveInterface()->copyFiles(m_entries, 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(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : Job(interface) , m_entry(entry) , m_passwordProtectedHint(passwordProtectedHint) { } QString TempExtractJob::validatedFilePath() const { - QString path = extractionDir() + QLatin1Char('/') + m_entry->property("fullPath").toString(); + QString path = extractionDir() + QLatin1Char('/') + m_entry->fullPath(); // 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_entry; bool ret = archiveInterface()->copyFiles({m_entry}, extractionDir(), extractionOptions()); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "PreviewJob started"; } QString PreviewJob::extractionDir() const { return m_tmpExtractDir.path(); } OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : TempExtractJob(entry, 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(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : OpenJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "OpenWithJob started"; } AddJob::AddJob(const QList &entries, const Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "AddJob started"; } void AddJob::doWork() { qCDebug(ARK) << "AddJob: going to add" << m_entries.count() << "file(s)"; emit description(this, i18np("Adding a file", "Adding %1 files", m_entries.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. foreach (Archive::Entry *entry, m_entries) { // #191821: workDir must be used instead of QDir::current() // so that symlinks aren't resolved automatically - const QString &fullPath = entry->property("fullPath").toString(); + const QString &fullPath = entry->fullPath(); QString relativePath = workDir.relativeFilePath(fullPath); if (fullPath.endsWith(QLatin1Char('/'))) { relativePath += QLatin1Char('/'); } qCDebug(ARK) << entry->property("fullPath") << entry->isDir() << relativePath; entry->setFullPath(relativePath); } connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void AddJob::onFinished(bool result) { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } Job::onFinished(result); } MoveJob::MoveJob(const QList &entries, Archive::Entry *destination, const CompressionOptions& options , ReadWriteArchiveInterface *interface) : Job(interface) , m_finishedSignalsCount(0) , m_entries(entries) , m_destination(destination) , m_options(options) { qCDebug(ARK) << "MoveJob started"; } void MoveJob::doWork() { qCDebug(ARK) << "MoveJob: going to move" << m_entries.count() << "file(s)"; emit description(this, i18np("Moving a file", "Moving %1 files", m_entries.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->moveFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void MoveJob::onFinished(bool result) { m_finishedSignalsCount++; if (m_finishedSignalsCount == archiveInterface()->moveRequiredSignals()) { Job::onFinished(result); } } DeleteJob::DeleteJob(const QList &entries, ReadWriteArchiveInterface *interface) : Job(interface) , m_entries(entries) { } void DeleteJob::doWork() { emit description(this, i18np("Deleting a file from the archive", "Deleting %1 files", m_entries.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->deleteFiles(m_entries); 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/part/archivemodel.cpp b/part/archivemodel.cpp index 6ce9a9f7..2508d1e5 100644 --- a/part/archivemodel.cpp +++ b/part/archivemodel.cpp @@ -1,892 +1,892 @@ /* * 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 "kerfuffle/jobs.h" #include #include #include #include #include #include using namespace Kerfuffle; //used to speed up the loading of large archives static Archive::Entry *s_previousMatch = Q_NULLPTR; Q_GLOBAL_STATIC(QStringList, s_previousPieces) /** * 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 { FullPath, /**< 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, }; /** * Mappings between column indexes and entry properties. */ static QMap initializePropertiesList() { QMap propertiesList = QMap(); propertiesList.insert(FullPath, QStringLiteral("fullPath")); 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 { if (m_sortOrder == Qt::AscendingOrder) { return lessThan(left, right); } else { return !lessThan(left, right); } } protected: bool lessThan(const QPair &left, const QPair &right) const { const Archive::Entry * const leftEntry = left.first; const Archive::Entry * const rightEntry = right.first; // #234373: sort folders before files if ((leftEntry->isDir()) && (!rightEntry->isDir())) { return (m_sortOrder == Qt::AscendingOrder); } else if ((!leftEntry->isDir()) && (rightEntry->isDir())) { return !(m_sortOrder == Qt::AscendingOrder); } 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 FullPath: return leftEntry->name() < rightEntry->name(); case Size: case CompressedSize: return leftEntryMetaData.toInt() < rightEntryMetaData.toInt(); default: return leftEntryMetaData.toString() < rightEntryMetaData.toString(); } // We should not get here. Q_ASSERT(false); return false; } private: int m_sortColumn; Qt::SortOrder m_sortOrder; }; ArchiveModel::ArchiveModel(const QString &dbusPathName, QObject *parent) : QAbstractItemModel(parent) , m_rootEntry() , m_dbusPathName(dbusPathName) { m_rootEntry.setProperty("isDirectory", true); } ArchiveModel::~ArchiveModel() { } QVariant ArchiveModel::data(const QModelIndex &index, int role) const { if (index.isValid()) { Archive::Entry *entry = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: { //TODO: complete the columns int column = m_showColumns.at(index.column()); switch (column) { case FullPath: return entry->name(); case Size: if (entry->isDir()) { int dirs; int files; const int children = childCount(index, dirs, files); return KIO::itemsSummaryString(children, files, dirs, 0, false); } else if (!entry->property("link").toString().isEmpty()) { return QVariant(); } else { return KIO::convertSize(entry->property("size").toULongLong()); } case CompressedSize: if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { qulonglong compressedSize = entry->property("compressedSize").toULongLong(); if (compressedSize != 0) { return KIO::convertSize(compressedSize); } else { return QVariant(); } } case Ratio: // TODO: Use entry->metaData()[Ratio] when available if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { 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 = entry->property("timestamp").toDateTime(); return QLocale().toString(timeStamp, QLocale::ShortFormat); } default: return entry->property(propertiesList[column].toStdString().c_str()); } } case Qt::DecorationRole: if (index.column() == 0) { - return *m_entryIcons.value(entry->property("fullPath").toString()); + return *m_entryIcons.value(entry->fullPath()); } return QVariant(); case Qt::FontRole: { QFont f; 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 FullPath: 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)) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : &m_rootEntry; Q_ASSERT(parentEntry->isDir()); const Archive::Entry *item = parentEntry->entries().value(row, Q_NULLPTR); if (item != Q_NULLPTR) { return createIndex(row, column, const_cast(item)); } } return QModelIndex(); } QModelIndex ArchiveModel::parent(const QModelIndex &index) const { if (index.isValid()) { Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); if (item->getParent() && (item->getParent() != &m_rootEntry)) { return createIndex(item->getParent()->row(), 0, item->getParent()); } } return QModelIndex(); } Archive::Entry *ArchiveModel::entryForIndex(const QModelIndex &index) { if (index.isValid()) { Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); return item; } return Q_NULLPTR; } int ArchiveModel::childCount(const QModelIndex &index, int &dirs, int &files) const { if (index.isValid()) { dirs = files = 0; Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); if (item->isDir()) { const QVector entries = 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) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : &m_rootEntry; if (parentEntry && parentEntry->isDir()) { return 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 dirEntries; m_rootEntry.returnDirEntries(&dirEntries); dirEntries.append(&m_rootEntry); const ArchiveModelSorter modelSorter(m_showColumns.at(column), order); foreach(Archive::Entry *dir, dirEntries) { QVector < QPair > sorting(dir->entries().count()); for (int i = 0; i < dir->entries().count(); ++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) { 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, 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); Archive::Entry *entry = entryForIndex(droppedOnto); if (entry->isDir()) { qCDebug(ARK) << "Using entry"; path = entry->fileName.toString(); } else { 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; } Archive::Entry *ArchiveModel::parentFor(const Archive::Entry *entry) { - QStringList pieces = entry->property("fullPath").toString().split(QLatin1Char( '/' ), QString::SkipEmptyParts); + QStringList pieces = entry->fullPath().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 s_previousMatch; } } } Archive::Entry *parent = &m_rootEntry; foreach(const QString &piece, pieces) { 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("fullPath", (parent == &m_rootEntry) ? piece - : parent->property("fullPath").toString() + QLatin1Char( '/' ) + piece); + : parent->fullPath() + QLatin1Char( '/' ) + piece); entry->setProperty("isDirectory", true); insertEntry(entry); } if (!entry->isDir()) { Archive::Entry *e = new Archive::Entry(parent); copyEntryMetaData(e, entry); // Maybe we have both a file and a directory of the same name. // We avoid removing previous entries unless necessary. insertEntry(e); } parent = entry; } s_previousMatch = parent; *s_previousPieces = pieces; return parent; } QModelIndex ArchiveModel::indexForEntry(Archive::Entry *entry) { 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; } Archive::Entry *entry = m_rootEntry.findByPath(entryFileName.split(QLatin1Char( '/' ), QString::SkipEmptyParts)); if (entry) { Archive::Entry *parent = entry->getParent(); QModelIndex index = indexForEntry(entry); Q_UNUSED(index); beginRemoveRows(indexForEntry(parent), entry->row(), entry->row()); - delete m_entryIcons.take(parent->entries().at(entry->row())->property("fullPath").toString()); + delete m_entryIcons.take(parent->entries().at(entry->row())->fullPath()); parent->removeEntryAt(entry->row()); endRemoveRows(); } } void ArchiveModel::slotUserQuery(Kerfuffle::Query *query) { query->execute(); } 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(Archive::Entry *entry) { newEntry(entry, NotifyViews); } void ArchiveModel::newEntry(Archive::Entry *receivedEntry, InsertBehaviour behaviour) { - if (receivedEntry->property("fullPath").toString().isEmpty()) { + if (receivedEntry->fullPath().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()) { QList toInsert; 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; } //#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(receivedEntry->property("fullPath").toString()); + QString entryFileName = cleanFileName(receivedEntry->fullPath()); if (entryFileName.isEmpty()) { // The entry contains only "." or "./" return; } receivedEntry->setProperty("fullPath", entryFileName); /// 1. Skip already created entries Archive::Entry *existing = m_rootEntry.findByPath(entryFileName.split(QLatin1Char( '/' ))); if (existing) { qCDebug(ARK) << "Refreshing entry for" << entryFileName; existing->setProperty("fullPath", 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->property("compressedSize").toULongLong(); existing->setProperty("compressedSize", currentCompressedSize + receivedEntry->property("compressedSize").toULongLong()); return; } /// 2. Find Parent Entry, creating missing direcotry ArchiveEntries in the process Archive::Entry *parent = parentFor(receivedEntry); /// 3. Create an Archive::Entry const QStringList path = entryFileName.split(QLatin1Char('/'), QString::SkipEmptyParts); const QString name = path.last(); Archive::Entry *entry = parent->find(name); if (entry) { copyEntryMetaData(entry, receivedEntry); entry->setProperty("fullPath", entryFileName); delete receivedEntry; } else { receivedEntry->setParent(parent); insertEntry(receivedEntry, behaviour); } } void ArchiveModel::slotLoadingFinished(KJob *job) { int i = 0; 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::copyEntryMetaData(Archive::Entry *destinationEntry, const Archive::Entry *sourceEntry) { destinationEntry->setProperty("fullPath", sourceEntry->property("fullPath")); 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(indexForEntry(parent), parent->entries().count(), parent->entries().count()); } parent->appendEntry(entry); if (behaviour == NotifyViews) { endInsertRows(); } // Save an icon for each newly added entry. QMimeDatabase db; const QPixmap *pixmap; if (entry->isDir()) { pixmap = new QPixmap(QIcon::fromTheme(db.mimeTypeForName(QStringLiteral("inode/directory")).iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small))); } else { - pixmap = new QPixmap(QIcon::fromTheme(db.mimeTypeForFile(entry->property("fullPath").toString()).iconName()).pixmap(IconSize(KIconLoader::Small), + pixmap = new QPixmap(QIcon::fromTheme(db.mimeTypeForFile(entry->fullPath()).iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small))); } - m_entryIcons.insert(entry->property("fullPath").toString(), pixmap); + m_entryIcons.insert(entry->fullPath(), pixmap); } Kerfuffle::Archive* ArchiveModel::archive() const { return m_archive.data(); } KJob* ArchiveModel::setArchive(Kerfuffle::Archive *archive) { m_archive.reset(archive); 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(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { QList files; files << file; 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(Archive::Entry *file) const { Q_ASSERT(m_archive); PreviewJob *job = m_archive->preview(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } OpenJob *ArchiveModel::open(Archive::Entry *file) const { Q_ASSERT(m_archive); OpenJob *job = m_archive->open(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } OpenWithJob *ArchiveModel::openWith(Archive::Entry *file) const { Q_ASSERT(m_archive); OpenWithJob *job = m_archive->openWith(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } AddJob* ArchiveModel::addFiles(QList &entries, const Archive::Entry *destination, const CompressionOptions& options) { if (!m_archive) { return Q_NULLPTR; } if (!m_archive->isReadOnly()) { AddJob *job = m_archive->addFiles(entries, destination, options); connect(job, &AddJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &AddJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return Q_NULLPTR; } DeleteJob* ArchiveModel::deleteFiles(QList entries) { Q_ASSERT(m_archive); if (!m_archive->isReadOnly()) { DeleteJob *job = m_archive->deleteFiles(entries); 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(); Archive::Entry *entry = entryForIndex(node); if (!hasChildren(node)) { - if (entry->property("fullPath").toString().isEmpty()) { + if (entry->fullPath().isEmpty()) { nodesToDelete << node; } } else { for (int i = 0; i < rowCount(node); ++i) { queue.append(QPersistentModelIndex(index(i, 0, node))); } } } foreach(const QPersistentModelIndex& node, nodesToDelete) { 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()); - delete m_entryIcons.take(rawEntry->getParent()->entries().at(rawEntry->row())->property("fullPath").toString()); + delete m_entryIcons.take(rawEntry->getParent()->entries().at(rawEntry->row())->fullPath()); rawEntry->getParent()->removeEntryAt(rawEntry->row()); endRemoveRows(); } } diff --git a/part/infopanel.cpp b/part/infopanel.cpp index d46f0e1b..23245cdd 100644 --- a/part/infopanel.cpp +++ b/part/infopanel.cpp @@ -1,206 +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 "kerfuffle/archiveentry.h" #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 Archive::Entry *entry = m_model->entryForIndex(index); QMimeDatabase db; QMimeType mimeType; if (entry->isDir()) { mimeType = db.mimeTypeForName(QStringLiteral("inode/directory")); } else { - mimeType = db.mimeTypeForFile(entry->property("fullPath").toString(), QMimeDatabase::MatchExtension); + mimeType = db.mimeTypeForFile(entry->fullPath(), QMimeDatabase::MatchExtension); } iconLabel->setPixmap(getDesktopIconForName(mimeType.iconName())); 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->property("link").toString().isEmpty()) { additionalInfo->setText(i18n("Symbolic Link")); } else { if (entry->property("size") != 0) { additionalInfo->setText(KIO::convertSize(entry->property("size").toULongLong())); } else { additionalInfo->setText(i18n("Unknown size")); } } - const QStringList nameParts = entry->property("fullPath").toString().split(QLatin1Char( '/' ), QString::SkipEmptyParts); - const QString name = (nameParts.count() > 0) ? nameParts.last() : entry->property("fullPath").toString(); + const QStringList nameParts = entry->fullPath().split(QLatin1Char( '/' ), QString::SkipEmptyParts); + const QString name = (nameParts.count() > 0) ? nameParts.last() : entry->fullPath(); 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 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 Archive::Entry *entry = m_model->entryForIndex(index); QMimeDatabase db; QMimeType mimeType; if (entry->isDir()) { mimeType = db.mimeTypeForName(QStringLiteral("inode/directory")); } else { - mimeType = db.mimeTypeForFile(entry->property("fullPath").toString(), QMimeDatabase::MatchExtension); + mimeType = db.mimeTypeForFile(entry->fullPath(), QMimeDatabase::MatchExtension); } m_typeLabel->setText(i18n("Type: %1", mimeType.comment())); if (!entry->property("owner").toString().isEmpty()) { m_ownerLabel->show(); m_ownerLabel->setText(i18n("Owner: %1", entry->property("owner").toString())); } else { m_ownerLabel->hide(); } if (!entry->property("group").toString().isEmpty()) { m_groupLabel->show(); m_groupLabel->setText(i18n("Group: %1", entry->property("group").toString())); } else { m_groupLabel->hide(); } if (!entry->property("link").toString().isEmpty()) { m_targetLabel->show(); m_targetLabel->setText(i18n("Target: %1", entry->property("link").toString())); } else { m_targetLabel->hide(); } 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 33066db1..6cd84ede 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1,1365 +1,1365 @@ /* * 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/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(); 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(); bool isPreviewable = (!limit || (limit && entry != Q_NULLPTR && entry->property("size").toLongLong() < maxPreviewSize)); m_previewAction->setEnabled(!isBusy() && isPreviewable && !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 && !entry->isDir() && (selectedEntriesCount == 1)); m_openFileWithAction->setEnabled(!isBusy() && isPreviewable && !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))->property("fullPath").toString() == QLatin1String("README.TXT")) { + m_model->entryForIndex(m_model->index(0, 0))->fullPath() == 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(); Archive::Entry *entry = m_model->entryForIndex(index); // Don't open directories. if (entry->isDir()) { return; } // We don't support opening symlinks. if (!entry->property("link").toString().isEmpty()) { displayMsgWidget(KMessageWidget::Information, i18n("Ark cannot open symlinks.")); return; } // Extract the entry. - if (!entry->property("fullPath").toString().isEmpty()) { + if (!entry->fullPath().isEmpty()) { m_openFileMode = static_cast(mode); KJob *job = Q_NULLPTR; if (m_openFileMode == Preview) { job = m_model->preview(entry); connect(job, &KJob::result, this, &Part::slotPreviewExtractedEntry); } else { job = (m_openFileMode == OpenFile) ? m_model->open(entry) : m_model->openWith(entry); 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); QList 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 { QList ret; foreach(const QModelIndex& index, list) { ret << m_model->entryForIndex(index); } return ret; } QList Part::filesAndRootNodesForIndexes(const QModelIndexList& list) const { QList fileList; QStringList fullPathsList; 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 rootFileName = selectionRoot.isValid() - ? m_model->entryForIndex(selectionRoot)->property("fullPath").toString() + ? m_model->entryForIndex(selectionRoot)->fullPath() : QString(); // Append index with root node to fileList. QModelIndexList alist = QModelIndexList() << index; foreach (Archive::Entry *entry, filesForIndexes(alist)) { - const QString fullPath = entry->property("fullPath").toString(); + const QString fullPath = entry->fullPath(); if (!fullPathsList.contains(fullPath)) { entry->rootNode = rootFileName; fileList.append(entry); fullPathsList.append(fullPath); } } } 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; // 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 = filesToAdd.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")]; } foreach (const QString& file, filesToAdd) { m_jobTempEntries.push_back(new Archive::Entry(Q_NULLPTR, file)); } AddJob *job = m_model->addFiles(m_jobTempEntries, Q_NULLPTR, 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) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); 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 cc8a5434..751db45d 100644 --- a/plugins/cli7zplugin/cliplugin.cpp +++ b/plugins/cli7zplugin/cliplugin.cpp @@ -1,270 +1,270 @@ /* * 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[MoveProgram] = p[AddProgram] = p[TestProgram] = QStringList() << QStringLiteral("7z"); p[ListArgs] = QStringList() << QStringLiteral("l") << QStringLiteral("-slt") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive"); p[ExtractArgs] = QStringList() << QStringLiteral("$PreservePathSwitch") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive") << QStringLiteral("$Files"); p[PreservePathSwitch] = QStringList() << QStringLiteral("x") << QStringLiteral("e"); p[PasswordSwitch] = QStringList() << QStringLiteral("-p$Password"); p[PasswordHeaderSwitch] = QStringList { QStringLiteral("-p$Password"), QStringLiteral("-mhe=on") }; p[WrongPasswordPatterns] = QStringList() << QStringLiteral("Wrong password"); p[CompressionLevelSwitch] = QStringLiteral("-mx=$CompressionLevel"); p[AddArgs] = QStringList() << QStringLiteral("a") << QStringLiteral("-l") << QStringLiteral("$Archive") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$CompressionLevelSwitch") << QStringLiteral("$Files"); p[MoveArgs] = QStringList() << QStringLiteral("rn") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive") << QStringLiteral("$PathPairs"); p[DeleteArgs] = QStringList() << QStringLiteral("d") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive") << QStringLiteral("$Files"); p[TestArgs] = QStringList() << QStringLiteral("t") << QStringLiteral("$Archive"); 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(); m_currentArchiveEntry->compressedSizeIsSet = false; } if (line.startsWith(QStringLiteral("Path = "))) { const QString entryFilename = QDir::fromNativeSeparators(line.mid(7).trimmed()); m_currentArchiveEntry->setProperty("fullPath", entryFilename); } else if (line.startsWith(QStringLiteral("Size = "))) { m_currentArchiveEntry->setProperty("size", line.mid(7).trimmed()); } else if (line.startsWith(QStringLiteral("Packed Size = "))) { // #236696: 7z files only show a single Packed Size value // corresponding to the whole archive. if (m_archiveType != ArchiveType7z) { m_currentArchiveEntry->compressedSizeIsSet = true; m_currentArchiveEntry->setProperty("compressedSize", line.mid(14).trimmed()); } } else if (line.startsWith(QStringLiteral("Modified = "))) { m_currentArchiveEntry->setProperty("timestamp", QDateTime::fromString(line.mid(11).trimmed(), QStringLiteral("yyyy-MM-dd hh:mm:ss"))); } else if (line.startsWith(QStringLiteral("Attributes = "))) { const QString attributes = line.mid(13).trimmed(); const bool isDirectory = attributes.startsWith(QLatin1Char('D')); m_currentArchiveEntry->setProperty("isDirectory", isDirectory); if (isDirectory) { const QString directoryName = - m_currentArchiveEntry->property("fullPath").toString(); + m_currentArchiveEntry->fullPath(); if (!directoryName.endsWith(QLatin1Char('/'))) { const bool isPasswordProtected = (line.at(12) == QLatin1Char('+')); m_currentArchiveEntry->setProperty("fullPath", QString(directoryName + QLatin1Char('/'))); m_currentArchiveEntry->setProperty("isPasswordProtected", isPasswordProtected); } } m_currentArchiveEntry->setProperty("permissions", attributes.mid(1)); } else if (line.startsWith(QStringLiteral("CRC = "))) { m_currentArchiveEntry->setProperty("CRC", line.mid(6).trimmed()); } else if (line.startsWith(QStringLiteral("Method = "))) { m_currentArchiveEntry->setProperty("method", line.mid(9).trimmed()); } else if (line.startsWith(QStringLiteral("Encrypted = ")) && line.size() >= 13) { m_currentArchiveEntry->setProperty("isPasswordProtected", line.at(12) == QLatin1Char('+')); } else if (line.startsWith(QStringLiteral("Block = ")) || line.startsWith(QStringLiteral("Version = "))) { m_isFirstInformationEntry = true; - if (!m_currentArchiveEntry->property("fullPath").toString().isEmpty()) { + if (!m_currentArchiveEntry->fullPath().isEmpty()) { emit entry(m_currentArchiveEntry); } else { delete m_currentArchiveEntry; } m_currentArchiveEntry = Q_NULLPTR; } } return true; } QStringList CliPlugin::passwordHeaderSwitch(const QString& password) const { if (password.isEmpty()) { return QStringList(); } Q_ASSERT(m_param.contains(PasswordHeaderSwitch)); QStringList passwordHeaderSwitch = m_param.value(PasswordHeaderSwitch).toStringList(); Q_ASSERT(!passwordHeaderSwitch.isEmpty() && passwordHeaderSwitch.size() == 2); passwordHeaderSwitch[0].replace(QLatin1String("$Password"), password); return passwordHeaderSwitch; } #include "cliplugin.moc" diff --git a/plugins/clirarplugin/cliplugin.cpp b/plugins/clirarplugin/cliplugin.cpp index 8a79d156..53350c3e 100644 --- a/plugins/clirarplugin/cliplugin.cpp +++ b/plugins/clirarplugin/cliplugin.cpp @@ -1,518 +1,506 @@ /* * 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/archiveentry.h" #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_clirar.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList& args) : CliInterface(parent, args) , m_parseState(ParseStateTitle) , m_isUnrar5(false) , m_isPasswordProtected(false) , m_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[MoveProgram] = p[AddProgram] = QStringList() << QStringLiteral( "rar" ); p[ListArgs] = QStringList() << QStringLiteral("vt") << QStringLiteral("-v") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive"); p[ExtractArgs] = QStringList() << QStringLiteral( "-kb" ) << QStringLiteral( "-p-" ) << QStringLiteral( "$PreservePathSwitch" ) << QStringLiteral( "$PasswordSwitch" ) << QStringLiteral( "$Archive" ) << QStringLiteral( "$Files" ); p[PreservePathSwitch] = QStringList() << QStringLiteral( "x" ) << QStringLiteral( "e" ); p[PasswordSwitch] = QStringList() << QStringLiteral( "-p$Password" ); p[PasswordHeaderSwitch] = QStringList() << QStringLiteral("-hp$Password"); p[CompressionLevelSwitch] = QStringLiteral("-m$CompressionLevel"); p[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[MoveArgs] = QStringList() << QStringLiteral( "rn" ) << QStringLiteral( "$PasswordSwitch" ) << QStringLiteral( "$Archive" ) << QStringLiteral( "$PathPairs" ); p[PasswordPromptPattern] = QLatin1String("Enter password \\(will not be echoed\\) for"); p[WrongPasswordPatterns] = QStringList() << QStringLiteral("password incorrect") << QStringLiteral("wrong password"); p[ExtractionFailedPatterns] = QStringList() << QStringLiteral( "CRC failed" ) << QStringLiteral( "Cannot find volume" ); p[CorruptArchivePatterns] = QStringList() << QStringLiteral("Unexpected end of archive") << QStringLiteral("the file header is corrupt"); p[DiskFullPatterns] = QStringList() << QStringLiteral("No space left on device"); p[CommentArgs] = QStringList() << QStringLiteral("c") << QStringLiteral("$CommentSwitch") << QStringLiteral("$Archive"); p[CommentSwitch] = QStringLiteral("-z$CommentFile"); p[TestArgs] = QStringList() << QStringLiteral("t") << QStringLiteral("$Archive"); 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() { Archive::Entry *e = new Archive::Entry(); QString compressionRatio = m_unrar5Details.value(QStringLiteral("ratio")); compressionRatio.chop(1); // Remove the '%' e->setProperty("ratio", compressionRatio); QString time = m_unrar5Details.value(QStringLiteral("mtime")); QDateTime ts = QDateTime::fromString(time, QStringLiteral("yyyy-MM-dd HH:mm:ss,zzz")); e->setProperty("timestamp", ts); bool isDirectory = (m_unrar5Details.value(QStringLiteral("type")) == QLatin1String("Directory")); e->setProperty("isDirectory", isDirectory); if (isDirectory && !m_unrar5Details.value(QStringLiteral("name")).endsWith(QLatin1Char('/'))) { m_unrar5Details[QStringLiteral("name")] += QLatin1Char('/'); } QString compression = m_unrar5Details.value(QStringLiteral("compression")); int optionPos = compression.indexOf(QLatin1Char('-')); if (optionPos != -1) { e->setProperty("method", compression.mid(optionPos)); e->setProperty("version", compression.left(optionPos).trimmed()); } else { // No method specified. e->setProperty("method", QStringLiteral("")); e->setProperty("version", compression); } m_isPasswordProtected = m_unrar5Details.value(QStringLiteral("flags")).contains(QStringLiteral("encrypted")); e->setProperty("isPasswordProtected", m_isPasswordProtected); e->setProperty("fullPath", m_unrar5Details.value(QStringLiteral("name"))); e->setProperty("size", m_unrar5Details.value(QStringLiteral("size"))); e->setProperty("compressedSize", m_unrar5Details.value(QStringLiteral("packed size"))); e->setProperty("permissions", m_unrar5Details.value(QStringLiteral("attributes"))); e->setProperty("CRC", m_unrar5Details.value(QStringLiteral("crc32"))); if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { e->setProperty("link", m_unrar5Details.value(QStringLiteral("target"))); } m_unrar5Details.clear(); emit entry(e); } 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() { Archive::Entry *e = new Archive::Entry(Q_NULLPTR); QDateTime ts = QDateTime::fromString(QString(m_unrar4Details.at(4) + QLatin1Char(' ') + m_unrar4Details.at(5)), QStringLiteral("dd-MM-yy hh:mm")); // Unrar 3 & 4 output dates with a 2-digit year but QDateTime takes it as // 19??. Let's take 1950 as cut-off; similar to KDateTime. if (ts.date().year() < 1950) { ts = ts.addYears(100); } e->setProperty("timestamp", ts); bool isDirectory = ((m_unrar4Details.at(6).at(0) == QLatin1Char('d')) || (m_unrar4Details.at(6).at(1) == QLatin1Char('D'))); e->setProperty("isDirectory", isDirectory); if (isDirectory && !m_unrar4Details.at(0).endsWith(QLatin1Char('/'))) { m_unrar4Details[0] += QLatin1Char('/'); } // Unrar reports the ratio as ((compressed size * 100) / size); // we consider ratio as (100 * ((size - compressed size) / size)). // If the archive is a multivolume archive, a string indicating // whether the archive's position in the volume is displayed // instead of the compression ratio. QString compressionRatio = m_unrar4Details.at(3); if ((compressionRatio == QStringLiteral("<--")) || (compressionRatio == QStringLiteral("<->")) || (compressionRatio == QStringLiteral("-->"))) { compressionRatio = QLatin1Char('0'); } else { compressionRatio.chop(1); // Remove the '%' } e->setProperty("ratio", compressionRatio); // TODO: // - Permissions differ depending on the system the entry was added // to the archive. e->setProperty("fullPath", m_unrar4Details.at(0)); e->setProperty("size", m_unrar4Details.at(1)); e->setProperty("compressedSize", m_unrar4Details.at(2)); e->setProperty("permissions", m_unrar4Details.at(6)); e->setProperty("CRC", m_unrar4Details.at(7)); e->setProperty("method", m_unrar4Details.at(8)); e->setProperty("version", m_unrar4Details.at(9)); e->setProperty("isPasswordProtected", m_isPasswordProtected); if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { e->setProperty("link", m_unrar4Details.at(10)); } m_unrar4Details.clear(); emit entry(e); } void CliPlugin::ignoreLines(int lines, ParseState nextState) { m_remainingIgnoreLines = lines; m_parseState = nextState; } #include "cliplugin.moc" diff --git a/plugins/clirarplugin/cliplugin.h b/plugins/clirarplugin/cliplugin.h index dd98088e..71b3f9de 100644 --- a/plugins/clirarplugin/cliplugin.h +++ b/plugins/clirarplugin/cliplugin.h @@ -1,70 +1,69 @@ /* * 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" class CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList &args); virtual ~CliPlugin(); virtual void resetParsing() Q_DECL_OVERRIDE; - virtual QString escapeFileName(const QString &fileName) const Q_DECL_OVERRIDE; virtual Kerfuffle::ParameterList parameterList() const Q_DECL_OVERRIDE; virtual bool readListLine(const QString &line) Q_DECL_OVERRIDE; private: enum ParseState { ParseStateTitle = 0, ParseStateComment, ParseStateHeader, ParseStateEntryFileName, ParseStateEntryDetails, ParseStateLinkTarget } m_parseState; void handleUnrar5Line(const QString &line); void handleUnrar5Entry(); void handleUnrar4Line(const QString &line); void handleUnrar4Entry(); void ignoreLines(int lines, ParseState nextState); QStringList m_unrar4Details; QHash m_unrar5Details; bool m_isUnrar5; bool m_isPasswordProtected; bool m_isMultiVolume; bool m_isSolid; int m_remainingIgnoreLines; int m_linesComment; }; #endif // CLIPLUGIN_H diff --git a/plugins/clizipplugin/cliplugin.cpp b/plugins/clizipplugin/cliplugin.cpp index 508cb29c..5a27abf7 100644 --- a/plugins/clizipplugin/cliplugin.cpp +++ b/plugins/clizipplugin/cliplugin.cpp @@ -1,297 +1,293 @@ /* * 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()) { Archive::Entry *e = new Archive::Entry(); e->setProperty("permissions", rxMatch.captured(1)); // #280354: infozip may not show the right attributes for a given directory, so an entry // ending with '/' is actually more reliable than 'd' bein in the attributes. e->setProperty("isDirectory", rxMatch.captured(10).endsWith(QLatin1Char('/'))); e->setProperty("size", rxMatch.captured(4)); QString status = rxMatch.captured(5); if (status[0].isUpper()) { e->setProperty("isPasswordProtected", true); } e->setProperty("compressedSize", rxMatch.captured(6).toInt()); const QDateTime ts(QDate::fromString(rxMatch.captured(8), QStringLiteral("yyyyMMdd")), QTime::fromString(rxMatch.captured(9), QStringLiteral("hhmmss"))); e->setProperty("timestamp", ts); e->setProperty("fullPath", rxMatch.captured(10)); emit entry(e); } break; } return true; } bool CliPlugin::moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { m_oldWorkingDir = QDir::currentPath(); m_moveExtractedDir = new QTemporaryDir(); m_moveAddedDir = new QTemporaryDir(); QDir::setCurrent(m_moveExtractedDir->path()); m_movedFiles = files; m_moveDestination = destination; m_moveCompressionOptions = options; m_moveSubOperation = Copy; connect(this, &CliPlugin::finished, this, &CliPlugin::continueMoving); return copyFiles(files, QDir::currentPath(), options); } int CliPlugin::moveRequiredSignals() const { return 4; } void CliPlugin::continueMoving(bool result) { if (!result) { cleanUpMoving(); emit cancelled(); emit finished(false); } switch (m_moveSubOperation) { case Copy: m_moveSubOperation = Delete; deleteFiles(m_movedFiles); break; case Delete: m_moveSubOperation = Add; setAddedFiles(); addFiles(m_moveAddedFiles, m_moveDestination, m_moveCompressionOptions); break; case Add: cleanUpMoving(); emit progress(1.0); emit finished(true); break; default: Q_ASSERT(false); } } void CliPlugin::setAddedFiles() { QDir::setCurrent(m_moveAddedDir->path()); // If there are more files being moved than 1, we have destination as a destination folder, // otherwise it's new entry full path. if (m_movedFiles.count() > 1) { foreach (const Archive::Entry *file, m_movedFiles) { - QString oldPath = m_moveExtractedDir->path() + QLatin1Char('/') + file->property("fullPath").toString(); + QString oldPath = m_moveExtractedDir->path() + QLatin1Char('/') + file->fullPath(); // Move function can't accept the second argument as a path with trailing slash. if (oldPath[oldPath.count() - 1] == QLatin1Char('/')) { oldPath.remove(oldPath.count() - 1, 1); } QString newPath = m_moveAddedDir->path() + QLatin1Char('/') + file->name(); QFile::rename(oldPath, newPath); m_moveAddedFiles << new Archive::Entry(Q_NULLPTR, file->name()); } } else { const Archive::Entry *file = m_movedFiles.at(0); - QString oldPath = m_moveExtractedDir->path() + QLatin1Char('/') + file->property("fullPath").toString(); - // Move function can't accept the second argument as a path with trailing slash. - if (oldPath[oldPath.count() - 1] == QLatin1Char('/')) { - oldPath.remove(oldPath.count() - 1, 1); - } + const QString oldPath = m_moveExtractedDir->path() + QLatin1Char('/') + file->fullPath(true); QString newPath = m_moveAddedDir->path() + QLatin1Char('/') + m_moveDestination->name(); QFile::rename(oldPath, newPath); m_moveAddedFiles << new Archive::Entry(Q_NULLPTR, m_moveDestination->name()); // We have to exclude file name from destination path in order to pass it to addFiles method. - const QString destinationPath = m_moveDestination->property("fullPath").toString(); + const QString destinationPath = m_moveDestination->fullPath(); int destinationLength = destinationPath.count(); bool iteratedChar = false; do { destinationLength--; if (destinationPath.at(destinationLength) != QLatin1Char('/')) { iteratedChar = true; } } while (destinationLength > 0 && !(iteratedChar && destinationPath.at(destinationLength) == QLatin1Char('/'))); m_moveDestination->setProperty("fullPath", destinationPath.left(destinationLength + 1)); } } void CliPlugin::cleanUpMoving() { disconnect(this, &CliPlugin::finished, this, &CliPlugin::continueMoving); qDeleteAll(m_moveAddedFiles); m_moveAddedFiles.clear(); QDir::setCurrent(m_oldWorkingDir); delete m_moveExtractedDir; m_moveExtractedDir = Q_NULLPTR; delete m_moveAddedDir; m_moveAddedDir = Q_NULLPTR; } #include "cliplugin.moc" diff --git a/plugins/libarchive/readwritelibarchiveplugin.cpp b/plugins/libarchive/readwritelibarchiveplugin.cpp index 265ef9dc..f0ce3d5e 100644 --- a/plugins/libarchive/readwritelibarchiveplugin.cpp +++ b/plugins/libarchive/readwritelibarchiveplugin.cpp @@ -1,539 +1,539 @@ /* * 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 "readwritelibarchiveplugin.h" #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(ReadWriteLibarchivePluginFactory, "kerfuffle_libarchive.json", registerPlugin();) ReadWriteLibarchivePlugin::ReadWriteLibarchivePlugin(QObject *parent, const QVariantList & args) : LibarchivePlugin(parent, args) { qCDebug(ARK) << "Loaded libarchive read-write plugin"; } ReadWriteLibarchivePlugin::~ReadWriteLibarchivePlugin() { } bool ReadWriteLibarchivePlugin::addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) { qCDebug(ARK) << "Adding" << files.size() << "entries with CompressionOptions" << options; const bool creatingNewFile = !QFileInfo::exists(filename()); m_writtenFiles.clear(); ArchiveRead arch_reader; if (!creatingNewFile) { arch_reader.reset(archive_read_new()); if (!initializeReader(arch_reader)) { return false; } } // |tempFile| needs to be created before |arch_writer| so that when we go // out of scope in a `return false' case ArchiveWriteCustomDeleter is // called before destructor of QSaveFile (ie. we call archive_write_close() // before close()'ing the file descriptor). QSaveFile tempFile(filename()); if (!tempFile.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) { emit error(xi18nc("@info", "Failed to create a temporary file to compress %1.", filename())); return false; } ArchiveWrite arch_writer(archive_write_new()); if (!(arch_writer.data())) { emit error(i18n("The archive writer could not be initialized.")); return false; } // pax_restricted is the libarchive default, let's go with that. archive_write_set_format_pax_restricted(arch_writer.data()); if (creatingNewFile) { if (!initializeNewFileWriter(arch_writer, options)) { return false; } } else { if (!initializeWriter(arch_writer, arch_reader)) { return false; } } if (archive_write_open_fd(arch_writer.data(), tempFile.handle()) != ARCHIVE_OK) { emit error(xi18nc("@info", "Opening the archive for writing failed with the following error:%1", QLatin1String(archive_error_string(arch_writer.data())))); return false; } // First write the new files. qCDebug(ARK) << "Writing new entries"; int no_entries = 0; // Recreate destination directory structure. - const QString destinationPath = destination->property("fullPath").toString(); + const QString destinationPath = destination->fullPath(); foreach(Archive::Entry *selectedFile, files) { if (m_abortOperation) { break; } - if (!writeFile(selectedFile->property("fullPath").toString(), destinationPath, arch_writer.data())) { + if (!writeFile(selectedFile->fullPath(), destinationPath, arch_writer.data())) { return false; } no_entries++; // For directories, write all subfiles/folders. - const QString &fullPath = selectedFile->property("fullPath").toString(); + const QString &fullPath = selectedFile->fullPath(); if (QFileInfo(fullPath).isDir()) { QDirIterator it(fullPath, QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (!m_abortOperation && it.hasNext()) { QString path = it.next(); if ((it.fileName() == QLatin1String("..")) || (it.fileName() == QLatin1String("."))) { continue; } const bool isRealDir = it.fileInfo().isDir() && !it.fileInfo().isSymLink(); if (isRealDir) { path.append(QLatin1Char('/')); } if (!writeFile(path, destinationPath, arch_writer.data())) { return false; } no_entries++; } } } qCDebug(ARK) << "Added" << no_entries << "new entries to archive"; // If we have old archive entries. if (!creatingNewFile) { qCDebug(ARK) << "Copying any old entries"; m_filePaths = m_writtenFiles; if (!processOldEntries(arch_reader, arch_writer, no_entries, Add)) { QFile::remove(tempFile.fileName()); return false; } qCDebug(ARK) << "Added" << no_entries << "old entries to archive"; } m_abortOperation = false; // In the success case, we need to manually close the archive_writer before // calling QSaveFile::commit(), otherwise the latter will close() the // file descriptor archive_writer is still working on. // TODO: We need to abstract this code better so that we only deal with one // object that manages both QSaveFile and ArchiveWriter. archive_write_close(arch_writer.data()); tempFile.commit(); return true; } bool ReadWriteLibarchivePlugin::moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { qCDebug(ARK) << "Moving" << files.size() << "entries"; ArchiveRead arch_reader(archive_read_new()); if (!initializeReader(arch_reader)) { return false; } // |tempFile| needs to be created before |arch_writer| so that when we go // out of scope in a `return false' case ArchiveWriteCustomDeleter is // called before destructor of QSaveFile (ie. we call archive_write_close() // before close()'ing the file descriptor). QSaveFile tempFile(filename()); if (!tempFile.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) { emit error(i18nc("@info", "Failed to create a temporary file.")); return false; } ArchiveWrite arch_writer(archive_write_new()); if (!(arch_writer.data())) { emit error(i18n("The archive writer could not be initialized.")); return false; } // pax_restricted is the libarchive default, let's go with that. archive_write_set_format_pax_restricted(arch_writer.data()); if (!initializeWriter(arch_writer, arch_reader)) { return false; } if (archive_write_open_fd(arch_writer.data(), tempFile.handle()) != ARCHIVE_OK) { emit error(xi18nc("@info", "Opening the archive for writing failed with the following error:" "%1", QLatin1String(archive_error_string(arch_writer.data())))); return false; } // Copy old elements from previous archive to new archive. int no_entries = 0; m_filePaths = entryFullPaths(files); m_destination = destination; processOldEntries(arch_reader, arch_writer, no_entries, Move); qCDebug(ARK) << "Removed" << no_entries << "entries from archive"; // In the success case, we need to manually close the archive_writer before // calling QSaveFile::commit(), otherwise the latter will close() the // file descriptor archive_writer is still working on. // TODO: We need to abstract this code better so that we only deal with one // object that manages both QSaveFile and ArchiveWriter. archive_write_close(arch_writer.data()); tempFile.commit(); return true; } bool ReadWriteLibarchivePlugin::deleteFiles(const QList& files) { qCDebug(ARK) << "Deleting" << files.size() << "entries"; ArchiveRead arch_reader(archive_read_new()); if (!initializeReader(arch_reader)) { return false; } // |tempFile| needs to be created before |arch_writer| so that when we go // out of scope in a `return false' case ArchiveWriteCustomDeleter is // called before destructor of QSaveFile (ie. we call archive_write_close() // before close()'ing the file descriptor). QSaveFile tempFile(filename()); if (!tempFile.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) { emit error(i18nc("@info", "Failed to create a temporary file.")); return false; } ArchiveWrite arch_writer(archive_write_new()); if (!(arch_writer.data())) { emit error(i18n("The archive writer could not be initialized.")); return false; } // pax_restricted is the libarchive default, let's go with that. archive_write_set_format_pax_restricted(arch_writer.data()); if (!initializeWriter(arch_writer, arch_reader)) { return false; } if (archive_write_open_fd(arch_writer.data(), tempFile.handle()) != ARCHIVE_OK) { emit error(xi18nc("@info", "Opening the archive for writing failed with the following error:" "%1", QLatin1String(archive_error_string(arch_writer.data())))); return false; } // Copy old elements from previous archive to new archive. int no_entries = 0; m_filePaths = entryFullPaths(files); processOldEntries(arch_reader, arch_writer, no_entries, Delete); qCDebug(ARK) << "Removed" << no_entries << "entries from archive"; // In the success case, we need to manually close the archive_writer before // calling QSaveFile::commit(), otherwise the latter will close() the // file descriptor archive_writer is still working on. // TODO: We need to abstract this code better so that we only deal with one // object that manages both QSaveFile and ArchiveWriter. archive_write_close(arch_writer.data()); tempFile.commit(); return true; } bool ReadWriteLibarchivePlugin::initializeNewFileWriter(const LibarchivePlugin::ArchiveWrite &archiveWrite, const CompressionOptions &options) { int ret; bool requiresExecutable = false; if (filename().right(2).toUpper() == QLatin1String("GZ")) { qCDebug(ARK) << "Detected gzip compression for new file"; ret = archive_write_add_filter_gzip(archiveWrite.data()); } else if (filename().right(3).toUpper() == QLatin1String("BZ2")) { qCDebug(ARK) << "Detected bzip2 compression for new file"; ret = archive_write_add_filter_bzip2(archiveWrite.data()); } else if (filename().right(2).toUpper() == QLatin1String("XZ")) { qCDebug(ARK) << "Detected xz compression for new file"; ret = archive_write_add_filter_xz(archiveWrite.data()); } else if (filename().right(4).toUpper() == QLatin1String("LZMA")) { qCDebug(ARK) << "Detected lzma compression for new file"; ret = archive_write_add_filter_lzma(archiveWrite.data()); } else if (filename().right(2).toUpper() == QLatin1String(".Z")) { qCDebug(ARK) << "Detected compress (.Z) compression for new file"; ret = archive_write_add_filter_compress(archiveWrite.data()); } else if (filename().right(2).toUpper() == QLatin1String("LZ")) { qCDebug(ARK) << "Detected lzip compression for new file"; ret = archive_write_add_filter_lzip(archiveWrite.data()); } else if (filename().right(3).toUpper() == QLatin1String("LZO")) { qCDebug(ARK) << "Detected lzop compression for new file"; ret = archive_write_add_filter_lzop(archiveWrite.data()); } else if (filename().right(3).toUpper() == QLatin1String("LRZ")) { qCDebug(ARK) << "Detected lrzip compression for new file"; ret = archive_write_add_filter_lrzip(archiveWrite.data()); requiresExecutable = true; #ifdef HAVE_LIBARCHIVE_3_2_0 } else if (filename().right(3).toUpper() == QLatin1String("LZ4")) { qCDebug(ARK) << "Detected lz4 compression for new file"; ret = archive_write_add_filter_lz4(archiveWrite.data()); #endif } else if (filename().right(3).toUpper() == QLatin1String("TAR")) { qCDebug(ARK) << "Detected no compression for new file (pure tar)"; ret = archive_write_add_filter_none(archiveWrite.data()); } else { qCDebug(ARK) << "Falling back to gzip"; ret = archive_write_add_filter_gzip(archiveWrite.data()); } // Libarchive emits a warning for lrzip due to using external executable. if ((requiresExecutable && ret != ARCHIVE_WARN) || (!requiresExecutable && ret != ARCHIVE_OK)) { emit error(xi18nc("@info", "Setting the compression method failed with the following error:%1", QLatin1String(archive_error_string(archiveWrite.data())))); return false; } // Set compression level if passed in CompressionOptions. if (options.contains(QStringLiteral("CompressionLevel"))) { qCDebug(ARK) << "Using compression level:" << options.value(QStringLiteral("CompressionLevel")).toString(); ret = archive_write_set_filter_option(archiveWrite.data(), NULL, "compression-level", options.value(QStringLiteral("CompressionLevel")).toString().toUtf8()); if (ret != ARCHIVE_OK) { qCWarning(ARK) << "Failed to set compression level"; emit error(xi18nc("@info", "Setting the compression level failed with the following error:%1", QLatin1String(archive_error_string(archiveWrite.data())))); return false; } } return true; } bool ReadWriteLibarchivePlugin::initializeWriter(const ArchiveWrite &archiveWrite, const ArchiveRead &archiveRead) { int ret; bool requiresExecutable = false; switch (archive_filter_code(archiveRead.data(), 0)) { case ARCHIVE_FILTER_GZIP: ret = archive_write_add_filter_gzip(archiveWrite.data()); break; case ARCHIVE_FILTER_BZIP2: ret = archive_write_add_filter_bzip2(archiveWrite.data()); break; case ARCHIVE_FILTER_XZ: ret = archive_write_add_filter_xz(archiveWrite.data()); break; case ARCHIVE_FILTER_LZMA: ret = archive_write_add_filter_lzma(archiveWrite.data()); break; case ARCHIVE_FILTER_COMPRESS: ret = archive_write_add_filter_compress(archiveWrite.data()); break; case ARCHIVE_FILTER_LZIP: ret = archive_write_add_filter_lzip(archiveWrite.data()); break; case ARCHIVE_FILTER_LZOP: ret = archive_write_add_filter_lzop(archiveWrite.data()); break; case ARCHIVE_FILTER_LRZIP: ret = archive_write_add_filter_lrzip(archiveWrite.data()); requiresExecutable = true; break; #ifdef HAVE_LIBARCHIVE_3_2_0 case ARCHIVE_FILTER_LZ4: ret = archive_write_add_filter_lz4(archiveWrite.data()); break; #endif case ARCHIVE_FILTER_NONE: ret = archive_write_add_filter_none(archiveWrite.data()); break; default: emit error(i18n("The compression type '%1' is not supported by Ark.", QLatin1String(archive_filter_name(archiveRead.data(), 0)))); return false; } // Libarchive emits a warning for lrzip due to using external executable. if ((requiresExecutable && ret != ARCHIVE_WARN) || (!requiresExecutable && ret != ARCHIVE_OK)) { emit error(xi18nc("@info", "Setting the compression method failed with the following error:%1", QLatin1String(archive_error_string(archiveWrite.data())))); return false; } return true; } bool ReadWriteLibarchivePlugin::processOldEntries(const LibarchivePlugin::ArchiveRead &archiveRead, const LibarchivePlugin::ArchiveWrite &archiveWrite, int &entriesCounter, OperationMode mode) { struct archive_entry *entry; m_lastMovedFolder = QString(); entriesCounter = 0; while ((mode != Add || !m_abortOperation) && archive_read_next_header(archiveRead.data(), &entry) == ARCHIVE_OK) { const QString file = QFile::decodeName(archive_entry_pathname(entry)); if (mode == Move) { QString destination; if (m_lastMovedFolder.count() > 0 && file.startsWith(m_lastMovedFolder)) { destination = file; - destination.replace(0, m_lastMovedFolder.count(), m_destination->property("fullPath").toString()); + destination.replace(0, m_lastMovedFolder.count(), m_destination->fullPath()); } else if (m_filePaths.contains(file)) { - destination = m_destination->property("fullPath").toString(); + destination = m_destination->fullPath(); if (m_filePaths.count() > 1) { destination = destination + file.split(QLatin1Char('/'), QString::SkipEmptyParts).last(); } else { - destination = m_destination->property("fullPath").toString(); + destination = m_destination->fullPath(); if (destination[destination.count() - 1] == QLatin1Char('/')) { destination.remove(destination.count() - 1, 1); m_lastMovedFolder = destination; } } } archive_entry_set_pathname(entry, destination.toStdString().c_str()); emit entryRemoved(file); } else if (m_filePaths.contains(file)) { archive_read_data_skip(archiveRead.data()); switch (mode) { case Delete: entriesCounter++; emit entryRemoved(file); break; case Add: qCDebug(ARK) << file << "is already present in the new archive, skipping."; break; } continue; } if (writeEntry(archiveRead, archiveWrite, entry)) { if (mode == Add) { entriesCounter++; } archive_entry_clear(entry); } else { return false; } } return true; } bool ReadWriteLibarchivePlugin::writeEntry(const LibarchivePlugin::ArchiveRead &archiveRead, const LibarchivePlugin::ArchiveWrite &archiveWrite, struct archive_entry *entry) { const int returnCode = archive_write_header(archiveWrite.data(), entry); const QString file = QFile::decodeName(archive_entry_pathname(entry)); switch (returnCode) { case ARCHIVE_OK: // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(QLatin1String(archive_entry_pathname(entry)), archiveRead.data(), archiveWrite.data(), false); break; case ARCHIVE_FAILED: case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(archiveWrite.data()); emit error(xi18nc("@info", "Compression failed while processing:" "%1Operation aborted.", file)); return false; default: qCDebug(ARK) << "archive_writer_header() has returned" << returnCode << "which will be ignored."; break; } return true; } // TODO: if we merge this with copyData(), we can pass more data // such as an fd to archive_read_disk_entry_from_file() bool ReadWriteLibarchivePlugin::writeFile(const QString& relativeName, const QString& destination, struct archive* arch_writer) { int header_response; const QString absoluteFilename = QFileInfo(relativeName).absoluteFilePath(); const QString destinationFilename = destination + relativeName; // #253059: Even if we use archive_read_disk_entry_from_file, // libarchive may have been compiled without HAVE_LSTAT, // or something may have caused it to follow symlinks, in // which case stat() will be called. To avoid this, we // call lstat() ourselves. struct stat st; lstat(QFile::encodeName(absoluteFilename).constData(), &st); struct archive_entry *entry = archive_entry_new(); archive_entry_set_pathname(entry, QFile::encodeName(destinationFilename).constData()); archive_entry_copy_sourcepath(entry, QFile::encodeName(absoluteFilename).constData()); archive_read_disk_entry_from_file(m_archiveReadDisk.data(), entry, -1, &st); if ((header_response = archive_write_header(arch_writer, entry)) == ARCHIVE_OK) { // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(absoluteFilename, arch_writer, false); } else { qCCritical(ARK) << "Writing header failed with error code " << header_response; qCCritical(ARK) << "Error while writing..." << archive_error_string(arch_writer) << "(error no =" << archive_errno(arch_writer) << ')'; emit error(xi18nc("@info Error in a message box", "Ark could not compress %1:%2", absoluteFilename, QString::fromUtf8(archive_error_string(arch_writer)))); archive_entry_free(entry); return false; } m_writtenFiles.push_back(destinationFilename); emitEntryFromArchiveEntry(entry); archive_entry_free(entry); return true; } #include "readwritelibarchiveplugin.moc"