diff --git a/autotests/kerfuffle/addtest.cpp b/autotests/kerfuffle/addtest.cpp index 264c2b2e..ef80edd5 100644 --- a/autotests/kerfuffle/addtest.cpp +++ b/autotests/kerfuffle/addtest.cpp @@ -1,133 +1,132 @@ /* - * Copyright (c) 2010-2011 Raphael Kubo da Costa - * Copyright (c) 2016 Elvis Angelaccio + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "autotests/testhelper/testhelper.h" using namespace Kerfuffle; class AddTest : public QObject { Q_OBJECT private: void addAllFormatsRows(const QString testName, const QString archiveName, QList entries, Archive::Entry *destination) { QStringList formats = QStringList() << QStringLiteral("7z") << QStringLiteral("rar") << QStringLiteral("tar.bz2") << QStringLiteral("zip"); foreach (QString format, formats) { const QString testNameWithFormat = testName + QStringLiteral(" (") + format + QStringLiteral(")"); QTest::newRow(testNameWithFormat.toUtf8()) << archiveName + QLatin1Char('.') + format << entries << destination; } } private Q_SLOTS: void testAdding_data(); void testAdding(); }; QTEST_GUILESS_MAIN(AddTest) void AddTest::testAdding_data() { QTest::addColumn("archiveName"); QTest::addColumn>("files"); QTest::addColumn("destination"); addAllFormatsRows(QStringLiteral("without destination"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("textfile1.txt")), new Archive::Entry(this, QStringLiteral("textfile2.txt")), }, new Archive::Entry(this)); addAllFormatsRows(QStringLiteral("with destination, files"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("textfile1.txt")), new Archive::Entry(this, QStringLiteral("textfile2.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/"))); addAllFormatsRows(QStringLiteral("with destination, directory"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("testdir/")), }, new Archive::Entry(this, QStringLiteral("empty_dir/"))); addAllFormatsRows(QStringLiteral("without destination, directory 2"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("testdir2/")), }, new Archive::Entry(this)); addAllFormatsRows(QStringLiteral("with destination, directory 2"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("testdir2/")), }, new Archive::Entry(this, QStringLiteral("empty_dir/"))); } void AddTest::testAdding() { QTemporaryDir temporaryDir; QFETCH(QString, archiveName); const QString archivePath = temporaryDir.path() + QLatin1Char('/') + archiveName; Q_ASSERT(QFile::copy(QFINDTESTDATA(QStringLiteral("data/") + archiveName), archivePath)); Archive *archive = Archive::create(archivePath, this); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not find a plugin to handle the archive. Skipping test.", SkipSingle); } QFETCH(QList, files); QFETCH(Archive::Entry*, destination); QList oldEntries = TestHelper::getEntryList(archive); CompressionOptions options = CompressionOptions(); options.insert(QStringLiteral("GlobalWorkDir"), QFINDTESTDATA("data")); AddJob *addJob = archive->addFiles(files, destination, options); TestHelper::startAndWaitForResult(addJob); QList resultedEntries = TestHelper::getEntryList(archive); TestHelper::verifyAddedEntriesWithDestination(files, destination, oldEntries, resultedEntries); archive->deleteLater(); } #include "addtest.moc" diff --git a/autotests/kerfuffle/copytest.cpp b/autotests/kerfuffle/copytest.cpp index 54c88bf1..55b22fcb 100644 --- a/autotests/kerfuffle/copytest.cpp +++ b/autotests/kerfuffle/copytest.cpp @@ -1,200 +1,199 @@ /* - * Copyright (c) 2010-2011 Raphael Kubo da Costa - * Copyright (c) 2016 Elvis Angelaccio + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "autotests/testhelper/testhelper.h" using namespace Kerfuffle; class CopyTest : public QObject { Q_OBJECT private: void addAllFormatsRows(const QString testName, const QString archiveName, QList entries, Archive::Entry *destination) { QStringList formats = QStringList() << QStringLiteral("7z") << QStringLiteral("rar") << QStringLiteral("tar.bz2") << QStringLiteral("zip"); foreach (QString format, formats) { const QString testNameWithFormat = testName + QStringLiteral(" (") + format + QStringLiteral(")"); QTest::newRow(testNameWithFormat.toUtf8()) << archiveName + QLatin1Char('.') + format << entries << destination; } } private Q_SLOTS: void testCopying_data(); void testCopying(); }; QTEST_GUILESS_MAIN(CopyTest) void CopyTest::testCopying_data() { QTest::addColumn("archiveName"); QTest::addColumn>("files"); QTest::addColumn("destination"); addAllFormatsRows(QStringLiteral("copy a single file"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("a.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/"))); addAllFormatsRows(QStringLiteral("copy several files"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("b.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/"))); addAllFormatsRows(QStringLiteral("copy a root directory"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("dir1/")), new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")), new Archive::Entry(this, QStringLiteral("dir1/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/b.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/"))); addAllFormatsRows(QStringLiteral("copy a root directory 2"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("dir2/")), new Archive::Entry(this, QStringLiteral("dir2/dir/")), new Archive::Entry(this, QStringLiteral("dir2/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir2/dir/b.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/"))); addAllFormatsRows(QStringLiteral("copy a root directory 3"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("dir2/")), new Archive::Entry(this, QStringLiteral("dir2/dir/")), new Archive::Entry(this, QStringLiteral("dir2/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir2/dir/b.txt")), }, new Archive::Entry(this, QStringLiteral("dir1/"))); addAllFormatsRows(QStringLiteral("copy a directory"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/"))); addAllFormatsRows(QStringLiteral("copy several directories"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("dir1/")), new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")), new Archive::Entry(this, QStringLiteral("dir1/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/b.txt")), new Archive::Entry(this, QStringLiteral("dir2/")), new Archive::Entry(this, QStringLiteral("dir2/dir/")), new Archive::Entry(this, QStringLiteral("dir2/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir2/dir/b.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/"))); addAllFormatsRows(QStringLiteral("copy several entries"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")), new Archive::Entry(this, QStringLiteral("dir1/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/b.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/"))); addAllFormatsRows(QStringLiteral("copy a directory inside itself"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("dir1/")), new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")), new Archive::Entry(this, QStringLiteral("dir1/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/b.txt")), }, new Archive::Entry(this, QStringLiteral("dir1/"))); addAllFormatsRows(QStringLiteral("copy a directory to the root"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")), }, new Archive::Entry(this, QStringLiteral(""))); } void CopyTest::testCopying() { QTemporaryDir temporaryDir; QFETCH(QString, archiveName); const QString archivePath = temporaryDir.path() + QLatin1Char('/') + archiveName; Q_ASSERT(QFile::copy(QFINDTESTDATA(QStringLiteral("data/") + archiveName), archivePath)); Archive *archive = Archive::create(archivePath, this); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not find a plugin to handle the archive. Skipping test.", SkipSingle); } QFETCH(QList, files); QFETCH(Archive::Entry*, destination); const QList oldEntries = TestHelper::getEntryList(archive); CompressionOptions options = CompressionOptions(); options.insert(QStringLiteral("GlobalWorkDir"), QFINDTESTDATA("data")); CopyJob *copyJob = archive->copyFiles(files, destination, options); TestHelper::startAndWaitForResult(copyJob); QList resultedEntries = TestHelper::getEntryList(archive); TestHelper::verifyCopiedEntriesWithDestination(files, destination, oldEntries, resultedEntries); archive->deleteLater(); } #include "copytest.moc" diff --git a/autotests/kerfuffle/movetest.cpp b/autotests/kerfuffle/movetest.cpp index f1433226..acfbbfd6 100644 --- a/autotests/kerfuffle/movetest.cpp +++ b/autotests/kerfuffle/movetest.cpp @@ -1,178 +1,177 @@ /* - * Copyright (c) 2010-2011 Raphael Kubo da Costa - * Copyright (c) 2016 Elvis Angelaccio + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "autotests/testhelper/testhelper.h" using namespace Kerfuffle; class MoveTest : public QObject { Q_OBJECT private: void addAllFormatsRows(const QString testName, const QString archiveName, QList entries, Archive::Entry *destination) { QStringList formats = QStringList() << QStringLiteral("7z") << QStringLiteral("rar") << QStringLiteral("tar.bz2") << QStringLiteral("zip"); foreach (QString format, formats) { const QString testNameWithFormat = testName + QStringLiteral(" (") + format + QStringLiteral(")"); QTest::newRow(testNameWithFormat.toUtf8()) << archiveName + QLatin1Char('.') + format << entries << destination; } } private Q_SLOTS: void testMoving_data(); void testMoving(); }; QTEST_GUILESS_MAIN(MoveTest) void MoveTest::testMoving_data() { QTest::addColumn("archiveName"); QTest::addColumn>("files"); QTest::addColumn("destination"); addAllFormatsRows(QStringLiteral("replace a single file"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("a.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/a.txt"))); addAllFormatsRows(QStringLiteral("replace several files"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("b.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/"))); addAllFormatsRows(QStringLiteral("replace a root directory"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("dir1/")), new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")), new Archive::Entry(this, QStringLiteral("dir1/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/b.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/dir/"))); addAllFormatsRows(QStringLiteral("replace a root directory 2"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("dir2/")), new Archive::Entry(this, QStringLiteral("dir2/dir/")), new Archive::Entry(this, QStringLiteral("dir2/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir2/dir/b.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/dir/"))); addAllFormatsRows(QStringLiteral("replace a directory"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/dir/"))); addAllFormatsRows(QStringLiteral("replace several directories"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("dir1/")), new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")), new Archive::Entry(this, QStringLiteral("dir1/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/b.txt")), new Archive::Entry(this, QStringLiteral("dir2/")), new Archive::Entry(this, QStringLiteral("dir2/dir/")), new Archive::Entry(this, QStringLiteral("dir2/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir2/dir/b.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/"))); addAllFormatsRows(QStringLiteral("replace several entries"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")), new Archive::Entry(this, QStringLiteral("dir1/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/b.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/"))); addAllFormatsRows(QStringLiteral("move a directory to the root"), QStringLiteral("test"), QList { new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")), }, new Archive::Entry(this, QStringLiteral("dir/"))); } void MoveTest::testMoving() { QTemporaryDir temporaryDir; QFETCH(QString, archiveName); const QString archivePath = temporaryDir.path() + QLatin1Char('/') + archiveName; Q_ASSERT(QFile::copy(QFINDTESTDATA(QStringLiteral("data/") + archiveName), archivePath)); Archive *archive = Archive::create(archivePath, this); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not find a plugin to handle the archive. Skipping test.", SkipSingle); } QFETCH(QList, files); QFETCH(Archive::Entry*, destination); QList oldEntries = TestHelper::getEntryList(archive); CompressionOptions options = CompressionOptions(); options.insert(QStringLiteral("GlobalWorkDir"), QFINDTESTDATA("data")); MoveJob *moveJob = archive->moveFiles(files, destination, options); TestHelper::startAndWaitForResult(moveJob); QList resultedEntries = TestHelper::getEntryList(archive); TestHelper::verifyMovedEntriesWithDestination(files, destination, oldEntries, resultedEntries); archive->deleteLater(); } #include "movetest.moc" diff --git a/autotests/testhelper/testhelper.cpp b/autotests/testhelper/testhelper.cpp index 5cff7c77..0f58fd8f 100644 --- a/autotests/testhelper/testhelper.cpp +++ b/autotests/testhelper/testhelper.cpp @@ -1,188 +1,213 @@ +/* + * Copyright (c) 2016 Vladyslav Batyrenko + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + #include "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(Archive *archive) { QList list = QList(); ListJob *listJob = archive->list(); QObject::connect(listJob, &Job::newEntry, [&list](Archive::Entry* entry) { list << entry; }); startAndWaitForResult(listJob); return list; } void TestHelper::verifyAddedEntriesWithDestination(const QList &argumentEntries, const Archive::Entry *destination, const QList &oldEntries, const QList &newEntries) { QStringList expectedPaths = getExpectedNewEntryPaths(argumentEntries, destination); QStringList actualPaths = ReadOnlyArchiveInterface::entryFullPaths(newEntries); foreach (const QString &path, expectedPaths) { QVERIFY2(actualPaths.contains(path), (QStringLiteral("No ") + path + QStringLiteral(" inside the archive (new entry)")).toUtf8()); } foreach (const Archive::Entry *entry, oldEntries) { const QString path = entry->fullPath(); QVERIFY2(actualPaths.contains(path), (QStringLiteral("No ") + path + QStringLiteral(" inside the archive (old entry)")).toUtf8()); } } void TestHelper::verifyMovedEntriesWithDestination(const QList &argumentEntries, const Archive::Entry *destination, const QList &oldEntries, const QList &newEntries) { QStringList expectedPaths = getExpectedMovedEntryPaths(oldEntries, argumentEntries, destination); QStringList actualPaths = ReadOnlyArchiveInterface::entryFullPaths(newEntries); foreach (const QString &path, expectedPaths) { QVERIFY2(actualPaths.contains(path), (QStringLiteral("No ") + path + QStringLiteral(" inside the archive")).toUtf8()); } foreach (const QString &path, actualPaths) { QVERIFY2(expectedPaths.contains(path), (QStringLiteral("Entry ") + path + QStringLiteral(" is not expected to be inside the archive")).toUtf8()); } foreach (const Archive::Entry *entry, argumentEntries) { const QString path = entry->fullPath(); QVERIFY2(!actualPaths.contains(path), (QStringLiteral("Entry ") + path + QStringLiteral(" is still inside the archive, when it shouldn't be")).toUtf8()); } } void TestHelper::verifyCopiedEntriesWithDestination(const QList &argumentEntries, const Archive::Entry *destination, const QList &oldEntries, const QList &newEntries) { QStringList expectedPaths = getExpectedCopiedEntryPaths(oldEntries, argumentEntries, destination); QStringList actualPaths = ReadOnlyArchiveInterface::entryFullPaths(newEntries); foreach (const QString &path, expectedPaths) { QVERIFY2(actualPaths.contains(path), (QStringLiteral("No ") + path + QStringLiteral(" inside the archive")).toUtf8()); } foreach (const QString &path, actualPaths) { QVERIFY2(expectedPaths.contains(path), (QStringLiteral("Entry ") + path + QStringLiteral(" is not expected to be inside the archive")).toUtf8()); } } QStringList TestHelper::getExpectedNewEntryPaths(const QList &argumentEntries, const Archive::Entry *destination) { QStringList expectedPaths = QStringList(); const QString testDataPath = QFINDTESTDATA("data") + QLatin1Char('/'); foreach (const Archive::Entry *entry, argumentEntries) { const QString entryPath = entry->fullPath(); expectedPaths << destination->fullPath() + entryPath; if (entryPath.right(1) == QLatin1String("/")) { const QString workingDirectory = testDataPath + QLatin1Char('/') + entry->fullPath(true); QDirIterator it(workingDirectory, QDir::AllEntries | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (it.hasNext()) { QString path = it.next(); path = destination->fullPath() + path.right(path.count() - testDataPath.count() - 1); if (it.fileInfo().isDir()) { path += QLatin1Char('/'); } expectedPaths << path; } } } return expectedPaths; } QStringList TestHelper::getExpectedMovedEntryPaths(const QList &entryList, const QList &argumentEntries, const Archive::Entry *destination) { QStringList expectedPaths = QStringList(); QMap entryMap = getEntryMap(entryList); QStringList argumentPaths = ReadOnlyArchiveInterface::entryFullPaths(argumentEntries); QString lastMovedFolder; if (ReadOnlyArchiveInterface::entriesWithoutChildren(argumentEntries).count() > 1) { // Destination path doesn't contain a target entry name, so we have to remember to include it while moving // folder contents. int nameLength = 0; foreach (const Archive::Entry *entry, entryMap) { const QString entryPath = entry->fullPath(); if (lastMovedFolder.count() > 0 && entryPath.startsWith(lastMovedFolder)) { expectedPaths << destination->fullPath() + entryPath.right(entryPath.count() - lastMovedFolder.count() + nameLength); } else if (argumentPaths.contains(entryPath)) { QString expectedPath = destination->fullPath() + entry->name(); if (entryPath.right(1) == QLatin1String("/")) { expectedPath += QLatin1Char('/'); nameLength = entry->name().count() + 1; // plus slash lastMovedFolder = entryPath; } else { nameLength = 0; lastMovedFolder = QString(); } expectedPaths << expectedPath; } else { expectedPaths << entryPath; nameLength = 0; lastMovedFolder = QString(); } } } else { foreach (const Archive::Entry *entry, entryMap) { const QString entryPath = entry->fullPath(); if (lastMovedFolder.count() > 0 && entryPath.startsWith(lastMovedFolder)) { expectedPaths << destination->fullPath() + entryPath.right(entryPath.count() - lastMovedFolder.count()); } else if (argumentPaths.contains(entryPath)) { if (entryPath.right(1) == QLatin1String("/")) { lastMovedFolder = entryPath; } else if (lastMovedFolder.count() > 0) { lastMovedFolder = QString(); } expectedPaths << destination->fullPath(); } else { expectedPaths << entryPath; } } } return expectedPaths; } QStringList TestHelper::getExpectedCopiedEntryPaths(const QList &entryList, const QList &argumentEntries, const Archive::Entry *destination) { QStringList expectedPaths = QStringList(); QMap entryMap = getEntryMap(entryList); QStringList argumentPaths = ReadOnlyArchiveInterface::entryFullPaths(argumentEntries); QString lastCopiedFolder; // Destination path doesn't contain a target entry name, so we have to remember to include it while copying // folder contents. int nameLength = 0; foreach (const Archive::Entry *entry, entryMap) { const QString entryPath = entry->fullPath(); if (lastCopiedFolder.count() > 0 && entryPath.startsWith(lastCopiedFolder)) { expectedPaths << destination->fullPath() + entryPath.right(entryPath.count() - lastCopiedFolder.count() + nameLength); } else if (argumentPaths.contains(entryPath)) { QString expectedPath = destination->fullPath() + entry->name(); if (entryPath.right(1) == QLatin1String("/")) { expectedPath += QLatin1Char('/'); nameLength = entry->name().count() + 1; // plus slash lastCopiedFolder = entryPath; } else { nameLength = 0; lastCopiedFolder = QString(); } expectedPaths << expectedPath; } else { nameLength = 0; lastCopiedFolder = QString(); } expectedPaths << entryPath; } return expectedPaths; } QMap TestHelper::getEntryMap(const QList entries) { QMap map; foreach (Archive::Entry* entry, entries) { map.insert(entry->fullPath(), entry); } return map; } diff --git a/autotests/testhelper/testhelper.h b/autotests/testhelper/testhelper.h index d9a4cba0..e50501d9 100644 --- a/autotests/testhelper/testhelper.h +++ b/autotests/testhelper/testhelper.h @@ -1,67 +1,66 @@ /* - * Copyright (c) 2010-2011 Raphael Kubo da Costa - * Copyright (c) 2016 Elvis Angelaccio + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef TESTHELPER_H #define TESTHELPER_H #include "kerfuffle/jobs.h" #include "kerfuffle/archiveentry.h" #include #include #include using namespace Kerfuffle; class TestHelper { public: static void startAndWaitForResult(KJob *job); static QList getEntryList(Archive *archive); static void verifyAddedEntriesWithDestination(const QList &argumentEntries, const Archive::Entry *destination, const QList &oldEntries, const QList &newEntries); static void verifyMovedEntriesWithDestination(const QList &argumentEntries, const Archive::Entry *destination, const QList &oldEntries, const QList &newEntries); static void verifyCopiedEntriesWithDestination(const QList &argumentEntries, const Archive::Entry *destination, const QList &oldEntries, const QList &newEntries); private: TestHelper() {} static QStringList getExpectedNewEntryPaths(const QList &argumentEntries, const Archive::Entry *destination); static QStringList getExpectedMovedEntryPaths(const QList &entryList, const QList &argumentEntries, const Archive::Entry* destination); static QStringList getExpectedCopiedEntryPaths(const QList &entryList, const QList &argumentEntries, const Archive::Entry* destination); /** * Returns map of entries. * * It's useful when we need a sorted list of entries. */ static QMap getEntryMap(const QList entries); static QEventLoop m_eventLoop; }; #endif //TESTHELPER_H diff --git a/kerfuffle/archive_kerfuffle.cpp b/kerfuffle/archive_kerfuffle.cpp index 7eaff32e..8e6cd59e 100644 --- a/kerfuffle/archive_kerfuffle.cpp +++ b/kerfuffle/archive_kerfuffle.cpp @@ -1,494 +1,495 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008 Harald Hvaal * Copyright (c) 2009-2011 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "archive_kerfuffle.h" #include "archiveentry.h" #include "archiveinterface.h" #include "jobs.h" #include "mimetypes.h" #include "pluginmanager.h" #include #include #include namespace Kerfuffle { Archive *Archive::create(const QString &fileName, QObject *parent) { return create(fileName, QString(), parent); } Archive *Archive::create(const QString &fileName, const QString &fixedMimeType, QObject *parent) { qCDebug(ARK) << "Going to create archive" << fileName; PluginManager pluginManager; const QMimeType mimeType = fixedMimeType.isEmpty() ? determineMimeType(fileName) : QMimeDatabase().mimeTypeForName(fixedMimeType); const QVector offers = pluginManager.preferredPluginsFor(mimeType); if (offers.isEmpty()) { qCCritical(ARK) << "Could not find a plugin to handle" << fileName; return new Archive(NoPlugin, parent); } Archive *archive; foreach (Plugin *plugin, offers) { archive = create(fileName, plugin, parent); // Use the first valid plugin, according to the priority sorting. if (archive->isValid()) { return archive; } } qCCritical(ARK) << "Failed to find a usable plugin for" << fileName; return archive; } Archive *Archive::create(const QString &fileName, Plugin *plugin, QObject *parent) { Q_ASSERT(plugin); qCDebug(ARK) << "Checking plugin" << plugin->metaData().pluginId(); KPluginFactory *factory = KPluginLoader(plugin->metaData().fileName()).factory(); if (!factory) { qCWarning(ARK) << "Invalid plugin factory for" << plugin->metaData().pluginId(); return new Archive(FailedPlugin, parent); } const QVariantList args = {QVariant(QFileInfo(fileName).absoluteFilePath())}; ReadOnlyArchiveInterface *iface = factory->create(Q_NULLPTR, args); if (!iface) { qCWarning(ARK) << "Could not create plugin instance" << plugin->metaData().pluginId(); return new Archive(FailedPlugin, parent); } if (!plugin->isValid()) { qCDebug(ARK) << "Cannot use plugin" << plugin->metaData().pluginId() << "- check whether" << plugin->readOnlyExecutables() << "are installed."; return new Archive(FailedPlugin, parent); } qCDebug(ARK) << "Successfully loaded plugin" << plugin->metaData().pluginId(); return new Archive(iface, !plugin->isReadWrite(), parent); } Archive::Archive(ArchiveError errorCode, QObject *parent) : QObject(parent) , m_iface(Q_NULLPTR) , m_error(errorCode) { qCDebug(ARK) << "Created archive instance with error"; } Archive::Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent) : QObject(parent) , m_iface(archiveInterface) , m_hasBeenListed(false) , m_isReadOnly(isReadOnly) , m_isSingleFolderArchive(false) , m_extractedFilesSize(0) , m_error(NoError) , m_encryptionType(Unencrypted) , m_numberOfFiles(0) { qCDebug(ARK) << "Created archive instance"; Q_ASSERT(archiveInterface); archiveInterface->setParent(this); connect(m_iface, &ReadOnlyArchiveInterface::entry, this, &Archive::onNewEntry); } Archive::~Archive() { } QString Archive::completeBaseName() const { QString base = QFileInfo(fileName()).completeBaseName(); // Special case for compressed tar archives. if (base.right(4).toUpper() == QLatin1String(".TAR")) { base.chop(4); } return base; } QString Archive::fileName() const { return isValid() ? m_iface->filename() : QString(); } QString Archive::comment() const { return isValid() ? m_iface->comment() : QString(); } CommentJob* Archive::addComment(const QString &comment) { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to add comment:" << comment; Q_ASSERT(!isReadOnly()); CommentJob *job = new CommentJob(comment, static_cast(m_iface)); return job; } TestJob* Archive::testArchive() { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to test archive"; TestJob *job = new TestJob(m_iface); return job; } QMimeType Archive::mimeType() { if (!isValid()) { return QMimeType(); } if (!m_mimeType.isValid()) { m_mimeType = determineMimeType(fileName()); } return m_mimeType; } bool Archive::isReadOnly() const { return isValid() ? (m_iface->isReadOnly() || m_isReadOnly) : false; } bool Archive::isSingleFolderArchive() { if (!isValid()) { return false; } listIfNotListed(); return m_isSingleFolderArchive; } bool Archive::hasComment() const { return isValid() ? !comment().isEmpty() : false; } Archive::EncryptionType Archive::encryptionType() { if (!isValid()) { return Unencrypted; } listIfNotListed(); return m_encryptionType; } qulonglong Archive::numberOfFiles() { if (!isValid()) { return 0; } listIfNotListed(); return m_numberOfFiles; } qulonglong Archive::unpackedSize() { if (!isValid()) { return 0; } listIfNotListed(); return m_extractedFilesSize; } qulonglong Archive::packedSize() const { return isValid() ? QFileInfo(fileName()).size() : 0; } QString Archive::subfolderName() { if (!isValid()) { return QString(); } listIfNotListed(); return m_subfolderName; } void Archive::onNewEntry(const Archive::Entry *entry) { if (!entry->isDir()) { m_numberOfFiles++; } } bool Archive::isValid() const { return m_iface && (m_error == NoError); } ArchiveError Archive::error() const { return m_error; } KJob* Archive::open() { return 0; } KJob* Archive::create() { return 0; } ListJob* Archive::list() { if (!isValid() || !QFileInfo::exists(fileName())) { return Q_NULLPTR; } qCDebug(ARK) << "Going to list files"; ListJob *job = new ListJob(m_iface); //if this job has not been listed before, we grab the opportunity to //collect some information about the archive if (!m_hasBeenListed) { connect(job, &ListJob::result, this, &Archive::onListFinished); } return job; } DeleteJob* Archive::deleteFiles(QList &entries) { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to delete entries" << entries; if (m_iface->isReadOnly()) { return 0; } DeleteJob *newJob = new DeleteJob(entries, static_cast(m_iface)); return newJob; } AddJob* Archive::addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) { if (!isValid()) { return Q_NULLPTR; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral("PasswordProtectedHint")] = true; } qCDebug(ARK) << "Going to add files" << files << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); AddJob *newJob = new AddJob(files, destination, newOptions, static_cast(m_iface)); connect(newJob, &AddJob::result, this, &Archive::onAddFinished); return newJob; } MoveJob* Archive::moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions& options) { if (!isValid()) { return Q_NULLPTR; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral("PasswordProtectedHint")] = true; } qCDebug(ARK) << "Going to move files" << files << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); MoveJob *newJob = new MoveJob(files, destination, newOptions, static_cast(m_iface)); return newJob; } CopyJob* Archive::copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { if (!isValid()) { return Q_NULLPTR; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral("PasswordProtectedHint")] = true; } qCDebug(ARK) << "Going to copy files" << files << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); CopyJob *newJob = new CopyJob(files, destination, newOptions, static_cast(m_iface)); return newJob; } ExtractJob* Archive::extractFiles(const QList &files, const QString &destinationDir, const ExtractionOptions &options) { if (!isValid()) { return Q_NULLPTR; } ExtractionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral( "PasswordProtectedHint" )] = true; } ExtractJob *newJob = new ExtractJob(files, destinationDir, newOptions, m_iface); return newJob; } PreviewJob *Archive::preview(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } PreviewJob *job = new PreviewJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } OpenJob *Archive::open(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } OpenJob *job = new OpenJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } OpenWithJob *Archive::openWith(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } OpenWithJob *job = new OpenWithJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } void Archive::encrypt(const QString &password, bool encryptHeader) { if (!isValid()) { return; } m_iface->setPassword(password); m_iface->setHeaderEncryptionEnabled(encryptHeader); m_encryptionType = encryptHeader ? HeaderEncrypted : Encrypted; } void Archive::onAddFinished(KJob* job) { //if the archive was previously a single folder archive and an add job //has successfully finished, then it is no longer a single folder //archive (for the current implementation, which does not allow adding //folders/files other places than the root. //TODO: handle the case of creating a new file and singlefolderarchive //then. if (m_isSingleFolderArchive && !job->error()) { m_isSingleFolderArchive = false; } } void Archive::onListFinished(KJob* job) { ListJob *ljob = qobject_cast(job); m_extractedFilesSize = ljob->extractedFilesSize(); m_isSingleFolderArchive = ljob->isSingleFolderArchive(); m_subfolderName = ljob->subfolderName(); if (m_subfolderName.isEmpty()) { m_subfolderName = completeBaseName(); } if (ljob->isPasswordProtected()) { // If we already know the password, it means that the archive is header-encrypted. m_encryptionType = m_iface->password().isEmpty() ? Encrypted : HeaderEncrypted; } m_hasBeenListed = true; } void Archive::listIfNotListed() { if (!m_hasBeenListed) { ListJob *job = list(); if (!job) { return; } connect(job, &ListJob::userQuery, this, &Archive::onUserQuery); QEventLoop loop(this); connect(job, &KJob::result, &loop, &QEventLoop::quit); job->start(); loop.exec(); // krazy:exclude=crashy } } void Archive::onUserQuery(Query* query) { query->execute(); } void Archive::setCompressionOptions(const CompressionOptions &opts) { m_compOptions = opts; } CompressionOptions Archive::compressionOptions() const { return m_compOptions; } } // namespace Kerfuffle diff --git a/kerfuffle/archive_kerfuffle.h b/kerfuffle/archive_kerfuffle.h index ff683e76..2ad37828 100644 --- a/kerfuffle/archive_kerfuffle.h +++ b/kerfuffle/archive_kerfuffle.h @@ -1,216 +1,217 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008 Harald Hvaal * Copyright (c) 2011 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVE_H #define ARCHIVE_H #include "kerfuffle_export.h" #include #include #include #include #include class KJob; namespace Kerfuffle { class ListJob; class ExtractJob; class DeleteJob; class AddJob; class MoveJob; class CopyJob; class CommentJob; class TestJob; class OpenJob; class OpenWithJob; class Plugin; class PreviewJob; class Query; class ReadOnlyArchiveInterface; enum ArchiveError { NoError = 0, NoPlugin, FailedPlugin }; /** These are the extra options for doing the compression. Naming convention is CamelCase with either Global, or the compression type (such as Zip, Rar, etc), followed by the property name used */ typedef QHash CompressionOptions; typedef QHash ExtractionOptions; class KERFUFFLE_EXPORT Archive : public QObject { Q_OBJECT Q_ENUMS(EncryptionType) /** * Complete base name, without the "tar" extension (if any). */ Q_PROPERTY(QString completeBaseName READ completeBaseName CONSTANT) Q_PROPERTY(QString fileName READ fileName CONSTANT) Q_PROPERTY(QString comment READ comment CONSTANT) Q_PROPERTY(QMimeType mimeType READ mimeType CONSTANT) Q_PROPERTY(bool isReadOnly READ isReadOnly CONSTANT) Q_PROPERTY(bool isSingleFolderArchive READ isSingleFolderArchive) Q_PROPERTY(EncryptionType encryptionType READ encryptionType) Q_PROPERTY(qulonglong numberOfFiles READ numberOfFiles) Q_PROPERTY(qulonglong unpackedSize READ unpackedSize) Q_PROPERTY(qulonglong packedSize READ packedSize) Q_PROPERTY(QString subfolderName READ subfolderName) public: enum EncryptionType { Unencrypted, Encrypted, HeaderEncrypted }; class Entry; QString completeBaseName() const; QString fileName() const; QString comment() const; QMimeType mimeType(); bool isReadOnly() const; bool isSingleFolderArchive(); bool hasComment() const; EncryptionType encryptionType(); qulonglong numberOfFiles(); qulonglong unpackedSize(); qulonglong packedSize() const; QString subfolderName(); void setCompressionOptions(const CompressionOptions &opts); CompressionOptions compressionOptions() const; static Archive *create(const QString &fileName, QObject *parent = 0); static Archive *create(const QString &fileName, const QString &fixedMimeType, QObject *parent = 0); /** * Create an archive instance from a given @p plugin. * @param fileName The name of the archive. * @return A valid archive if the plugin could be loaded, an invalid one otherwise (with the FailedPlugin error set). */ static Archive *create(const QString &fileName, Plugin *plugin, QObject *parent = Q_NULLPTR); ~Archive(); ArchiveError error() const; bool isValid() const; KJob* open(); KJob* create(); /** * @return A ListJob if the archive already exists. A null pointer otherwise. */ ListJob* list(); DeleteJob* deleteFiles(QList &entries); CommentJob* addComment(const QString &comment); TestJob* testArchive(); /** * Compression options that should be handled by all interfaces: * * GlobalWorkDir - Change to this dir before adding the new files. * The path names should then be added relative to this directory. */ AddJob* addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options = CompressionOptions()); /** * Renames or moves entries within the archive. * * @param files All the renamed or moved files and their child entries (for renaming a directory too). * @param destination New entry name (for renaming) or destination folder (for moving). * If ReadOnlyArchiveInterface::entriesWithoutChildren(files).count() returns 1, then it's renaming, * so you must specify the resulted entry name, even if it's not going to be changed. * Otherwise (if count is more than 1) it's moving, so destination must conatin only targeted folder path * or be empty, if moving to the root. */ MoveJob* moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions& options = CompressionOptions()); /** * Copies entries within the archive. * * @param files All the renamed or moved files and their child entries (for renaming a directory too). * @param destination Destination path. It must conatin only targeted folder path or be empty, * if copying to the root. */ CopyJob* copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions& options = CompressionOptions()); ExtractJob* extractFiles(const QList &files, const QString &destinationDir, const ExtractionOptions &options = ExtractionOptions()); PreviewJob* preview(Archive::Entry *entry); OpenJob* open(Archive::Entry *entry); OpenWithJob* openWith(Archive::Entry *entry); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ void encrypt(const QString &password, bool encryptHeader); private slots: void onListFinished(KJob*); void onAddFinished(KJob*); void onUserQuery(Kerfuffle::Query*); void onNewEntry(const Archive::Entry *entry); private: Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent = 0); Archive(ArchiveError errorCode, QObject *parent = 0); void listIfNotListed(); ReadOnlyArchiveInterface *m_iface; bool m_hasBeenListed; bool m_isReadOnly; bool m_isSingleFolderArchive; QString m_subfolderName; qulonglong m_extractedFilesSize; ArchiveError m_error; EncryptionType m_encryptionType; qulonglong m_numberOfFiles; CompressionOptions m_compOptions; QMimeType m_mimeType; }; } // namespace Kerfuffle Q_DECLARE_METATYPE(Kerfuffle::Archive::EncryptionType) #endif // ARCHIVE_H diff --git a/kerfuffle/archiveentry.cpp b/kerfuffle/archiveentry.cpp index 7064f692..a36008f9 100644 --- a/kerfuffle/archiveentry.cpp +++ b/kerfuffle/archiveentry.cpp @@ -1,195 +1,216 @@ -// -// Created by mvlabat on 5/27/16. -// +/* + * Copyright (c) 2016 Vladyslav Batyrenko + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ #include "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(); } void Archive::Entry::copyMetaData(const Archive::Entry *sourceEntry) { setProperty("fullPath", sourceEntry->property("fullPath")); setProperty("permissions", sourceEntry->property("permissions")); setProperty("owner", sourceEntry->property("owner")); setProperty("group", sourceEntry->property("group")); setProperty("size", sourceEntry->property("size")); setProperty("compressedSize", sourceEntry->property("compressedSize")); setProperty("link", sourceEntry->property("link")); setProperty("ratio", sourceEntry->property("ratio")); setProperty("CRC", sourceEntry->property("CRC")); setProperty("method", sourceEntry->property("method")); setProperty("version", sourceEntry->property("version")); setProperty("timestamp", sourceEntry->property("timestamp").toDateTime()); setProperty("isDirectory", sourceEntry->property("isDirectory")); setProperty("comment", sourceEntry->property("comment")); setProperty("isPasswordProtected", sourceEntry->property("isPasswordProtected")); } 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(); } QString Archive::Entry::fullPath(bool withoutTrailingSlash) const { return (withoutTrailingSlash) ? m_fullPathWithoutTrailingSlash : m_fullPath; } QString Archive::Entry::name() const { return m_name; } void Archive::Entry::setIsDirectory(const bool isDirectory) { m_isDirectory = isDirectory; } bool Archive::Entry::isDir() const { return m_isDirectory; } int Archive::Entry::row() const { if (getParent()) { return getParent()->entries().indexOf(const_cast(this)); } return 0; } Archive::Entry *Archive::Entry::find(const QString &name) const { foreach (Entry *entry, m_entries) { if (entry && (entry->name() == name)) { return entry; } } return Q_NULLPTR; } Archive::Entry *Archive::Entry::findByPath(const QStringList &pieces, int index) const { if (index == pieces.count()) { return Q_NULLPTR; } Entry *next = find(pieces.at(index)); if (index == pieces.count() - 1) { return next; } if (next && next->isDir()) { return next->findByPath(pieces, index + 1); } return Q_NULLPTR; } 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 0a973888..e9779a15 100644 --- a/kerfuffle/archiveentry.h +++ b/kerfuffle/archiveentry.h @@ -1,111 +1,132 @@ -// -// Created by mvlabat on 5/27/16. -// +/* + * Copyright (c) 2016 Vladyslav Batyrenko + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions + * are met: + * + * 1. Redistributions of source code must retain the above copyright + * notice, this list of conditions and the following disclaimer. + * 2. Redistributions in binary form must reproduce the above copyright + * notice, this list of conditions and the following disclaimer in the + * documentation and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR + * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES + * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. + * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, + * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT + * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, + * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY + * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT + * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF + * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ #ifndef 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(); void copyMetaData(const Archive::Entry *sourceEntry); 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); bool isDir() const; int row() const; Entry *find(const QString &name) const; Entry *findByPath(const QStringList & pieces, int index = 0) const; 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 d0bfd754..c3df9ab5 100644 --- a/kerfuffle/archiveinterface.cpp +++ b/kerfuffle/archiveinterface.cpp @@ -1,242 +1,243 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "archiveinterface.h" #include "ark_debug.h" #include #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; } int ReadOnlyArchiveInterface::copyRequiredSignals() const { return 1; } void ReadOnlyArchiveInterface::setWaitForFinishedSignal(bool value) { m_waitForFinishedSignal = value; } QStringList ReadOnlyArchiveInterface::entryFullPaths(const QList &entries, const bool withoutTrailingSlashes) { QStringList filesList; foreach (const Archive::Entry *file, entries) { filesList << file->fullPath(withoutTrailingSlashes); } return filesList; } QList ReadOnlyArchiveInterface::entriesWithoutChildren(const QList &entries) { // QMap is easy way to get entries sorted by their fullPath. QMap sortedEntries; foreach (Archive::Entry *entry, entries) { sortedEntries.insert(entry->fullPath(), entry); } QList filteredEntries; QString lastFolder; foreach (Archive::Entry *entry, sortedEntries) { if (lastFolder.count() > 0 && entry->fullPath().startsWith(lastFolder)) continue; lastFolder = (entry->fullPath().right(1) == QLatin1String("/")) ? entry->fullPath() : QString(); filteredEntries << entry; } return filteredEntries; } QStringList ReadOnlyArchiveInterface::entryPathsFromDestination(QStringList entries, const Archive::Entry *destination, int entriesWithoutChildren) { QStringList paths = QStringList(); entries.sort(); QString lastFolder; const QString destinationPath = (destination == Q_NULLPTR) ? QString() : destination->fullPath(); QString newPath; int nameLength = 0; foreach (const QString &entryPath, entries) { if (lastFolder.count() > 0 && entryPath.startsWith(lastFolder)) { // Replace last moved or copied folder path with destination path. int charsCount = entryPath.count() - lastFolder.count(); if (entriesWithoutChildren != 1) { charsCount += nameLength; } newPath = destinationPath + entryPath.right(charsCount); } else { const QString name = entryPath.split(QLatin1Char('/'), QString::SkipEmptyParts).last(); if (entriesWithoutChildren != 1) { newPath = destinationPath + name; if (entryPath.right(1) == QLatin1String("/")) { newPath += QLatin1Char('/'); } } else { // If the mode is set to Move and there is only one passed file in the list, // we have to use destination as newPath. newPath = destinationPath; } if (entryPath.right(1) == QLatin1String("/")) { nameLength = name.count() + 1; // plus slash lastFolder = entryPath; } else { nameLength = 0; lastFolder = QString(); } } paths << newPath; } return paths; } bool ReadOnlyArchiveInterface::isHeaderEncryptionEnabled() const { return m_isHeaderEncryptionEnabled; } 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/archiveinterface.h b/kerfuffle/archiveinterface.h index 5906aaf6..2cb0fcc0 100644 --- a/kerfuffle/archiveinterface.h +++ b/kerfuffle/archiveinterface.h @@ -1,209 +1,210 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVEINTERFACE_H #define ARCHIVEINTERFACE_H #include "archive_kerfuffle.h" #include "archive_entry.h" #include "kerfuffle_export.h" #include "kerfuffle/archiveentry.h" #include #include #include #include namespace Kerfuffle { class Query; class KERFUFFLE_EXPORT ReadOnlyArchiveInterface: public QObject { Q_OBJECT public: explicit ReadOnlyArchiveInterface(QObject *parent, const QVariantList & args); virtual ~ReadOnlyArchiveInterface(); /** * Returns the filename of the archive currently being handled. */ QString filename() const; /** * Returns the comment of the archive. */ QString comment() const; /** * @return The password of the archive, if any. */ QString password() const; /** * Returns whether the file can only be read. * * @return @c true The file cannot be written. * @return @c false The file can be read and written. */ virtual bool isReadOnly() const; virtual bool open(); /** * List archive contents. * This runs the process of reading archive contents. * When subclassing, you can block as long as you need (unless you called setWaitForFinishedSignal(true)). * @returns whether the listing succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ virtual bool list() = 0; virtual bool testArchive() = 0; void setPassword(const QString &password); void setHeaderEncryptionEnabled(bool enabled); /** * Extract files from archive. * Globally recognized extraction options: * @li PreservePaths - preserve file paths (extract flat if false) * @li RootNode - node in the archive which will correspond to the @arg destinationDirectory * When subclassing, you can block as long as you need (unless you called setWaitForFinishedSignal(true)). * @returns whether the listing succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ virtual bool extractFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) = 0; bool waitForFinishedSignal(); /** * Returns count of required finish signals for a job to be finished. * * These two methods are used by move and copy jobs, which in some plugins implementations have to call * several processes sequentually. For instance, moving entries in zip archive is only possible if * extracting the entries, deleting them, recreating destination folder structure and adding them back again. */ virtual int moveRequiredSignals() const; virtual int copyRequiredSignals() const; /** * Returns the list of filenames retrieved from the list of entries. */ static QStringList entryFullPaths(const QList &entries, const bool withoutTrailingSlashes = false); /** * Returns the list of the entries, excluding their children. * * This method relies on entries paths so doesn't require parents to be set. */ static QList entriesWithoutChildren(const QList &entries); /** * Returns the string list of entry paths, which will be a result of adding/moving/copying entries. * * @param entries The entries which will be added/moved/copied. * @param destination Destination path within the archive to which entries have to be added. For renaming an entry * the path has to contain a new filename too. * @param entriesWithoutChildren Entries count, excluding their children. For AddJob or CopyJob 0 MUST be passed. * * @return For entries * some/dir/ * some/dir/entry * some/dir/some/entry * some/another/entry * and destination * some/destination * will return * some/destination/dir/ * some/destination/dir/entry * some/destination/dir/some/enty * some/destination/entry */ static QStringList entryPathsFromDestination(QStringList entries, const Archive::Entry *destination, int entriesWithoutChildren); virtual bool doKill(); virtual bool doSuspend(); virtual bool doResume(); bool isHeaderEncryptionEnabled() const; signals: void cancelled(); void error(const QString &message, const QString &details = QString()); void entry(Archive::Entry *archiveEntry); void entryRemoved(const QString &path); void progress(double progress); void info(const QString &info); void finished(bool result); void userQuery(Query *query); void testSuccess(); protected: /** * Setting this option to true will not run the functions in their own thread. * Instead it will be necessary to call finished(bool) when the operation is actually finished. */ void setWaitForFinishedSignal(bool value); void setCorrupt(bool isCorrupt); bool isCorrupt() const; QString m_comment; private: QString m_filename; QString m_password; bool m_waitForFinishedSignal; bool m_isHeaderEncryptionEnabled; bool m_isCorrupt; }; class KERFUFFLE_EXPORT ReadWriteArchiveInterface: public ReadOnlyArchiveInterface { Q_OBJECT public: enum OperationMode { List, Extract, Add, Move, Copy, Delete, Comment, Test }; explicit ReadWriteArchiveInterface(QObject *parent, const QVariantList & args); virtual ~ReadWriteArchiveInterface(); bool isReadOnly() const Q_DECL_OVERRIDE; //see archive.h for a list of what the compressionoptions might //contain virtual bool addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) = 0; virtual bool moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions& options) = 0; virtual bool copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions& options) = 0; virtual bool deleteFiles(const QList &files) = 0; virtual bool addComment(const QString &comment) = 0; }; } // namespace Kerfuffle #endif // ARCHIVEINTERFACE_H diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp index 547e4eff..43fc1559 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,1495 +1,1496 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "cliinterface.h" #include "ark_debug.h" #include "queries.h" #ifdef Q_OS_WIN # include #else # include # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include 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; } int CliInterface::copyRequiredSignals() const { return 2; } 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::extractFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) { qCDebug(ARK) << Q_FUNC_INFO << "to" << destinationDirectory; cacheParameterList(); m_operationMode = Extract; m_compressionOptions = options; m_extractedFiles = 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 = substituteExtractVariables(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->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->fullPath(true); const QString newFilePath = absoluteDestinationPath + file->fullPath(true); if (QFile::link(filePath, newFilePath)) { qCDebug(ARK) << "Symlink's created:" << filePath << newFilePath; } else { qCDebug(ARK) << "Can't create symlink" << filePath << newFilePath; delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; return false; } } qCDebug(ARK) << "Changing working dir again to " << m_extractTempDir->path(); QDir::setCurrent(m_extractTempDir->path()); filesToPass.push_back(new Archive::Entry(preservedParent, destinationPath.split(QLatin1Char('/'), QString::SkipEmptyParts).at(0))); } else { filesToPass = files; } if (addArgs.contains(QStringLiteral("$PasswordSwitch")) && options.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; QList withoutChildren = entriesWithoutChildren(files); setNewMovedFiles(files, destination, withoutChildren.count()); const auto moveArgs = m_param.value(MoveArgs).toStringList(); const auto args = substituteMoveVariables(moveArgs, withoutChildren, destination, password()); return runProcess(m_param.value(MoveProgram).toStringList(), args); } bool CliInterface::copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { m_oldWorkingDir = QDir::currentPath(); m_tempExtractDir = new QTemporaryDir(); m_tempAddDir = new QTemporaryDir(); QDir::setCurrent(m_tempExtractDir->path()); m_passedFiles = files; m_passedDestination = destination; m_passedOptions = options; m_passedOptions[QStringLiteral("PreservePaths")] = true; m_subOperation = Extract; connect(this, &CliInterface::finished, this, &CliInterface::continueCopying); return extractFiles(files, QDir::currentPath(), m_passedOptions); } 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 == Extract) { // Extraction jobs need a dedicated post-processing function. connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::extractProcessFinished, 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); } foreach (Archive::Entry *e, m_newMovedFiles) { emit entry(e); } m_newMovedFiles.clear(); } 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::extractProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_ASSERT(m_operationMode == Extract); m_exitCode = exitCode; qCDebug(ARK) << "Extraction process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { // Handle all the remaining data in the process. readStdout(true); delete m_process; m_process = Q_NULLPTR; } 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()); } cleanUpExtracting(); 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_extractedFiles.size())); cleanUpExtracting(); emit finished(false); return; } cleanUpExtracting(); } } if (m_compressionOptions.value(QStringLiteral("DragAndDrop")).toBool()) { if (!moveDroppedFilesToDest(m_extractedFiles, m_extractDestDir)) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_extractedFiles.size())); cleanUpExtracting(); emit finished(false); return; } cleanUpExtracting(); } emit progress(1.0); emit finished(true); } void CliInterface::continueCopying(bool result) { if (!result) { finishCopying(false); return; } switch (m_subOperation) { case Extract: m_subOperation = Add; m_passedFiles = entriesWithoutChildren(m_passedFiles); if (!setAddedFiles() || !addFiles(m_tempAddedFiles, m_passedDestination, m_passedOptions)) { finishCopying(false); } break; case Add: finishCopying(true); break; default: Q_ASSERT(false); } } bool CliInterface::moveDroppedFilesToDest(const 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->fullPath().remove(file->rootNode)); QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + file->fullPath()); QFileInfo absDestEntry(finalDestDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absSourceEntry.isDir()) { // For directories, just create the path. if (!finalDestDir.mkpath(relEntry.filePath())) { qCWarning(ARK) << "Failed to create directory" << relEntry.filePath() << "in final destination."; } } else { // If destination file exists, prompt the user. if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; if (!skipAll && !overwriteAll) { Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); emit userQuery(&query); query.waitForResponse(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } } // Create any parent directories. if (!finalDestDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } // Move files to the final destination. if (!QFile(absSourceEntry.absoluteFilePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << absSourceEntry.filePath() << "to final destination."; return false; } } } return true; } bool CliInterface::isEmptyDir(const QDir &dir) { QDir d = dir; d.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); return d.count() == 0; } void CliInterface::cleanUpExtracting() { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } if (m_extractTempDir) { delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; } } void CliInterface::finishCopying(bool result) { disconnect(this, &CliInterface::finished, this, &CliInterface::continueCopying); emit progress(1.0); emit finished(result); cleanUp(); } bool CliInterface::moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths) { qCDebug(ARK) << "Moving extracted files from temp dir" << tempDir.path() << "to final destination" << destDir.path(); bool overwriteAll = false; bool skipAll = false; QDirIterator dirIt(tempDir.path(), QDir::AllEntries | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (dirIt.hasNext()) { dirIt.next(); // We skip directories if: // 1. We are not preserving paths // 2. The dir is not empty. Only empty directories need to be explicitly moved. // The non-empty ones are created by QDir::mkpath() below. if (dirIt.fileInfo().isDir()) { if (!preservePaths || !isEmptyDir(QDir(dirIt.filePath()))) { continue; } } QFileInfo relEntry; if (preservePaths) { relEntry = QFileInfo(dirIt.filePath().remove(tempDir.path() + QLatin1Char('/'))); } else { relEntry = QFileInfo(dirIt.fileName()); } QFileInfo absDestEntry(destDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); emit userQuery(&query); query.waitForResponse(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } if (preservePaths) { // Create any parent directories. if (!destDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } } // Move file to the final destination. if (!QFile(dirIt.filePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << dirIt.filePath() << "to final destination."; return false; } } return true; } QStringList CliInterface::substituteListVariables(const QStringList &listArgs, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, listArgs) { if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } // Simple argument (e.g. -slt in 7z), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteExtractVariables(const QStringList &extractArgs, const 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 << extractFilesList(entries); continue; } // Simple argument (e.g. -kb in unrar), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteAddVariables(const QStringList &addArgs, const 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, true); 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 &entriesWithoutChildren, const Archive::Entry *destination, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, moveArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } if (arg == QLatin1String("$PathPairs")) { args << entryPathDestinationPairs(entriesWithoutChildren, destination); continue; } // Simple argument (e.g. a in 7z), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteDeleteVariables(const QStringList &deleteArgs, const 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->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; } void CliInterface::setNewMovedFiles(const QList &entries, const Archive::Entry *destination, int entriesWithoutChildren) { m_newMovedFiles.clear(); QMap entryMap; foreach (const Archive::Entry* entry, entries) { entryMap.insert(entry->fullPath(), entry); } QString lastFolder; QString newPath; int nameLength = 0; foreach (const Archive::Entry* entry, entryMap) { if (lastFolder.count() > 0 && entry->fullPath().startsWith(lastFolder)) { // Replace last moved or copied folder path with destination path. int charsCount = entry->fullPath().count() - lastFolder.count(); if (entriesWithoutChildren > 1) { charsCount += nameLength; } newPath = destination->fullPath() + entry->fullPath().right(charsCount); } else { if (entriesWithoutChildren > 1) { newPath = destination->fullPath() + entry->name(); } else { // If there is only one passed file in the list, // we have to use destination as newPath. newPath = destination->fullPath(true); } if (entry->isDir()) { newPath += QLatin1Char('/'); nameLength = entry->name().count() + 1; // plus slash lastFolder = entry->fullPath(); } else { nameLength = 0; lastFolder = QString(); } } Archive::Entry *newEntry = new Archive::Entry(Q_NULLPTR); newEntry->copyMetaData(entry); newEntry->setFullPath(newPath); m_newMovedFiles << newEntry; } } QString CliInterface::preservePathSwitch(bool preservePaths) const { Q_ASSERT(m_param.contains(PreservePathSwitch)); const QStringList theSwitch = m_param.value(PreservePathSwitch).toStringList(); Q_ASSERT(theSwitch.size() == 2); return (preservePaths ? theSwitch.at(0) : theSwitch.at(1)); } QStringList CliInterface::passwordHeaderSwitch(const QString& password) const { if (password.isEmpty()) { return QStringList(); } Q_ASSERT(m_param.contains(PasswordHeaderSwitch)); QStringList passwordHeaderSwitch = m_param.value(PasswordHeaderSwitch).toStringList(); Q_ASSERT(!passwordHeaderSwitch.isEmpty() && passwordHeaderSwitch.size() <= 2); if (passwordHeaderSwitch.size() == 1) { passwordHeaderSwitch[0].replace(QLatin1String("$Password"), password); } else { passwordHeaderSwitch[1] = password; } return passwordHeaderSwitch; } QStringList CliInterface::passwordSwitch(const QString& password) const { if (password.isEmpty()) { return QStringList(); } Q_ASSERT(m_param.contains(PasswordSwitch)); QStringList passwordSwitch = m_param.value(PasswordSwitch).toStringList(); Q_ASSERT(!passwordSwitch.isEmpty() && passwordSwitch.size() <= 2); if (passwordSwitch.size() == 1) { passwordSwitch[0].replace(QLatin1String("$Password"), password); } else { passwordSwitch[1] = password; } return passwordSwitch; } QString CliInterface::compressionLevelSwitch(int level) const { if (level < 0 || level > 9) { return QString(); } Q_ASSERT(m_param.contains(CompressionLevelSwitch)); QString compLevelSwitch = m_param.value(CompressionLevelSwitch).toString(); Q_ASSERT(!compLevelSwitch.isEmpty()); compLevelSwitch.replace(QLatin1String("$CompressionLevel"), QString::number(level)); return compLevelSwitch; } QStringList CliInterface::extractFilesList(const QList &entries) const { QStringList filesList; foreach (const Archive::Entry *e, entries) { 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::cleanUp() { qDeleteAll(m_tempAddedFiles); m_tempAddedFiles.clear(); QDir::setCurrent(m_oldWorkingDir); delete m_tempExtractDir; m_tempExtractDir = Q_NULLPTR; delete m_tempAddDir; m_tempAddDir = Q_NULLPTR; } void CliInterface::readStdout(bool handleAll) { //when hacking this function, please remember the following: //- standard output comes in unpredictable chunks, this is why //you can never know if the last part of the output is a complete line or not //- console applications are not really consistent about what //characters they send out (newline, backspace, carriage return, //etc), so keep in mind that this function is supposed to handle //all those special cases and be the lowest common denominator if (m_abortingOperation) return; Q_ASSERT(m_process); if (!m_process->bytesAvailable()) { //if process has no more data, we can just bail out return; } QByteArray dd = m_process->readAllStandardOutput(); m_stdOutData += dd; QList lines = m_stdOutData.split('\n'); //The reason for this check is that archivers often do not end //queries (such as file exists, wrong password) on a new line, but //freeze waiting for input. So we check for errors on the last line in //all cases. // TODO: QLatin1String() might not be the best choice here. // The call to handleLine() at the end of the method uses // QString::fromLocal8Bit(), for example. // TODO: The same check methods are called in handleLine(), this // is suboptimal. bool wrongPasswordMessage = checkForErrorMessage(QLatin1String( lines.last() ), WrongPasswordPatterns); bool 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)); } } } bool CliInterface::setAddedFiles() { QDir::setCurrent(m_tempAddDir->path()); foreach (const Archive::Entry *file, m_passedFiles) { const QString oldPath = m_tempExtractDir->path() + QLatin1Char('/') + file->fullPath(true); const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + file->name(); if (!QFile::rename(oldPath, newPath)) { return false; } m_tempAddedFiles << new Archive::Entry(Q_NULLPTR, file->name()); } return true; } 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 == Extract || 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 == Extract) { 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 &entriesWithoutChildren, const Archive::Entry *destination) { QStringList pairList; if (entriesWithoutChildren.count() > 1) { foreach (const Archive::Entry *file, entriesWithoutChildren) { pairList << file->fullPath(true) << destination->fullPath() + file->name(); } } else { pairList << entriesWithoutChildren.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/cliinterface.h b/kerfuffle/cliinterface.h index 2bcf89d5..072a2781 100644 --- a/kerfuffle/cliinterface.h +++ b/kerfuffle/cliinterface.h @@ -1,504 +1,505 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CLIINTERFACE_H #define CLIINTERFACE_H #include "archiveinterface.h" #include "archiveentry.h" #include "kerfuffle_export.h" #include "part/archivemodel.h" #include #include class KProcess; class KPtyProcess; class QDir; class QTemporaryDir; class QTemporaryFile; namespace Kerfuffle { enum CliInterfaceParameters { ///////////////[ COMMON ]///////////// /** * Bool (default false) * Will look for the %-sign in the stdout while working, in the form of * (2%, 14%, 35%, etc etc), and report progress based upon this */ CaptureProgress = 0, /** * QString * Default: empty * A regexp pattern that matches the program's password prompt. */ PasswordPromptPattern, ///////////////[ LIST ]///////////// /** * QStringList * The names to the program that will handle listing of this * archive (eg "rar"). Will be searched for in PATH */ ListProgram, /** * QStringList * The arguments that are passed to the program above for * listing the archive. Special strings that will be * substituted: * $Archive - the path of the archive */ ListArgs, /** * QStringList (default empty) * List of regexp patterns that indicate a corrupt archive. */ CorruptArchivePatterns, ///////////////[ EXTRACT ]///////////// /** * QStringList * The names to the program that will handle extracting of this * archive (eg "rar"). Will be searched for in PATH */ ExtractProgram, /** * QStringList * The arguments that are passed to the program above for * extracting the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be extracted, if any * $PreservePathSwitch - the flag for extracting with full paths * $PasswordSwitch - the switch setting the password. Note that this * will not be inserted unless the listing function has emitted an * entry with the IsPasswordProtected property set to true. */ ExtractArgs, /** * Bool (default false) * When passing directories to the extract program, do not * include trailing slashes * e.g. if the user selected "foo/" and "foo/bar" in the gui, the * paths "foo" and "foo/bar" will be sent to the program. */ NoTrailingSlashes, /** * QStringList * This should be a qstringlist with either two elements. The first * string is what PreservePathSwitch in the ExtractArgs will be replaced * with if PreservePath is True/enabled. The second is for the disabled * case. An empty string means that the argument will not be used in * that case. * Example: for rar, "x" means extract with full paths, and "e" means * extract without full paths. in this case we will use the stringlist * ("x", "e"). Or, for another format that might use the switch * "--extractFull" for preservePaths, and nothing otherwise: we use the * stringlist ("--extractFull", "") */ PreservePathSwitch, /** * QStringList (default empty) * The format of the root node switch. The variable $Password will be * substituted for the password string. NOTE: supplying passwords * through a virtual terminal is not supported (yet?), because this * is not cross platform compatible. As of KDE 4.3 there are no plans to * change this. * Example: ("-p$Password) * or ("--password", "$Password") */ PasswordSwitch, /** * QString * The format of the compression level switch. The variable $CompressionLevel * will be substituted for the level. * Example: ("-mx=$CompressionLevel) */ CompressionLevelSwitch, /** * QStringList * This is a stringlist with regexps, defining how to recognize the last * line in a "File already exists" prompt when extracting. */ FileExistsExpression, /** * QStringList * This is a stringlist with regexps defining how to recognize the line * containing the filename in a "File already exists" prompt when * extracting. It should have one captured string, which is the filename * of the file/folder that already exists. */ FileExistsFileName, /** * int * This sets on what output channel the FileExistsExpression regex * should be applied on, in other words, on what stream the "file * exists" output will appear in. Values accepted: * 0 - Standard error, stderr (default) * 1 - Standard output, stdout */ FileExistsMode, /** * QStringList * The various responses that can be supplied as a response to the * "file exists" prompt. The various items are to be supplied in the * following order: * index 0 - Yes (overwrite) * index 1 - No (skip/do not overwrite) * index 2 - All (overwrite all) * index 3 - Do not overwrite any files (autoskip) * index 4 - Cancel operation */ FileExistsInput, /** * QStringList * Regexp patterns capturing disk is full error messages. */ DiskFullPatterns, ///////////////[ DELETE ]///////////// /** * QStringList * The names to the program that will handle deleting of elements in this * archive format (eg "rar"). Will be searched for in PATH */ DeleteProgram, /** * QStringList * The arguments that are passed to the program above for * deleting from the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be deleted */ DeleteArgs, /** * QStringList * Default: empty * A list of regexp patterns that will cause the extraction to exit * with a general fail message */ ExtractionFailedPatterns, /** * QStringList * Default: empty * A list of regexp patterns that will alert the user that the password * was wrong. */ WrongPasswordPatterns, ///////////////[ ADD ]///////////// /** * QStringList * The names to the program that will handle adding in this * archive format (eg "rar"). Will be searched for in PATH */ AddProgram, /** * QStringList * The arguments that are passed to the program above for * adding to the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be added */ AddArgs, ///////////////[ MOVE ]///////////// /** * QStringList * The names to the program that will handle adding in this * archive format (eg "rar"). Will be searched for in PATH */ MoveProgram, /** * QStringList * The arguments that are passed to the program above for * moving inside the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be moved * $Destinations - new path of each file selected to be moved */ MoveArgs, ///////////////[ ENCRYPT ]///////////// /** * QStringList (default empty) * The variable $Password will be * substituted for the password string used to encrypt the header. * Example (rar plugin): ("-hp$Password") */ PasswordHeaderSwitch, ///////////////[ COMMENT ]///////////// /** * QStringList * The arguments that are passed to AddProgram when adding * a comment. */ CommentArgs, /** * QString * The variable $CommentFile will be substituted for the file * containing the comment. * Example (rar plugin): -z$CommentFile */ CommentSwitch, TestProgram, TestArgs, TestPassedPattern }; typedef QHash ParameterList; class KERFUFFLE_EXPORT CliInterface : public ReadWriteArchiveInterface { Q_OBJECT public: OperationMode m_operationMode; explicit CliInterface(QObject *parent, const QVariantList & args); virtual ~CliInterface(); virtual int copyRequiredSignals() const; virtual bool list() Q_DECL_OVERRIDE; virtual bool extractFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) Q_DECL_OVERRIDE; virtual bool addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool deleteFiles(const QList &files) Q_DECL_OVERRIDE; virtual bool addComment(const QString &comment) Q_DECL_OVERRIDE; virtual bool testArchive() Q_DECL_OVERRIDE; virtual void resetParsing() = 0; virtual ParameterList parameterList() const = 0; virtual bool readListLine(const QString &line) = 0; bool doKill() Q_DECL_OVERRIDE; bool doSuspend() Q_DECL_OVERRIDE; bool doResume() Q_DECL_OVERRIDE; /** * Sets if the listing should include empty lines. * * The default value is false. */ void setListEmptyLines(bool emptyLines); /** * Move all files from @p tmpDir to @p destDir, preserving paths if @p preservePaths is true. * @return Whether the operation has been successful. */ bool moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths); QStringList substituteListVariables(const QStringList &listArgs, const QString &password); QStringList substituteExtractVariables(const QStringList &extractArgs, const QList &entries, bool preservePaths, const QString &password); QStringList substituteAddVariables(const QStringList &addArgs, const QList &entries, const QString &password, bool encryptHeader, int compLevel); QStringList substituteMoveVariables(const QStringList &moveArgs, const QList &entriesWithoutChildren, const Archive::Entry *destination, const QString &password); QStringList substituteDeleteVariables(const QStringList &deleteArgs, const QList &entries, const QString &password); QStringList substituteCommentVariables(const QStringList &commentArgs, const QString &commentFile); QStringList substituteTestVariables(const QStringList &testArgs); /** * @see ArchiveModel::entryPathsFromDestination */ void setNewMovedFiles(const QList &entries, const Archive::Entry *destination, int entriesWithoutChildren); /** * @return The preserve path switch, according to the @p preservePaths extraction option. */ QString preservePathSwitch(bool preservePaths) const; /** * @return The password header-switch with the given @p password. */ virtual QStringList passwordHeaderSwitch(const QString& password) const; /** * @return The password switch with the given @p password. */ QStringList passwordSwitch(const QString& password) const; /** * @return The compression level switch with the given @p level. */ QString compressionLevelSwitch(int level) const; /** * @return The list of selected files to extract. */ QStringList extractFilesList(const QList &files) const; protected: bool setAddedFiles(); virtual void handleLine(const QString& line); virtual void cacheParameterList(); /** * Run @p programName with the given @p arguments. * * @param programName The program that will be run (not the whole path). * @param arguments A list of arguments that will be passed to the program. * * @return @c true if the program was found and the process was started correctly, * @c false otherwise (in which case finished(false) is emitted). */ bool runProcess(const QStringList& programNames, const QStringList& arguments); /** * Kill the running process. The finished signal is emitted according to @p emitFinished. */ void killProcess(bool emitFinished = true); /** * Ask the password *before* running any process. * @return True if the user supplies a password, false otherwise (in which case finished() is emitted). */ bool passwordQuery(); void cleanUp(); QString m_oldWorkingDir; ParameterList m_param; int m_exitCode; QTemporaryDir *m_tempExtractDir; QTemporaryDir *m_tempAddDir; OperationMode m_subOperation; QList m_passedFiles; QList m_tempAddedFiles; Archive::Entry *m_passedDestination; CompressionOptions m_passedOptions; protected slots: virtual void readStdout(bool handleAll = false); private: /** * Checks whether a line of the program's output is a password prompt. * * It uses the regular expression in the @c PasswordPromptPattern parameter * for the check. * * @param line A line of the program's output. * * @return @c true if the given @p line is a password prompt, @c false * otherwise. */ bool checkForPasswordPromptMessage(const QString& line); bool handleFileExistsMessage(const QString& filename); bool checkForErrorMessage(const QString& line, int parameterIndex); bool checkForTestSuccessMessage(const QString& line); /** * Performs any additional escaping and processing on @p fileName * before passing it to the underlying process. * * The default implementation returns @p fileName unchanged. * * @param fileName String to escape. */ virtual QString escapeFileName(const QString &fileName) const; /** * Returns a list of path pairs which will be supplied to rn command. * [ ... ] * Also constructs a list of new entries resulted in moving. * * @param entriesWithoutChildren List of archive entries * @param destination Must be a directory entry if QList contains more that one entry */ QStringList entryPathDestinationPairs(const QList &entriesWithoutChildren, const Archive::Entry *destination); /** * Wrapper around KProcess::write() or KPtyDevice::write(), depending on * the platform. */ void writeToProcess(const QByteArray& data); bool moveDroppedFilesToDest(const QList &files, const QString &finalDest); /** * @return Whether @p dir is an empty directory. */ bool isEmptyDir(const QDir &dir); void cleanUpExtracting(); void finishCopying(bool result); QByteArray m_stdOutData; QRegularExpression m_passwordPromptPattern; QHash > m_patternCache; #ifdef Q_OS_WIN KProcess *m_process; #else KPtyProcess *m_process; #endif QList m_removedFiles; QList m_newMovedFiles; bool m_listEmptyLines; bool m_abortingOperation; QString m_storedFileName; CompressionOptions m_compressionOptions; QString m_extractDestDir; QTemporaryDir *m_extractTempDir; QTemporaryFile *m_commentTempFile; QList m_extractedFiles; protected slots: virtual void processFinished(int exitCode, QProcess::ExitStatus exitStatus); private slots: void extractProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); void continueCopying(bool result); }; } #endif /* CLIINTERFACE_H */ diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp index f473cd15..bb815b12 100644 --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -1,637 +1,638 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "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->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()->extractFiles(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->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()->extractFiles({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->fullPath(); QString relativePath = workDir.relativeFilePath(fullPath); if (fullPath.endsWith(QLatin1Char('/'))) { relativePath += QLatin1Char('/'); } 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); } } CopyJob::CopyJob(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) << "CopyJob started"; } void CopyJob::doWork() { qCDebug(ARK) << "CopyJob: going to copy" << m_entries.count() << "file(s)"; emit description(this, i18np("Copying a file", "Copying %1 files", m_entries.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->copyFiles(m_entries, m_destination, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void CopyJob::onFinished(bool result) { m_finishedSignalsCount++; if (m_finishedSignalsCount == archiveInterface()->copyRequiredSignals()) { 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/kerfuffle/jobs.h b/kerfuffle/jobs.h index f2368e6f..50f3c281 100644 --- a/kerfuffle/jobs.h +++ b/kerfuffle/jobs.h @@ -1,339 +1,340 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef JOBS_H #define JOBS_H #include "kerfuffle_export.h" #include "archiveinterface.h" #include "archive_kerfuffle.h" #include "archiveentry.h" #include "queries.h" #include #include #include namespace Kerfuffle { class KERFUFFLE_EXPORT Job : public KJob { Q_OBJECT public: void start(); bool isRunning() const; protected: Job(ReadOnlyArchiveInterface *interface); virtual ~Job(); virtual bool doKill(); virtual void emitResult(); ReadOnlyArchiveInterface *archiveInterface(); QList m_archiveEntries; void connectToArchiveInterfaceSignals(); public slots: virtual void doWork() = 0; protected slots: virtual void onCancelled(); virtual void onError(const QString &message, const QString &details); virtual void onInfo(const QString &info); virtual void onEntry(Archive::Entry *entry); virtual void onProgress(double progress); virtual void onEntryRemoved(const QString &path); virtual void onFinished(bool result); virtual void onUserQuery(Query *query); signals: void entryRemoved(const QString & entry); void error(const QString& errorMessage, const QString& details); void newEntry(Archive::Entry*); void userQuery(Kerfuffle::Query*); private: ReadOnlyArchiveInterface *m_archiveInterface; bool m_isRunning; QElapsedTimer jobTimer; class Private; Private * const d; }; class KERFUFFLE_EXPORT ListJob : public Job { Q_OBJECT public: explicit ListJob(ReadOnlyArchiveInterface *interface); qlonglong extractedFilesSize() const; bool isPasswordProtected() const; bool isSingleFolderArchive() const; QString subfolderName() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: bool m_isSingleFolderArchive; bool m_isPasswordProtected; QString m_subfolderName; QString m_basePath; qlonglong m_extractedFilesSize; qlonglong m_dirCount; qlonglong m_filesCount; private slots: void onNewEntry(const Archive::Entry*); }; class KERFUFFLE_EXPORT ExtractJob : public Job { Q_OBJECT public: ExtractJob(const QList &entries, const QString& destinationDir, const ExtractionOptions& options, ReadOnlyArchiveInterface *interface); QString destinationDirectory() const; ExtractionOptions extractionOptions() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: // TODO: Maybe this should be a method if ExtractionOptions were a class? void setDefaultOptions(); QList m_entries; QString m_destinationDir; ExtractionOptions m_options; }; /** * Abstract base class for jobs that extract a single file to a temporary dir. * It's not possible to pass extraction options and paths will be always preserved. * The only option that the job needs to know is whether the file is password protected. */ class KERFUFFLE_EXPORT TempExtractJob : public Job { Q_OBJECT public: TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); /** * @return The absolute path of the extracted file. * The path is validated in order to prevent directory traversal attacks. */ QString validatedFilePath() const; ExtractionOptions extractionOptions() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: virtual QString extractionDir() const = 0; Archive::Entry *m_entry; bool m_passwordProtectedHint; }; /** * This TempExtractJob can be used to preview a file. * The temporary extraction directory will be deleted upon job's completion. */ class KERFUFFLE_EXPORT PreviewJob : public TempExtractJob { Q_OBJECT public: PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); private: QString extractionDir() const Q_DECL_OVERRIDE; QTemporaryDir m_tmpExtractDir; }; /** * This TempExtractJob can be used to open a file in its dedicated application. * For this reason, the temporary extraction directory will NOT be deleted upon job's completion. */ class KERFUFFLE_EXPORT OpenJob : public TempExtractJob { Q_OBJECT public: OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); /** * @return The temporary dir used for the extraction. * It is safe to delete this pointer in order to remove the directory. */ QTemporaryDir *tempDir() const; private: QString extractionDir() const Q_DECL_OVERRIDE; QTemporaryDir *m_tmpExtractDir; }; class KERFUFFLE_EXPORT OpenWithJob : public OpenJob { Q_OBJECT public: OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); }; class KERFUFFLE_EXPORT AddJob : public Job { Q_OBJECT public: AddJob(const QList &files, const Archive::Entry *destination, const CompressionOptions& options, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; protected slots: virtual void onFinished(bool result) Q_DECL_OVERRIDE; private: QString m_oldWorkingDir; const QList m_entries; const Archive::Entry *m_destination; CompressionOptions m_options; }; /** * This MoveJob can be used to rename or move entries withing the archive. * @see Archive::moveFiles for more details. */ class KERFUFFLE_EXPORT MoveJob : public Job { Q_OBJECT public: MoveJob(const QList &files, Archive::Entry *destination, const CompressionOptions& options, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; protected slots: virtual void onFinished(bool result) Q_DECL_OVERRIDE; private: int m_finishedSignalsCount; const QList m_entries; Archive::Entry *m_destination; CompressionOptions m_options; }; /** * This CopyJob can be used to copy entries withing the archive. * @see Archive::copyFiles for more details. */ class KERFUFFLE_EXPORT CopyJob : public Job { Q_OBJECT public: CopyJob(const QList &entries, Archive::Entry *destination, const CompressionOptions& options, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; protected slots: virtual void onFinished(bool result) Q_DECL_OVERRIDE; private: int m_finishedSignalsCount; const QList m_entries; Archive::Entry *m_destination; CompressionOptions m_options; }; class KERFUFFLE_EXPORT DeleteJob : public Job { Q_OBJECT public: DeleteJob(const QList &files, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QList m_entries; }; class KERFUFFLE_EXPORT CommentJob : public Job { Q_OBJECT public: CommentJob(const QString& comment, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QString m_comment; }; class KERFUFFLE_EXPORT TestJob : public Job { Q_OBJECT public: TestJob(ReadOnlyArchiveInterface *interface); bool testSucceeded(); public slots: virtual void doWork() Q_DECL_OVERRIDE; private slots: virtual void onTestSuccess(); private: bool m_testSuccess; }; } // namespace Kerfuffle #endif // JOBS_H diff --git a/part/archivemodel.cpp b/part/archivemodel.cpp index b3ff66ec..85e5389a 100644 --- a/part/archivemodel.cpp +++ b/part/archivemodel.cpp @@ -1,986 +1,987 @@ /* * 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 + * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "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].toUtf8()); const QVariant &rightEntryMetaData = rightEntry->property(propertiesList[column].toUtf8()); 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].toUtf8()); } } case Qt::DecorationRole: if (index.column() == 0) { const Archive::Entry *e = static_cast(index.internalPointer()); QIcon::Mode mode = (filesToMove.contains(e->fullPath())) ? QIcon::Disabled : QIcon::Normal; return m_entryIcons.value(e->fullPath(true)).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small), mode); } 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) if (!data->hasUrls()) { return false; } QStringList paths; foreach(const QUrl &url, data->urls()) { paths << url.toLocalFile(); } const Archive::Entry *entry = Q_NULLPTR; QModelIndex droppedOnto = index(row, column, parent); if (droppedOnto.isValid()) { entry = entryForIndex(droppedOnto); if (!entry->isDir()) { entry = entry->getParent(); } } emit droppedFiles(paths, entry, QString()); 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->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->fullPath(true) + QLatin1Char('/') + piece); entry->setProperty("isDirectory", true); insertEntry(entry); } if (!entry->isDir()) { Archive::Entry *e = new Archive::Entry(parent); e->copyMetaData(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()); m_entryIcons.remove(parent->entries().at(entry->row())->fullPath(true)); 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->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().toUtf8()).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->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) { entry->copyMetaData(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::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; QIcon icon; if (entry->isDir()) { icon = QIcon::fromTheme(db.mimeTypeForName(QStringLiteral("inode/directory")).iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small)); } else { icon = QIcon::fromTheme(db.mimeTypeForFile(entry->fullPath()).iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small)); } m_entryIcons.insert(entry->fullPath(true), icon); } 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->extractFiles(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; } Kerfuffle::MoveJob *ArchiveModel::moveFiles(QList &entries, Archive::Entry *destination, const CompressionOptions &options) { if (!m_archive) { return Q_NULLPTR; } if (!m_archive->isReadOnly()) { MoveJob *job = m_archive->moveFiles(entries, destination, options); connect(job, &MoveJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &MoveJob::userQuery, this, &ArchiveModel::slotUserQuery); connect(job, &MoveJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved); connect(job, &MoveJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs); return job; } return Q_NULLPTR; } Kerfuffle::CopyJob *ArchiveModel::copyFiles(QList &entries, Archive::Entry *destination, const CompressionOptions &options) { if (!m_archive) { return Q_NULLPTR; } if (!m_archive->isReadOnly()) { CopyJob *job = m_archive->copyFiles(entries, destination, options); connect(job, &CopyJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &CopyJob::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); } bool ArchiveModel::conflictingEntries(QList &conflictingEntries, const QStringList &entries, bool allowMerging) const { bool error = false; // We can't accept destination as an argument, because it can be a new entry path for renaming. const Archive::Entry *destination; { QStringList destinationParts = entries.first().split(QLatin1Char('/'), QString::SkipEmptyParts); destinationParts.removeLast(); if (destinationParts.count() > 0) { destination = m_rootEntry.findByPath(destinationParts); } else { destination = &m_rootEntry; } } const Archive::Entry *lastDirEntry = destination; QString skippedDirPath; foreach (const QString &entry, entries) { if (skippedDirPath.count() > 0 && entry.startsWith(skippedDirPath)) { continue; } else { skippedDirPath.clear(); } while (!entry.startsWith(lastDirEntry->fullPath())) { lastDirEntry = lastDirEntry->getParent(); } bool isDir = entry.right(1) == QLatin1String("/"); const Archive::Entry *archiveEntry = lastDirEntry->find(entry.split(QLatin1Char('/'), QString::SkipEmptyParts).last()); if (archiveEntry != Q_NULLPTR) { if (archiveEntry->isDir() != isDir || !allowMerging) { if (isDir) { skippedDirPath = lastDirEntry->fullPath(); } if (!error) { conflictingEntries.clear(); error = true; } conflictingEntries << archiveEntry; } else { if (isDir) { lastDirEntry = archiveEntry; } else if (!error) { conflictingEntries << archiveEntry; } } } else if (isDir) { skippedDirPath = entry; } } return error; } bool ArchiveModel::hasDuplicatedEntries(const QStringList &entries) { QStringList tempList; foreach (const QString &entry, entries) { if (tempList.contains(entry)) { return true; } tempList << entry; } return false; } QMap ArchiveModel::entryMap(const QList &entries) { QMap map; foreach (Archive::Entry *entry, entries) { map.insert(entry->fullPath(), entry); } return map; } const QHash ArchiveModel::entryIcons() const { return m_entryIcons; } 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->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()); m_entryIcons.remove(rawEntry->getParent()->entries().at(rawEntry->row())->fullPath(true)); rawEntry->getParent()->removeEntryAt(rawEntry->row()); endRemoveRows(); } } diff --git a/part/archivemodel.h b/part/archivemodel.h index b30d6836..fcca1ca8 100644 --- a/part/archivemodel.h +++ b/part/archivemodel.h @@ -1,160 +1,161 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal + * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef ARCHIVEMODEL_H #define ARCHIVEMODEL_H #include #include #include #include "kerfuffle/archiveentry.h" using Kerfuffle::Archive; namespace Kerfuffle { class Query; } class ArchiveModel: public QAbstractItemModel { Q_OBJECT public: explicit ArchiveModel(const QString &dbusPathName, QObject *parent = 0); ~ArchiveModel(); QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; virtual void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) Q_DECL_OVERRIDE; //drag and drop related Qt::DropActions supportedDropActions() const Q_DECL_OVERRIDE; QStringList mimeTypes() const Q_DECL_OVERRIDE; QMimeData *mimeData(const QModelIndexList & indexes) const Q_DECL_OVERRIDE; bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) Q_DECL_OVERRIDE; KJob* setArchive(Kerfuffle::Archive *archive); Kerfuffle::Archive *archive() const; Archive::Entry *entryForIndex(const QModelIndex &index); int childCount(const QModelIndex &index, int &dirs, int &files) const; Kerfuffle::ExtractJob* extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; Kerfuffle::ExtractJob* extractFiles(const QList& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; Kerfuffle::PreviewJob* preview(Archive::Entry *file) const; Kerfuffle::OpenJob* open(Archive::Entry *file) const; Kerfuffle::OpenWithJob* openWith(Archive::Entry *file) const; Kerfuffle::AddJob* addFiles(QList &entries, const Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::MoveJob* moveFiles(QList &entries, Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::CopyJob* copyFiles(QList &entries, Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::DeleteJob* deleteFiles(QList entries); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ void encryptArchive(const QString &password, bool encryptHeader); /** * Constructs a list of conflicting entries. * * @param conflictingEntries Reference to the empty mutable entries list, which will be constructed. * If the method returns false, this list will contain only entries which produce a critical conflict. * @param entries New entries paths list. * @param allowMerging Boolean variable indicating whether merging is permitted. * If true, existing entries won't generate an error. * * @return Boolean variable indicating whether conflicts are not critical (true for not critical, * false for critical). For example, if there are both "some/file" (not a directory) and "some/file/" (a directory) * entries for both new and existing paths, the method will return false. Also, if merging is not allowed, * this method will return false for entries with the same path and types. */ bool conflictingEntries(QList &conflictingEntries, const QStringList &entries, bool allowMerging) const; static bool hasDuplicatedEntries(const QStringList &entries); static QMap entryMap(const QList &entries); const QHash entryIcons() const; QMap filesToMove; QMap filesToCopy; signals: void loadingStarted(); void loadingFinished(KJob *); void extractionFinished(bool success); void error(const QString& error, const QString& details); void droppedFiles(const QStringList& files, const Archive::Entry*, const QString&); private slots: void slotNewEntryFromSetArchive(Archive::Entry *entry); void slotNewEntry(Archive::Entry *entry); void slotLoadingFinished(KJob *job); void slotEntryRemoved(const QString & path); void slotUserQuery(Kerfuffle::Query *query); void slotCleanupEmptyDirs(); private: /** * Strips file names that start with './'. * * For more information, see bug 194241. * * @param fileName The file name that will be stripped. * * @return @p fileName without the leading './' */ QString cleanFileName(const QString& fileName); Archive::Entry *parentFor(const Kerfuffle::Archive::Entry *entry); QModelIndex indexForEntry(Archive::Entry *entry); static bool compareAscending(const QModelIndex& a, const QModelIndex& b); static bool compareDescending(const QModelIndex& a, const QModelIndex& b); /** * Insert the node @p node into the model, ensuring all views are notified * of the change. */ enum InsertBehaviour { NotifyViews, DoNotNotifyViews }; void insertEntry(Archive::Entry *entry, InsertBehaviour behaviour = NotifyViews); void newEntry(Kerfuffle::Archive::Entry *receivedEntry, InsertBehaviour behaviour); QList m_newArchiveEntries; // holds entries from opening a new archive until it's totally open QList m_showColumns; QScopedPointer m_archive; Archive::Entry m_rootEntry; QHash m_entryIcons; QString m_dbusPathName; }; #endif // ARCHIVEMODEL_H diff --git a/part/archiveview.cpp b/part/archiveview.cpp index 1eca8b7a..bf5333c4 100644 --- a/part/archiveview.cpp +++ b/part/archiveview.cpp @@ -1,167 +1,168 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008-2009 Harald Hvaal + * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "archiveview.h" #include "ark_debug.h" #include #include #include #include #include #include #include ArchiveView::ArchiveView(QWidget *parent) : QTreeView(parent) { } void ArchiveView::setModel(QAbstractItemModel *model) { QTreeView::setModel(model); setSelectionMode(QAbstractItemView::ExtendedSelection); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setAlternatingRowColors(true); setAnimated(true); setAllColumnsShowFocus(true); setSortingEnabled(true); //drag and drop setDragEnabled(true); setAcceptDrops(true); setDropIndicatorShown(true); setDragDropMode(QAbstractItemView::DragDrop); } void ArchiveView::startDrag(Qt::DropActions supportedActions) { //only start the drag if it's over the filename column. this allows dragging selection in //tree/detail view if (currentIndex().column() != 0) { return; } QTreeView::startDrag(supportedActions); } void ArchiveView::dragEnterEvent(QDragEnterEvent * event) { //TODO: if no model, trigger some mechanism to create one automatically! qCDebug(ARK) << "dragEnterEvent" << event; if (event->source() == this) { //we don't support internal drops yet. return; } QTreeView::dragEnterEvent(event); } void ArchiveView::dropEvent(QDropEvent * event) { qCDebug(ARK) << "dropEvent" << event; if (event->source() == this) { //we don't support internal drops yet. return; } QTreeView::dropEvent(event); } void ArchiveView::dragMoveEvent(QDragMoveEvent * event) { qCDebug(ARK) << "dragMoveEvent" << event; if (event->source() == this) { //we don't support internal drops yet. return; } QTreeView::dragMoveEvent(event); if (event->mimeData()->hasFormat(QStringLiteral("text/uri-list"))) { event->acceptProposedAction(); } } bool ArchiveView::eventFilter(QObject *object, QEvent *event) { if (object == m_entryEditor && event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Escape) { closeEntryEditor(); return true; } } return false; } void ArchiveView::mouseReleaseEvent(QMouseEvent *event) { if (m_editorIndex.isValid()) { closeEntryEditor(); } else { QTreeView::mouseReleaseEvent(event); } } void ArchiveView::keyPressEvent(QKeyEvent *event) { if (m_editorIndex.isValid()) { switch (event->key()) { case Qt::Key_Return: case Qt::Key_Enter: { QLineEdit* editor = static_cast(indexWidget(m_editorIndex)); emit entryChanged(editor->text()); closeEntryEditor(); break; } default: QTreeView::keyPressEvent(event); } } else { QTreeView::keyPressEvent(event); } } void ArchiveView::openEntryEditor(QModelIndex index) { m_editorIndex = index; openPersistentEditor(index); m_entryEditor = static_cast(indexWidget(m_editorIndex)); m_entryEditor->installEventFilter(this); m_entryEditor->setText(index.data().toString()); m_entryEditor->setFocus(Qt::OtherFocusReason); m_entryEditor->selectAll(); } void ArchiveView::closeEntryEditor() { m_entryEditor->removeEventFilter(this); closePersistentEditor(m_editorIndex); m_editorIndex = QModelIndex(); } diff --git a/part/archiveview.h b/part/archiveview.h index 25ffa4f6..4dd164a1 100644 --- a/part/archiveview.h +++ b/part/archiveview.h @@ -1,57 +1,58 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Harald Hvaal + * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef ARCHIVEVIEW_H #define ARCHIVEVIEW_H #include #include class ArchiveView : public QTreeView { Q_OBJECT public: explicit ArchiveView(QWidget *parent = 0); virtual void dragEnterEvent(class QDragEnterEvent * event) Q_DECL_OVERRIDE; virtual void dropEvent(class QDropEvent * event) Q_DECL_OVERRIDE; virtual void dragMoveEvent(class QDragMoveEvent * event) Q_DECL_OVERRIDE; virtual void startDrag(Qt::DropActions supportedActions) Q_DECL_OVERRIDE; void setModel(QAbstractItemModel *model) Q_DECL_OVERRIDE; void openEntryEditor(QModelIndex index); protected: virtual bool eventFilter(QObject *object, QEvent *event) Q_DECL_OVERRIDE; virtual void mouseReleaseEvent(QMouseEvent *event) Q_DECL_OVERRIDE; virtual void keyPressEvent(QKeyEvent *event) Q_DECL_OVERRIDE; signals: void entryChanged(QString name); private: void closeEntryEditor(); QModelIndex m_editorIndex; QLineEdit *m_entryEditor; }; #endif /* ARCHIVEVIEW_H */ diff --git a/part/part.cpp b/part/part.cpp index a803c7d5..b6a9f596 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1,1664 +1,1665 @@ /* * 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 + * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "part.h" #include "ark_debug.h" #include "adddialog.h" #include "overwritedialog.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_WITH_JSON(Factory, "ark_part.json", 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_archiveIsLoaded(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_messageWidget = new KMessageWidget(parentWidget); m_messageWidget->hide(); 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_messageWidget); 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_view, &ArchiveView::entryChanged, this, &Part::slotRenameFile); 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_model->archive()) { return; } 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, &QAction::triggered, m_signalMapper, static_cast(&QSignalMapper::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, &QAction::triggered, m_signalMapper, static_cast(&QSignalMapper::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, &QAction::triggered, m_signalMapper, static_cast(&QSignalMapper::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 &Files to...")); m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive")); connect(m_addFilesAction, &QAction::triggered, this, static_cast(&Part::slotAddFiles)); m_renameFileAction = actionCollection()->addAction(QStringLiteral("rename")); m_renameFileAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); m_renameFileAction->setText(i18n("&Rename")); actionCollection()->setDefaultShortcut(m_renameFileAction, Qt::Key_F2); m_renameFileAction->setToolTip(i18nc("@info:tooltip", "Click to rename the selected file")); connect(m_renameFileAction, &QAction::triggered, this, &Part::slotEditFileName); 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_cutFilesAction = actionCollection()->addAction(QStringLiteral("cut")); m_cutFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-cut"))); m_cutFilesAction->setText(i18nc("@action:inmenu", "C&ut")); actionCollection()->setDefaultShortcut(m_cutFilesAction, Qt::CTRL + Qt::Key_X); m_cutFilesAction->setToolTip(i18nc("@info:tooltip", "Click to cut the selected files")); connect(m_cutFilesAction, &QAction::triggered, this, &Part::slotCutFiles); m_copyFilesAction = actionCollection()->addAction(QStringLiteral("copy")); m_copyFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); m_copyFilesAction->setText(i18nc("@action:inmenu", "C&opy")); actionCollection()->setDefaultShortcut(m_copyFilesAction, Qt::CTRL + Qt::Key_C); m_copyFilesAction->setToolTip(i18nc("@info:tooltip", "Click to copy the selected files")); connect(m_copyFilesAction, &QAction::triggered, this, &Part::slotCopyFiles); m_pasteFilesAction = actionCollection()->addAction(QStringLiteral("paste")); m_pasteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-paste"))); m_pasteFilesAction->setText(i18nc("@action:inmenu", "Pa&ste")); actionCollection()->setDefaultShortcut(m_pasteFilesAction, Qt::CTRL + Qt::Key_V); m_pasteFilesAction->setToolTip(i18nc("@info:tooltip", "Click to paste the files here")); connect(m_pasteFilesAction, &QAction::triggered, this, static_cast(&Part::slotPasteFiles)); 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)); const bool isDir = (entry == Q_NULLPTR) ? false : entry->isDir(); m_previewAction->setEnabled(!isBusy() && isPreviewable && !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_deleteFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0)); m_openFileAction->setEnabled(!isBusy() && isPreviewable && !isDir && (selectedEntriesCount == 1)); m_openFileWithAction->setEnabled(!isBusy() && isPreviewable && !isDir && (selectedEntriesCount == 1)); m_propertiesAction->setEnabled(!isBusy() && m_model->archive()); m_renameFileAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount == 1)); m_cutFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0)); m_copyFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0)); m_pasteFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount == 0 || (selectedEntriesCount == 1 && isDir)) && (m_model->filesToMove.count() > 0 || m_model->filesToCopy.count() > 0)); 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::resetGui() { m_messageWidget->hide(); m_commentView->clear(); m_commentBox->hide(); } 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(); resetGui(); 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() { m_model->filesToMove.clear(); m_model->filesToCopy.clear(); } 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_archiveIsLoaded = false; m_infoPanel->setPrettyFileName(QString()); m_infoPanel->updateWithDefaults(); emit setWindowCaption(QString()); } } else { m_archiveIsLoaded = true; } 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))->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->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, Q_NULLPTR, 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)->fullPath() : QString(); // Append index with root node to fileList. QModelIndexList alist = QModelIndexList() << index; foreach (Archive::Entry *entry, filesForIndexes(alist)) { 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 Archive::Entry *destination, const QString &relPath) { if (!m_archiveIsLoaded || filesToAdd.isEmpty()) { return; } QStringList withChildPaths; foreach (const QString& file, filesToAdd) { m_jobTempEntries.push_back(new Archive::Entry(Q_NULLPTR, file)); if (QFileInfo(file).isDir()) { withChildPaths << file + QLatin1Char('/'); QDirIterator it(file, QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (it.hasNext()) { QString path = it.next(); if (it.fileInfo().isDir()) { path += QLatin1Char('/'); } withChildPaths << path; } } else { withChildPaths << file; } } withChildPaths = ReadOnlyArchiveInterface::entryPathsFromDestination(withChildPaths, destination, 0); QList conflictingEntries; bool error = m_model->conflictingEntries(conflictingEntries, withChildPaths, true); if (conflictingEntries.count() > 0) { QPointer overwriteDialog = new OverwriteDialog(widget(), conflictingEntries, m_model->entryIcons(), error); int ret = overwriteDialog->exec(); delete overwriteDialog; if (ret == QDialog::Rejected) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); return; } } // 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 (!relPath.isEmpty()) { globalWorkDir.remove(relPath); qCDebug(ARK) << "Adding" << filesToAdd << "to" << relPath; } else { qCDebug(ARK) << "Adding " << filesToAdd << ((destination == Q_NULLPTR) ? QString() : QStringLiteral("to ") + destination->fullPath()); } // Remove trailing slash (needed when adding dirs). if (globalWorkDir.right(1) == QLatin1String("/")) { globalWorkDir.chop(1); } CompressionOptions options(m_model->archive()->compressionOptions()); // Now take the absolute path of the parent directory. globalWorkDir = QFileInfo(globalWorkDir).dir().absolutePath(); qCDebug(ARK) << "Detected GlobalWorkDir to be " << globalWorkDir; options[QStringLiteral("GlobalWorkDir")] = globalWorkDir; AddJob *job = m_model->addFiles(m_jobTempEntries, destination, options); if (!job) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); return; } connect(job, &KJob::result, this, &Part::slotAddFilesDone); registerJob(job); job->start(); } void Part::slotAddFiles() { // If compression options are already set, we don't use the values from CreateDialog. CompressionOptions opts; if (m_model->archive()->compressionOptions().isEmpty()) { if (arguments().metaData().contains(QStringLiteral("compressionLevel"))) { opts[QStringLiteral("CompressionLevel")] = arguments().metaData()[QStringLiteral("compressionLevel")]; } m_model->archive()->setCompressionOptions(opts); } else { opts = m_model->archive()->compressionOptions(); } QString dialogTitle = i18nc("@title:window", "Add Files"); const Archive::Entry *destination = Q_NULLPTR; if (m_view->selectionModel()->selectedRows().count() == 1) { destination = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); if (destination->isDir()) { dialogTitle = i18nc("@title:window", "Add Files to %1", destination->fullPath());; } else { destination = Q_NULLPTR; } } qCDebug(ARK) << "Opening AddDialog with opts:" << opts; // #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. QPointer dlg = new AddDialog(widget(), dialogTitle, m_lastUsedAddPath, m_model->archive()->mimeType(), opts); if (dlg->exec() == QDialog::Accepted) { qCDebug(ARK) << "Selected files:" << dlg->selectedFiles(); qCDebug(ARK) << "Options:" << dlg->compressionOptions(); m_model->archive()->setCompressionOptions(dlg->compressionOptions()); slotAddFiles(dlg->selectedFiles(), destination, QString()); } delete dlg; } void Part::slotEditFileName() { QModelIndex currentIndex = m_view->selectionModel()->currentIndex(); currentIndex = (currentIndex.parent().isValid()) ? currentIndex.parent().child(currentIndex.row(), 0) : m_model->index(currentIndex.row(), 0); m_view->openEntryEditor(currentIndex); } void Part::slotCutFiles() { QModelIndexList selectedRows = addChildren(m_view->selectionModel()->selectedRows()); m_model->filesToMove = ArchiveModel::entryMap(filesForIndexes(selectedRows)); qCDebug(ARK) << "Entries marked to cut:" << m_model->filesToMove.values(); m_model->filesToCopy.clear(); foreach (const QModelIndex &row, m_cutIndexes) { m_view->dataChanged(row, row); } m_cutIndexes = selectedRows; foreach (const QModelIndex &row, m_cutIndexes) { m_view->dataChanged(row, row); } updateActions(); } void Part::slotCopyFiles() { m_model->filesToCopy = ArchiveModel::entryMap(filesForIndexes(addChildren(m_view->selectionModel()->selectedRows()))); qCDebug(ARK) << "Entries marked to copy:" << m_model->filesToCopy.values(); foreach (const QModelIndex &row, m_cutIndexes) { m_view->dataChanged(row, row); } m_cutIndexes.clear(); m_model->filesToMove.clear(); updateActions(); } void Part::slotRenameFile(QString name) { if (name == QStringLiteral(".") || name == QStringLiteral("..") || name.contains(QLatin1Char('/'))) { QMessageBox messageBox(QMessageBox::Warning, i18n("Invalid filename"), i18n("Filename can't contain slashes and can't be equal to \".\" or \"..\""), QMessageBox::Ok); messageBox.exec(); return; } const Archive::Entry *entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); QList entriesToMove = filesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); m_destination = new Archive::Entry(); const QString &entryPath = entry->fullPath(true); const QString rootPath = entryPath.left(entryPath.count() - entry->name().count()); m_destination->setFullPath(rootPath + name + ((entry->isDir()) ? QLatin1Char('/') : QChar())); slotPasteFiles(entriesToMove, m_destination, 1); } void Part::slotPasteFiles() { m_destination = (m_view->selectionModel()->selectedRows().count() > 0) ? m_model->entryForIndex(m_view->selectionModel()->currentIndex()) : Q_NULLPTR; if (m_destination == Q_NULLPTR) { m_destination = new Archive::Entry(Q_NULLPTR, QString()); } else { m_destination = new Archive::Entry(Q_NULLPTR, m_destination->fullPath()); } if (m_model->filesToMove.count() > 0) { // Changing destination to include new entry path if pasting only 1 entry. QList entriesWithoutChildren = ReadOnlyArchiveInterface::entriesWithoutChildren(m_model->filesToMove.values()); if (entriesWithoutChildren.count() == 1) { const Archive::Entry *entry = entriesWithoutChildren.first(); const QString nameWithSlash = entry->name() + ((entry->isDir()) ? QLatin1String("/") : QString()); m_destination->setFullPath(m_destination->fullPath() + nameWithSlash); } foreach (const Archive::Entry *entry, entriesWithoutChildren) { if (entry->isDir() && m_destination->fullPath().startsWith(entry->fullPath())) { QMessageBox messageBox(QMessageBox::Warning, i18n("Moving a folder into itself"), i18n("Folders can't be moved into themselves."), QMessageBox::Ok); messageBox.exec(); delete m_destination; return; } } QList entryList = m_model->filesToMove.values(); slotPasteFiles(entryList, m_destination, entriesWithoutChildren.count()); m_model->filesToMove.clear(); } else { QList entryList = m_model->filesToCopy.values(); slotPasteFiles(entryList, m_destination, 0); m_model->filesToCopy.clear(); } m_cutIndexes.clear(); updateActions(); } void Part::slotPasteFiles(QList &files, Kerfuffle::Archive::Entry *destination, int entriesWithoutChildren) { if (files.isEmpty()) { delete m_destination; return; } QStringList filesPaths = ReadOnlyArchiveInterface::entryFullPaths(files); QStringList newPaths = ReadOnlyArchiveInterface::entryPathsFromDestination(filesPaths, destination, entriesWithoutChildren); if (ArchiveModel::hasDuplicatedEntries(newPaths)) { QMessageBox messageBox(QMessageBox::Warning, i18n("Pasting entries with the same name"), i18n("Entries with the same names can't be pasted to the same destination."), QMessageBox::Ok); messageBox.exec(); delete m_destination; return; } QList conflictingEntries; bool error = m_model->conflictingEntries(conflictingEntries, newPaths, false); if (conflictingEntries.count() != 0) { QPointer overwriteDialog = new OverwriteDialog(widget(), conflictingEntries, m_model->entryIcons(), error); int ret = overwriteDialog->exec(); delete overwriteDialog; if (ret == QDialog::Rejected) { delete m_destination; return; } } if (entriesWithoutChildren > 0) { qCDebug(ARK) << "Moving" << files << "to" << destination; } else { qCDebug(ARK) << "Copying " << files << "to" << destination; } CompressionOptions options(m_model->archive()->compressionOptions()); KJob *job; if (entriesWithoutChildren != 0) { job = m_model->moveFiles(files, destination, options); } else { job = m_model->copyFiles(files, destination, options); } if (job) { connect(job, &KJob::result, this, &Part::slotPasteFilesDone); registerJob(job); job->start(); } else { delete m_destination; } } void Part::slotAddFilesDone(KJob* job) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else { // Hide the "archive will be created as soon as you add a file" message. m_messageWidget->hide(); } m_cutIndexes.clear(); m_model->filesToMove.clear(); m_model->filesToCopy.clear(); } void Part::slotPasteFilesDone(KJob *job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } m_cutIndexes.clear(); m_model->filesToMove.clear(); m_model->filesToCopy.clear(); } void Part::slotDeleteFilesDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } m_cutIndexes.clear(); m_model->filesToMove.clear(); m_model->filesToCopy.clear(); } 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) { // The widget could be already visible, so hide it. m_messageWidget->hide(); m_messageWidget->setText(msg); m_messageWidget->setMessageType(type); m_messageWidget->animatedShow(); } } // namespace Ark #include "part.moc" diff --git a/part/part.h b/part/part.h index b428388c..ce62810f 100644 --- a/part/part.h +++ b/part/part.h @@ -1,219 +1,220 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2009 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef PART_H #define PART_H #include "interface.h" #include "kerfuffle/archiveentry.h" #include #include #include #include #include class ArchiveModel; class ArchiveView; class InfoPanel; class KAboutData; class KAbstractWidgetJobTracker; class KJob; class KToggleAction; class QAction; class QSplitter; class QTreeView; class QTemporaryDir; class QVBoxLayout; class QSignalMapper; class QFileSystemWatcher; class QGroupBox; class QPlainTextEdit; namespace Ark { class Part: public KParts::ReadWritePart, public Interface { Q_OBJECT Q_INTERFACES(Interface) public: enum OpenFileMode { Preview, OpenFile, OpenFileWith }; Part(QWidget *parentWidget, QObject *parent, const QVariantList &); ~Part(); static KAboutData *createAboutData(); bool openFile() Q_DECL_OVERRIDE; bool saveFile() Q_DECL_OVERRIDE; bool isBusy() const Q_DECL_OVERRIDE; KConfigSkeleton *config() const Q_DECL_OVERRIDE; QList settingsPages(QWidget *parent) const Q_DECL_OVERRIDE; /** * Validate the localFilePath() associated to this part. * If the file is not valid, an error message is displayed to the user. * @return Whether the localFilePath() can be loaded by the part. */ bool isLocalFileValid(); /** * Ask the user whether to overwrite @p targetFile, when creating a new archive with the same path. * @return True if the file has been successfully removed upon user's will. False otherwise. */ bool confirmAndDelete(const QString& targetFile); public slots: void extractSelectedFilesTo(const QString& localPath); private slots: void slotLoadingStarted(); void slotLoadingFinished(KJob *job); void slotOpenExtractedEntry(KJob*); void slotPreviewExtractedEntry(KJob* job); void slotOpenEntry(int mode); void slotError(const QString& errorMessage, const QString& details); void slotExtractArchive(); void slotShowExtractionDialog(); void slotExtractionDone(KJob*); void slotQuickExtractFiles(QAction*); /** * Creates and starts AddJob. * * @param files Files to add. * @param destination Destination path within the archive to which entries have to be added. Is used on addto action * or drag'n'drop event, for adding a watched file it has empty. * @param relPath Relative path of watched entry inside the archive. Is used only for adding temporarily extracted * watched file. */ void slotAddFiles(const QStringList &files, const Kerfuffle::Archive::Entry *destination, const QString &relPath); /** * Creates and starts MoveJob or CopyJob. * * @param files Files to paste. * @param destination Destination path within the archive to which entries have to be added. For renaming an entry * the path has to contain a new filename too. * @param entriesWithoutChildren Entries count, excluding their children. For CopyJob 0 MUST be passed. */ void slotPasteFiles(QList &files, Kerfuffle::Archive::Entry *destination, int entriesWithoutChildren); void slotAddFiles(); void slotEditFileName(); void slotCutFiles(); void slotCopyFiles(); void slotRenameFile(QString name); void slotPasteFiles(); void slotAddFilesDone(KJob*); void slotPasteFilesDone(KJob*); void slotTestingDone(KJob*); void slotDeleteFiles(); void slotDeleteFilesDone(KJob*); void slotShowProperties(); void slotShowContextMenu(); void slotActivated(QModelIndex); void slotToggleInfoPanel(bool); void slotSaveAs(); void updateActions(); void updateQuickExtractMenu(QAction *extractAction); void selectionChanged(); void adjustColumns(); void setBusyGui(); void setReadyGui(); void setFileNameFromArchive(); void slotWatchedFileModified(const QString& file); void slotShowComment(); void slotAddComment(); void slotCommentChanged(); void slotTestArchive(); signals: void busy(); void ready(); void quit(); private: void resetGui(); void setupView(); void setupActions(); bool isSingleFolderArchive() const; QString detectSubfolder() const; QList filesForIndexes(const QModelIndexList& list) const; QList filesAndRootNodesForIndexes(const QModelIndexList& list) const; QModelIndexList addChildren(const QModelIndexList &list) const; void registerJob(KJob *job); void displayMsgWidget(KMessageWidget::MessageType type, const QString& msg); ArchiveModel *m_model; ArchiveView *m_view; QAction *m_previewAction; QAction *m_openFileAction; QAction *m_openFileWithAction; QAction *m_extractArchiveAction; QAction *m_extractAction; QAction *m_addFilesAction; QAction *m_renameFileAction; QAction *m_deleteFilesAction; QAction *m_cutFilesAction; QAction *m_copyFilesAction; QAction *m_pasteFilesAction; QAction *m_saveAsAction; QAction *m_propertiesAction; QAction *m_editCommentAction; QAction *m_testArchiveAction; KToggleAction *m_showInfoPanelAction; InfoPanel *m_infoPanel; QSplitter *m_splitter; QList m_tmpOpenDirList; bool m_busy; bool m_archiveIsLoaded; OpenFileMode m_openFileMode; QUrl m_lastUsedAddPath; QList m_jobTempEntries; Kerfuffle::Archive::Entry *m_destination; QModelIndexList m_cutIndexes; KAbstractWidgetJobTracker *m_jobTracker; KParts::StatusBarExtension *m_statusBarExtension; QVBoxLayout *m_vlayout; QSignalMapper *m_signalMapper; QFileSystemWatcher *m_fileWatcher; QSplitter *m_commentSplitter; QGroupBox *m_commentBox; QPlainTextEdit *m_commentView; KMessageWidget *m_commentMsgWidget; KMessageWidget *m_messageWidget; }; } // namespace Ark #endif // PART_H diff --git a/plugins/cli7zplugin/cliplugin.cpp b/plugins/cli7zplugin/cliplugin.cpp index 751db45d..51837af2 100644 --- a/plugins/cli7zplugin/cliplugin.cpp +++ b/plugins/cli7zplugin/cliplugin.cpp @@ -1,270 +1,271 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/cliinterface.h" #include "kerfuffle/kerfuffle_export.h" #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_cli7z.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList & args) : CliInterface(parent, args) , m_archiveType(ArchiveType7z) , m_parseState(ParseStateTitle) , m_linesComment(0) , m_isFirstInformationEntry(true) { qCDebug(ARK) << "Loaded cli_7z plugin"; } 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->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->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/cli7zplugin/cliplugin.h b/plugins/cli7zplugin/cliplugin.h index 806c6475..c60a62d1 100644 --- a/plugins/cli7zplugin/cliplugin.h +++ b/plugins/cli7zplugin/cliplugin.h @@ -1,71 +1,72 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2010 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "kerfuffle/cliinterface.h" #include "kerfuffle/archiveentry.h" class CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList & args); virtual ~CliPlugin(); virtual void resetParsing() Q_DECL_OVERRIDE; virtual Kerfuffle::ParameterList parameterList() const Q_DECL_OVERRIDE; virtual bool readListLine(const QString &line) Q_DECL_OVERRIDE; /** * @return The password header-switch with the given @p password. */ virtual QStringList passwordHeaderSwitch(const QString& password) const Q_DECL_OVERRIDE; private: enum ArchiveType { ArchiveType7z = 0, ArchiveTypeBZip2, ArchiveTypeGZip, ArchiveTypeXz, ArchiveTypeTar, ArchiveTypeZip, ArchiveTypeRar } m_archiveType; enum ParseState { ParseStateTitle = 0, ParseStateHeader, ParseStateArchiveInformation, ParseStateComment, ParseStateEntryInformation } m_parseState; int m_linesComment; Kerfuffle::Archive::Entry *m_currentArchiveEntry; bool m_isFirstInformationEntry; }; #endif // CLIPLUGIN_H diff --git a/plugins/clirarplugin/cliplugin.cpp b/plugins/clirarplugin/cliplugin.cpp index 53350c3e..d701a8b8 100644 --- a/plugins/clirarplugin/cliplugin.cpp +++ b/plugins/clirarplugin/cliplugin.cpp @@ -1,506 +1,507 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2010-2011,2014 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/archiveentry.h" #include #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(); } 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 71b3f9de..dc7dbfa2 100644 --- a/plugins/clirarplugin/cliplugin.h +++ b/plugins/clirarplugin/cliplugin.h @@ -1,69 +1,70 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2010 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "kerfuffle/cliinterface.h" class CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList &args); virtual ~CliPlugin(); virtual void resetParsing() Q_DECL_OVERRIDE; virtual Kerfuffle::ParameterList parameterList() const Q_DECL_OVERRIDE; virtual bool readListLine(const QString &line) Q_DECL_OVERRIDE; 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 c30d1f4c..83265d18 100644 --- a/plugins/clizipplugin/cliplugin.cpp +++ b/plugins/clizipplugin/cliplugin.cpp @@ -1,284 +1,285 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/cliinterface.h" #include "kerfuffle/kerfuffle_export.h" #include "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_tempExtractDir = new QTemporaryDir(); m_tempAddDir = new QTemporaryDir(); QDir::setCurrent(m_tempExtractDir->path()); m_passedFiles = files; m_passedDestination = destination; m_passedOptions = options; m_subOperation = Extract; connect(this, &CliPlugin::finished, this, &CliPlugin::continueMoving); return extractFiles(files, QDir::currentPath(), options); } int CliPlugin::moveRequiredSignals() const { return 4; } void CliPlugin::continueMoving(bool result) { if (!result) { finishMoving(false); return; } switch (m_subOperation) { case Extract: m_subOperation = Delete; if (!deleteFiles(m_passedFiles)) { finishMoving(false); } break; case Delete: m_subOperation = Add; if (!setMovingAddedFiles() || !addFiles(m_tempAddedFiles, m_passedDestination, m_passedOptions)) { finishMoving(false); } break; case Add: finishMoving(true); break; default: Q_ASSERT(false); } } bool CliPlugin::setMovingAddedFiles() { m_passedFiles = entriesWithoutChildren(m_passedFiles); // If there are more files being moved than 1, we have destination as a destination folder, // otherwise it's new entry full path. if (m_passedFiles.count() > 1) { return setAddedFiles(); } QDir::setCurrent(m_tempAddDir->path()); const Archive::Entry *file = m_passedFiles.at(0); const QString oldPath = m_tempExtractDir->path() + QLatin1Char('/') + file->fullPath(true); const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + m_passedDestination->name(); if (!QFile::rename(oldPath, newPath)) { return false; } m_tempAddedFiles << new Archive::Entry(Q_NULLPTR, m_passedDestination->name()); // We have to exclude file name from destination path in order to pass it to addFiles method. const QString destinationPath = m_passedDestination->fullPath(); int destinationLength = destinationPath.count(); bool iteratedChar = false; do { destinationLength--; if (destinationPath.at(destinationLength) != QLatin1Char('/')) { iteratedChar = true; } } while (destinationLength > 0 && !(iteratedChar && destinationPath.at(destinationLength) == QLatin1Char('/'))); m_passedDestination->setProperty("fullPath", destinationPath.left(destinationLength + 1)); return true; } void CliPlugin::finishMoving(bool result) { disconnect(this, &CliPlugin::finished, this, &CliPlugin::continueMoving); emit progress(1.0); emit finished(result); cleanUp(); } #include "cliplugin.moc" diff --git a/plugins/clizipplugin/cliplugin.h b/plugins/clizipplugin/cliplugin.h index b7250f26..c85f73ab 100644 --- a/plugins/clizipplugin/cliplugin.h +++ b/plugins/clizipplugin/cliplugin.h @@ -1,63 +1,64 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2011 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "kerfuffle/cliinterface.h" #include using namespace Kerfuffle; class KERFUFFLE_EXPORT CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList &args); virtual ~CliPlugin(); virtual void resetParsing() Q_DECL_OVERRIDE; virtual QString escapeFileName(const QString &fileName) const Q_DECL_OVERRIDE; virtual Kerfuffle::ParameterList parameterList() const Q_DECL_OVERRIDE; virtual bool readListLine(const QString &line) Q_DECL_OVERRIDE; virtual bool moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual int moveRequiredSignals() const Q_DECL_OVERRIDE; private slots: void continueMoving(bool result); private: bool setMovingAddedFiles(); void finishMoving(bool result); enum ParseState { ParseStateHeader = 0, ParseStateComment, ParseStateEntry } m_parseState; int m_linesComment; QString m_tempComment; }; #endif // CLIPLUGIN_H diff --git a/plugins/libarchive/libarchiveplugin.cpp b/plugins/libarchive/libarchiveplugin.cpp index 0aed0cb6..ed81152f 100644 --- a/plugins/libarchive/libarchiveplugin.cpp +++ b/plugins/libarchive/libarchiveplugin.cpp @@ -1,525 +1,526 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2010 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "libarchiveplugin.h" #include "kerfuffle/queries.h" #include #include LibarchivePlugin::LibarchivePlugin(QObject *parent, const QVariantList &args) : ReadWriteArchiveInterface(parent, args) , m_archiveReadDisk(archive_read_disk_new()) , m_abortOperation(false) , m_cachedArchiveEntryCount(0) , m_emitNoEntries(false) , m_extractedFilesSize(0) { qCDebug(ARK) << "Initializing libarchive plugin"; archive_read_disk_set_standard_lookup(m_archiveReadDisk.data()); } LibarchivePlugin::~LibarchivePlugin() { } bool LibarchivePlugin::list() { qCDebug(ARK) << "Listing archive contents"; if (!initializeReader()) { return false; } qDebug(ARK) << "Detected compression filter:" << archive_filter_name(m_archiveReader.data(), 0); m_cachedArchiveEntryCount = 0; m_extractedFilesSize = 0; struct archive_entry *aentry; int result = ARCHIVE_RETRY; bool firstEntry = true; while (!m_abortOperation && (result = archive_read_next_header(m_archiveReader.data(), &aentry)) == ARCHIVE_OK) { if (firstEntry) { qDebug(ARK) << "Detected format for first entry:" << archive_format_name(m_archiveReader.data()); firstEntry = false; } if (!m_emitNoEntries) { emitEntryFromArchiveEntry(aentry); } m_extractedFilesSize += (qlonglong)archive_entry_size(aentry); m_cachedArchiveEntryCount++; archive_read_data_skip(m_archiveReader.data()); } m_abortOperation = false; if (result != ARCHIVE_EOF) { const QString errorString = QLatin1String(archive_error_string(m_archiveReader.data())); // FIXME: what about the other archive_error_string() calls? Do they also happen to return empty strings? emit error(errorString.isEmpty() ? i18nc("@info", "Could not read until the end of the archive") : errorString); return false; } return archive_read_close(m_archiveReader.data()) == ARCHIVE_OK; } bool LibarchivePlugin::addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(files) Q_UNUSED(destination) Q_UNUSED(options) return false; } bool LibarchivePlugin::moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(files) Q_UNUSED(destination) Q_UNUSED(options) return false; } bool LibarchivePlugin::copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(files) Q_UNUSED(destination) Q_UNUSED(options) return false; } bool LibarchivePlugin::deleteFiles(const QList &files) { Q_UNUSED(files) return false; } bool LibarchivePlugin::addComment(const QString &comment) { Q_UNUSED(comment) return false; } bool LibarchivePlugin::testArchive() { return false; } bool LibarchivePlugin::doKill() { m_abortOperation = true; return true; } bool LibarchivePlugin::extractFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) { qCDebug(ARK) << "Changing current directory to " << destinationDirectory; QDir::setCurrent(destinationDirectory); const bool extractAll = files.isEmpty(); const bool preservePaths = options.value(QStringLiteral( "PreservePaths" )).toBool(); bool removeRootNode = options.value(QStringLiteral("RemoveRootNode"), QVariant()).toBool(); // To avoid traversing the entire archive when extracting a limited set of // entries, we maintain a list of remaining entries and stop when it's // empty. QStringList fullPaths = entryFullPaths(files); QStringList remainingFiles = entryFullPaths(files); if (!initializeReader()) { return false; } ArchiveWrite writer(archive_write_disk_new()); if (!writer.data()) { return false; } archive_write_disk_set_options(writer.data(), extractionFlags()); int entryNr = 0; int totalCount = 0; if (extractAll) { if (!m_cachedArchiveEntryCount) { emit progress(0); //TODO: once information progress has been implemented, send //feedback here that the archive is being read qCDebug(ARK) << "For getting progress information, the archive will be listed once"; m_emitNoEntries = true; list(); m_emitNoEntries = false; } totalCount = m_cachedArchiveEntryCount; } else { totalCount = files.size(); } qCDebug(ARK) << "Going to extract" << totalCount << "entries"; // Initialize variables. bool overwriteAll = false; // Whether to overwrite all files bool skipAll = false; // Whether to skip all files bool dontPromptErrors = false; // Whether to prompt for errors m_currentExtractedFilesSize = 0; int no_entries = 0; struct archive_entry *entry; QString fileBeingRenamed; // Iterate through all entries in archive. while (!m_abortOperation && (archive_read_next_header(m_archiveReader.data(), &entry) == ARCHIVE_OK)) { if (!extractAll && remainingFiles.isEmpty()) { break; } fileBeingRenamed.clear(); int index = -1; // Retry with renamed entry, fire an overwrite query again // if the new entry also exists. retry: const bool entryIsDir = S_ISDIR(archive_entry_mode(entry)); // Skip directories if not preserving paths. if (!preservePaths && entryIsDir) { archive_read_data_skip(m_archiveReader.data()); continue; } // entryName is the name inside the archive, full path QString entryName = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry))); // For now we just can't handle absolute filenames in a tar archive. // TODO: find out what to do here!! if (entryName.startsWith(QLatin1Char( '/' ))) { emit error(i18n("This archive contains archive entries with absolute paths, " "which are not supported by Ark.")); return false; } // Should the entry be extracted? if (extractAll || remainingFiles.contains(entryName) || entryName == fileBeingRenamed) { // Find the index of entry. if (entryName != fileBeingRenamed) { index = fullPaths.indexOf(entryName); } if (!extractAll && index == -1) { // If entry is not found in files, skip entry. continue; } // entryFI is the fileinfo pointing to where the file will be // written from the archive. QFileInfo entryFI(entryName); //qCDebug(ARK) << "setting path to " << archive_entry_pathname( entry ); const QString fileWithoutPath(entryFI.fileName()); // If we DON'T preserve paths, we cut the path and set the entryFI // fileinfo to the one without the path. if (!preservePaths) { // Empty filenames (ie dirs) should have been skipped already, // so asserting. Q_ASSERT(!fileWithoutPath.isEmpty()); archive_entry_copy_pathname(entry, QFile::encodeName(fileWithoutPath).constData()); entryFI = QFileInfo(fileWithoutPath); // OR, if the file has a rootNode attached, remove it from file path. } else if (!extractAll && removeRootNode && entryName != fileBeingRenamed) { const QString &rootNode = files.at(index)->rootNode; if (!rootNode.isEmpty()) { //qCDebug(ARK) << "Removing" << files.at(index).value().rootNode << "from" << entryName; const QString truncatedFilename(entryName.remove(0, rootNode.size())); archive_entry_copy_pathname(entry, QFile::encodeName(truncatedFilename).constData()); entryFI = QFileInfo(truncatedFilename); } } // Check if the file about to be written already exists. if (!entryIsDir && entryFI.exists()) { if (skipAll) { archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); continue; } else if (!overwriteAll && !skipAll) { Kerfuffle::OverwriteQuery query(entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); break; } else if (query.responseSkip()) { archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); continue; } else if (query.responseAutoSkip()) { archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); skipAll = true; continue; } else if (query.responseRename()) { const QString newName(query.newFilename()); fileBeingRenamed = newName; archive_entry_copy_pathname(entry, QFile::encodeName(newName).constData()); goto retry; } else if (query.responseOverwriteAll()) { overwriteAll = true; } } } // If there is an already existing directory. if (entryIsDir && entryFI.exists()) { if (entryFI.isWritable()) { qCWarning(ARK) << "Warning, existing, but writable dir"; } else { qCWarning(ARK) << "Warning, existing, but non-writable dir. skipping"; archive_entry_clear(entry); archive_read_data_skip(m_archiveReader.data()); continue; } } // Write the entry header and check return value. const int returnCode = archive_write_header(writer.data(), entry); switch (returnCode) { case ARCHIVE_OK: // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(entryName, m_archiveReader.data(), writer.data(), (extractAll && m_extractedFilesSize)); break; case ARCHIVE_FAILED: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); // If they user previously decided to ignore future errors, // don't bother prompting again. if (!dontPromptErrors) { // Ask the user if he wants to continue extraction despite an error for this entry. Kerfuffle::ContinueExtractionQuery query(QLatin1String(archive_error_string(writer.data())), entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } dontPromptErrors = query.dontAskAgain(); } break; case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); emit error(xi18nc("@info", "Extraction failed at:%1", entryName)); return false; default: qCDebug(ARK) << "archive_write_header() returned" << returnCode << "which will be ignored."; break; } // If we only partially extract the archive and the number of // archive entries is available we use a simple progress based on // number of items extracted. if (!extractAll && m_cachedArchiveEntryCount) { ++entryNr; emit progress(float(entryNr) / totalCount); } no_entries++; remainingFiles.removeOne(entryName); } else { // Archive entry not among selected files, skip it. archive_read_data_skip(m_archiveReader.data()); } } // While entries left to read in archive. m_abortOperation = false; qCDebug(ARK) << "Extracted" << no_entries << "entries"; return archive_read_close(m_archiveReader.data()) == ARCHIVE_OK; } bool LibarchivePlugin::initializeReader() { m_archiveReader.reset(archive_read_new()); if (!(m_archiveReader.data())) { emit error(i18n("The archive reader could not be initialized.")); return false; } if (archive_read_support_filter_all(m_archiveReader.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(m_archiveReader.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(m_archiveReader.data(), QFile::encodeName(filename()), 10240) != ARCHIVE_OK) { emit error(xi18nc("@info", "Could not open the archive %1." "Check whether you have sufficient permissions.", filename())); return false; } return true; } void LibarchivePlugin::emitEntryFromArchiveEntry(struct archive_entry *aentry) { Archive::Entry *e = new Archive::Entry(Q_NULLPTR); #ifdef _MSC_VER e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromUtf16((ushort*)archive_entry_pathname_w(aentry)))); #else e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromWCharArray(archive_entry_pathname_w(aentry)))); #endif const QString owner = QString::fromLatin1(archive_entry_uname(aentry)); if (!owner.isEmpty()) { e->setProperty("owner", owner); } const QString group = QString::fromLatin1(archive_entry_gname(aentry)); if (!group.isEmpty()) { e->setProperty("group", group); } e->compressedSizeIsSet = false; e->setProperty("size", (qlonglong)archive_entry_size(aentry)); e->setProperty("isDirectory", S_ISDIR(archive_entry_mode(aentry))); if (archive_entry_symlink(aentry)) { e->setProperty("link", QLatin1String( archive_entry_symlink(aentry) )); } e->setProperty("timestamp", QDateTime::fromTime_t(archive_entry_mtime(aentry))); emit entry(e); } int LibarchivePlugin::extractionFlags() const { int result = ARCHIVE_EXTRACT_TIME; result |= ARCHIVE_EXTRACT_SECURE_NODOTDOT; // TODO: Don't use arksettings here /*if ( ArkSettings::preservePerms() ) { result &= ARCHIVE_EXTRACT_PERM; } if ( !ArkSettings::extractOverwrite() ) { result &= ARCHIVE_EXTRACT_NO_OVERWRITE; }*/ return result; } void LibarchivePlugin::copyData(const QString& filename, struct archive *dest, bool partialprogress) { char buff[10240]; ssize_t readBytes; QFile file(filename); if (!file.open(QIODevice::ReadOnly)) { return; } readBytes = file.read(buff, sizeof(buff)); while (readBytes > 0) { archive_write_data(dest, buff, readBytes); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while writing" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = file.read(buff, sizeof(buff)); } file.close(); } void LibarchivePlugin::copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress) { char buff[10240]; ssize_t readBytes; readBytes = archive_read_data(source, buff, sizeof(buff)); while (readBytes > 0) { archive_write_data(dest, buff, readBytes); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while extracting" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = archive_read_data(source, buff, sizeof(buff)); } } #include "libarchiveplugin.moc" diff --git a/plugins/libarchive/libarchiveplugin.h b/plugins/libarchive/libarchiveplugin.h index dff3724f..56dda50c 100644 --- a/plugins/libarchive/libarchiveplugin.h +++ b/plugins/libarchive/libarchiveplugin.h @@ -1,100 +1,101 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef LIBARCHIVEPLUGIN_H #define LIBARCHIVEPLUGIN_H #include "kerfuffle/archiveinterface.h" #include "kerfuffle/archiveentry.h" #include #include using namespace Kerfuffle; class LibarchivePlugin : public ReadWriteArchiveInterface { Q_OBJECT public: explicit LibarchivePlugin(QObject *parent, const QVariantList &args); virtual ~LibarchivePlugin(); virtual bool list() Q_DECL_OVERRIDE; virtual bool doKill() Q_DECL_OVERRIDE; virtual bool extractFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) Q_DECL_OVERRIDE; virtual bool addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions &options) Q_DECL_OVERRIDE; virtual bool moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) Q_DECL_OVERRIDE; virtual bool copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) Q_DECL_OVERRIDE; virtual bool deleteFiles(const QList &files) Q_DECL_OVERRIDE; virtual bool addComment(const QString &comment) Q_DECL_OVERRIDE; virtual bool testArchive() Q_DECL_OVERRIDE; protected: struct ArchiveReadCustomDeleter { static inline void cleanup(struct archive *a) { if (a) { archive_read_free(a); } } }; struct ArchiveWriteCustomDeleter { static inline void cleanup(struct archive *a) { if (a) { archive_write_free(a); } } }; typedef QScopedPointer ArchiveRead; typedef QScopedPointer ArchiveWrite; bool initializeReader(); void emitEntryFromArchiveEntry(struct archive_entry *entry); void copyData(const QString& filename, struct archive *dest, bool partialprogress = true); void copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress = true); ArchiveRead m_archiveReader; ArchiveRead m_archiveReadDisk; bool m_abortOperation; private: int extractionFlags() const; int m_cachedArchiveEntryCount; qlonglong m_currentExtractedFilesSize; bool m_emitNoEntries; qlonglong m_extractedFilesSize; }; #endif // LIBARCHIVEPLUGIN_H diff --git a/plugins/libarchive/readwritelibarchiveplugin.cpp b/plugins/libarchive/readwritelibarchiveplugin.cpp index 65c9ad99..d856b9ed 100644 --- a/plugins/libarchive/readwritelibarchiveplugin.cpp +++ b/plugins/libarchive/readwritelibarchiveplugin.cpp @@ -1,540 +1,541 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2010 Raphael Kubo da Costa + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "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(); if (!creatingNewFile && !initializeReader()) { return false; } if (!initializeWriter(creatingNewFile, options)) { return false; } // First write the new files. qCDebug(ARK) << "Writing new entries"; int no_entries = 0; // Recreate destination directory structure. const QString destinationPath = (destination == Q_NULLPTR) ? QString() : destination->fullPath(); foreach(Archive::Entry *selectedFile, files) { if (m_abortOperation) { break; } if (!writeFile(selectedFile->fullPath(), destinationPath)) { finish(false); return false; } no_entries++; // For directories, write all subfiles/folders. 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)) { finish(false); return false; } no_entries++; } } } qCDebug(ARK) << "Added" << no_entries << "new entries to archive"; bool isSuccessful = true; // If we have old archive entries. if (!creatingNewFile) { qCDebug(ARK) << "Copying any old entries"; m_filesPaths = m_writtenFiles; isSuccessful = processOldEntries(no_entries, Add); if (isSuccessful) { qCDebug(ARK) << "Added" << no_entries << "old entries to archive"; } else { qCDebug(ARK) << "Adding entries failed"; } } m_abortOperation = false; finish(isSuccessful); return isSuccessful; } bool ReadWriteLibarchivePlugin::moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(options); qCDebug(ARK) << "Moving" << files.size() << "entries"; if (!initializeReader()) { return false; } if (!initializeWriter()) { return false; } // Copy old elements from previous archive to new archive. int no_entries = 0; m_filesPaths = entryFullPaths(files); m_entriesWithoutChildren = entriesWithoutChildren(files).count(); m_destination = destination; const bool isSuccessful = processOldEntries(no_entries, Move); if (isSuccessful) { qCDebug(ARK) << "Moved" << no_entries << "entries within archive"; } else { qCDebug(ARK) << "Moving entries failed"; } finish(isSuccessful); return isSuccessful; } bool ReadWriteLibarchivePlugin::copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(options); qCDebug(ARK) << "Copying" << files.size() << "entries"; if (!initializeReader()) { return false; } if (!initializeWriter()) { return false; } // Copy old elements from previous archive to new archive. int no_entries = 0; m_filesPaths = entryFullPaths(files); m_entriesWithoutChildren = 0; // we don't care m_destination = destination; const bool isSuccessful = processOldEntries(no_entries, Copy); if (isSuccessful) { qCDebug(ARK) << "Copied" << no_entries << "entries within archive"; } else { qCDebug(ARK) << "Copying entries failed"; } finish(isSuccessful); return isSuccessful; } bool ReadWriteLibarchivePlugin::deleteFiles(const QList &files) { qCDebug(ARK) << "Deleting" << files.size() << "entries"; if (!initializeReader()) { return false; } if (!initializeWriter()) { return false; } // Copy old elements from previous archive to new archive. int no_entries = 0; m_filesPaths = entryFullPaths(files); const bool isSuccessful = processOldEntries(no_entries, Delete); if (isSuccessful) { qCDebug(ARK) << "Removed" << no_entries << "entries from archive"; } else { qCDebug(ARK) << "Removing entries failed"; } finish(isSuccessful); return isSuccessful; } bool ReadWriteLibarchivePlugin::initializeWriter(const bool creatingNewFile, const CompressionOptions &options) { // |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). m_tempFile.setFileName(filename()); if (!m_tempFile.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) { emit error(xi18nc("@info", "Failed to create a temporary file to compress %1.", filename())); return false; } m_archiveWriter.reset(archive_write_new()); if (!(m_archiveWriter.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(m_archiveWriter.data()); if (creatingNewFile) { if (!initializeNewFileWriterFilters(options)) { return false; } } else { if (!initializeWriterFilters()) { return false; } } if (archive_write_open_fd(m_archiveWriter.data(), m_tempFile.handle()) != ARCHIVE_OK) { emit error(xi18nc("@info", "Opening the archive for writing failed with the following error:" "%1", QLatin1String(archive_error_string(m_archiveWriter.data())))); return false; } return true; } bool ReadWriteLibarchivePlugin::initializeWriterFilters() { int ret; bool requiresExecutable = false; switch (archive_filter_code(m_archiveReader.data(), 0)) { case ARCHIVE_FILTER_GZIP: ret = archive_write_add_filter_gzip(m_archiveWriter.data()); break; case ARCHIVE_FILTER_BZIP2: ret = archive_write_add_filter_bzip2(m_archiveWriter.data()); break; case ARCHIVE_FILTER_XZ: ret = archive_write_add_filter_xz(m_archiveWriter.data()); break; case ARCHIVE_FILTER_LZMA: ret = archive_write_add_filter_lzma(m_archiveWriter.data()); break; case ARCHIVE_FILTER_COMPRESS: ret = archive_write_add_filter_compress(m_archiveWriter.data()); break; case ARCHIVE_FILTER_LZIP: ret = archive_write_add_filter_lzip(m_archiveWriter.data()); break; case ARCHIVE_FILTER_LZOP: ret = archive_write_add_filter_lzop(m_archiveWriter.data()); break; case ARCHIVE_FILTER_LRZIP: ret = archive_write_add_filter_lrzip(m_archiveWriter.data()); requiresExecutable = true; break; #ifdef HAVE_LIBARCHIVE_3_2_0 case ARCHIVE_FILTER_LZ4: ret = archive_write_add_filter_lz4(m_archiveWriter.data()); break; #endif case ARCHIVE_FILTER_NONE: ret = archive_write_add_filter_none(m_archiveWriter.data()); break; default: emit error(i18n("The compression type '%1' is not supported by Ark.", QLatin1String(archive_filter_name(m_archiveReader.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(m_archiveWriter.data())))); return false; } return true; } bool ReadWriteLibarchivePlugin::initializeNewFileWriterFilters(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(m_archiveWriter.data()); } else if (filename().right(3).toUpper() == QLatin1String("BZ2")) { qCDebug(ARK) << "Detected bzip2 compression for new file"; ret = archive_write_add_filter_bzip2(m_archiveWriter.data()); } else if (filename().right(2).toUpper() == QLatin1String("XZ")) { qCDebug(ARK) << "Detected xz compression for new file"; ret = archive_write_add_filter_xz(m_archiveWriter.data()); } else if (filename().right(4).toUpper() == QLatin1String("LZMA")) { qCDebug(ARK) << "Detected lzma compression for new file"; ret = archive_write_add_filter_lzma(m_archiveWriter.data()); } else if (filename().right(2).toUpper() == QLatin1String(".Z")) { qCDebug(ARK) << "Detected compress (.Z) compression for new file"; ret = archive_write_add_filter_compress(m_archiveWriter.data()); } else if (filename().right(2).toUpper() == QLatin1String("LZ")) { qCDebug(ARK) << "Detected lzip compression for new file"; ret = archive_write_add_filter_lzip(m_archiveWriter.data()); } else if (filename().right(3).toUpper() == QLatin1String("LZO")) { qCDebug(ARK) << "Detected lzop compression for new file"; ret = archive_write_add_filter_lzop(m_archiveWriter.data()); } else if (filename().right(3).toUpper() == QLatin1String("LRZ")) { qCDebug(ARK) << "Detected lrzip compression for new file"; ret = archive_write_add_filter_lrzip(m_archiveWriter.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(m_archiveWriter.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(m_archiveWriter.data()); } else { qCDebug(ARK) << "Falling back to gzip"; ret = archive_write_add_filter_gzip(m_archiveWriter.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(m_archiveWriter.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(m_archiveWriter.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(m_archiveWriter.data())))); return false; } } return true; } void ReadWriteLibarchivePlugin::finish(const bool isSuccessful) { if (!isSuccessful) { m_tempFile.cancelWriting(); } archive_write_close(m_archiveWriter.data()); m_tempFile.commit(); } bool ReadWriteLibarchivePlugin::processOldEntries(int &entriesCounter, OperationMode mode) { struct archive_entry *entry; entriesCounter = 0; QMap pathMap; if (mode == Move || mode == Copy) { m_filesPaths.sort(); QStringList resultList = entryPathsFromDestination(m_filesPaths, m_destination, m_entriesWithoutChildren); const int listSize = m_filesPaths.count(); Q_ASSERT(listSize == resultList.count()); for (int i = 0; i < listSize; ++i) { pathMap.insert(m_filesPaths.at(i), resultList.at(i)); } } while ((mode != Add || !m_abortOperation) && archive_read_next_header(m_archiveReader.data(), &entry) == ARCHIVE_OK) { const QString file = QFile::decodeName(archive_entry_pathname(entry)); if (mode == Move || mode == Copy) { const QString newPathname = pathMap.value(file); if (!newPathname.isEmpty()) { if (mode == Copy) { if (!writeEntry(entry)) { return false; } } else { emit entryRemoved(file); } entriesCounter++; archive_entry_set_pathname(entry, newPathname.toUtf8()); } } else if (m_filesPaths.contains(file)) { archive_read_data_skip(m_archiveReader.data()); switch (mode) { case Delete: entriesCounter++; emit entryRemoved(file); break; case Add: qCDebug(ARK) << file << "is already present in the new archive, skipping."; break; default: qCDebug(ARK) << "Mode" << mode << "is not considered for processing old libarchive entries"; Q_ASSERT(false); } continue; } if (writeEntry(entry)) { if (mode == Add) { entriesCounter++; } else if (mode == Move || mode == Copy) { emitEntryFromArchiveEntry(entry); } } else { return false; } } return true; } bool ReadWriteLibarchivePlugin::writeEntry(struct archive_entry *entry) { const int returnCode = archive_write_header(m_archiveWriter.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)), m_archiveReader.data(), m_archiveWriter.data(), false); break; case ARCHIVE_FAILED: case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(m_archiveWriter.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) { 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(m_archiveWriter.data(), entry)) == ARCHIVE_OK) { // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(absoluteFilename, m_archiveWriter.data(), false); } else { qCCritical(ARK) << "Writing header failed with error code " << header_response; qCCritical(ARK) << "Error while writing..." << archive_error_string(m_archiveWriter.data()) << "(error no =" << archive_errno(m_archiveWriter.data()) << ')'; emit error(xi18nc("@info Error in a message box", "Ark could not compress %1:%2", absoluteFilename, QString::fromUtf8(archive_error_string(m_archiveWriter.data())))); archive_entry_free(entry); return false; } m_writtenFiles.push_back(destinationFilename); emitEntryFromArchiveEntry(entry); archive_entry_free(entry); return true; } #include "readwritelibarchiveplugin.moc" diff --git a/plugins/libarchive/readwritelibarchiveplugin.h b/plugins/libarchive/readwritelibarchiveplugin.h index 955e7077..81f87d0c 100644 --- a/plugins/libarchive/readwritelibarchiveplugin.h +++ b/plugins/libarchive/readwritelibarchiveplugin.h @@ -1,95 +1,96 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal + * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef READWRITELIBARCHIVEPLUGIN_H #define READWRITELIBARCHIVEPLUGIN_H #include "libarchiveplugin.h" #include #include #include using namespace Kerfuffle; class ReadWriteLibarchivePlugin : public LibarchivePlugin { Q_OBJECT public: explicit ReadWriteLibarchivePlugin(QObject *parent, const QVariantList &args); ~ReadWriteLibarchivePlugin(); bool addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions &options) Q_DECL_OVERRIDE; bool moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) Q_DECL_OVERRIDE; bool copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) Q_DECL_OVERRIDE; bool deleteFiles(const QList &files) Q_DECL_OVERRIDE; protected: bool initializeWriter(const bool creatingNewFile = false, const CompressionOptions &options = CompressionOptions()); bool initializeWriterFilters(); bool initializeNewFileWriterFilters(const CompressionOptions &options); void finish(const bool isSuccessful); private: /** * Processes all the existing entries and does manipulations to them * based on the OperationMode (Add/Move/Copy/Delete). * * @param entriesCounter Counter of added/moved/copied/deleted entries. * * @return bool indicating whether the operation was successful. */ bool processOldEntries(int &entriesCounter, OperationMode mode); /** * Writes entry being read into memory. * * @return bool indicating whether the operation was successful. */ bool writeEntry(struct archive_entry *entry); /** * Writes entry from physical disk. * * @return bool indicating whether the operation was successful. */ bool writeFile(const QString &relativeName, const QString &destination); QSaveFile m_tempFile; ArchiveWrite m_archiveWriter; // New added files by addFiles methods. It's assigned to m_filesPaths // and then is used by processOldEntries method (in Add mode) for skipping already written entries. QStringList m_writtenFiles; // Passed argument from job which is used by processOldEntries method. QStringList m_filesPaths; int m_entriesWithoutChildren; const Archive::Entry *m_destination; }; #endif // READWRITELIBARCHIVEPLUGIN_H