diff --git a/autotests/kerfuffle/data/addonsu-remove-14.1-x86-signed.zip b/autotests/kerfuffle/data/addonsu-remove-14.1-x86-signed.zip new file mode 100644 index 00000000..584504ae Binary files /dev/null and b/autotests/kerfuffle/data/addonsu-remove-14.1-x86-signed.zip differ diff --git a/autotests/kerfuffle/loadtest.cpp b/autotests/kerfuffle/loadtest.cpp index b4157352..075bf2b4 100644 --- a/autotests/kerfuffle/loadtest.cpp +++ b/autotests/kerfuffle/loadtest.cpp @@ -1,263 +1,296 @@ /* * Copyright (c) 2010-2011 Raphael Kubo da Costa * Copyright (c) 2016 Elvis Angelaccio * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "archive_kerfuffle.h" #include "jobs.h" #include "pluginmanager.h" #include "testhelper.h" #include #include using namespace Kerfuffle; class LoadTest : public QObject { Q_OBJECT private Q_SLOTS: void testProperties_data(); void testProperties(); }; QTEST_GUILESS_MAIN(LoadTest) void LoadTest::testProperties_data() { QTest::addColumn("archivePath"); QTest::addColumn("expectedBaseName"); QTest::addColumn("isReadOnly"); QTest::addColumn("canFallbackOnReadOnly"); QTest::addColumn("isSingleFile"); QTest::addColumn("isSingleFolder"); QTest::addColumn("isMultiVolume"); QTest::addColumn("numberOfVolumes"); QTest::addColumn("expectedEncryptionType"); QTest::addColumn("expectedSubfolderName"); + QTest::addColumn("expectedComment"); // Test non-existent tar archive. QTest::newRow("non-existent tar archive") << QStringLiteral("/tmp/foo.tar.gz") << QStringLiteral("foo") << false << false << false << false << false << 0 << Archive::Unencrypted + << QString() << QString(); // Test non-archive file QTest::newRow("not an archive") << QStringLiteral("/tmp/foo.pdf") << QString() << false << false << false << false << false << 0 << Archive::Unencrypted + << QString() << QString(); // Test dummy source code tarball. QTest::newRow("dummy source code tarball") << QFINDTESTDATA("data/code-x.y.z.tar.gz") << QStringLiteral("code-x.y.z") << false << false << false << true << false << 0 << Archive::Unencrypted - << QStringLiteral("awesome_project"); + << QStringLiteral("awesome_project") + << QString(); QTest::newRow("simple compressed tar archive") << QFINDTESTDATA("data/simplearchive.tar.gz") << QStringLiteral("simplearchive") << false << false << false << false << false << 0 << Archive::Unencrypted - << QStringLiteral("simplearchive"); + << QStringLiteral("simplearchive") + << QString(); QTest::newRow("encrypted zip, single entry") << QFINDTESTDATA("data/archivetest_encrypted.zip") << QStringLiteral("archivetest_encrypted") << false << true << true << false << false << 0 << Archive::Encrypted - << QStringLiteral("archivetest_encrypted"); + << QStringLiteral("archivetest_encrypted") + << QString(); QTest::newRow("simple zip, one unencrypted entry") << QFINDTESTDATA("data/archivetest_unencrypted.zip") << QStringLiteral("archivetest_unencrypted") << false << true << true << false << false << 0 << Archive::Unencrypted - << QStringLiteral("archivetest_unencrypted"); + << QStringLiteral("archivetest_unencrypted") + << QString(); QTest::newRow("rpm archive, no single folder") << QFINDTESTDATA("data/wget.rpm") << QStringLiteral("wget") << true << false << false << false << false << 0 << Archive::Unencrypted - << QStringLiteral("wget"); + << QStringLiteral("wget") + << QString(); QTest::newRow("bzip2-compressed tarball") << QFINDTESTDATA("data/simplearchive.tar.bz2") << QStringLiteral("simplearchive") << false << false << false << false << false << 0 << Archive::Unencrypted - << QStringLiteral("simplearchive"); + << QStringLiteral("simplearchive") + << QString(); QTest::newRow("xz-compressed tarball") << QFINDTESTDATA("data/simplearchive.tar.xz") << QStringLiteral("simplearchive") << false << false << false << false << false << 0 << Archive::Unencrypted - << QStringLiteral("simplearchive"); + << QStringLiteral("simplearchive") + << QString(); QTest::newRow("lzma-compressed tarball") << QFINDTESTDATA("data/simplearchive.tar.lzma") << QStringLiteral("simplearchive") << false << false << false << false << false << 0 << Archive::Unencrypted - << QStringLiteral("simplearchive"); + << QStringLiteral("simplearchive") + << QString(); QTest::newRow("compress (.Z) tarball") << QFINDTESTDATA("data/simplearchive.tar.Z") << QStringLiteral("simplearchive") << false << false << false << false << false << 0 << Archive::Unencrypted - << QStringLiteral("simplearchive"); + << QStringLiteral("simplearchive") + << QString(); QTest::newRow("lzipped tarball") << QFINDTESTDATA("data/simplearchive.tar.lz") << QStringLiteral("simplearchive") << false << false << false << false << false << 0 << Archive::Unencrypted - << QStringLiteral("simplearchive"); + << QStringLiteral("simplearchive") + << QString(); if (PluginManager().supportedMimeTypes().contains(QStringLiteral("application/x-tzo"))) { QTest::newRow("lzop-compressed tarball") << QFINDTESTDATA("data/simplearchive.tar.lzo") << QStringLiteral("simplearchive") << false << false << false << false << false << 0 << Archive::Unencrypted - << QStringLiteral("simplearchive"); + << QStringLiteral("simplearchive") + << QString(); } else { qDebug() << "tar.lzo format not available. Skipping lzo test."; } // Only run test for lrzipped tar if lrzip executable is found in path. if (!QStandardPaths::findExecutable(QStringLiteral("lrzip")).isEmpty()) { QTest::newRow("lrzipped tarball") << QFINDTESTDATA("data/simplearchive.tar.lrz") << QStringLiteral("simplearchive") << false << false << false << false << false << 0 << Archive::Unencrypted - << QStringLiteral("simplearchive"); + << QStringLiteral("simplearchive") + << QString(); } else { qDebug() << "lrzip executable not found in path. Skipping lrzip test."; } // Only run test for lz4-compressed tar if lz4 executable is found in path. if (!QStandardPaths::findExecutable(QStringLiteral("lz4")).isEmpty()) { QTest::newRow("lz4-compressed tarball") << QFINDTESTDATA("data/simplearchive.tar.lz4") << QStringLiteral("simplearchive") << false << false << false << false << false << 0 << Archive::Unencrypted - << QStringLiteral("simplearchive"); + << QStringLiteral("simplearchive") + << QString(); } else { qDebug() << "lz4 executable not found in path. Skipping lz4 test."; } QTest::newRow("xar archive") << QFINDTESTDATA("data/simplearchive.xar") << QStringLiteral("simplearchive") << true << false << false << false << false << 0 << Archive::Unencrypted - << QStringLiteral("simplearchive"); + << QStringLiteral("simplearchive") + << QString(); QTest::newRow("mimetype child of application/zip") << QFINDTESTDATA("data/test.odt") << QStringLiteral("test") << false << true << false << false << false << 0 << Archive::Unencrypted - << QStringLiteral("test"); + << QStringLiteral("test") + << QString(); QTest::newRow("AppImage") << QFINDTESTDATA("data/hello-1.0-x86_64.AppImage") << QStringLiteral("hello-1.0-x86_64") << true << false << false << false << false << 0 << Archive::Unencrypted - << QStringLiteral("hello-1.0-x86_64"); + << QStringLiteral("hello-1.0-x86_64") + << QString(); QTest::newRow("7z multivolume") << QFINDTESTDATA("data/archive-multivolume.7z.001") << QStringLiteral("archive-multivolume") << true << false << false << false << true << 3 << Archive::Unencrypted - << QStringLiteral("archive-multivolume"); + << QStringLiteral("archive-multivolume") + << QString(); QTest::newRow("rar multivolume") << QFINDTESTDATA("data/archive-multivolume.part1.rar") << QStringLiteral("archive-multivolume") << true << false << false << false << true << 3 << Archive::Unencrypted - << QStringLiteral("archive-multivolume"); + << QStringLiteral("archive-multivolume") + << QString(); QTest::newRow("zip with only an empty folder") << QFINDTESTDATA("data/single-empty-folder.zip") << QStringLiteral("single-empty-folder") << false << true << false << true << false << 0 << Archive::Unencrypted - << QStringLiteral("empty"); + << QStringLiteral("empty") + << QString(); + + QTest::newRow("zip created by lineageos with comment") + << QFINDTESTDATA("data/addonsu-remove-14.1-x86-signed.zip") + << QStringLiteral("addonsu-remove-14.1-x86-signed") + << false << true << false << false << false << 0 << Archive::Unencrypted + << QStringLiteral("addonsu-remove-14.1-x86-signed") + << QStringLiteral("signed by SignApk"); } void LoadTest::testProperties() { QFETCH(QString, archivePath); auto loadJob = Archive::load(archivePath, this); QVERIFY(loadJob); loadJob->setAutoDelete(false); TestHelper::startAndWaitForResult(loadJob); auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { QVERIFY(archive->fileName().isEmpty()); QVERIFY(!archive->hasComment()); QVERIFY(archive->error() != NoError); QSKIP("Could not find a plugin to handle the archive. Skipping test.", SkipSingle); } QFETCH(QString, expectedBaseName); QCOMPARE(archive->completeBaseName(), expectedBaseName); QFETCH(bool, isReadOnly); QFETCH(bool, canFallbackOnReadOnly); // If the plugin supports fallback on read-only mode, we cannot be sure at this point // if the archive is going to be read-write or read-only. if (!canFallbackOnReadOnly) { QCOMPARE(archive->isReadOnly(), isReadOnly); } QFETCH(bool, isSingleFile); QCOMPARE(archive->isSingleFile(), isSingleFile); QFETCH(bool, isSingleFolder); QCOMPARE(archive->isSingleFolder(), isSingleFolder); if (isSingleFile || isSingleFolder) { QVERIFY(!archive->hasMultipleTopLevelEntries()); } QFETCH(bool, isMultiVolume); QCOMPARE(archive->isMultiVolume(), isMultiVolume); QFETCH(int, numberOfVolumes); QCOMPARE(archive->numberOfVolumes(), numberOfVolumes); QFETCH(Archive::EncryptionType, expectedEncryptionType); QCOMPARE(archive->encryptionType(), expectedEncryptionType); QFETCH(QString, expectedSubfolderName); QCOMPARE(archive->subfolderName(), expectedSubfolderName); + QFETCH(QString, expectedComment); + QCOMPARE(archive->hasComment(), !expectedComment.isEmpty()); + QCOMPARE(archive->comment(), expectedComment); + loadJob->deleteLater(); archive->deleteLater(); } #include "loadtest.moc" diff --git a/autotests/plugins/clizipplugin/cliziptest.cpp b/autotests/plugins/clizipplugin/cliziptest.cpp index 162b829e..da51860e 100644 --- a/autotests/plugins/clizipplugin/cliziptest.cpp +++ b/autotests/plugins/clizipplugin/cliziptest.cpp @@ -1,233 +1,246 @@ /* * Copyright (c) 2016 Elvis Angelaccio * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "cliziptest.h" #include "cliplugin.h" #include QTEST_GUILESS_MAIN(CliZipTest) using namespace Kerfuffle; void CliZipTest::initTestCase() { m_plugin = new Plugin(this); foreach (Plugin *plugin, m_pluginManger.availablePlugins()) { if (plugin->metaData().pluginId() == QStringLiteral("kerfuffle_clizip")) { m_plugin = plugin; return; } } } void CliZipTest::testListArgs_data() { QTest::addColumn("archiveName"); + QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("fake zip") << QStringLiteral("/tmp/foo.zip") + << QString() + << QStringList { + QStringLiteral("-l"), + QStringLiteral("-T"), + QStringLiteral("-z"), + QStringLiteral("/tmp/foo.zip") + }; + + QTest::newRow("fake encrypted zip") + << QStringLiteral("/tmp/foo.zip") + << QStringLiteral("1234") << QStringList { QStringLiteral("-l"), QStringLiteral("-T"), QStringLiteral("-z"), QStringLiteral("/tmp/foo.zip") }; } void CliZipTest::testListArgs() { if (!m_plugin->isValid()) { QSKIP("clizip plugin not available. Skipping test.", SkipSingle); } QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName), QVariant::fromValue(m_plugin->metaData())}); QVERIFY(plugin); - const auto replacedArgs = plugin->cliProperties()->listArgs(archiveName, QString()); + QFETCH(QString, password); + const auto replacedArgs = plugin->cliProperties()->listArgs(archiveName, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void CliZipTest::testAddArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("compressionLevel"); QTest::addColumn("compressionMethod"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.zip") << QString() << 3 << QStringLiteral("Deflate") << QStringList { QStringLiteral("-r"), QStringLiteral("-3"), QStringLiteral("-Zdeflate"), QStringLiteral("/tmp/foo.zip") }; QTest::newRow("encrypted") << QStringLiteral("/tmp/foo.zip") << QStringLiteral("1234") << 3 << QString() << QStringList { QStringLiteral("-r"), QStringLiteral("-P1234"), QStringLiteral("-3"), QStringLiteral("/tmp/foo.zip") }; QTest::newRow("comp-method-bzip2") << QStringLiteral("/tmp/foo.zip") << QString() << 3 << QStringLiteral("BZip2") << QStringList { QStringLiteral("-r"), QStringLiteral("-3"), QStringLiteral("-Zbzip2"), QStringLiteral("/tmp/foo.zip") }; } void CliZipTest::testAddArgs() { if (!m_plugin->isValid()) { QSKIP("clizip plugin not available. Skipping test.", SkipSingle); } QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName), QVariant::fromValue(m_plugin->metaData())}); QVERIFY(plugin); QFETCH(QString, password); QFETCH(int, compressionLevel); QFETCH(QString, compressionMethod); const auto replacedArgs = plugin->cliProperties()->addArgs(archiveName, {}, password, false, compressionLevel, compressionMethod, QString(), 0); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void CliZipTest::testExtractArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn>("files"); QTest::addColumn("preservePaths"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("preserve paths, encrypted") << QStringLiteral("/tmp/foo.zip") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << true << QStringLiteral("1234") << QStringList { QStringLiteral("-P1234"), QStringLiteral("/tmp/foo.zip"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("preserve paths, unencrypted") << QStringLiteral("/tmp/foo.zip") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << true << QString() << QStringList { QStringLiteral("/tmp/foo.zip"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, encrypted") << QStringLiteral("/tmp/foo.zip") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << false << QStringLiteral("1234") << QStringList { QStringLiteral("-j"), QStringLiteral("-P1234"), QStringLiteral("/tmp/foo.zip"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, unencrypted") << QStringLiteral("/tmp/foo.zip") << QVector { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << false << QString() << QStringList { QStringLiteral("-j"), QStringLiteral("/tmp/foo.zip"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; } void CliZipTest::testExtractArgs() { if (!m_plugin->isValid()) { QSKIP("clizip plugin not available. Skipping test.", SkipSingle); } QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName), QVariant::fromValue(m_plugin->metaData())}); QVERIFY(plugin); QFETCH(QVector, files); QStringList filesList; foreach (const Archive::Entry *e, files) { filesList << e->fullPath(NoTrailingSlash); } QFETCH(bool, preservePaths); QFETCH(QString, password); const auto replacedArgs = plugin->cliProperties()->extractArgs(archiveName, filesList, preservePaths, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } diff --git a/kerfuffle/cliproperties.cpp b/kerfuffle/cliproperties.cpp index b2965193..8eb05fe3 100644 --- a/kerfuffle/cliproperties.cpp +++ b/kerfuffle/cliproperties.cpp @@ -1,361 +1,363 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "cliproperties.h" #include "ark_debug.h" #include "archiveformat.h" #include "pluginmanager.h" namespace Kerfuffle { CliProperties::CliProperties(QObject *parent, const KPluginMetaData &metaData, const QMimeType &archiveType) : QObject(parent) , m_mimeType(archiveType) , m_metaData(metaData) { } QStringList CliProperties::addArgs(const QString &archive, const QStringList &files, const QString &password, bool headerEncryption, int compressionLevel, const QString &compressionMethod, const QString &encryptionMethod, ulong volumeSize) { if (!encryptionMethod.isEmpty()) { Q_ASSERT(!password.isEmpty()); } QStringList args; foreach (const QString &s, m_addSwitch) { args << s; } if (!password.isEmpty()) { args << substitutePasswordSwitch(password, headerEncryption); } if (compressionLevel > -1) { args << substituteCompressionLevelSwitch(compressionLevel); } if (!compressionMethod.isEmpty()) { args << substituteCompressionMethodSwitch(compressionMethod); } if (!encryptionMethod.isEmpty()) { args << substituteEncryptionMethodSwitch(encryptionMethod); } if (volumeSize > 0) { args << substituteMultiVolumeSwitch(volumeSize); } args << archive; args << files; args.removeAll(QString()); return args; } QStringList CliProperties::commentArgs(const QString &archive, const QString &commentfile) { QStringList args; foreach (const QString &s, substituteCommentSwitch(commentfile)) { args << s; } args << archive; args.removeAll(QString()); return args; } QStringList CliProperties::deleteArgs(const QString &archive, const QVector &files, const QString &password) { QStringList args; args << m_deleteSwitch; if (!password.isEmpty()) { args << substitutePasswordSwitch(password); } args << archive; foreach (const Archive::Entry *e, files) { args << e->fullPath(NoTrailingSlash); } args.removeAll(QString()); return args; } QStringList CliProperties::extractArgs(const QString &archive, const QStringList &files, bool preservePaths, const QString &password) { QStringList args; if (preservePaths && !m_extractSwitch.isEmpty()) { args << m_extractSwitch; } else if (!preservePaths && !m_extractSwitchNoPreserve.isEmpty()) { args << m_extractSwitchNoPreserve; } if (!password.isEmpty()) { args << substitutePasswordSwitch(password); } args << archive; args << files; args.removeAll(QString()); return args; } QStringList CliProperties::listArgs(const QString &archive, const QString &password) { QStringList args; foreach (const QString &s, m_listSwitch) { args << s; } - if (!password.isEmpty()) { + + const auto encryptionType = ArchiveFormat::fromMetadata(m_mimeType, m_metaData).encryptionType(); + if (!password.isEmpty() && encryptionType == Archive::EncryptionType::HeaderEncrypted) { args << substitutePasswordSwitch(password); } args << archive; args.removeAll(QString()); return args; } QStringList CliProperties::moveArgs(const QString &archive, const QVector &entries, Archive::Entry *destination, const QString &password) { QStringList args; args << m_moveSwitch; if (!password.isEmpty()) { args << substitutePasswordSwitch(password); } args << archive; if (entries.count() > 1) { foreach (const Archive::Entry *file, entries) { args << file->fullPath(NoTrailingSlash) << destination->fullPath() + file->name(); } } else { args << entries.at(0)->fullPath(NoTrailingSlash) << destination->fullPath(NoTrailingSlash); } args.removeAll(QString()); return args; } QStringList CliProperties::testArgs(const QString &archive, const QString &password) { QStringList args; foreach (const QString &s, m_testSwitch) { args << s; } if (!password.isEmpty()) { args << substitutePasswordSwitch(password); } args << archive; args.removeAll(QString()); return args; } QStringList CliProperties::substituteCommentSwitch(const QString &commentfile) const { Q_ASSERT(!commentfile.isEmpty()); Q_ASSERT(ArchiveFormat::fromMetadata(m_mimeType, m_metaData).supportsWriteComment()); QStringList commentSwitches = m_commentSwitch; Q_ASSERT(!commentSwitches.isEmpty()); QMutableListIterator i(commentSwitches); while (i.hasNext()) { i.next(); i.value().replace(QLatin1String("$CommentFile"), commentfile); } return commentSwitches; } QStringList CliProperties::substitutePasswordSwitch(const QString &password, bool headerEnc) const { if (password.isEmpty()) { return QStringList(); } Archive::EncryptionType encryptionType = ArchiveFormat::fromMetadata(m_mimeType, m_metaData).encryptionType(); Q_ASSERT(encryptionType != Archive::EncryptionType::Unencrypted); QStringList passwordSwitch; if (headerEnc) { passwordSwitch = m_passwordSwitchHeaderEnc; } else { passwordSwitch = m_passwordSwitch; } Q_ASSERT(!passwordSwitch.isEmpty()); QMutableListIterator i(passwordSwitch); while (i.hasNext()) { i.next(); i.value().replace(QLatin1String("$Password"), password); } return passwordSwitch; } QString CliProperties::substituteCompressionLevelSwitch(int level) const { if (level < 0 || level > 9) { return QString(); } Q_ASSERT(ArchiveFormat::fromMetadata(m_mimeType, m_metaData).maxCompressionLevel() != -1); QString compLevelSwitch = m_compressionLevelSwitch; Q_ASSERT(!compLevelSwitch.isEmpty()); compLevelSwitch.replace(QLatin1String("$CompressionLevel"), QString::number(level)); return compLevelSwitch; } QString CliProperties::substituteCompressionMethodSwitch(const QString &method) const { if (method.isEmpty()) { return QString(); } Q_ASSERT(!ArchiveFormat::fromMetadata(m_mimeType, m_metaData).compressionMethods().isEmpty()); QString compMethodSwitch = m_compressionMethodSwitch[m_mimeType.name()].toString(); Q_ASSERT(!compMethodSwitch.isEmpty()); QString cliMethod = ArchiveFormat::fromMetadata(m_mimeType, m_metaData).compressionMethods().value(method).toString(); compMethodSwitch.replace(QLatin1String("$CompressionMethod"), cliMethod); return compMethodSwitch; } QString CliProperties::substituteEncryptionMethodSwitch(const QString &method) const { if (method.isEmpty()) { return QString(); } const ArchiveFormat format = ArchiveFormat::fromMetadata(m_mimeType, m_metaData); Q_ASSERT(!format.encryptionMethods().isEmpty()); QString encMethodSwitch = m_encryptionMethodSwitch[m_mimeType.name()].toString(); if (encMethodSwitch.isEmpty()) { return QString(); } Q_ASSERT(format.encryptionMethods().contains(method)); encMethodSwitch.replace(QLatin1String("$EncryptionMethod"), method); return encMethodSwitch; } QString CliProperties::substituteMultiVolumeSwitch(ulong volumeSize) const { // The maximum value we allow in the QDoubleSpinBox is 1,000,000MB. Converted to // KB this is 1,024,000,000. if (volumeSize <= 0 || volumeSize > 1024000000) { return QString(); } Q_ASSERT(ArchiveFormat::fromMetadata(m_mimeType, m_metaData).supportsMultiVolume()); QString multiVolumeSwitch = m_multiVolumeSwitch; Q_ASSERT(!multiVolumeSwitch.isEmpty()); multiVolumeSwitch.replace(QLatin1String("$VolumeSize"), QString::number(volumeSize)); return multiVolumeSwitch; } bool CliProperties::isPasswordPrompt(const QString &line) { foreach(const QString &rx, m_passwordPromptPatterns) { if (QRegularExpression(rx).match(line).hasMatch()) { return true; } } return false; } bool CliProperties::isWrongPasswordMsg(const QString &line) { foreach(const QString &rx, m_wrongPasswordPatterns) { if (QRegularExpression(rx).match(line).hasMatch()) { return true; } } return false; } bool CliProperties::isTestPassedMsg(const QString &line) { foreach(const QString &rx, m_testPassedPatterns) { if (QRegularExpression(rx).match(line).hasMatch()) { return true; } } return false; } bool CliProperties::isfileExistsMsg(const QString &line) { foreach(const QString &rx, m_fileExistsPatterns) { if (QRegularExpression(rx).match(line).hasMatch()) { return true; } } return false; } bool CliProperties::isFileExistsFileName(const QString &line) { foreach(const QString &rx, m_fileExistsFileName) { if (QRegularExpression(rx).match(line).hasMatch()) { return true; } } return false; } bool CliProperties::isCorruptArchiveMsg(const QString &line) { foreach(const QString &rx, m_corruptArchivePatterns) { if (QRegularExpression(rx).match(line).hasMatch()) { return true; } } return false; } bool CliProperties::isDiskFullMsg(const QString &line) { foreach(const QString &rx, m_diskFullPatterns) { if (QRegularExpression(rx).match(line).hasMatch()) { return true; } } return false; } } diff --git a/part/archivemodel.cpp b/part/archivemodel.cpp index 9bd04c77..9e6a4509 100644 --- a/part/archivemodel.cpp +++ b/part/archivemodel.cpp @@ -1,905 +1,905 @@ /* * 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 "ark_debug.h" #include "jobs.h" #include #include #include #include #include #include #include using namespace Kerfuffle; // Used to speed up the loading of large archives. static Archive::Entry *s_previousMatch = nullptr; Q_GLOBAL_STATIC(QStringList, s_previousPieces) ArchiveModel::ArchiveModel(const QString &dbusPathName, QObject *parent) : QAbstractItemModel(parent) , m_dbusPathName(dbusPathName) , m_numberOfFiles(0) , m_numberOfFolders(0) { initRootEntry(); // Mappings between column indexes and entry properties. m_propertiesMap = { { FullPath, "fullPath" }, { Size, "size" }, { CompressedSize, "compressedSize" }, { Permissions, "permissions" }, { Owner, "owner" }, { Group, "group" }, { Ratio, "ratio" }, { CRC, "CRC" }, { Method, "method" }, { Version, "version" }, { Timestamp, "timestamp" }, }; } 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()) { uint dirs; uint files; entry->countChildren(dirs, files); return KIO::itemsSummaryString(dirs + files, 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(m_propertiesMap[column]); } } 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(NoTrailingSlash)).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 nullptr; } 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"); 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.data(); Q_ASSERT(parentEntry->isDir()); const Archive::Entry *item = parentEntry->entries().value(row, nullptr); if (item != 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.data())) { 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 nullptr; } int ArchiveModel::rowCount(const QModelIndex &parent) const { if (parent.column() <= 0) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : m_rootEntry.data(); if (parentEntry && parentEntry->isDir()) { return parentEntry->entries().count(); } } return 0; } int ArchiveModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) return m_showColumns.size(); } 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; } if (archive()->isReadOnly() || (archive()->encryptionType() != Archive::Unencrypted && archive()->password().isEmpty())) { emit messageWidget(KMessageWidget::Error, i18n("Adding files is not supported for this archive.")); return false; } QStringList paths; foreach(const QUrl &url, data->urls()) { paths << url.toLocalFile(); } const Archive::Entry *entry = nullptr; QModelIndex droppedOnto = index(row, column, parent); if (droppedOnto.isValid()) { entry = entryForIndex(droppedOnto); if (!entry->isDir()) { entry = entry->getParent(); } } - emit droppedFiles(paths, entry, QString()); + emit droppedFiles(paths, entry); 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; } void ArchiveModel::initRootEntry() { m_rootEntry.reset(new Archive::Entry()); m_rootEntry->setProperty("isDirectory", true); } Archive::Entry *ArchiveModel::parentFor(const Archive::Entry *entry, InsertBehaviour behaviour) { QStringList pieces = entry->fullPath().split(QLatin1Char('/'), QString::SkipEmptyParts); if (pieces.isEmpty()) { return nullptr; } pieces.removeLast(); // Used to speed up loading of large archives. 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; // Check if all pieces match. 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.data(); 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.data()) ? QString(piece + QLatin1Char('/')) : QString(parent->fullPath(WithTrailingSlash) + piece + QLatin1Char('/'))); entry->setProperty("isDirectory", true); insertEntry(entry, behaviour); } 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, behaviour); } parent = entry; } s_previousMatch = parent; *s_previousPieces = pieces; return parent; } QModelIndex ArchiveModel::indexForEntry(Archive::Entry *entry) { Q_ASSERT(entry); if (entry != m_rootEntry.data()) { 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(NoTrailingSlash)); parent->removeEntryAt(entry->row()); endRemoveRows(); } } void ArchiveModel::slotUserQuery(Kerfuffle::Query *query) { query->execute(); } void ArchiveModel::slotNewEntry(Archive::Entry *entry) { newEntry(entry, NotifyViews); } void ArchiveModel::slotListEntry(Archive::Entry *entry) { newEntry(entry, DoNotNotifyViews); } 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; const auto size = receivedEntry->property("size").toULongLong(); const auto compressedSize = receivedEntry->property("compressedSize").toULongLong(); for (auto i = m_propertiesMap.begin(); i != m_propertiesMap.end(); ++i) { // Singlefile plugin doesn't report the uncompressed size. if (i.key() == Size && size == 0 && compressedSize > 0) { continue; } if (!receivedEntry->property(i.value()).toString().isEmpty()) { if (i.key() != CompressedSize || receivedEntry->compressedSizeIsSet) { toInsert << i.key(); } } } if (behaviour == NotifyViews) { beginInsertColumns(QModelIndex(), 0, toInsert.size() - 1); } m_showColumns << toInsert; if (behaviour == NotifyViews) { 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); // For some archive formats (e.g. AppImage and RPM) paths of folders do not // contain a trailing slash, so we append it. if (receivedEntry->property("isDirectory").toBool() && !receivedEntry->property("fullPath").toString().endsWith(QLatin1Char('/'))) { receivedEntry->setProperty("fullPath", QString(receivedEntry->property("fullPath").toString() + QLatin1Char('/'))); qCDebug(ARK) << "Trailing slash appended to entry:" << receivedEntry->property("fullPath"); } // Skip already created entries. Archive::Entry *existing = m_rootEntry->findByPath(entryFileName.split(QLatin1Char('/'))); if (existing) { 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; } // Find parent entry, creating missing directory Archive::Entry's in the process. Archive::Entry *parent = parentFor(receivedEntry, behaviour); // Create an Archive::Entry. const QStringList path = entryFileName.split(QLatin1Char('/'), QString::SkipEmptyParts); Archive::Entry *entry = parent->find(path.last()); if (entry) { entry->copyMetaData(receivedEntry); entry->setProperty("fullPath", entryFileName); } else { receivedEntry->setParent(parent); insertEntry(receivedEntry, behaviour); } } void ArchiveModel::slotLoadingFinished(KJob *job) { if (!job->error()) { m_archive.reset(qobject_cast(job)->archive()); beginResetModel(); endResetModel(); } 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; entry->isDir() ? icon = QIcon::fromTheme(db.mimeTypeForName(QStringLiteral("inode/directory")).iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small)) : icon = QIcon::fromTheme(db.mimeTypeForFile(entry->fullPath()).iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small)); m_entryIcons.insert(entry->fullPath(NoTrailingSlash), icon); } Kerfuffle::Archive* ArchiveModel::archive() const { return m_archive.data(); } void ArchiveModel::reset() { m_archive.reset(nullptr); s_previousMatch = nullptr; s_previousPieces->clear(); initRootEntry(); // TODO: make sure if it's ok to not have calls to beginRemoveColumns here m_showColumns.clear(); beginResetModel(); endResetModel(); } void ArchiveModel::createEmptyArchive(const QString &path, const QString &mimeType, QObject *parent) { reset(); m_archive.reset(Archive::createEmpty(path, mimeType, parent)); } KJob *ArchiveModel::loadArchive(const QString &path, const QString &mimeType, QObject *parent) { reset(); auto loadJob = Archive::load(path, mimeType, parent); connect(loadJob, &KJob::result, this, &ArchiveModel::slotLoadingFinished); connect(loadJob, &Job::newEntry, this, &ArchiveModel::slotListEntry); connect(loadJob, &Job::userQuery, this, &ArchiveModel::slotUserQuery); emit loadingStarted(); return loadJob; } ExtractJob* ArchiveModel::extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { QVector files({file}); return extractFiles(files, destinationDir, options); } ExtractJob* ArchiveModel::extractFiles(const QVector& 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(QVector &entries, const Archive::Entry *destination, const CompressionOptions& options) { if (!m_archive) { return 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 nullptr; } Kerfuffle::MoveJob *ArchiveModel::moveFiles(QVector &entries, Archive::Entry *destination, const CompressionOptions &options) { if (!m_archive) { return 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 nullptr; } Kerfuffle::CopyJob *ArchiveModel::copyFiles(QVector &entries, Archive::Entry *destination, const CompressionOptions &options) { if (!m_archive) { return 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 nullptr; } DeleteJob* ArchiveModel::deleteFiles(QVector 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 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.data(); } } 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 != 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 QVector &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(NoTrailingSlash)); rawEntry->getParent()->removeEntryAt(rawEntry->row()); endRemoveRows(); } } void ArchiveModel::countEntriesAndSize() { // This function is used to count the number of folders/files and // the total compressed size. This is needed for PropertiesDialog // to update the corresponding values after adding/deleting files. // When ArchiveModel has been properly fixed, this code can likely // be removed. m_numberOfFiles = 0; m_numberOfFolders = 0; m_uncompressedSize = 0; QElapsedTimer timer; timer.start(); traverseAndCountDirNode(m_rootEntry.data()); qCDebug(ARK) << "Time to count entries and size:" << timer.elapsed() << "ms"; } void ArchiveModel::traverseAndCountDirNode(Archive::Entry *dir) { foreach(Archive::Entry *entry, dir->entries()) { if (entry->isDir()) { traverseAndCountDirNode(entry); m_numberOfFolders++; } else { m_numberOfFiles++; m_uncompressedSize += entry->property("size").toULongLong(); } } } qulonglong ArchiveModel::numberOfFiles() const { return m_numberOfFiles; } qulonglong ArchiveModel::numberOfFolders() const { return m_numberOfFolders; } qulonglong ArchiveModel::uncompressedSize() const { return m_uncompressedSize; } QList ArchiveModel::shownColumns() const { return m_showColumns; } QMap ArchiveModel::propertiesMap() const { return m_propertiesMap; } diff --git a/part/archivemodel.h b/part/archivemodel.h index ead6a219..83bc7854 100644 --- a/part/archivemodel.h +++ b/part/archivemodel.h @@ -1,199 +1,199 @@ /* * 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 "archiveentry.h" #include #include #include using Kerfuffle::Archive; namespace Kerfuffle { class Query; } /** * 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 */ }; class ArchiveModel: public QAbstractItemModel { Q_OBJECT public: explicit ArchiveModel(const QString &dbusPathName, QObject *parent = nullptr); ~ArchiveModel() override; QVariant data(const QModelIndex &index, int role) const override; Qt::ItemFlags flags(const QModelIndex &index) const override; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const override; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; QModelIndex parent(const QModelIndex &index) const override; int rowCount(const QModelIndex &parent = QModelIndex()) const override; int columnCount(const QModelIndex &parent = QModelIndex()) const override; //drag and drop related Qt::DropActions supportedDropActions() const override; QStringList mimeTypes() const override; QMimeData *mimeData(const QModelIndexList & indexes) const override; bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) override; void reset(); void createEmptyArchive(const QString &path, const QString &mimeType, QObject *parent); KJob* loadArchive(const QString &path, const QString &mimeType, QObject *parent); Kerfuffle::Archive *archive() const; QList shownColumns() const; QMap propertiesMap() const; Archive::Entry *entryForIndex(const QModelIndex &index); Kerfuffle::ExtractJob* extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; Kerfuffle::ExtractJob* extractFiles(const QVector& 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(QVector &entries, const Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::MoveJob* moveFiles(QVector &entries, Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::CopyJob* copyFiles(QVector &entries, Archive::Entry *destination, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); Kerfuffle::DeleteJob* deleteFiles(QVector 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); void countEntriesAndSize(); qulonglong numberOfFiles() const; qulonglong numberOfFolders() const; qulonglong uncompressedSize() const; /** * 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 QVector &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&); + void droppedFiles(const QStringList& files, const Archive::Entry*); void messageWidget(KMessageWidget::MessageType type, const QString& msg); private slots: void slotNewEntry(Archive::Entry *entry); void slotListEntry(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); void initRootEntry(); enum InsertBehaviour { NotifyViews, DoNotNotifyViews }; Archive::Entry *parentFor(const Kerfuffle::Archive::Entry *entry, InsertBehaviour behaviour = NotifyViews); 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. */ void insertEntry(Archive::Entry *entry, InsertBehaviour behaviour = NotifyViews); void newEntry(Kerfuffle::Archive::Entry *receivedEntry, InsertBehaviour behaviour); void traverseAndCountDirNode(Archive::Entry *dir); QList m_showColumns; QScopedPointer m_archive; QScopedPointer m_rootEntry; QHash m_entryIcons; QMap m_propertiesMap; QString m_dbusPathName; qulonglong m_numberOfFiles; qulonglong m_numberOfFolders; qulonglong m_uncompressedSize; }; #endif // ARCHIVEMODEL_H diff --git a/part/part.cpp b/part/part.cpp index c7eea80f..d8713a85 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1,1740 +1,1751 @@ /* * 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 "archivesortfiltermodel.h" #include "archiveview.h" #include "arkviewer.h" #include "dnddbusinterfaceadaptor.h" #include "infopanel.h" #include "jobtracker.h" #include "generalsettingspage.h" #include "extractiondialog.h" #include "extractionsettingspage.h" #include "jobs.h" #include "settings.h" #include "previewsettingspage.h" #include "propertiesdialog.h" #include "pluginsettingspage.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 #include #include using namespace Kerfuffle; namespace Ark { static quint32 s_instanceCounter = 1; Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList& args) : KParts::ReadWritePart(parent), m_splitter(nullptr), m_busy(false), m_jobTracker(nullptr) { Q_UNUSED(args) KAboutData aboutData(QStringLiteral("ark"), i18n("ArkPart"), QStringLiteral("3.0")); setComponentData(aboutData, 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_filterModel = new ArchiveSortFilterModel(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->setWordWrap(true); 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); // Setup search widget. m_searchWidget = new QWidget(parentWidget); m_searchWidget->setVisible(false); m_searchWidget->setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Maximum); QHBoxLayout *searchLayout = new QHBoxLayout; searchLayout->setContentsMargins(2, 2, 2, 2); m_vlayout->addWidget(m_searchWidget); m_searchWidget->setLayout(searchLayout); m_searchCloseButton = new QPushButton(QIcon::fromTheme(QStringLiteral("dialog-close")), QString(), m_searchWidget); m_searchCloseButton->setFlat(true); m_searchLineEdit = new QLineEdit(m_searchWidget); m_searchLineEdit->setClearButtonEnabled(true); m_searchLineEdit->setPlaceholderText(i18n("Type to search...")); mainWidget->installEventFilter(this); searchLayout->addWidget(m_searchCloseButton); searchLayout->addWidget(m_searchLineEdit); connect(m_searchCloseButton, &QPushButton::clicked, this, [=]() { m_searchWidget->hide(); m_searchLineEdit->clear(); }); connect(m_searchLineEdit, &QLineEdit::textChanged, this, &Part::searchEdited); // 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, QOverload::of(&Part::slotAddFiles)); + this, &Part::slotDroppedFiles); connect(m_model, &ArchiveModel::error, this, &Part::slotError); connect(m_model, &ArchiveModel::messageWidget, this, &Part::displayMsgWidget); connect(this, &Part::busy, this, &Part::setBusyGui); connect(this, &Part::ready, this, &Part::setReadyGui); connect(this, &KParts::ReadOnlyPart::urlChanged, this, &Part::setFileNameFromArchive); connect(this, QOverload<>::of(&KParts::ReadOnlyPart::completed), this, &Part::setFileNameFromArchive); connect(this, QOverload<>::of(&KParts::ReadOnlyPart::completed), this, &Part::slotCompleted); connect(ArkSettings::self(), &KCoreConfigSkeleton::configChanged, this, &Part::updateActions); m_statusBarExtension = new KParts::StatusBarExtension(this); setXMLFile(QStringLiteral("ark_part.rc")); } Part::~Part() { qDeleteAll(m_tmpExtractDirList); // 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()) { + if (!m_model->archive() || m_commentView->toPlainText().isEmpty()) { 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(); } } void Part::registerJob(KJob* job) { if (!m_jobTracker) { m_jobTracker = new JobTracker(widget()); m_statusBarExtension->addStatusBarItem(m_jobTracker->widget(nullptr), 0, true); m_jobTracker->widget(job)->show(); } KIO::getJobTracker()->registerJob(job); 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.setDragAndDropEnabled(true); // Create and start the ExtractJob. ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(getSelectedIndexes())), destination, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } void Part::guiActivateEvent(KParts::GUIActivateEvent *event) { // #357660: prevent parent's implementation from changing the window title. Q_UNUSED(event) } void Part::setupView() { m_view->setContextMenuPolicy(Qt::CustomContextMenu); m_filterModel->setSourceModel(m_model); m_view->setModel(m_filterModel); m_filterModel->setFilterKeyColumn(0); m_filterModel->setFilterCaseSensitivity(Qt::CaseInsensitive); 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); } void Part::slotActivated(const QModelIndex &index) { Q_UNUSED(index) // 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(this); 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 = KStandardAction::saveAs(this, &Part::slotSaveAs, nullptr); actionCollection()->addAction(QStringLiteral("ark_file_save_as"), m_saveAsAction); 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, QOverload<>::of(&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, QOverload<>::of(&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, QOverload<>::of(&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...")); m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive")); actionCollection()->setDefaultShortcut(m_addFilesAction, Qt::ALT + Qt::Key_A); connect(m_addFilesAction, &QAction::triggered, this, QOverload<>::of(&Part::slotAddFiles)); m_renameFileAction = KStandardAction::renameFile(m_view, &ArchiveView::renameSelectedEntry, actionCollection()); m_deleteFilesAction = KStandardAction::deleteFile(this, &Part::slotDeleteFiles, actionCollection()); m_deleteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-remove"))); actionCollection()->setDefaultShortcut(m_deleteFilesAction, Qt::Key_Delete); m_cutFilesAction = KStandardAction::cut(this, &Part::slotCutFiles, actionCollection()); m_copyFilesAction = KStandardAction::copy(this, &Part::slotCopyFiles, actionCollection()); m_pasteFilesAction = KStandardAction::paste(this, QOverload<>::of(&Part::slotPasteFiles), actionCollection()); 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); m_searchAction = KStandardAction::find(this, &Part::slotShowFind, actionCollection()); connect(m_signalMapper, QOverload::of(&QSignalMapper::mapped), this, &Part::slotOpenEntry); updateActions(); updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); } void Part::updateActions() { const bool isWritable = isArchiveWritable(); const Archive::Entry *entry = m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex())); int selectedEntriesCount = m_view->selectionModel()->selectedRows().count(); // We disable adding files if the archive is encrypted but the password is // unknown (this happens when opening existing non-he password-protected // archives). If we added files they would not get encrypted resulting in an // archive with a mixture of encrypted and unencrypted files. const bool isEncryptedButUnknownPassword = m_model->archive() && m_model->archive()->encryptionType() != Archive::Unencrypted && m_model->archive()->password().isEmpty(); if (isEncryptedButUnknownPassword) { m_addFilesAction->setToolTip(xi18nc("@info:tooltip", "Adding files to existing password-protected archives with no header-encryption is currently not supported." "Extract the files and create a new archive if you want to add files.")); m_testArchiveAction->setToolTip(xi18nc("@info:tooltip", "Testing password-protected archives with no header-encryption is currently not supported.")); } else { m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive")); m_testArchiveAction->setToolTip(i18nc("@info:tooltip", "Click to test the archive for integrity")); } // 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 != nullptr && entry->property("size").toLongLong() < maxPreviewSize)); const bool isDir = (entry == 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 && !isEncryptedButUnknownPassword); 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_searchAction->setEnabled(!isBusy() && m_model->rowCount() > 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 && !isEncryptedButUnknownPassword); } 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() << static_cast(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(); } bool Part::isArchiveWritable() const { return isReadWrite() && m_model->archive() && !m_model->archive()->isReadOnly(); } bool Part::isCreatingNewArchive() const { return arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true"); } void Part::createArchive() { const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")]; m_model->createEmptyArchive(localFilePath(), fixedMimeType, m_model); if (arguments().metaData().contains(QStringLiteral("volumeSize"))) { m_model->archive()->setMultiVolume(true); } const QString password = arguments().metaData()[QStringLiteral("encryptionPassword")]; if (!password.isEmpty()) { m_model->encryptArchive(password, arguments().metaData()[QStringLiteral("encryptHeader")] == QLatin1String("true")); } } void Part::loadArchive() { const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")]; auto job = m_model->loadArchive(localFilePath(), fixedMimeType, m_model); if (job) { registerJob(job); job->start(); } else { updateActions(); } } void Part::resetGui() { m_messageWidget->hide(); m_commentView->clear(); m_commentBox->hide(); m_infoPanel->setIndex(QModelIndex()); // Also reset format-specific compression options. m_compressionOptions = CompressionOptions(); } 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().constLast()); } 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()) { QString userDestination = triggeredAction->data().toString(); QString finalDestinationDirectory; const QString detectedSubfolder = detectSubfolder(); qCDebug(ARK) << "Detected subfolder" << detectedSubfolder; if (m_model->archive()->hasMultipleTopLevelEntries()) { if (!userDestination.endsWith(QDir::separator())) { userDestination.append(QDir::separator()); } finalDestinationDirectory = userDestination + detectedSubfolder; QDir(userDestination).mkdir(detectedSubfolder); } else { finalDestinationDirectory = userDestination; } qCDebug(ARK) << "Extracting to:" << finalDestinationDirectory; ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(getSelectedIndexes())), finalDestinationDirectory, ExtractionOptions()); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } } void Part::selectionChanged() { m_infoPanel->setIndexes(getSelectedIndexes()); } QModelIndexList Part::getSelectedIndexes() { QModelIndexList list; foreach (const QModelIndex &i, m_view->selectionModel()->selectedRows()) { list.append(m_filterModel->mapToSource(i)); } return list; } +void Part::readCompressionOptions() +{ + // Store options from CreateDialog if they are set. + if (!m_compressionOptions.isCompressionLevelSet() && arguments().metaData().contains(QStringLiteral("compressionLevel"))) { + m_compressionOptions.setCompressionLevel(arguments().metaData()[QStringLiteral("compressionLevel")].toInt()); + } + if (m_compressionOptions.compressionMethod().isEmpty() && arguments().metaData().contains(QStringLiteral("compressionMethod"))) { + m_compressionOptions.setCompressionMethod(arguments().metaData()[QStringLiteral("compressionMethod")]); + } + if (m_compressionOptions.encryptionMethod().isEmpty() && arguments().metaData().contains(QStringLiteral("encryptionMethod"))) { + m_compressionOptions.setEncryptionMethod(arguments().metaData()[QStringLiteral("encryptionMethod")]); + } + if (!m_compressionOptions.isVolumeSizeSet() && arguments().metaData().contains(QStringLiteral("volumeSize"))) { + m_compressionOptions.setVolumeSize(arguments().metaData()[QStringLiteral("volumeSize")].toULong()); + } + + const auto compressionMethods = m_model->archive()->property("compressionMethods").toStringList(); + qCDebug(ARK) << "compmethods:" << compressionMethods; + if (compressionMethods.size() == 1) { + m_compressionOptions.setCompressionMethod(compressionMethods.first()); + } +} + bool Part::openFile() { qCDebug(ARK) << "Attempting to open archive" << localFilePath(); resetGui(); if (!isLocalFileValid()) { return false; } if (isCreatingNewArchive()) { createArchive(); return true; } loadArchive(); // Loading is async, we don't know yet whether we got a valid archive. return false; } 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 GeneralSettingsPage(parent, i18nc("@title:tab", "General Settings"), QStringLiteral("go-home"))); pages.append(new ExtractionSettingsPage(parent, i18nc("@title:tab", "Extraction Settings"), QStringLiteral("archive-extract"))); pages.append(new PluginSettingsPage(parent, i18nc("@title:tab", "Plugin Settings"), QStringLiteral("plugins"))); 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); if (localFileInfo.isDir()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "%1 is a directory.", localFile)); return false; } if (isCreatingNewArchive()) { 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"), KStandardGuiItem::overwrite(), KStandardGuiItem::cancel()); if (buttonCode != KMessageBox::Yes || !targetInfo.isWritable()) { return false; } qCDebug(ARK) << "Removing file" << targetFile; return QFile(targetFile).remove(); } void Part::slotCompleted() { if (isCreatingNewArchive()) { m_view->setDropsEnabled(true); updateActions(); return; } // Existing archive, setup the view for it. m_view->sortByColumn(0, Qt::AscendingOrder); m_view->expandIfSingleFolder(); m_view->header()->resizeSections(QHeaderView::ResizeToContents); m_view->setDropsEnabled(isArchiveWritable()); 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.")); } } if (arguments().metaData()[QStringLiteral("showExtractDialog")] == QLatin1String("true")) { QTimer::singleShot(0, this, &Part::slotShowExtractionDialog); } updateActions(); } void Part::slotLoadingStarted() { m_model->filesToMove.clear(); m_model->filesToCopy.clear(); } void Part::slotLoadingFinished(KJob *job) { if (!job->error()) { emit completed(); return; } // Loading failed or was canceled by the user (e.g. password dialog rejected). emit canceled(job->errorString()); m_view->setDropsEnabled(false); m_model->reset(); closeUrl(); setFileNameFromArchive(); updateActions(); if (job->error() != KJob::KilledJobError) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Loading the archive %1 failed with the following error:%2", localFilePath(), job->errorString())); } } 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) { QModelIndex index = m_filterModel->mapToSource(m_view->selectionModel()->currentIndex()); Archive::Entry *entry = m_model->entryForIndex(index); // Don't open directories. if (entry->isDir()) { return; } // Don't open files bigger than the size limit. const int maxPreviewSize = ArkSettings::previewFileSizeLimit() * 1024 * 1024; if (ArkSettings::limitPreviewFileSize() && entry->property("size").toLongLong() >= maxPreviewSize) { 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()) { qCDebug(ARK) << "Opening with mode" << mode; m_openFileMode = static_cast(mode); KJob *job = 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_tmpExtractDirList << openJob->tempDir(); const QString fullName = openJob->validatedFilePath(); if (isArchiveWritable()) { m_fileWatcher = new QFileSystemWatcher; connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &Part::slotWatchedFileModified); m_fileWatcher->addPath(fullName); } else { // 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. QFile::setPermissions(fullName, QFileDevice::ReadOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther); } 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(), KRun::RunFlags()); } } 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); m_tmpExtractDirList << previewJob->tempDir(); 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_tmpExtractDirList) { 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, 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); } } 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(widget())); dialog.data()->setModal(true); if (m_view->selectionModel()->selectedRows().count() > 0) { dialog.data()->setShowSelectedFiles(true); } dialog.data()->setExtractToSubfolder(m_model->archive()->hasMultipleTopLevelEntries()); 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); QVector files; // If the user has chosen to extract only selected entries, fetch these // from the QTreeView. if (!dialog.data()->extractAllFiles()) { files = filesAndRootNodesForIndexes(addChildren(getSelectedIndexes())); } qCDebug(ARK) << "Selected " << files; Kerfuffle::ExtractionOptions options; options.setPreservePaths(dialog->preservePaths()); const QString destinationDirectory = dialog.data()->destinationDirectory().toLocalFile(); 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; } QVector Part::filesForIndexes(const QModelIndexList& list) const { QVector ret; foreach(const QModelIndex& index, list) { ret << m_model->entryForIndex(index); } return ret; } QVector Part::filesAndRootNodesForIndexes(const QModelIndexList& list) const { QVector 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); 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(), KRun::RunExecutables, QString(), QByteArray()); } if (ArkSettings::closeAfterExtraction()) { emit quit(); } } } void Part::slotAddFiles(const QStringList& filesToAdd, const Archive::Entry *destination, const QString &relPath) { if (!m_model->archive() || filesToAdd.isEmpty()) { return; } QStringList withChildPaths; foreach (const QString& file, filesToAdd) { m_jobTempEntries.push_back(new Archive::Entry(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 == nullptr) ? QString() : QStringLiteral("to ") + destination->fullPath()); } // Remove trailing slash (needed when adding dirs). if (globalWorkDir.right(1) == QLatin1String("/")) { globalWorkDir.chop(1); } // We need to override the global options with a working directory. CompressionOptions compOptions = m_compressionOptions; // Now take the absolute path of the parent directory. globalWorkDir = QFileInfo(globalWorkDir).dir().absolutePath(); qCDebug(ARK) << "Detected GlobalWorkDir to be " << globalWorkDir; compOptions.setGlobalWorkDir(globalWorkDir); AddJob *job = m_model->addFiles(m_jobTempEntries, destination, compOptions); if (!job) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); return; } connect(job, &KJob::result, this, &Part::slotAddFilesDone); registerJob(job); job->start(); } -void Part::slotAddFiles() +void Part::slotDroppedFiles(const QStringList &files, const Archive::Entry *destination) { - // Store options from CreateDialog if they are set. - if (!m_compressionOptions.isCompressionLevelSet() && arguments().metaData().contains(QStringLiteral("compressionLevel"))) { - m_compressionOptions.setCompressionLevel(arguments().metaData()[QStringLiteral("compressionLevel")].toInt()); - } - if (m_compressionOptions.compressionMethod().isEmpty() && arguments().metaData().contains(QStringLiteral("compressionMethod"))) { - m_compressionOptions.setCompressionMethod(arguments().metaData()[QStringLiteral("compressionMethod")]); - } - if (m_compressionOptions.encryptionMethod().isEmpty() && arguments().metaData().contains(QStringLiteral("encryptionMethod"))) { - m_compressionOptions.setEncryptionMethod(arguments().metaData()[QStringLiteral("encryptionMethod")]); - } - if (!m_compressionOptions.isVolumeSizeSet() && arguments().metaData().contains(QStringLiteral("volumeSize"))) { - m_compressionOptions.setVolumeSize(arguments().metaData()[QStringLiteral("volumeSize")].toULong()); - } + readCompressionOptions(); + slotAddFiles(files, destination, QString()); +} - const auto compressionMethods = m_model->archive()->property("compressionMethods").toStringList(); - qCDebug(ARK) << "compmethods:" << compressionMethods; - if (compressionMethods.size() == 1) { - m_compressionOptions.setCompressionMethod(compressionMethods.first()); - } +void Part::slotAddFiles() +{ + readCompressionOptions(); QString dialogTitle = i18nc("@title:window", "Add Files"); const Archive::Entry *destination = nullptr; if (m_view->selectionModel()->selectedRows().count() == 1) { destination = m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex())); if (destination->isDir()) { dialogTitle = i18nc("@title:window", "Add Files to %1", destination->fullPath());; } else { destination = nullptr; } } qCDebug(ARK) << "Opening AddDialog with opts:" << m_compressionOptions; // #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(), m_compressionOptions); if (dlg->exec() == QDialog::Accepted) { qCDebug(ARK) << "Selected files:" << dlg->selectedFiles(); qCDebug(ARK) << "Options:" << dlg->compressionOptions(); m_compressionOptions = dlg->compressionOptions(); slotAddFiles(dlg->selectedFiles(), destination, QString()); } delete dlg; } void Part::slotCutFiles() { QModelIndexList selectedRows = addChildren(getSelectedIndexes()); 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(getSelectedIndexes()))); 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(const QString &name) { if (name == QLatin1String(".") || name == QLatin1String("..") || name.contains(QLatin1Char('/'))) { displayMsgWidget(KMessageWidget::Error, i18n("Filename can't contain slashes and can't be equal to \".\" or \"..\"")); return; } const Archive::Entry *entry = m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex())); QVector entriesToMove = filesForIndexes(addChildren(getSelectedIndexes())); m_destination = new Archive::Entry(); const QString &entryPath = entry->fullPath(NoTrailingSlash); const QString rootPath = entryPath.left(entryPath.count() - entry->name().count()); QString path = rootPath + name; if (entry->isDir()) { path += QLatin1Char('/'); } m_destination->setFullPath(path); slotPasteFiles(entriesToMove, m_destination, 1); } void Part::slotPasteFiles() { m_destination = (m_view->selectionModel()->selectedRows().count() > 0) ? m_model->entryForIndex(m_filterModel->mapToSource(m_view->selectionModel()->currentIndex())) : nullptr; if (m_destination == nullptr) { m_destination = new Archive::Entry(nullptr, QString()); } else { m_destination = new Archive::Entry(nullptr, m_destination->fullPath()); } if (m_model->filesToMove.count() > 0) { // Changing destination to include new entry path if pasting only 1 entry. QVector entriesWithoutChildren = ReadOnlyArchiveInterface::entriesWithoutChildren(QVector::fromList(m_model->filesToMove.values())); if (entriesWithoutChildren.count() == 1) { const Archive::Entry *entry = entriesWithoutChildren.first(); auto entryName = entry->name(); if (entry->isDir()) { entryName += QLatin1Char('/'); } m_destination->setFullPath(m_destination->fullPath() + entryName); } foreach (const Archive::Entry *entry, entriesWithoutChildren) { if (entry->isDir() && m_destination->fullPath().startsWith(entry->fullPath())) { KMessageBox::error(widget(), i18n("Folders can't be moved into themselves."), i18n("Moving a folder into itself")); delete m_destination; return; } } auto entryList = QVector::fromList(m_model->filesToMove.values()); slotPasteFiles(entryList, m_destination, entriesWithoutChildren.count()); m_model->filesToMove.clear(); } else { auto entryList = QVector::fromList(m_model->filesToCopy.values()); slotPasteFiles(entryList, m_destination, 0); m_model->filesToCopy.clear(); } m_cutIndexes.clear(); updateActions(); } void Part::slotPasteFiles(QVector &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)) { displayMsgWidget(KMessageWidget::Error, i18n("Entries with the same names can't be pasted to the same destination.")); 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; } KJob *job; if (entriesWithoutChildren != 0) { job = m_model->moveFiles(files, destination, CompressionOptions()); } else { job = m_model->copyFiles(files, destination, CompressionOptions()); } 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(); // For multi-volume archive, we need to re-open the archive after adding files // because the name changes from e.g name.rar to name.part1.rar. if (m_model->archive()->isMultiVolume()) { qCDebug(ARK) << "Multi-volume archive detected, re-opening..."; KParts::OpenUrlArguments args = arguments(); args.metaData()[QStringLiteral("createNewArchive")] = QStringLiteral("false"); setArguments(args); openUrl(QUrl::fromLocalFile(m_model->archive()->multiVolumeName())); } } 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(getSelectedIndexes()))); connect(job, &KJob::result, this, &Part::slotDeleteFilesDone); registerJob(job); job->start(); } void Part::slotShowProperties() { m_model->countEntriesAndSize(); QPointer dialog(new Kerfuffle::PropertiesDialog(0, m_model->archive(), m_model->numberOfFiles(), m_model->numberOfFolders(), m_model->uncompressedSize())); 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()); } bool Part::eventFilter(QObject *target, QEvent *event) { Q_UNUSED(target) if (event->type() == QEvent::KeyPress) { QKeyEvent *e = static_cast(event); if (e->key() == Qt::Key_Escape) { m_searchWidget->hide(); m_searchLineEdit->clear(); return true; } } return false; } void Part::slotShowFind() { if (m_searchWidget->isVisible()) { m_searchLineEdit->selectAll(); } else { m_searchWidget->show(); } m_searchLineEdit->setFocus(); } void Part::searchEdited(const QString &text) { m_view->collapseAll(); m_filterModel->setFilterFixedString(text); if(text.isEmpty()) { m_view->collapseAll(); m_view->expandIfSingleFolder(); } else { m_view->expandAll(); } } 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 diff --git a/part/part.h b/part/part.h index 23179d13..e4394d06 100644 --- a/part/part.h +++ b/part/part.h @@ -1,244 +1,246 @@ /* * 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 "archiveentry.h" #include #include #include #include #include class ArchiveModel; class ArchiveSortFilterModel; class ArchiveView; class InfoPanel; class KAboutData; class KAbstractWidgetJobTracker; class KJob; class KToggleAction; class QAction; class QLineEdit; class QSplitter; class QTreeView; class QTemporaryDir; class QVBoxLayout; class QSignalMapper; class QFileSystemWatcher; class QGroupBox; class QPlainTextEdit; class QPushButton; 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() override; bool openFile() override; bool saveFile() override; bool isBusy() const override; KConfigSkeleton *config() const override; QList settingsPages(QWidget *parent) const override; bool eventFilter(QObject *target, QEvent *event) 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); protected: void guiActivateEvent(KParts::GUIActivateEvent *event) override; private slots: void slotCompleted(); 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); + void slotDroppedFiles(const QStringList &files, const Kerfuffle::Archive::Entry *destination); /** * 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(QVector &files, Kerfuffle::Archive::Entry *destination, int entriesWithoutChildren); void slotAddFiles(); void slotCutFiles(); void slotCopyFiles(); void slotRenameFile(const QString &name); void slotPasteFiles(); void slotAddFilesDone(KJob*); void slotPasteFilesDone(KJob*); void slotTestingDone(KJob*); void slotDeleteFiles(); void slotDeleteFilesDone(KJob*); void slotShowProperties(); void slotShowContextMenu(); void slotActivated(const QModelIndex &index); void slotToggleInfoPanel(bool); void slotSaveAs(); void updateActions(); void updateQuickExtractMenu(QAction *extractAction); void selectionChanged(); void setBusyGui(); void setReadyGui(); void setFileNameFromArchive(); void slotWatchedFileModified(const QString& file); void slotShowComment(); void slotAddComment(); void slotCommentChanged(); void slotTestArchive(); void slotShowFind(); void displayMsgWidget(KMessageWidget::MessageType type, const QString& msg); void searchEdited(const QString &text); signals: void busy(); void ready(); void quit(); private: /** * @return true if both the current archive and the part are read-write, false otherwise. */ bool isArchiveWritable() const; /** * @return Whether the part has been told to create a new archive. */ bool isCreatingNewArchive() const; void createArchive(); void loadArchive(); void resetGui(); void setupView(); void setupActions(); QString detectSubfolder() const; QVector filesForIndexes(const QModelIndexList& list) const; QVector filesAndRootNodesForIndexes(const QModelIndexList& list) const; QModelIndexList addChildren(const QModelIndexList &list) const; void registerJob(KJob *job); QModelIndexList getSelectedIndexes(); + void readCompressionOptions(); 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; QAction *m_searchAction; KToggleAction *m_showInfoPanelAction; InfoPanel *m_infoPanel; QSplitter *m_splitter; QList m_tmpExtractDirList; bool m_busy; OpenFileMode m_openFileMode; QUrl m_lastUsedAddPath; QVector 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; Kerfuffle::CompressionOptions m_compressionOptions; ArchiveSortFilterModel *m_filterModel; QWidget *m_searchWidget; QLineEdit *m_searchLineEdit; QPushButton *m_searchCloseButton; }; } // namespace Ark #endif // PART_H diff --git a/plugins/libzipplugin/libzipplugin.cpp b/plugins/libzipplugin/libzipplugin.cpp index a92d691d..6122392b 100644 --- a/plugins/libzipplugin/libzipplugin.cpp +++ b/plugins/libzipplugin/libzipplugin.cpp @@ -1,999 +1,999 @@ /* * Copyright (c) 2017 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "libzipplugin.h" #include "ark_debug.h" #include "queries.h" #include #include #include #include #include #include #include #include #include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(LibZipPluginFactory, "kerfuffle_libzip.json", registerPlugin();) // This is needed for hooking a C callback to a C++ non-static member // function. template struct Callback; template struct Callback { template static Ret callback(Args... args) { return func(args...); } static std::function func; }; // Initialize the static member. template std::function Callback::func; LibzipPlugin::LibzipPlugin(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args) , m_overwriteAll(false) , m_skipAll(false) , m_listAfterAdd(false) { qCDebug(ARK) << "Initializing libzip plugin"; } LibzipPlugin::~LibzipPlugin() { foreach (const auto e, m_emittedEntries) { // Entries might be passed to pending slots, so we just schedule their deletion. e->deleteLater(); } } bool LibzipPlugin::list() { qCDebug(ARK) << "Listing archive contents for:" << QFile::encodeName(filename()); setOperationMode(List); m_numberOfEntries = 0; int errcode = 0; zip_error_t err; // Open archive. zip_t *archive = zip_open(QFile::encodeName(filename()), ZIP_RDONLY, &errcode); zip_error_init_with_code(&err, errcode); if (!archive) { qCCritical(ARK) << "Failed to open archive. Code:" << errcode; emit error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err)))); return false; } // Fetch archive comment. - m_comment = QString::fromUtf8(zip_get_archive_comment(archive, nullptr, ZIP_FL_ENC_GUESS)); + m_comment = QString::fromLocal8Bit(zip_get_archive_comment(archive, nullptr, ZIP_FL_ENC_RAW)); // Get number of archive entries. const auto nofEntries = zip_get_num_entries(archive, 0); qCDebug(ARK) << "Found entries:" << nofEntries; // Loop through all archive entries. for (int i = 0; i < nofEntries; i++) { if (QThread::currentThread()->isInterruptionRequested()) { break; } emitEntryForIndex(archive, i); if (m_listAfterAdd) { // Start at 50%. emit progress(0.5 + (0.5 * float(i + 1) / nofEntries)); } else { emit progress(float(i + 1) / nofEntries); } } zip_close(archive); m_listAfterAdd = false; return true; } bool LibzipPlugin::addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd) { Q_UNUSED(numberOfEntriesToAdd) setOperationMode(Add); int errcode = 0; zip_error_t err; // Open archive. zip_t *archive = zip_open(QFile::encodeName(filename()), ZIP_CREATE, &errcode); zip_error_init_with_code(&err, errcode); if (!archive) { qCCritical(ARK) << "Failed to open archive. Code:" << errcode; emit error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err)))); return false; } uint i = 0; foreach (const Archive::Entry* e, files) { if (QThread::currentThread()->isInterruptionRequested()) { break; } // If entry is a directory, traverse and add all its files and subfolders. if (QFileInfo(e->fullPath()).isDir()) { if (!writeEntry(archive, e->fullPath(), destination, options, true)) { return false; } QDirIterator it(e->fullPath(), QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (!QThread::currentThread()->isInterruptionRequested() && it.hasNext()) { const QString path = it.next(); if (QFileInfo(path).isDir()) { if (!writeEntry(archive, path, destination, options, true)) { return false; } } else { if (!writeEntry(archive, path, destination, options)) { return false; } } i++; } } else { if (!writeEntry(archive, e->fullPath(), destination, options)) { return false; } } i++; } qCDebug(ARK) << "Added" << i << "entries"; // Register the callback function to get progress feedback. Callback::func = std::bind(&LibzipPlugin::progressEmitted, this, std::placeholders::_1); void (*c_func)(double) = static_cast(Callback::callback); zip_register_progress_callback(archive, c_func); qCDebug(ARK) << "Writing entries to disk..."; if (zip_close(archive)) { qCCritical(ARK) << "Failed to write archive"; emit error(xi18n("Failed to write archive.")); return false; } // We list the entire archive after adding files to ensure entry // properties are up-to-date. m_listAfterAdd = true; list(); return true; } void LibzipPlugin::progressEmitted(double pct) { // Go from 0 to 50%. The second half is the subsequent listing. emit progress(0.5 * pct); } bool LibzipPlugin::writeEntry(zip_t *archive, const QString &file, const Archive::Entry* destination, const CompressionOptions& options, bool isDir) { Q_ASSERT(archive); QByteArray destFile; if (destination) { destFile = QString(destination->fullPath() + file).toUtf8(); } else { destFile = file.toUtf8(); } qlonglong index; if (isDir) { index = zip_dir_add(archive, destFile, ZIP_FL_ENC_GUESS); if (index == -1) { // If directory already exists in archive, we get an error. qCWarning(ARK) << "Failed to add dir " << file << ":" << zip_strerror(archive); return true; } } else { zip_source_t *src = zip_source_file(archive, QFile::encodeName(file).constData(), 0, -1); Q_ASSERT(src); index = zip_file_add(archive, destFile, src, ZIP_FL_ENC_GUESS | ZIP_FL_OVERWRITE); if (index == -1) { zip_source_free(src); qCCritical(ARK) << "Could not add entry" << file << ":" << zip_strerror(archive); emit error(xi18n("Failed to add entry: %1", QString::fromUtf8(zip_strerror(archive)))); return false; } } #ifndef Q_OS_WIN // Set permissions. QT_STATBUF result; if (QT_STAT(QFile::encodeName(file), &result) != 0) { qCWarning(ARK) << "Failed to read permissions for:" << file; } else { zip_uint32_t attributes = result.st_mode << 16; if (zip_file_set_external_attributes(archive, index, ZIP_FL_UNCHANGED, ZIP_OPSYS_UNIX, attributes) != 0) { qCWarning(ARK) << "Failed to set external attributes for:" << file; } } #endif if (!password().isEmpty()) { Q_ASSERT(!options.encryptionMethod().isEmpty()); if (options.encryptionMethod() == QLatin1String("AES128")) { zip_file_set_encryption(archive, index, ZIP_EM_AES_128, password().toUtf8()); } else if (options.encryptionMethod() == QLatin1String("AES192")) { zip_file_set_encryption(archive, index, ZIP_EM_AES_192, password().toUtf8()); } else if (options.encryptionMethod() == QLatin1String("AES256")) { zip_file_set_encryption(archive, index, ZIP_EM_AES_256, password().toUtf8()); } } // Set compression level and method. zip_int32_t compMethod = ZIP_CM_DEFAULT; if (!options.compressionMethod().isEmpty()) { if (options.compressionMethod() == QLatin1String("Deflate")) { compMethod = ZIP_CM_DEFLATE; } else if (options.compressionMethod() == QLatin1String("BZip2")) { compMethod = ZIP_CM_BZIP2; } else if (options.compressionMethod() == QLatin1String("Store")) { compMethod = ZIP_CM_STORE; } } const int compLevel = options.isCompressionLevelSet() ? options.compressionLevel() : 6; if (zip_set_file_compression(archive, index, compMethod, compLevel) != 0) { qCCritical(ARK) << "Could not set compression options for" << file << ":" << zip_strerror(archive); emit error(xi18n("Failed to set compression options for entry: %1", QString::fromUtf8(zip_strerror(archive)))); return false; } return true; } bool LibzipPlugin::emitEntryForIndex(zip_t *archive, qlonglong index) { Q_ASSERT(archive); zip_stat_t statBuffer; if (zip_stat_index(archive, index, ZIP_FL_ENC_GUESS, &statBuffer)) { qCCritical(ARK) << "Failed to read stat for index" << index; return false; } auto e = new Archive::Entry(); if (statBuffer.valid & ZIP_STAT_NAME) { e->setFullPath(QString::fromUtf8(statBuffer.name)); } if (e->fullPath(PathFormat::WithTrailingSlash).endsWith(QDir::separator())) { e->setProperty("isDirectory", true); } if (statBuffer.valid & ZIP_STAT_MTIME) { e->setProperty("timestamp", QDateTime::fromTime_t(statBuffer.mtime)); } if (statBuffer.valid & ZIP_STAT_SIZE) { e->setProperty("size", (qulonglong)statBuffer.size); } if (statBuffer.valid & ZIP_STAT_COMP_SIZE) { e->setProperty("compressedSize", (qlonglong)statBuffer.comp_size); } if (statBuffer.valid & ZIP_STAT_CRC) { if (!e->isDir()) { e->setProperty("CRC", QString::number((qulonglong)statBuffer.crc, 16).toUpper()); } } if (statBuffer.valid & ZIP_STAT_COMP_METHOD) { switch(statBuffer.comp_method) { case ZIP_CM_STORE: e->setProperty("method", QStringLiteral("Store")); emit compressionMethodFound(QStringLiteral("Store")); break; case ZIP_CM_DEFLATE: e->setProperty("method", QStringLiteral("Deflate")); emit compressionMethodFound(QStringLiteral("Deflate")); break; case ZIP_CM_DEFLATE64: e->setProperty("method", QStringLiteral("Deflate64")); emit compressionMethodFound(QStringLiteral("Deflate64")); break; case ZIP_CM_BZIP2: e->setProperty("method", QStringLiteral("BZip2")); emit compressionMethodFound(QStringLiteral("BZip2")); break; case ZIP_CM_LZMA: e->setProperty("method", QStringLiteral("LZMA")); emit compressionMethodFound(QStringLiteral("LZMA")); break; case ZIP_CM_XZ: e->setProperty("method", QStringLiteral("XZ")); emit compressionMethodFound(QStringLiteral("XZ")); break; } } if (statBuffer.valid & ZIP_STAT_ENCRYPTION_METHOD) { if (statBuffer.encryption_method != ZIP_EM_NONE) { e->setProperty("isPasswordProtected", true); switch(statBuffer.encryption_method) { case ZIP_EM_TRAD_PKWARE: emit encryptionMethodFound(QStringLiteral("ZipCrypto")); break; case ZIP_EM_AES_128: emit encryptionMethodFound(QStringLiteral("AES128")); break; case ZIP_EM_AES_192: emit encryptionMethodFound(QStringLiteral("AES192")); break; case ZIP_EM_AES_256: emit encryptionMethodFound(QStringLiteral("AES256")); break; } } } // Read external attributes, which contains the file permissions. zip_uint8_t opsys; zip_uint32_t attributes; if (zip_file_get_external_attributes(archive, index, ZIP_FL_UNCHANGED, &opsys, &attributes) == -1) { qCCritical(ARK) << "Could not read external attributes for entry:" << QString::fromUtf8(statBuffer.name); emit error(xi18n("Failed to read metadata for entry: %1", QString::fromUtf8(statBuffer.name))); return false; } // Set permissions. switch (opsys) { case ZIP_OPSYS_UNIX: // Unix permissions are stored in the leftmost 16 bits of the external file attribute. e->setProperty("permissions", permissionsToString(attributes >> 16)); break; default: // TODO: non-UNIX. break; } emit entry(e); m_emittedEntries << e; return true; } bool LibzipPlugin::deleteFiles(const QVector &files) { setOperationMode(Delete); int errcode = 0; zip_error_t err; // Open archive. zip_t *archive = zip_open(QFile::encodeName(filename()), 0, &errcode); zip_error_init_with_code(&err, errcode); if (archive == nullptr) { qCCritical(ARK) << "Failed to open archive. Code:" << errcode; emit error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err)))); return false; } qulonglong i = 0; foreach (const Archive::Entry* e, files) { if (QThread::currentThread()->isInterruptionRequested()) { break; } const qlonglong index = zip_name_locate(archive, e->fullPath().toUtf8(), ZIP_FL_ENC_GUESS); if (index == -1) { qCCritical(ARK) << "Could not find entry to delete:" << e->fullPath(); emit error(xi18n("Failed to delete entry: %1", e->fullPath())); return false; } if (zip_delete(archive, index) == -1) { qCCritical(ARK) << "Could not delete entry" << e->fullPath() << ":" << zip_strerror(archive); emit error(xi18n("Failed to delete entry: %1", QString::fromUtf8(zip_strerror(archive)))); return false; } emit entryRemoved(e->fullPath()); emit progress(float(++i) / files.size()); } qCDebug(ARK) << "Deleted" << i << "entries"; if (zip_close(archive)) { qCCritical(ARK) << "Failed to write archive"; emit error(xi18n("Failed to write archive.")); return false; } return true; } bool LibzipPlugin::addComment(const QString& comment) { setOperationMode(Comment); int errcode = 0; zip_error_t err; // Open archive. zip_t *archive = zip_open(QFile::encodeName(filename()), 0, &errcode); zip_error_init_with_code(&err, errcode); if (archive == nullptr) { qCCritical(ARK) << "Failed to open archive. Code:" << errcode; emit error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err)))); return false; } // Set archive comment. if (zip_set_archive_comment(archive, comment.toUtf8(), comment.length())) { qCCritical(ARK) << "Failed to set comment:" << zip_strerror(archive); return false; } if (zip_close(archive)) { qCCritical(ARK) << "Failed to write archive"; emit error(xi18n("Failed to write archive.")); return false; } return true; } bool LibzipPlugin::testArchive() { qCDebug(ARK) << "Testing archive"; setOperationMode(Test); int errcode = 0; zip_error_t err; // Open archive performing extra consistency checks. zip_t *archive = zip_open(QFile::encodeName(filename()), ZIP_CHECKCONS, &errcode); zip_error_init_with_code(&err, errcode); if (archive == nullptr) { qCCritical(ARK) << "Failed to open archive:" << zip_error_strerror(&err); return false; } // Check CRC-32 for each archive entry. const int nofEntries = zip_get_num_entries(archive, 0); for (int i = 0; i < nofEntries; i++) { if (QThread::currentThread()->isInterruptionRequested()) { return false; } // Get statistic for entry. Used to get entry size. zip_stat_t statBuffer; if (zip_stat_index(archive, i, 0, &statBuffer) != 0) { qCCritical(ARK) << "Failed to read stat for" << statBuffer.name; return false; } zip_file *zipFile = zip_fopen_index(archive, i, 0); std::unique_ptr buf(new uchar[statBuffer.size]); const int len = zip_fread(zipFile, buf.get(), statBuffer.size); if (len == -1 || uint(len) != statBuffer.size) { qCCritical(ARK) << "Failed to read data for" << statBuffer.name; return false; } if (statBuffer.crc != crc32(0, &buf.get()[0], len)) { qCCritical(ARK) << "CRC check failed for" << statBuffer.name; return false; } emit progress(float(i) / nofEntries); } zip_close(archive); emit testSuccess(); return true; } bool LibzipPlugin::doKill() { QMutexLocker mutexLocker(&m_mutex); switch (m_operationMode) { case Add: case Copy: case Delete: case Move: return false; default: break; } return true; } bool LibzipPlugin::extractFiles(const QVector &files, const QString& destinationDirectory, const ExtractionOptions& options) { qCDebug(ARK) << "Extracting files to:" << destinationDirectory; setOperationMode(Extract); const bool extractAll = files.isEmpty(); const bool removeRootNode = options.isDragAndDropEnabled(); int errcode = 0; zip_error_t err; // Open archive. zip_t *archive = zip_open(QFile::encodeName(filename()), ZIP_RDONLY, &errcode); zip_error_init_with_code(&err, errcode); if (archive == nullptr) { qCCritical(ARK) << "Failed to open archive. Code:" << errcode; emit error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err)))); return false; } // Set password if known. if (!password().isEmpty()) { qCDebug(ARK) << "Password already known. Setting..."; zip_set_default_password(archive, password().toUtf8()); } // Get number of archive entries. const qlonglong nofEntries = extractAll ? zip_get_num_entries(archive, 0) : files.size(); // Extract entries. m_overwriteAll = false; // Whether to overwrite all files m_skipAll = false; // Whether to skip all files if (extractAll) { // We extract all entries. for (qlonglong i = 0; i < nofEntries; i++) { if (QThread::currentThread()->isInterruptionRequested()) { break; } if (!extractEntry(archive, QDir::fromNativeSeparators(QString::fromUtf8(zip_get_name(archive, i, ZIP_FL_ENC_GUESS))), QString(), destinationDirectory, options.preservePaths(), removeRootNode)) { qCDebug(ARK) << "Extraction failed"; return false; } emit progress(float(i + 1) / nofEntries); } } else { // We extract only the entries in files. qulonglong i = 0; foreach (const Archive::Entry* e, files) { if (QThread::currentThread()->isInterruptionRequested()) { break; } if (!extractEntry(archive, e->fullPath(), e->rootNode, destinationDirectory, options.preservePaths(), removeRootNode)) { qCDebug(ARK) << "Extraction failed"; return false; } emit progress(float(++i) / nofEntries); } } zip_close(archive); return true; } bool LibzipPlugin::extractEntry(zip_t *archive, const QString &entry, const QString &rootNode, const QString &destDir, bool preservePaths, bool removeRootNode) { const bool isDirectory = entry.endsWith(QDir::separator()); // Add trailing slash to destDir if not present. QString destDirCorrected(destDir); if (!destDir.endsWith(QDir::separator())) { destDirCorrected.append(QDir::separator()); } // Remove rootnode if supplied and set destination path. QString destination; if (preservePaths) { if (!removeRootNode || rootNode.isEmpty()) { destination = destDirCorrected + entry; } else { QString truncatedEntry = entry; truncatedEntry.remove(0, rootNode.size()); destination = destDirCorrected + truncatedEntry; } } else { if (isDirectory) { qCDebug(ARK) << "Skipping directory:" << entry; return true; } destination = destDirCorrected + QFileInfo(entry).fileName(); } // Store parent mtime. QString parentDir; if (isDirectory) { QDir pDir = QFileInfo(destination).dir(); pDir.cdUp(); parentDir = pDir.path(); } else { parentDir = QFileInfo(destination).path(); } // For top-level items, don't restore parent dir mtime. const bool restoreParentMtime = (parentDir + QDir::separator() != destDirCorrected); time_t parent_mtime; if (restoreParentMtime) { parent_mtime = QFileInfo(parentDir).lastModified().toMSecsSinceEpoch() / 1000; } // Create parent directories for files. For directories create them. if (!QDir().mkpath(QFileInfo(destination).path())) { qCDebug(ARK) << "Failed to create directory:" << QFileInfo(destination).path(); emit error(xi18n("Failed to create directory: %1", QFileInfo(destination).path())); return false; } // Get statistic for entry. Used to get entry size and mtime. zip_stat_t statBuffer; if (zip_stat(archive, entry.toUtf8(), 0, &statBuffer) != 0) { if (isDirectory && zip_error_code_zip(zip_get_error(archive)) == ZIP_ER_NOENT) { qCWarning(ARK) << "Skipping folder without entry:" << entry; return true; } qCCritical(ARK) << "Failed to read stat for entry" << entry; return false; } if (!isDirectory) { // Handle existing destination files. QString renamedEntry = entry; while (!m_overwriteAll && QFileInfo::exists(destination)) { if (m_skipAll) { return true; } else { Kerfuffle::OverwriteQuery query(renamedEntry); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } else if (query.responseSkip()) { return true; } else if (query.responseAutoSkip()) { m_skipAll = true; return true; } else if (query.responseRename()) { const QString newName(query.newFilename()); destination = QFileInfo(destination).path() + QDir::separator() + QFileInfo(newName).fileName(); renamedEntry = QFileInfo(entry).path() + QDir::separator() + QFileInfo(newName).fileName(); } else if (query.responseOverwriteAll()) { m_overwriteAll = true; break; } else if (query.responseOverwrite()) { break; } } } // Handle password-protected files. zip_file *zipFile = nullptr; bool firstTry = true; while (!zipFile) { zipFile = zip_fopen(archive, entry.toUtf8(), 0); if (zipFile) { break; } else if (zip_error_code_zip(zip_get_error(archive)) == ZIP_ER_NOPASSWD || zip_error_code_zip(zip_get_error(archive)) == ZIP_ER_WRONGPASSWD) { Kerfuffle::PasswordNeededQuery query(filename(), !firstTry); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } setPassword(query.password()); if (zip_set_default_password(archive, password().toUtf8())) { qCDebug(ARK) << "Failed to set password for:" << entry; } firstTry = false; } else { qCCritical(ARK) << "Failed to open file:" << zip_strerror(archive); emit error(xi18n("Failed to open '%1':%2", entry, QString::fromUtf8(zip_strerror(archive)))); return false; } } QFile file(destination); if (!file.open(QIODevice::WriteOnly)) { qCCritical(ARK) << "Failed to open file for writing"; emit error(xi18n("Failed to open file for writing: %1", destination)); return false; } QDataStream out(&file); // Write archive entry to file. We use a read/write buffer of 1000 chars. qulonglong sum = 0; char buf[1000]; while (sum != statBuffer.size) { const auto readBytes = zip_fread(zipFile, buf, 1000); if (readBytes < 0) { qCCritical(ARK) << "Failed to read data"; emit error(xi18n("Failed to read data for entry: %1", entry)); return false; } if (out.writeRawData(buf, readBytes) != readBytes) { qCCritical(ARK) << "Failed to write data"; emit error(xi18n("Failed to write data for entry: %1", entry)); return false; } sum += readBytes; } const auto index = zip_name_locate(archive, entry.toUtf8(), ZIP_FL_ENC_GUESS); if (index == -1) { qCCritical(ARK) << "Could not locate entry:" << entry; emit error(xi18n("Failed to locate entry: %1", entry)); return false; } zip_uint8_t opsys; zip_uint32_t attributes; if (zip_file_get_external_attributes(archive, index, ZIP_FL_UNCHANGED, &opsys, &attributes) == -1) { qCCritical(ARK) << "Could not read external attributes for entry:" << entry; emit error(xi18n("Failed to read metadata for entry: %1", entry)); return false; } // Inspired by fuse-zip source code: fuse-zip/lib/fileNode.cpp switch (opsys) { case ZIP_OPSYS_UNIX: // Unix permissions are stored in the leftmost 16 bits of the external file attribute. file.setPermissions(KIO::convertPermissions(attributes >> 16)); break; default: // TODO: non-UNIX. break; } file.close(); } // Set mtime for entry. utimbuf times; times.modtime = statBuffer.mtime; if (utime(destination.toUtf8(), ×) != 0) { qCWarning(ARK) << "Failed to restore mtime:" << destination; } if (restoreParentMtime) { // Restore mtime for parent dir. times.modtime = parent_mtime; if (utime(parentDir.toUtf8(), ×) != 0) { qCWarning(ARK) << "Failed to restore mtime for parent dir of:" << destination; } } return true; } bool LibzipPlugin::moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(options) setOperationMode(Move); int errcode = 0; zip_error_t err; // Open archive. zip_t *archive = zip_open(QFile::encodeName(filename()), 0, &errcode); zip_error_init_with_code(&err, errcode); if (archive == nullptr) { qCCritical(ARK) << "Failed to open archive. Code:" << errcode; emit error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err)))); return false; } QStringList filePaths = entryFullPaths(files); filePaths.sort(); const QStringList destPaths = entryPathsFromDestination(filePaths, destination, entriesWithoutChildren(files).count()); int i; for (i = 0; i < filePaths.size(); ++i) { const int index = zip_name_locate(archive, filePaths.at(i).toUtf8(), ZIP_FL_ENC_GUESS); if (index == -1) { qCCritical(ARK) << "Could not find entry to move:" << filePaths.at(i); emit error(xi18n("Failed to move entry: %1", filePaths.at(i))); return false; } if (zip_file_rename(archive, index, destPaths.at(i).toUtf8(), ZIP_FL_ENC_GUESS) == -1) { qCCritical(ARK) << "Could not move entry:" << filePaths.at(i); emit error(xi18n("Failed to move entry: %1", filePaths.at(i))); return false; } emit entryRemoved(filePaths.at(i)); emitEntryForIndex(archive, index); emit progress(i/filePaths.count()); } if (zip_close(archive)) { qCCritical(ARK) << "Failed to write archive"; emit error(xi18n("Failed to write archive.")); return false; } qCDebug(ARK) << "Moved" << i << "entries"; return true; } bool LibzipPlugin::copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(options) setOperationMode(Copy); int errcode = 0; zip_error_t err; // Open archive. zip_t *archive = zip_open(QFile::encodeName(filename()), 0, &errcode); zip_error_init_with_code(&err, errcode); if (archive == nullptr) { qCCritical(ARK) << "Failed to open archive. Code:" << errcode; emit error(xi18n("Failed to open archive: %1", QString::fromUtf8(zip_error_strerror(&err)))); return false; } const QStringList filePaths = entryFullPaths(files); const QStringList destPaths = entryPathsFromDestination(filePaths, destination, 0); int i; for (i = 0; i < filePaths.size(); ++i) { QString dest = destPaths.at(i); if (dest.endsWith(QDir::separator())) { if (zip_dir_add(archive, dest.toUtf8(), ZIP_FL_ENC_GUESS) == -1) { // If directory already exists in archive, we get an error. qCWarning(ARK) << "Failed to add dir " << dest << ":" << zip_strerror(archive); continue; } } const int srcIndex = zip_name_locate(archive, filePaths.at(i).toUtf8(), ZIP_FL_ENC_GUESS); if (srcIndex == -1) { qCCritical(ARK) << "Could not find entry to copy:" << filePaths.at(i); emit error(xi18n("Failed to copy entry: %1", filePaths.at(i))); return false; } zip_source_t *src = zip_source_zip(archive, archive, srcIndex, 0, 0, -1); if (!src) { qCCritical(ARK) << "Failed to create source for:" << filePaths.at(i); return false; } const int destIndex = zip_file_add(archive, dest.toUtf8(), src, ZIP_FL_ENC_GUESS | ZIP_FL_OVERWRITE); if (destIndex == -1) { zip_source_free(src); qCCritical(ARK) << "Could not add entry" << dest << ":" << zip_strerror(archive); emit error(xi18n("Failed to add entry: %1", QString::fromUtf8(zip_strerror(archive)))); return false; } // Get permissions from source entry. zip_uint8_t opsys; zip_uint32_t attributes; if (zip_file_get_external_attributes(archive, srcIndex, ZIP_FL_UNCHANGED, &opsys, &attributes) == -1) { qCCritical(ARK) << "Failed to read external attributes for source:" << filePaths.at(i); emit error(xi18n("Failed to read metadata for entry: %1", filePaths.at(i))); return false; } // Set permissions on dest entry. if (zip_file_set_external_attributes(archive, destIndex, ZIP_FL_UNCHANGED, opsys, attributes) != 0) { qCCritical(ARK) << "Failed to set external attributes for destination:" << dest; emit error(xi18n("Failed to set metadata for entry: %1", dest)); return false; } } // Register the callback function to get progress feedback. Callback::func = std::bind(&LibzipPlugin::progressEmitted, this, std::placeholders::_1); void (*c_func)(double) = static_cast(Callback::callback); zip_register_progress_callback(archive, c_func); if (zip_close(archive)) { qCCritical(ARK) << "Failed to write archive"; emit error(xi18n("Failed to write archive.")); return false; } // List the archive to update the model. m_listAfterAdd = true; list(); qCDebug(ARK) << "Copied" << i << "entries"; return true; } QString LibzipPlugin::permissionsToString(const mode_t &perm) { QString modeval; if ((perm & S_IFMT) == S_IFDIR) { modeval.append(QLatin1Char('d')); } else if ((perm & S_IFMT) == S_IFLNK) { modeval.append(QLatin1Char('l')); } else { modeval.append(QLatin1Char('-')); } modeval.append((perm & S_IRUSR) ? QLatin1Char('r') : QLatin1Char('-')); modeval.append((perm & S_IWUSR) ? QLatin1Char('w') : QLatin1Char('-')); if ((perm & S_ISUID) && (perm & S_IXUSR)) { modeval.append(QLatin1Char('s')); } else if ((perm & S_ISUID)) { modeval.append(QLatin1Char('S')); } else if ((perm & S_IXUSR)) { modeval.append(QLatin1Char('x')); } else { modeval.append(QLatin1Char('-')); } modeval.append((perm & S_IRGRP) ? QLatin1Char('r') : QLatin1Char('-')); modeval.append((perm & S_IWGRP) ? QLatin1Char('w') : QLatin1Char('-')); if ((perm & S_ISGID) && (perm & S_IXGRP)) { modeval.append(QLatin1Char('s')); } else if ((perm & S_ISGID)) { modeval.append(QLatin1Char('S')); } else if ((perm & S_IXGRP)) { modeval.append(QLatin1Char('x')); } else { modeval.append(QLatin1Char('-')); } modeval.append((perm & S_IROTH) ? QLatin1Char('r') : QLatin1Char('-')); modeval.append((perm & S_IWOTH) ? QLatin1Char('w') : QLatin1Char('-')); if ((perm & S_ISVTX) && (perm & S_IXOTH)) { modeval.append(QLatin1Char('t')); } else if ((perm & S_ISVTX)) { modeval.append(QLatin1Char('T')); } else if ((perm & S_IXOTH)) { modeval.append(QLatin1Char('x')); } else { modeval.append(QLatin1Char('-')); } return modeval; } void LibzipPlugin::setOperationMode(ReadWriteArchiveInterface::OperationMode operationMode) { QMutexLocker mutexLocker(&m_mutex); m_operationMode = operationMode; } #include "libzipplugin.moc"