diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp index a38e9122..99852e22 100644 --- a/app/mainwindow.cpp +++ b/app/mainwindow.cpp @@ -1,324 +1,327 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2002-2003: Georg Robbers * Copyright (C) 2003: Helio Chissini de Castro * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008 Harald Hvaal * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "mainwindow.h" #include "ark_debug.h" #include "archive_kerfuffle.h" #include "createdialog.h" #include "settingspage.h" #include "pluginmanager.h" #include "part/interface.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include static bool isValidArchiveDrag(const QMimeData *data) { return ((data->hasUrls()) && (data->urls().count() == 1)); } MainWindow::MainWindow(QWidget *) : KParts::MainWindow() { setupActions(); setAcceptDrops(true); } MainWindow::~MainWindow() { if (m_recentFilesAction) { m_recentFilesAction->saveEntries(KSharedConfig::openConfig()->group("Recent Files")); } guiFactory()->removeClient(m_part); delete m_part; m_part = 0; } void MainWindow::dragEnterEvent(QDragEnterEvent * event) { qCDebug(ARK) << "dragEnterEvent" << event; Interface *iface = qobject_cast(m_part); if (iface->isBusy()) { return; } if (!event->source() && isValidArchiveDrag(event->mimeData())) { event->acceptProposedAction(); } return; } void MainWindow::dropEvent(QDropEvent * event) { qCDebug(ARK) << "dropEvent" << event; Interface *iface = qobject_cast(m_part); if (iface->isBusy()) { return; } if ((event->source() == NULL) && (isValidArchiveDrag(event->mimeData()))) { event->acceptProposedAction(); } //TODO: if this call provokes a message box the drag will still be going //while the box is onscreen. looks buggy, do something about it openUrl(event->mimeData()->urls().at(0)); } void MainWindow::dragMoveEvent(QDragMoveEvent * event) { qCDebug(ARK) << "dragMoveEvent" << event; Interface *iface = qobject_cast(m_part); if (iface->isBusy()) { return; } if ((event->source() == NULL) && (isValidArchiveDrag(event->mimeData()))) { event->acceptProposedAction(); } } bool MainWindow::loadPart() { KPluginFactory *factory = Q_NULLPTR; const auto plugins = KPluginLoader::findPlugins(QString(), [](const KPluginMetaData& metaData) { return metaData.pluginId() == QStringLiteral("arkpart") && metaData.serviceTypes().contains(QStringLiteral("KParts/ReadOnlyPart")) && metaData.serviceTypes().contains(QStringLiteral("Browser/View")); }); if (!plugins.isEmpty()) { factory = KPluginLoader(plugins.first().fileName()).factory(); } m_part = factory ? static_cast(factory->create(this)) : Q_NULLPTR; if (!m_part) { KMessageBox::error(this, i18n("Unable to find Ark's KPart component, please check your installation.")); qCWarning(ARK) << "Error loading Ark KPart."; return false; } m_part->setObjectName(QStringLiteral("ArkPart")); setCentralWidget(m_part->widget()); setupGUI(ToolBar | Keys | Save, QStringLiteral("arkui.rc")); createGUI(m_part); statusBar()->hide(); connect(m_part, SIGNAL(busy()), this, SLOT(updateActions())); connect(m_part, SIGNAL(ready()), this, SLOT(updateActions())); connect(m_part, SIGNAL(quit()), this, SLOT(quit())); return true; } void MainWindow::setupActions() { m_newAction = actionCollection()->addAction(KStandardAction::New, QStringLiteral("ark_file_new"), this, SLOT(newArchive())); m_openAction = actionCollection()->addAction(KStandardAction::Open, QStringLiteral("ark_file_open"), this, SLOT(openArchive())); actionCollection()->addAction(KStandardAction::Quit, QStringLiteral("ark_quit"), this, SLOT(quit())); m_recentFilesAction = KStandardAction::openRecent(this, SLOT(openUrl(QUrl)), Q_NULLPTR); actionCollection()->addAction(QStringLiteral("ark_file_open_recent"), m_recentFilesAction); m_recentFilesAction->setToolBarMode(KRecentFilesAction::MenuMode); m_recentFilesAction->setToolButtonPopupMode(QToolButton::DelayedPopup); m_recentFilesAction->setIconText(i18nc("action, to open an archive", "Open")); m_recentFilesAction->setToolTip(i18n("Open an archive")); m_recentFilesAction->loadEntries(KSharedConfig::openConfig()->group("Recent Files")); KStandardAction::preferences(this, SLOT(showSettings()), actionCollection()); } void MainWindow::updateActions() { Interface *iface = qobject_cast(m_part); m_newAction->setEnabled(!iface->isBusy()); m_openAction->setEnabled(!iface->isBusy()); m_recentFilesAction->setEnabled(!iface->isBusy()); } void MainWindow::openArchive() { Interface *iface = qobject_cast(m_part); Q_ASSERT(iface); Q_UNUSED(iface); Kerfuffle::PluginManager pluginManager; auto dlg = new QFileDialog(this, i18nc("to open an archive", "Open Archive")); dlg->setMimeTypeFilters(pluginManager.supportedMimeTypes()); dlg->setFileMode(QFileDialog::ExistingFile); dlg->setAcceptMode(QFileDialog::AcceptOpen); connect(dlg, &QDialog::finished, this, [this, dlg](int result) { if (result == QDialog::Accepted) { openUrl(dlg->selectedUrls().first()); } dlg->deleteLater(); }); dlg->open(); } void MainWindow::openUrl(const QUrl& url) { if (!url.isEmpty()) { m_part->setArguments(m_openArgs); if (m_part->openUrl(url)) { m_recentFilesAction->addUrl(url); } else { m_recentFilesAction->removeUrl(url); } } } void MainWindow::setShowExtractDialog(bool option) { if (option) { m_openArgs.metaData()[QStringLiteral("showExtractDialog")] = QStringLiteral("true"); } else { m_openArgs.metaData().remove(QStringLiteral("showExtractDialog")); } } void MainWindow::closeEvent(QCloseEvent *event) { // Preview windows don't have a parent, so we need to manually close them. foreach (QWidget *widget, qApp->topLevelWidgets()) { if (widget->isVisible()) { widget->close(); } } KParts::MainWindow::closeEvent(event); } void MainWindow::quit() { close(); } void MainWindow::showSettings() { Interface *iface = qobject_cast(m_part); Q_ASSERT(iface); KConfigDialog *dialog = new KConfigDialog(this, QStringLiteral("settings"), iface->config()); foreach (Kerfuffle::SettingsPage *page, iface->settingsPages(this)) { dialog->addPage(page, page->name(), page->iconName()); } // Hide the icons list if only one page has been added. dialog->setFaceType(KPageDialog::Auto); connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::writeSettings); dialog->show(); } void MainWindow::writeSettings() { Interface *iface = qobject_cast(m_part); Q_ASSERT(iface); iface->config()->save(); } void MainWindow::newArchive() { qCDebug(ARK) << "Creating new archive"; Interface *iface = qobject_cast(m_part); Q_ASSERT(iface); Q_UNUSED(iface); QPointer dialog = new Kerfuffle::CreateDialog( Q_NULLPTR, // parent i18n("Create New Archive"), // caption QUrl()); // startDir if (dialog.data()->exec()) { const QUrl saveFileUrl = dialog.data()->selectedUrl(); const QString password = dialog.data()->password(); const QString fixedMimeType = dialog.data()->currentMimeType().name(); qCDebug(ARK) << "CreateDialog returned URL:" << saveFileUrl.toString(); qCDebug(ARK) << "CreateDialog returned mime:" << fixedMimeType; m_openArgs.metaData()[QStringLiteral("createNewArchive")] = QStringLiteral("true"); m_openArgs.metaData()[QStringLiteral("fixedMimeType")] = fixedMimeType; if (dialog.data()->compressionLevel() > -1) { m_openArgs.metaData()[QStringLiteral("compressionLevel")] = QString::number(dialog.data()->compressionLevel()); } if (dialog.data()->volumeSize() > 0) { qCDebug(ARK) << "Setting volume size:" << QString::number(dialog.data()->volumeSize()); m_openArgs.metaData()[QStringLiteral("volumeSize")] = QString::number(dialog.data()->volumeSize()); } + if (!dialog.data()->compressionMethod().isEmpty()) { + m_openArgs.metaData()[QStringLiteral("compressionMethod")] = dialog.data()->compressionMethod(); + } m_openArgs.metaData()[QStringLiteral("encryptionPassword")] = password; if (dialog.data()->isHeaderEncryptionEnabled()) { m_openArgs.metaData()[QStringLiteral("encryptHeader")] = QStringLiteral("true"); } openUrl(saveFileUrl); m_openArgs.metaData().remove(QStringLiteral("showExtractDialog")); m_openArgs.metaData().remove(QStringLiteral("createNewArchive")); m_openArgs.metaData().remove(QStringLiteral("fixedMimeType")); m_openArgs.metaData().remove(QStringLiteral("compressionLevel")); m_openArgs.metaData().remove(QStringLiteral("encryptionPassword")); m_openArgs.metaData().remove(QStringLiteral("encryptHeader")); } delete dialog.data(); } diff --git a/autotests/kerfuffle/addtoarchivetest.cpp b/autotests/kerfuffle/addtoarchivetest.cpp index 2816f79e..4961d8d8 100644 --- a/autotests/kerfuffle/addtoarchivetest.cpp +++ b/autotests/kerfuffle/addtoarchivetest.cpp @@ -1,277 +1,277 @@ /* * 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 "addtoarchive.h" #include "pluginmanager.h" #include "testhelper.h" #include #include #include using namespace Kerfuffle; class AddToArchiveTest : public QObject { Q_OBJECT private Q_SLOTS: void init(); void testCompressHere_data(); void testCompressHere(); }; void AddToArchiveTest::init() { // The test needs an empty subfolder, but git doesn't support tracking of empty directories. QDir(QFINDTESTDATA("data/testdirwithemptysubdir")).mkdir(QStringLiteral("emptydir")); } void AddToArchiveTest::testCompressHere_data() { QTest::addColumn("expectedSuffix"); QTest::addColumn("expectedEncryptionType"); QTest::addColumn("inputFiles"); QTest::addColumn("expectedArchiveName"); QTest::addColumn("expectedNumberOfFiles"); QTest::addColumn("expectedNumberOfFolders"); QTest::newRow("compress here (as TAR) - dir with files") << QStringLiteral("tar.gz") << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdir")} << QStringLiteral("testdir.tar.gz") << 2ULL << 1ULL; QTest::newRow("compress here (as TAR) - dir with subdirs") << QStringLiteral("tar.gz") << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdirwithsubdirs")} << QStringLiteral("testdirwithsubdirs.tar.gz") << 4ULL << 4ULL; QTest::newRow("compress here (as TAR) - dir with empty subdir") << QStringLiteral("tar.gz") << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdirwithemptysubdir")} << QStringLiteral("testdirwithemptysubdir.tar.gz") << 2ULL << 2ULL; QTest::newRow("compress here (as TAR) - single file") << QStringLiteral("tar.gz") << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testfile.txt")} << QStringLiteral("testfile.tar.gz") << 1ULL << 0ULL; QTest::newRow("compress here (as TAR) - file + folder") << QStringLiteral("tar.gz") << Archive::Unencrypted << QStringList { QFINDTESTDATA("data/testdir"), QFINDTESTDATA("data/testfile.txt") } << QStringLiteral("data.tar.gz") << 3ULL << 1ULL; QTest::newRow("compress here (as TAR) - bug #362690") << QStringLiteral("tar.gz") << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/test-3.4.0")} << QStringLiteral("test-3.4.0.tar.gz") << 1ULL << 1ULL; if (!PluginManager().preferredWritePluginsFor(QMimeDatabase().mimeTypeForName(QStringLiteral("application/zip"))).isEmpty()) { QTest::newRow("compress here (as ZIP) - dir with files") << QStringLiteral("zip") << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdir")} << QStringLiteral("testdir.zip") << 2ULL << 1ULL; QTest::newRow("compress here (as ZIP) - dir with subdirs") << QStringLiteral("zip") << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdirwithsubdirs")} << QStringLiteral("testdirwithsubdirs.zip") << 4ULL << 4ULL; QTest::newRow("compress here (as ZIP) - dir with empty subdir") << QStringLiteral("zip") << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdirwithemptysubdir")} << QStringLiteral("testdirwithemptysubdir.zip") << 2ULL << 2ULL; QTest::newRow("compress here (as ZIP) - single file") << QStringLiteral("zip") << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testfile.txt")} << QStringLiteral("testfile.zip") << 1ULL << 0ULL; QTest::newRow("compress here (as ZIP) - file + folder") << QStringLiteral("zip") << Archive::Unencrypted << QStringList { QFINDTESTDATA("data/testdir"), QFINDTESTDATA("data/testfile.txt") } << QStringLiteral("data.zip") << 3ULL << 1ULL; - QTest::newRow("compress here (as TAR) - dir with special name (see #365798)") << QStringLiteral("tar.gz") << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/test%dir")} << QStringLiteral("test%dir.tar.gz") << 2ULL << 1ULL; + } else { qDebug() << "7z/zip executable not found in path. Skipping compress-here-(ZIP) tests."; } if (!PluginManager().preferredWritePluginsFor(QMimeDatabase().mimeTypeForName(QStringLiteral("application/vnd.rar"))).isEmpty()) { QTest::newRow("compress here (as RAR) - dir with files") << QStringLiteral("rar") << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdir")} << QStringLiteral("testdir.rar") << 2ULL << 1ULL; QTest::newRow("compress here (as RAR) - dir with subdirs") << QStringLiteral("rar") << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdirwithsubdirs")} << QStringLiteral("testdirwithsubdirs.rar") << 4ULL << 4ULL; QTest::newRow("compress here (as RAR) - dir with empty subdir") << QStringLiteral("rar") << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testdirwithemptysubdir")} << QStringLiteral("testdirwithemptysubdir.rar") << 2ULL << 2ULL; QTest::newRow("compress here (as RAR) - single file") << QStringLiteral("rar") << Archive::Unencrypted << QStringList {QFINDTESTDATA("data/testfile.txt")} << QStringLiteral("testfile.rar") << 1ULL << 0ULL; QTest::newRow("compress here (as RAR) - file + folder") << QStringLiteral("rar") << Archive::Unencrypted << QStringList { QFINDTESTDATA("data/testdir"), QFINDTESTDATA("data/testfile.txt") } << QStringLiteral("data.rar") << 3ULL << 1ULL; QTest::newRow("compress to encrypted RAR - file + folder") << QStringLiteral("rar") << Archive::Encrypted << QStringList { QFINDTESTDATA("data/testdir"), QFINDTESTDATA("data/testfile.txt") } << QStringLiteral("data.rar") << 3ULL << 1ULL; } else { qDebug() << "rar executable not found in path. Skipping compress-here-(RAR) tests."; } } void AddToArchiveTest::testCompressHere() { AddToArchive *addToArchiveJob = new AddToArchive(this); addToArchiveJob->setChangeToFirstPath(true); QFETCH(QString, expectedSuffix); addToArchiveJob->setAutoFilenameSuffix(expectedSuffix); QFETCH(Archive::EncryptionType, expectedEncryptionType); if (expectedEncryptionType == Archive::Encrypted) { addToArchiveJob->setPassword(QLatin1String("1234")); } QFETCH(QStringList, inputFiles); foreach (const QString &file, inputFiles) { addToArchiveJob->addInput(QUrl::fromUserInput(file)); } // Run the job. TestHelper::startAndWaitForResult(addToArchiveJob); // Check the properties of the generated test archive, then remove it. QFETCH(QString, expectedArchiveName); auto loadJob = Archive::load(QFINDTESTDATA(QStringLiteral("data/%1").arg(expectedArchiveName))); QVERIFY(loadJob); loadJob->setAutoDelete(false); TestHelper::startAndWaitForResult(loadJob); auto archive = loadJob->archive(); QVERIFY(archive); QVERIFY(archive->isValid()); QCOMPARE(archive->completeBaseName() + QLatin1Char('.') + expectedSuffix, expectedArchiveName); QCOMPARE(archive->encryptionType(), expectedEncryptionType); QFETCH(qulonglong, expectedNumberOfFiles); QCOMPARE(archive->numberOfFiles(), expectedNumberOfFiles); QFETCH(qulonglong, expectedNumberOfFolders); QCOMPARE(archive->numberOfFolders(), expectedNumberOfFolders); QVERIFY(QFile(archive->fileName()).remove()); loadJob->deleteLater(); archive->deleteLater(); } QTEST_MAIN(AddToArchiveTest) #include "addtoarchivetest.moc" diff --git a/autotests/plugins/cli7zplugin/cli7ztest.cpp b/autotests/plugins/cli7zplugin/cli7ztest.cpp index 71a17056..4625d600 100644 --- a/autotests/plugins/cli7zplugin/cli7ztest.cpp +++ b/autotests/plugins/cli7zplugin/cli7ztest.cpp @@ -1,428 +1,445 @@ /* * Copyright (c) 2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "cli7ztest.h" #include "testhelper.h" #include #include #include #include #include QTEST_GUILESS_MAIN(Cli7zTest) using namespace Kerfuffle; void Cli7zTest::initTestCase() { m_plugin = new Plugin(this); foreach (Plugin *plugin, m_pluginManger.availablePlugins()) { if (plugin->metaData().pluginId() == QStringLiteral("kerfuffle_cli7z")) { m_plugin = plugin; return; } } } void Cli7zTest::testArchive_data() { QTest::addColumn("archivePath"); QTest::addColumn("expectedFileName"); QTest::addColumn("isReadOnly"); QTest::addColumn("isSingleFolder"); QTest::addColumn("expectedEncryptionType"); QTest::addColumn("expectedSubfolderName"); QString archivePath = QFINDTESTDATA("data/one_toplevel_folder.7z"); QTest::newRow("archive with one top-level folder") << archivePath << QFileInfo(archivePath).fileName() << false << true << Archive::Unencrypted << QStringLiteral("A"); } void Cli7zTest::testArchive() { if (!m_plugin->isValid()) { QSKIP("cli7z plugin not available. Skipping test.", SkipSingle); } QFETCH(QString, archivePath); auto loadJob = Archive::load(archivePath, m_plugin, this); QVERIFY(loadJob); TestHelper::startAndWaitForResult(loadJob); auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not load the cli7z plugin. Skipping test.", SkipSingle); } QFETCH(QString, expectedFileName); QCOMPARE(QFileInfo(archive->fileName()).fileName(), expectedFileName); QFETCH(bool, isReadOnly); QCOMPARE(archive->isReadOnly(), isReadOnly); QFETCH(bool, isSingleFolder); QCOMPARE(archive->isSingleFolder(), isSingleFolder); QFETCH(Archive::EncryptionType, expectedEncryptionType); QCOMPARE(archive->encryptionType(), expectedEncryptionType); QFETCH(QString, expectedSubfolderName); QCOMPARE(archive->subfolderName(), expectedSubfolderName); } void Cli7zTest::testList_data() { QTest::addColumn("outputTextFile"); QTest::addColumn("expectedEntriesCount"); QTest::addColumn("isMultiVolume"); // Is zero for non-multi-volume archives: QTest::addColumn("numberOfVolumes"); QTest::addColumn("compressionMethods"); // Index of some entry to be tested. QTest::addColumn("someEntryIndex"); // Entry metadata. QTest::addColumn("expectedName"); QTest::addColumn("isDirectory"); QTest::addColumn("isPasswordProtected"); QTest::addColumn("expectedSize"); QTest::addColumn("expectedTimestamp"); // p7zip version 16.02 tests QTest::newRow("normal-file-1602") << QFINDTESTDATA("data/archive-with-symlink-1602.txt") << 10 << false << 0 << QStringList{QStringLiteral("LZMA2")} << 4 << QStringLiteral("testarchive/dir2/file2.txt") << false << false << (qulonglong) 32 << QStringLiteral("2015-05-17T20:41:48"); QTest::newRow("encrypted-1602") << QFINDTESTDATA("data/archive-encrypted-1602.txt") << 4 << false << 0 << QStringList{QStringLiteral("LZMA2"), QStringLiteral("7zAES")} << 1 << QStringLiteral("file2.txt") << false << true << (qulonglong) 14 << QStringLiteral("2016-03-02T22:37:55"); QTest::newRow("multi-volume-1602") << QFINDTESTDATA("data/archive-multivol-1602.txt") << 2 << true << 5 << QStringList{QStringLiteral("LZMA2")} << 1 << QStringLiteral("largefile2") << false << false << (qulonglong) 2097152 << QStringLiteral("2016-07-17T11:26:19"); // p7zip version 15.14 tests QTest::newRow("normal-file-1514") << QFINDTESTDATA("data/archive-with-symlink-1514.txt") << 10 << false << 0 << QStringList{QStringLiteral("LZMA2")} << 4 << QStringLiteral("testarchive/dir2/file2.txt") << false << false << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); QTest::newRow("encrypted-1514") << QFINDTESTDATA("data/archive-encrypted-1514.txt") << 9 << false << 0 << QStringList{QStringLiteral("LZMA2"), QStringLiteral("7zAES")} << 3 << QStringLiteral("testarchive/dir1/file1.txt") << false << true << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); // p7zip version 15.09 tests QTest::newRow("normal-file-1509") << QFINDTESTDATA("data/archive-with-symlink-1509.txt") << 10 << false << 0 << QStringList{QStringLiteral("LZMA2")} << 4 << QStringLiteral("testarchive/dir2/file2.txt") << false << false << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); QTest::newRow("encrypted-1509") << QFINDTESTDATA("data/archive-encrypted-1509.txt") << 9 << false << 0 << QStringList{QStringLiteral("LZMA2"), QStringLiteral("7zAES")} << 3 << QStringLiteral("testarchive/dir1/file1.txt") << false << true << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); // p7zip version 9.38.1 tests QTest::newRow("normal-file-9381") << QFINDTESTDATA("data/archive-with-symlink-9381.txt") << 10 << false << 0 << QStringList{QStringLiteral("LZMA2")} << 4 << QStringLiteral("testarchive/dir2/file2.txt") << false << false << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); QTest::newRow("encrypted-9381") << QFINDTESTDATA("data/archive-encrypted-9381.txt") << 9 << false << 0 << QStringList{QStringLiteral("LZMA2"), QStringLiteral("7zAES")} << 3 << QStringLiteral("testarchive/dir1/file1.txt") << false << true << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); } void Cli7zTest::testList() { qRegisterMetaType("Archive::Entry*"); CliPlugin *plugin = new CliPlugin(this, {QStringLiteral("dummy.7z")}); QSignalSpy signalSpyEntry(plugin, &CliPlugin::entry); QSignalSpy signalSpyCompMethod(plugin, &CliPlugin::compressionMethodFound); QFETCH(QString, outputTextFile); QFETCH(int, expectedEntriesCount); QFile outputText(outputTextFile); QVERIFY(outputText.open(QIODevice::ReadOnly)); QTextStream outputStream(&outputText); while (!outputStream.atEnd()) { const QString line(outputStream.readLine()); QVERIFY(plugin->readListLine(line)); } QCOMPARE(signalSpyEntry.count(), expectedEntriesCount); QFETCH(bool, isMultiVolume); QCOMPARE(plugin->isMultiVolume(), isMultiVolume); QFETCH(int, numberOfVolumes); QCOMPARE(plugin->numberOfVolumes(), numberOfVolumes); QCOMPARE(signalSpyCompMethod.count(), 1); QFETCH(QStringList, compressionMethods); QCOMPARE(signalSpyCompMethod.at(0).at(0).toStringList(), compressionMethods); QFETCH(int, someEntryIndex); QVERIFY(someEntryIndex < signalSpyEntry.count()); Archive::Entry *entry = signalSpyEntry.at(someEntryIndex).at(0).value(); QFETCH(QString, expectedName); QCOMPARE(entry->fullPath(), expectedName); QFETCH(bool, isDirectory); QCOMPARE(entry->isDir(), isDirectory); QFETCH(bool, isPasswordProtected); QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected); QFETCH(qulonglong, expectedSize); QCOMPARE(entry->property("size").toULongLong(), expectedSize); QFETCH(QString, expectedTimestamp); QCOMPARE(entry->property("timestamp").toString(), expectedTimestamp); plugin->deleteLater(); } void Cli7zTest::testListArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.7z") << QString() << QStringList { QStringLiteral("l"), QStringLiteral("-slt"), QStringLiteral("/tmp/foo.7z") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.7z") << QStringLiteral("1234") << QStringList { QStringLiteral("l"), QStringLiteral("-slt"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.7z") }; } void Cli7zTest::testListArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList listArgs = { QStringLiteral("l"), QStringLiteral("-slt"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$Archive") }; QFETCH(QString, password); const auto replacedArgs = plugin->substituteListVariables(listArgs, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void Cli7zTest::testAddArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("encryptHeader"); QTest::addColumn("compressionLevel"); + QTest::addColumn("compressionMethod"); QTest::addColumn("volumeSize"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.7z") - << QString() << false << 5 << 0UL + << QString() << false << 5 << QStringLiteral("LZMA2") << 0UL << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.7z"), - QStringLiteral("-mx=5") + QStringLiteral("-mx=5"), + QStringLiteral("-m0=LZMA2") }; QTest::newRow("encrypted") << QStringLiteral("/tmp/foo.7z") - << QStringLiteral("1234") << false << 5 << 0UL + << QStringLiteral("1234") << false << 5 << QStringLiteral("LZMA2") << 0UL << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("-p1234"), - QStringLiteral("-mx=5") + QStringLiteral("-mx=5"), + QStringLiteral("-m0=LZMA2") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.7z") - << QStringLiteral("1234") << true << 5 << 0UL + << QStringLiteral("1234") << true << 5 << QStringLiteral("LZMA2") << 0UL << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("-p1234"), QStringLiteral("-mhe=on"), - QStringLiteral("-mx=5") + QStringLiteral("-mx=5"), + QStringLiteral("-m0=LZMA2") }; QTest::newRow("multi-volume") << QStringLiteral("/tmp/foo.7z") - << QString() << false << 5 << 2500UL + << QString() << false << 5 << QStringLiteral("LZMA2") << 2500UL << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("-mx=5"), + QStringLiteral("-m0=LZMA2"), QStringLiteral("-v2500k") }; + + QTest::newRow("comp-method-bzip2") + << QStringLiteral("/tmp/foo.7z") + << QString() << false << 5 << QStringLiteral("BZip2") << 0UL + << QStringList { + QStringLiteral("a"), + QStringLiteral("/tmp/foo.7z"), + QStringLiteral("-mx=5"), + QStringLiteral("-m0=BZip2") + }; } void Cli7zTest::testAddArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList addArgs = { QStringLiteral("a"), QStringLiteral("$Archive"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$CompressionLevelSwitch"), + QStringLiteral("$CompressionMethodSwitch"), QStringLiteral("$MultiVolumeSwitch"), QStringLiteral("$Files") }; QFETCH(QString, password); QFETCH(bool, encryptHeader); QFETCH(int, compressionLevel); QFETCH(ulong, volumeSize); + QFETCH(QString, compressionMethod); - QStringList replacedArgs = plugin->substituteAddVariables(addArgs, {}, password, encryptHeader, compressionLevel, volumeSize); + QStringList replacedArgs = plugin->substituteAddVariables(addArgs, {}, password, encryptHeader, compressionLevel, volumeSize, compressionMethod); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void Cli7zTest::testExtractArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn>("files"); QTest::addColumn("preservePaths"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("preserve paths, encrypted") << QStringLiteral("/tmp/foo.7z") << QList { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << true << QStringLiteral("1234") << QStringList { QStringLiteral("x"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("preserve paths, unencrypted") << QStringLiteral("/tmp/foo.7z") << QList { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << true << QString() << QStringList { QStringLiteral("x"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, encrypted") << QStringLiteral("/tmp/foo.7z") << QList { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << false << QStringLiteral("1234") << QStringList { QStringLiteral("e"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, unencrypted") << QStringLiteral("/tmp/foo.7z") << QList { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << false << QString() << QStringList { QStringLiteral("e"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; } void Cli7zTest::testExtractArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList extractArgs = { QStringLiteral("$PreservePathSwitch"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$Archive"), QStringLiteral("$Files") }; QFETCH(QList, files); QFETCH(bool, preservePaths); QFETCH(QString, password); QStringList replacedArgs = plugin->substituteExtractVariables(extractArgs, files, preservePaths, password); QVERIFY(replacedArgs.size() >= extractArgs.size()); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } diff --git a/autotests/plugins/clirarplugin/clirartest.cpp b/autotests/plugins/clirarplugin/clirartest.cpp index 9e27b87d..93c750c1 100644 --- a/autotests/plugins/clirarplugin/clirartest.cpp +++ b/autotests/plugins/clirarplugin/clirartest.cpp @@ -1,495 +1,508 @@ /* * Copyright (c) 2011,2014 Raphael Kubo da Costa * Copyright (c) 2015,2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "clirartest.h" #include #include #include #include #include QTEST_GUILESS_MAIN(CliRarTest) using namespace Kerfuffle; void CliRarTest::initTestCase() { m_plugin = new Plugin(this); foreach (Plugin *plugin, m_pluginManger.availablePlugins()) { if (plugin->metaData().pluginId() == QStringLiteral("kerfuffle_clirar")) { m_plugin = plugin; return; } } } void CliRarTest::testArchive_data() { QTest::addColumn("archivePath"); QTest::addColumn("expectedFileName"); QTest::addColumn("isReadOnly"); QTest::addColumn("isSingleFolder"); QTest::addColumn("expectedEncryptionType"); QTest::addColumn("expectedSubfolderName"); QString archivePath = QFINDTESTDATA("data/one_toplevel_folder.rar"); QTest::newRow("archive with one top-level folder") << archivePath << QFileInfo(archivePath).fileName() << false << true << Archive::Unencrypted << QStringLiteral("A"); } void CliRarTest::testArchive() { if (!m_plugin->isValid()) { QSKIP("clirar plugin not available. Skipping test.", SkipSingle); } QFETCH(QString, archivePath); auto loadJob = Archive::load(archivePath, m_plugin, this); QVERIFY(loadJob); TestHelper::startAndWaitForResult(loadJob); auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not load the clirar plugin. Skipping test.", SkipSingle); } QFETCH(QString, expectedFileName); QCOMPARE(QFileInfo(archive->fileName()).fileName(), expectedFileName); QFETCH(bool, isReadOnly); QCOMPARE(archive->isReadOnly(), isReadOnly); QFETCH(bool, isSingleFolder); QCOMPARE(archive->isSingleFolder(), isSingleFolder); QFETCH(Archive::EncryptionType, expectedEncryptionType); QCOMPARE(archive->encryptionType(), expectedEncryptionType); QFETCH(QString, expectedSubfolderName); QCOMPARE(archive->subfolderName(), expectedSubfolderName); } void CliRarTest::testList_data() { QTest::addColumn("outputTextFile"); QTest::addColumn("errorMessage"); QTest::addColumn("expectedEntriesCount"); QTest::addColumn("isMultiVolume"); // Is zero for non-multi-volume archives: QTest::addColumn("numberOfVolumes"); QTest::addColumn("compressionMethods"); // Index of some entry to be tested. QTest::addColumn("someEntryIndex"); // Entry metadata. QTest::addColumn("expectedName"); QTest::addColumn("isDirectory"); QTest::addColumn("isPasswordProtected"); QTest::addColumn("symlinkTarget"); QTest::addColumn("expectedSize"); QTest::addColumn("expectedCompressedSize"); QTest::addColumn("expectedTimestamp"); // Unrar 5 tests QTest::newRow("normal-file-unrar5") << QFINDTESTDATA("data/archive-with-symlink-unrar5.txt") << QString() << 8 << false << 0 << QStringList{QStringLiteral("RAR4")} << 2 << QStringLiteral("rartest/file2.txt") << false << false << QString() << (qulonglong) 14 << (qulonglong) 23 << QStringLiteral("2016-03-21T08:57:36"); QTest::newRow("symlink-unrar5") << QFINDTESTDATA("data/archive-with-symlink-unrar5.txt") << QString() << 8 << false << 0 << QStringList{QStringLiteral("RAR4")} << 3 << QStringLiteral("rartest/linktofile1.txt") << false << false << QStringLiteral("file1.txt") << (qulonglong) 9 << (qulonglong) 9 << QStringLiteral("2016-03-21T08:58:16"); QTest::newRow("encrypted-unrar5") << QFINDTESTDATA("data/archive-encrypted-unrar5.txt") << QString() << 7 << false << 0 << QStringList{QStringLiteral("RAR4")} << 2 << QStringLiteral("rartest/file2.txt") << false << true << QString() << (qulonglong) 14 << (qulonglong) 32 << QStringLiteral("2016-03-21T17:03:36"); QTest::newRow("recovery-record-unrar5") << QFINDTESTDATA("data/archive-recovery-record-unrar5.txt") << QString() << 3 << false << 0 << QStringList{QStringLiteral("RAR4")} << 0 << QStringLiteral("file1.txt") << false << false << QString() << (qulonglong) 32 << (qulonglong) 33 << QStringLiteral("2015-07-26T19:04:38"); QTest::newRow("corrupt-archive-unrar5") << QFINDTESTDATA("data/archive-corrupt-file-header-unrar5.txt") << QString() << 8 << false << 0 << QStringList{QStringLiteral("RAR4")} << 6 << QStringLiteral("dir1/") << true << false << QString() << (qulonglong) 0 << (qulonglong) 0 << QStringLiteral("2015-05-14T01:45:24"); //Note: The number of entries will be the total number of all entries in all volumes, i.e. if a file spans 3 volumes it will count as 3 entries. QTest::newRow("multivolume-archive-unrar5") << QFINDTESTDATA("data/archive-multivol-unrar5.txt") << QString() << 6 << true << 5 << QStringList{QStringLiteral("RAR4")} << 5 << QStringLiteral("largefile2") << false << false << QString() << (qulonglong) 2097152 << (qulonglong) 11231 << QStringLiteral("2016-07-17T11:26:19"); QTest::newRow("RAR5-open-with-unrar5") << QFINDTESTDATA("data/archive-RARv5-unrar5.txt") << QString() << 9 << false << 0 << QStringList{QStringLiteral("RAR5")} << 4 << QStringLiteral("testarchive/dir1/file1.txt") << false << false << QString() << (qulonglong) 32 << (qulonglong) 32 << QStringLiteral("2015-05-17T20:41:48"); // Unrar 4 tests QTest::newRow("normal-file-unrar4") << QFINDTESTDATA("data/archive-with-symlink-unrar4.txt") << QString() << 8 << false << 0 << QStringList{QStringLiteral("RAR4")} << 2 << QStringLiteral("rartest/file2.txt") << false << false << QString() << (qulonglong) 14 << (qulonglong) 23 << QStringLiteral("2016-03-21T08:57:00"); QTest::newRow("symlink-unrar4") << QFINDTESTDATA("data/archive-with-symlink-unrar4.txt") << QString() << 8 << false << 0 << QStringList{QStringLiteral("RAR4")} << 3 << QStringLiteral("rartest/linktofile1.txt") << false << false << QStringLiteral("file1.txt") << (qulonglong) 9 << (qulonglong) 9 << QStringLiteral("2016-03-21T08:58:00"); QTest::newRow("encrypted-unrar4") << QFINDTESTDATA("data/archive-encrypted-unrar4.txt") << QString() << 7 << false << 0 << QStringList{QStringLiteral("RAR4")} << 2 << QStringLiteral("rartest/file2.txt") << false << true << QString() << (qulonglong) 14 << (qulonglong) 32 << QStringLiteral("2016-03-21T17:03:00"); QTest::newRow("recovery-record-unrar4") << QFINDTESTDATA("data/archive-recovery-record-unrar4.txt") << QString() << 3 << false << 0 << QStringList{QStringLiteral("RAR4")} << 0 << QStringLiteral("file1.txt") << false << false << QString() << (qulonglong) 32 << (qulonglong) 33 << QStringLiteral("2015-07-26T19:04:00"); QTest::newRow("corrupt-archive-unrar4") << QFINDTESTDATA("data/archive-corrupt-file-header-unrar4.txt") << QString() << 8 << false << 0 << QStringList{QStringLiteral("RAR4")} << 6 << QStringLiteral("dir1/") << true << false << QString() << (qulonglong) 0 << (qulonglong) 0 << QStringLiteral("2015-05-14T01:45:00"); QTest::newRow("RAR5-open-with-unrar4") << QFINDTESTDATA("data/archive-RARv5-unrar4.txt") << QStringLiteral("Your unrar executable is version 4.20, which is too old to handle this archive. Please update to a more recent version.") << 0 << false << 0 << QStringList() << 0 << QString() << true << false << QString() << (qulonglong) 0 << (qulonglong) 0 << QString(); //Note: The number of entries will be the total number of all entries in all volumes, i.e. if a file spans 3 volumes it will count as 3 entries. QTest::newRow("multivolume-archive-unrar4") << QFINDTESTDATA("data/archive-multivol-unrar4.txt") << QString() << 6 << true << 5 << QStringList{QStringLiteral("RAR4")} << 5 << QStringLiteral("largefile2") << false << false << QString() << (qulonglong) 2097152 << (qulonglong) 11231 << QStringLiteral("2016-07-17T11:26:00"); // Unrar 3 tests QTest::newRow("RAR5-open-with-unrar3") << QFINDTESTDATA("data/archive-RARv5-unrar3.txt") << QStringLiteral("Unrar reported a non-RAR archive. The installed unrar version (3.71) is old. Try updating your unrar.") << 0 << false << 0 << QStringList() << 0 << QString() << true << false << QString() << (qulonglong) 0 << (qulonglong) 0 << QString(); /* * Check that the plugin will not crash when reading corrupted archives, which * have lines such as "Unexpected end of archive" or "??? - the file header is * corrupt" instead of a file name and the header string after it. * * See bug 262857 and commit 2042997013432cdc6974f5b26d39893a21e21011. */ QTest::newRow("corrupt-archive-unrar3") << QFINDTESTDATA("data/archive-corrupt-file-header-unrar3.txt") << QString() << 1 << true << 1 << QStringList{QStringLiteral("RAR4")} << 0 << QStringLiteral("some-file.ext") << false << false << QString() << (qulonglong) 732522496 << (qulonglong) 14851208 << QStringLiteral("2010-10-29T20:47:00"); } void CliRarTest::testList() { qRegisterMetaType("Archive::Entry*"); CliPlugin *rarPlugin = new CliPlugin(this, {QStringLiteral("dummy.rar")}); QSignalSpy signalSpyEntry(rarPlugin, &CliPlugin::entry); QSignalSpy signalSpyCompMethod(rarPlugin, &CliPlugin::compressionMethodFound); QSignalSpy signalSpyError(rarPlugin, &CliPlugin::error); QFETCH(QString, outputTextFile); QFETCH(int, expectedEntriesCount); QFile outputText(outputTextFile); QVERIFY(outputText.open(QIODevice::ReadOnly)); QTextStream outputStream(&outputText); while (!outputStream.atEnd()) { const QString line(outputStream.readLine()); if (!rarPlugin->readListLine(line)) { break; } } QFETCH(QString, errorMessage); if (!errorMessage.isEmpty()) { QCOMPARE(signalSpyError.count(), 1); QCOMPARE(signalSpyError.at(0).at(0).toString(), errorMessage); return; } QCOMPARE(signalSpyEntry.count(), expectedEntriesCount); QFETCH(bool, isMultiVolume); QCOMPARE(rarPlugin->isMultiVolume(), isMultiVolume); QFETCH(int, numberOfVolumes); QCOMPARE(rarPlugin->numberOfVolumes(), numberOfVolumes); QVERIFY(signalSpyCompMethod.count() > 0); QFETCH(QStringList, compressionMethods); if (!compressionMethods.isEmpty()) { QCOMPARE(signalSpyCompMethod.at(0).at(0).toStringList(), compressionMethods); } QFETCH(int, someEntryIndex); QVERIFY(someEntryIndex < signalSpyEntry.count()); Archive::Entry *entry = signalSpyEntry.at(someEntryIndex).at(0).value(); QFETCH(QString, expectedName); QCOMPARE(entry->fullPath(), expectedName); QFETCH(bool, isDirectory); QCOMPARE(entry->isDir(), isDirectory); QFETCH(bool, isPasswordProtected); QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected); QFETCH(QString, symlinkTarget); QCOMPARE(entry->property("link").toString(), symlinkTarget); QFETCH(qulonglong, expectedSize); QCOMPARE(entry->property("size").toULongLong(), expectedSize); QFETCH(qulonglong, expectedCompressedSize); QCOMPARE(entry->property("compressedSize").toULongLong(), expectedCompressedSize); QFETCH(QString, expectedTimestamp); QCOMPARE(entry->property("timestamp").toString(), expectedTimestamp); rarPlugin->deleteLater(); } void CliRarTest::testListArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.rar") << QString() << QStringList { QStringLiteral("vt"), QStringLiteral("-v"), QStringLiteral("/tmp/foo.rar") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.rar") << QStringLiteral("1234") << QStringList { QStringLiteral("vt"), QStringLiteral("-v"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.rar") }; } void CliRarTest::testListArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList listArgs = { QStringLiteral("vt"), QStringLiteral("-v"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$Archive") }; QFETCH(QString, password); const auto replacedArgs = plugin->substituteListVariables(listArgs, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void CliRarTest::testAddArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("encryptHeader"); QTest::addColumn("compressionLevel"); + QTest::addColumn("compressionMethod"); QTest::addColumn("volumeSize"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.rar") - << QString() << false << 3 << 0UL + << QString() << false << 3 << QStringLiteral("RAR4") << 0UL << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.rar"), - QStringLiteral("-m3") + QStringLiteral("-m3"), + QStringLiteral("-ma4") }; QTest::newRow("encrypted") << QStringLiteral("/tmp/foo.rar") - << QStringLiteral("1234") << false << 3 << 0UL + << QStringLiteral("1234") << false << 3 << QString() << 0UL << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("-p1234"), QStringLiteral("-m3") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.rar") - << QStringLiteral("1234") << true << 3 << 0UL + << QStringLiteral("1234") << true << 3 << QString() << 0UL << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("-hp1234"), QStringLiteral("-m3") }; QTest::newRow("multi-volume") << QStringLiteral("/tmp/foo.rar") - << QString() << false << 3 << 2500UL + << QString() << false << 3 << QString() << 2500UL << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("-m3"), QStringLiteral("-v2500k") }; + QTest::newRow("comp-method-RAR5") + << QStringLiteral("/tmp/foo.rar") + << QString() << false << 3 << QStringLiteral("RAR5") << 0UL + << QStringList { + QStringLiteral("a"), + QStringLiteral("/tmp/foo.rar"), + QStringLiteral("-m3"), + QStringLiteral("-ma5") + }; } void CliRarTest::testAddArgs() { QFETCH(QString, archiveName); CliPlugin *rarPlugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(rarPlugin); const QStringList addArgs = { QStringLiteral("a"), QStringLiteral("$Archive"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$CompressionLevelSwitch"), + QStringLiteral("$CompressionMethodSwitch"), QStringLiteral("$MultiVolumeSwitch"), QStringLiteral("$Files") }; QFETCH(QString, password); QFETCH(bool, encryptHeader); QFETCH(int, compressionLevel); + QFETCH(QString, compressionMethod); QFETCH(ulong, volumeSize); - QStringList replacedArgs = rarPlugin->substituteAddVariables(addArgs, {}, password, encryptHeader, compressionLevel, volumeSize); + QStringList replacedArgs = rarPlugin->substituteAddVariables(addArgs, {}, password, encryptHeader, compressionLevel, volumeSize, compressionMethod); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); rarPlugin->deleteLater(); } void CliRarTest::testExtractArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn>("files"); QTest::addColumn("preservePaths"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("preserve paths, encrypted") << QStringLiteral("/tmp/foo.rar") << QList { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << true << QStringLiteral("1234") << QStringList { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("x"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("preserve paths, unencrypted") << QStringLiteral("/tmp/foo.rar") << QList { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << true << QString() << QStringList { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("x"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, encrypted") << QStringLiteral("/tmp/foo.rar") << QList { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << false << QStringLiteral("1234") << QStringList { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("e"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, unencrypted") << QStringLiteral("/tmp/foo.rar") << QList { new Archive::Entry(this, QStringLiteral("aDir/textfile2.txt"), QStringLiteral("aDir")), new Archive::Entry(this, QStringLiteral("c.txt"), QString()) } << false << QString() << QStringList { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("e"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/textfile2.txt"), QStringLiteral("c.txt"), }; } void CliRarTest::testExtractArgs() { QFETCH(QString, archiveName); CliPlugin *rarPlugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(rarPlugin); const QStringList extractArgs = { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("$PreservePathSwitch"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$Archive"), QStringLiteral("$Files") }; QFETCH(QList, files); QFETCH(bool, preservePaths); QFETCH(QString, password); QStringList replacedArgs = rarPlugin->substituteExtractVariables(extractArgs, files, preservePaths, password); QVERIFY(replacedArgs.size() >= extractArgs.size()); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); rarPlugin->deleteLater(); } diff --git a/autotests/plugins/clizipplugin/cliziptest.cpp b/autotests/plugins/clizipplugin/cliziptest.cpp index 3cf7ecf4..2e59ce37 100644 --- a/autotests/plugins/clizipplugin/cliziptest.cpp +++ b/autotests/plugins/clizipplugin/cliziptest.cpp @@ -1,203 +1,217 @@ /* * Copyright (c) 2016 Elvis Angelaccio * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "cliziptest.h" #include QTEST_GUILESS_MAIN(CliZipTest) using namespace Kerfuffle; void CliZipTest::testListArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("expectedArgs"); QTest::newRow("fake zip") << QStringLiteral("/tmp/foo.zip") << QStringList { QStringLiteral("-l"), QStringLiteral("-T"), QStringLiteral("-z"), QStringLiteral("/tmp/foo.zip") }; } void CliZipTest::testListArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList listArgs = { QStringLiteral("-l"), QStringLiteral("-T"), QStringLiteral("-z"), QStringLiteral("$Archive") }; const auto replacedArgs = plugin->substituteListVariables(listArgs, QString()); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void CliZipTest::testAddArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("compressionLevel"); + QTest::addColumn("compressionMethod"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.zip") - << QString() << 3 + << QString() << 3 << QStringLiteral("deflate") << QStringList { QStringLiteral("-r"), QStringLiteral("/tmp/foo.zip"), - QStringLiteral("-3") + QStringLiteral("-3"), + QStringLiteral("-Zdeflate") }; QTest::newRow("encrypted") << QStringLiteral("/tmp/foo.zip") - << QStringLiteral("1234") << 3 + << QStringLiteral("1234") << 3 << QString() << QStringList { QStringLiteral("-r"), QStringLiteral("/tmp/foo.zip"), QStringLiteral("-P1234"), QStringLiteral("-3") }; + + QTest::newRow("comp-method-bzip2") + << QStringLiteral("/tmp/foo.zip") + << QString() << 3 << QStringLiteral("bzip2") + << QStringList { + QStringLiteral("-r"), + QStringLiteral("/tmp/foo.zip"), + QStringLiteral("-3"), + QStringLiteral("-Zbzip2") + }; } void CliZipTest::testAddArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList addArgs = { QStringLiteral("-r"), QStringLiteral("$Archive"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$CompressionLevelSwitch"), + QStringLiteral("$CompressionMethodSwitch"), QStringLiteral("$Files") }; QFETCH(QString, password); QFETCH(int, compressionLevel); + QFETCH(QString, compressionMethod); - QStringList replacedArgs = plugin->substituteAddVariables(addArgs, {}, password, false, compressionLevel, 0); + QStringList replacedArgs = plugin->substituteAddVariables(addArgs, {}, password, false, compressionLevel, 0, compressionMethod); 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") << QList { 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") << QList { 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") << QList { 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") << QList { 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() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList extractArgs = { QStringLiteral("$PreservePathSwitch"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$Archive"), QStringLiteral("$Files") }; QFETCH(QList, files); QFETCH(bool, preservePaths); QFETCH(QString, password); QStringList replacedArgs = plugin->substituteExtractVariables(extractArgs, files, preservePaths, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } diff --git a/kerfuffle/archiveformat.cpp b/kerfuffle/archiveformat.cpp index f35c7ae6..a32b8ec8 100644 --- a/kerfuffle/archiveformat.cpp +++ b/kerfuffle/archiveformat.cpp @@ -1,126 +1,149 @@ /* * Copyright (c) 2016 Elvis Angelaccio * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "archiveformat.h" +#include + namespace Kerfuffle { ArchiveFormat::ArchiveFormat() : m_encryptionType(Archive::Unencrypted) { } ArchiveFormat::ArchiveFormat(const QMimeType& mimeType, Archive::EncryptionType encryptionType, int minCompLevel, int maxCompLevel, int defaultCompLevel, bool supportsWriteComment, bool supportsTesting, - bool supportsMultiVolume) : + bool supportsMultiVolume, + QStringList compressionMethods, + QString defaultCompressionMethod) : m_mimeType(mimeType), m_encryptionType(encryptionType), m_minCompressionLevel(minCompLevel), m_maxCompressionLevel(maxCompLevel), m_defaultCompressionLevel(defaultCompLevel), m_supportsWriteComment(supportsWriteComment), m_supportsTesting(supportsTesting), - m_supportsMultiVolume(supportsMultiVolume) + m_supportsMultiVolume(supportsMultiVolume), + m_compressionMethods(compressionMethods), + m_defaultCompressionMethod(defaultCompressionMethod) { } ArchiveFormat ArchiveFormat::fromMetadata(const QMimeType& mimeType, const KPluginMetaData& metadata) { const QJsonObject json = metadata.rawData(); foreach (const QString& mime, metadata.mimeTypes()) { if (mimeType.name() != mime) { continue; } const QJsonObject formatProps = json[mime].toObject(); int minCompLevel = formatProps[QStringLiteral("CompressionLevelMin")].toInt(); int maxCompLevel = formatProps[QStringLiteral("CompressionLevelMax")].toInt(); int defaultCompLevel = formatProps[QStringLiteral("CompressionLevelDefault")].toInt(); bool supportsWriteComment = formatProps[QStringLiteral("SupportsWriteComment")].toBool(); bool supportsTesting = formatProps[QStringLiteral("SupportsTesting")].toBool(); bool supportsMultiVolume = formatProps[QStringLiteral("SupportsMultiVolume")].toBool(); + QStringList compressionMethods; + QJsonArray array = formatProps[QStringLiteral("CompressionMethods")].toArray(); + foreach (const QJsonValue &value, array) { + compressionMethods.append(value.toString()); + } + QString defaultCompMethod = formatProps[QStringLiteral("CompressionMethodDefault")].toString(); + Archive::EncryptionType encType = Archive::Unencrypted; if (formatProps[QStringLiteral("HeaderEncryption")].toBool()) { encType = Archive::HeaderEncrypted; } else if (formatProps[QStringLiteral("Encryption")].toBool()) { encType = Archive::Encrypted; } - return ArchiveFormat(mimeType, encType, minCompLevel, maxCompLevel, defaultCompLevel, supportsWriteComment, supportsTesting, supportsMultiVolume); + return ArchiveFormat(mimeType, encType, minCompLevel, maxCompLevel, defaultCompLevel, supportsWriteComment, supportsTesting, supportsMultiVolume, compressionMethods, defaultCompMethod); } return ArchiveFormat(); } bool ArchiveFormat::isValid() const { return m_mimeType.isValid(); } Archive::EncryptionType ArchiveFormat::encryptionType() const { return m_encryptionType; } int ArchiveFormat::minCompressionLevel() const { return m_minCompressionLevel; } int ArchiveFormat::maxCompressionLevel() const { return m_maxCompressionLevel; } int ArchiveFormat::defaultCompressionLevel() const { return m_defaultCompressionLevel; } bool ArchiveFormat::supportsWriteComment() const { return m_supportsWriteComment; } bool ArchiveFormat::supportsTesting() const { return m_supportsTesting; } bool ArchiveFormat::supportsMultiVolume() const { return m_supportsMultiVolume; } +QStringList ArchiveFormat::compressionMethods() const +{ + return m_compressionMethods; +} + +QString ArchiveFormat::defaultCompressionMethod() const +{ + return m_defaultCompressionMethod; +} + } diff --git a/kerfuffle/archiveformat.h b/kerfuffle/archiveformat.h index a9adf11e..4e741aa2 100644 --- a/kerfuffle/archiveformat.h +++ b/kerfuffle/archiveformat.h @@ -1,84 +1,90 @@ /* * Copyright (c) 2016 Elvis Angelaccio * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVEFORMAT_H #define ARCHIVEFORMAT_H #include "archive_kerfuffle.h" #include namespace Kerfuffle { class KERFUFFLE_EXPORT ArchiveFormat { public: explicit ArchiveFormat(); explicit ArchiveFormat(const QMimeType& mimeType, Kerfuffle::Archive::EncryptionType encryptionType, int minCompLevel, int maxCompLevel, int defaultCompLevel, bool supportsWriteComment, bool supportsTesting, - bool suppportsMultiVolume); + bool suppportsMultiVolume, + QStringList compressionMethods, + QString defaultCompressionMethod); /** * @return The archive format of the given @p mimeType, according to the given @p metadata. */ static ArchiveFormat fromMetadata(const QMimeType& mimeType, const KPluginMetaData& metadata); /** * @return Whether the format is associated to a valid mimetype. */ bool isValid() const; /** * @return The encryption type supported by the archive format. */ Kerfuffle::Archive::EncryptionType encryptionType() const; int minCompressionLevel() const; int maxCompressionLevel() const; int defaultCompressionLevel() const; bool supportsWriteComment() const; bool supportsTesting() const; bool supportsMultiVolume() const; + QStringList compressionMethods() const; + QString defaultCompressionMethod() const; private: QMimeType m_mimeType; Kerfuffle::Archive::EncryptionType m_encryptionType; int m_minCompressionLevel; int m_maxCompressionLevel; int m_defaultCompressionLevel; bool m_supportsWriteComment; bool m_supportsTesting; bool m_supportsMultiVolume; + QStringList m_compressionMethods; + QString m_defaultCompressionMethod; }; } #endif // ARCHIVEFORMAT_H diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp index 59d60686..87490372 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,1539 +1,1561 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "cliinterface.h" #include "ark_debug.h" #include "queries.h" #ifdef Q_OS_WIN # include #else # include # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Kerfuffle { CliInterface::CliInterface(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args), m_process(0), m_abortingOperation(false), m_listEmptyLines(false), m_extractTempDir(Q_NULLPTR), m_commentTempFile(Q_NULLPTR) { //because this interface uses the event loop setWaitForFinishedSignal(true); if (QMetaType::type("QProcess::ExitStatus") == 0) { qRegisterMetaType("QProcess::ExitStatus"); } } void CliInterface::cacheParameterList() { m_param = parameterList(); Q_ASSERT(m_param.contains(ExtractProgram)); Q_ASSERT(m_param.contains(ListProgram)); Q_ASSERT(m_param.contains(PreservePathSwitch)); Q_ASSERT(m_param.contains(FileExistsExpression)); Q_ASSERT(m_param.contains(FileExistsInput)); } CliInterface::~CliInterface() { Q_ASSERT(!m_process); delete m_commentTempFile; } void CliInterface::setListEmptyLines(bool emptyLines) { m_listEmptyLines = emptyLines; } int CliInterface::copyRequiredSignals() const { return 2; } bool CliInterface::list() { resetParsing(); cacheParameterList(); m_operationMode = List; const auto args = substituteListVariables(m_param.value(ListArgs).toStringList(), password()); if (!runProcess(m_param.value(ListProgram).toStringList(), args)) { return false; } return true; } bool CliInterface::extractFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) { qCDebug(ARK) << Q_FUNC_INFO << "to" << destinationDirectory; cacheParameterList(); m_operationMode = Extract; m_compressionOptions = options; m_extractedFiles = files; m_extractDestDir = destinationDirectory; const QStringList extractArgs = m_param.value(ExtractArgs).toStringList(); if (extractArgs.contains(QStringLiteral("$PasswordSwitch")) && options.value(QStringLiteral("PasswordProtectedHint")).toBool() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } // Populate the argument list. const QStringList args = substituteExtractVariables(extractArgs, files, options.value(QStringLiteral("PreservePaths")).toBool(), password()); QUrl destDir = QUrl(destinationDirectory); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); bool useTmpExtractDir = options.value(QStringLiteral("DragAndDrop")).toBool() || options.value(QStringLiteral("AlwaysUseTmpDir")).toBool(); if (useTmpExtractDir) { Q_ASSERT(!m_extractTempDir); m_extractTempDir = new QTemporaryDir(QApplication::applicationName() + QLatin1Char('-')); qCDebug(ARK) << "Using temporary extraction dir:" << m_extractTempDir->path(); if (!m_extractTempDir->isValid()) { qCDebug(ARK) << "Creation of temporary directory failed."; emit finished(false); return false; } m_oldWorkingDir = QDir::currentPath(); destDir = QUrl(m_extractTempDir->path()); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); } if (!runProcess(m_param.value(ExtractProgram).toStringList(), args)) { return false; } return true; } bool CliInterface::addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) { cacheParameterList(); m_operationMode = Add; const QStringList addArgs = m_param.value(AddArgs).toStringList(); QList filesToPass = QList(); // If destination path is specified, we have recreate its structure inside the temp directory // and then place symlinks of targeted files there. const QString destinationPath = (destination == Q_NULLPTR) ? QString() : destination->fullPath(); if (!destinationPath.isEmpty()) { m_extractTempDir = new QTemporaryDir(); const QString absoluteDestinationPath = m_extractTempDir->path() + QLatin1Char('/') + destinationPath; QDir qDir; qDir.mkpath(absoluteDestinationPath); QObject *preservedParent = Q_NULLPTR; foreach (Archive::Entry *file, files) { // The entries may have parent. We have to save and apply it to our new entry in order to prevent memory // leaks. if (preservedParent == Q_NULLPTR) { preservedParent = file->parent(); } const QString filePath = QDir::currentPath() + QLatin1Char('/') + file->fullPath(true); const QString newFilePath = absoluteDestinationPath + file->fullPath(true); if (QFile::link(filePath, newFilePath)) { qCDebug(ARK) << "Symlink's created:" << filePath << newFilePath; } else { qCDebug(ARK) << "Can't create symlink" << filePath << newFilePath; delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; return false; } } qCDebug(ARK) << "Changing working dir again to " << m_extractTempDir->path(); QDir::setCurrent(m_extractTempDir->path()); filesToPass.push_back(new Archive::Entry(preservedParent, destinationPath.split(QLatin1Char('/'), QString::SkipEmptyParts).at(0))); } else { filesToPass = files; } if (addArgs.contains(QStringLiteral("$PasswordSwitch")) && options.value(QStringLiteral("PasswordProtectedHint")).toBool() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } int compLevel = options.value(QStringLiteral("CompressionLevel"), -1).toInt(); ulong volumeSize = options.value(QStringLiteral("VolumeSize"), 0).toULongLong(); + QString compMethod = options.value(QStringLiteral("CompressionMethod")).toString(); const auto args = substituteAddVariables(m_param.value(AddArgs).toStringList(), filesToPass, password(), isHeaderEncryptionEnabled(), compLevel, - volumeSize); + volumeSize, + compMethod); return runProcess(m_param.value(AddProgram).toStringList(), args); } bool CliInterface::moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(options); cacheParameterList(); m_operationMode = Move; m_removedFiles = files; QList withoutChildren = entriesWithoutChildren(files); setNewMovedFiles(files, destination, withoutChildren.count()); const auto moveArgs = m_param.value(MoveArgs).toStringList(); const auto args = substituteMoveVariables(moveArgs, withoutChildren, destination, password()); return runProcess(m_param.value(MoveProgram).toStringList(), args); } bool CliInterface::copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { m_oldWorkingDir = QDir::currentPath(); m_tempExtractDir = new QTemporaryDir(); m_tempAddDir = new QTemporaryDir(); QDir::setCurrent(m_tempExtractDir->path()); m_passedFiles = files; m_passedDestination = destination; m_passedOptions = options; m_passedOptions[QStringLiteral("PreservePaths")] = true; m_subOperation = Extract; connect(this, &CliInterface::finished, this, &CliInterface::continueCopying); return extractFiles(files, QDir::currentPath(), m_passedOptions); } bool CliInterface::deleteFiles(const QList &files) { cacheParameterList(); m_operationMode = Delete; m_removedFiles = files; const auto deleteArgs = m_param.value(DeleteArgs).toStringList(); const auto args = substituteDeleteVariables(deleteArgs, files, password()); return runProcess(m_param.value(DeleteProgram).toStringList(), args); } bool CliInterface::testArchive() { resetParsing(); cacheParameterList(); m_operationMode = Test; const auto args = substituteTestVariables(m_param.value(TestArgs).toStringList(), password()); return runProcess(m_param.value(TestProgram).toStringList(), args); } bool CliInterface::runProcess(const QStringList& programNames, const QStringList& arguments) { Q_ASSERT(!m_process); QString programPath; for (int i = 0; i < programNames.count(); i++) { programPath = QStandardPaths::findExecutable(programNames.at(i)); if (!programPath.isEmpty()) break; } if (programPath.isEmpty()) { const QString names = programNames.join(QStringLiteral(", ")); emit error(xi18ncp("@info", "Failed to locate program %2 on disk.", "Failed to locate programs %2 on disk.", programNames.count(), names)); emit finished(false); return false; } qCDebug(ARK) << "Executing" << programPath << arguments << "within directory" << QDir::currentPath(); #ifdef Q_OS_WIN m_process = new KProcess; #else m_process = new KPtyProcess; m_process->setPtyChannels(KPtyProcess::StdinChannel); #endif m_process->setOutputChannelMode(KProcess::MergedChannels); m_process->setNextOpenMode(QIODevice::ReadWrite | QIODevice::Unbuffered | QIODevice::Text); m_process->setProgram(programPath, arguments); connect(m_process, &QProcess::readyReadStandardOutput, this, [=]() { readStdout(); }); if (m_operationMode == Extract) { // Extraction jobs need a dedicated post-processing function. connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::extractProcessFinished); } else { connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::processFinished); } m_stdOutData.clear(); m_process->start(); return true; } void CliInterface::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { m_exitCode = exitCode; qCDebug(ARK) << "Process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { //handle all the remaining data in the process readStdout(true); delete m_process; m_process = Q_NULLPTR; } // #193908 - #222392 // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_operationMode == Delete || m_operationMode == Move) { QStringList removedFullPaths = entryFullPaths(m_removedFiles); foreach (const QString &fullPath, removedFullPaths) { emit entryRemoved(fullPath); } foreach (Archive::Entry *e, m_newMovedFiles) { emit entry(e); } m_newMovedFiles.clear(); } if (m_operationMode == Add && !isMultiVolume()) { if (m_extractTempDir) { delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; } list(); } else if (m_operationMode == List && isCorrupt()) { Kerfuffle::LoadCorruptQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (!query.responseYes()) { emit cancelled(); emit finished(false); } else { emit progress(1.0); emit finished(true); } } else { emit progress(1.0); emit finished(true); } } void CliInterface::extractProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_ASSERT(m_operationMode == Extract); m_exitCode = exitCode; qCDebug(ARK) << "Extraction process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { // Handle all the remaining data in the process. readStdout(true); delete m_process; m_process = Q_NULLPTR; } if (m_compressionOptions.value(QStringLiteral("AlwaysUseTmpDir")).toBool()) { // unar exits with code 1 if extraction fails. // This happens at least with wrong passwords or not enough space in the destination folder. if (m_exitCode == 1) { if (password().isEmpty()) { qCWarning(ARK) << "Extraction aborted, destination folder might not have enough space."; emit error(i18n("Extraction failed. Make sure that enough space is available.")); } else { qCWarning(ARK) << "Extraction aborted, either the password is wrong or the destination folder doesn't have enough space."; emit error(i18n("Extraction failed. Make sure you provided the correct password and that enough space is available.")); setPassword(QString()); } cleanUpExtracting(); emit finished(false); return; } if (!m_compressionOptions.value(QStringLiteral("DragAndDrop")).toBool()) { if (!moveToDestination(QDir::current(), QDir(m_extractDestDir), m_compressionOptions[QStringLiteral("PreservePaths")].toBool())) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_extractedFiles.size())); cleanUpExtracting(); emit finished(false); return; } cleanUpExtracting(); } } if (m_compressionOptions.value(QStringLiteral("DragAndDrop")).toBool()) { if (!moveDroppedFilesToDest(m_extractedFiles, m_extractDestDir)) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_extractedFiles.size())); cleanUpExtracting(); emit finished(false); return; } cleanUpExtracting(); } emit progress(1.0); emit finished(true); } void CliInterface::continueCopying(bool result) { if (!result) { finishCopying(false); return; } switch (m_subOperation) { case Extract: m_subOperation = Add; m_passedFiles = entriesWithoutChildren(m_passedFiles); if (!setAddedFiles() || !addFiles(m_tempAddedFiles, m_passedDestination, m_passedOptions)) { finishCopying(false); } break; case Add: finishCopying(true); break; default: Q_ASSERT(false); } } bool CliInterface::moveDroppedFilesToDest(const QList &files, const QString &finalDest) { // Move extracted files from a QTemporaryDir to the final destination. QDir finalDestDir(finalDest); qCDebug(ARK) << "Setting final dir to" << finalDest; bool overwriteAll = false; bool skipAll = false; foreach (const Archive::Entry *file, files) { QFileInfo relEntry(file->fullPath().remove(file->rootNode)); QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + file->fullPath()); QFileInfo absDestEntry(finalDestDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absSourceEntry.isDir()) { // For directories, just create the path. if (!finalDestDir.mkpath(relEntry.filePath())) { qCWarning(ARK) << "Failed to create directory" << relEntry.filePath() << "in final destination."; } } else { // If destination file exists, prompt the user. if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; if (!skipAll && !overwriteAll) { Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); emit userQuery(&query); query.waitForResponse(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } } // Create any parent directories. if (!finalDestDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } // Move files to the final destination. if (!QFile(absSourceEntry.absoluteFilePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << absSourceEntry.filePath() << "to final destination."; return false; } } } return true; } bool CliInterface::isEmptyDir(const QDir &dir) { QDir d = dir; d.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); return d.count() == 0; } void CliInterface::cleanUpExtracting() { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } if (m_extractTempDir) { delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; } } void CliInterface::finishCopying(bool result) { disconnect(this, &CliInterface::finished, this, &CliInterface::continueCopying); emit progress(1.0); emit finished(result); cleanUp(); } bool CliInterface::moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths) { qCDebug(ARK) << "Moving extracted files from temp dir" << tempDir.path() << "to final destination" << destDir.path(); bool overwriteAll = false; bool skipAll = false; QDirIterator dirIt(tempDir.path(), QDir::AllEntries | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (dirIt.hasNext()) { dirIt.next(); // We skip directories if: // 1. We are not preserving paths // 2. The dir is not empty. Only empty directories need to be explicitly moved. // The non-empty ones are created by QDir::mkpath() below. if (dirIt.fileInfo().isDir()) { if (!preservePaths || !isEmptyDir(QDir(dirIt.filePath()))) { continue; } } QFileInfo relEntry; if (preservePaths) { relEntry = QFileInfo(dirIt.filePath().remove(tempDir.path() + QLatin1Char('/'))); } else { relEntry = QFileInfo(dirIt.fileName()); } QFileInfo absDestEntry(destDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); emit userQuery(&query); query.waitForResponse(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } if (preservePaths) { // Create any parent directories. if (!destDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } } // Move file to the final destination. if (!QFile(dirIt.filePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << dirIt.filePath() << "to final destination."; return false; } } return true; } QStringList CliInterface::substituteListVariables(const QStringList &listArgs, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, listArgs) { if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } // Simple argument (e.g. -slt in 7z), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteExtractVariables(const QStringList &extractArgs, const QList &entries, bool preservePaths, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, extractArgs) { qCDebug(ARK) << "Processing argument" << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PreservePathSwitch")) { args << preservePathSwitch(preservePaths); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } if (arg == QLatin1String("$Files")) { args << extractFilesList(entries); continue; } // Simple argument (e.g. -kb in unrar), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } -QStringList CliInterface::substituteAddVariables(const QStringList &addArgs, const QList &entries, const QString &password, bool encryptHeader, int compLevel, ulong volumeSize) +QStringList CliInterface::substituteAddVariables(const QStringList &addArgs, const QList &entries, const QString &password, bool encryptHeader, int compLevel, ulong volumeSize, QString compMethod) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, addArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << (encryptHeader ? passwordHeaderSwitch(password) : passwordSwitch(password)); continue; } if (arg == QLatin1String("$CompressionLevelSwitch")) { args << compressionLevelSwitch(compLevel); continue; } + if (arg == QLatin1String("$CompressionMethodSwitch")) { + args << compressionMethodSwitch(compMethod); + continue; + } + if (arg == QLatin1String("$MultiVolumeSwitch")) { args << multiVolumeSwitch(volumeSize); continue; } if (arg == QLatin1String("$Files")) { args << entryFullPaths(entries, true); continue; } // Simple argument (e.g. a in 7z), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteMoveVariables(const QStringList &moveArgs, const QList &entriesWithoutChildren, const Archive::Entry *destination, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, moveArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } if (arg == QLatin1String("$PathPairs")) { args << entryPathDestinationPairs(entriesWithoutChildren, destination); continue; } // Simple argument (e.g. a in 7z), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteDeleteVariables(const QStringList &deleteArgs, const QList &entries, const QString &password) { cacheParameterList(); QStringList args; foreach (const QString& arg, deleteArgs) { qCDebug(ARK) << "Processing argument" << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } if (arg == QLatin1String("$Files")) { foreach (const Archive::Entry *e, entries) { args << escapeFileName(e->fullPath(true)); } continue; } // Simple argument (e.g. d in rar), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteCommentVariables(const QStringList &commentArgs, const QString &commentFile) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, commentArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$CommentSwitch")) { QString commentSwitch = m_param.value(CommentSwitch).toString(); commentSwitch.replace(QStringLiteral("$CommentFile"), commentFile); args << commentSwitch; continue; } args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteTestVariables(const QStringList &testArgs, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, testArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } void CliInterface::setNewMovedFiles(const QList &entries, const Archive::Entry *destination, int entriesWithoutChildren) { m_newMovedFiles.clear(); QMap entryMap; foreach (const Archive::Entry* entry, entries) { entryMap.insert(entry->fullPath(), entry); } QString lastFolder; QString newPath; int nameLength = 0; foreach (const Archive::Entry* entry, entryMap) { if (lastFolder.count() > 0 && entry->fullPath().startsWith(lastFolder)) { // Replace last moved or copied folder path with destination path. int charsCount = entry->fullPath().count() - lastFolder.count(); if (entriesWithoutChildren > 1) { charsCount += nameLength; } newPath = destination->fullPath() + entry->fullPath().right(charsCount); } else { if (entriesWithoutChildren > 1) { newPath = destination->fullPath() + entry->name(); } else { // If there is only one passed file in the list, // we have to use destination as newPath. newPath = destination->fullPath(true); } if (entry->isDir()) { newPath += QLatin1Char('/'); nameLength = entry->name().count() + 1; // plus slash lastFolder = entry->fullPath(); } else { nameLength = 0; lastFolder = QString(); } } Archive::Entry *newEntry = new Archive::Entry(Q_NULLPTR); newEntry->copyMetaData(entry); newEntry->setFullPath(newPath); m_newMovedFiles << newEntry; } } QString CliInterface::preservePathSwitch(bool preservePaths) const { Q_ASSERT(m_param.contains(PreservePathSwitch)); const QStringList theSwitch = m_param.value(PreservePathSwitch).toStringList(); Q_ASSERT(theSwitch.size() == 2); return (preservePaths ? theSwitch.at(0) : theSwitch.at(1)); } QStringList CliInterface::passwordHeaderSwitch(const QString& password) const { if (password.isEmpty()) { return QStringList(); } Q_ASSERT(m_param.contains(PasswordHeaderSwitch)); QStringList passwordHeaderSwitch = m_param.value(PasswordHeaderSwitch).toStringList(); Q_ASSERT(!passwordHeaderSwitch.isEmpty() && passwordHeaderSwitch.size() <= 2); if (passwordHeaderSwitch.size() == 1) { passwordHeaderSwitch[0].replace(QLatin1String("$Password"), password); } else { passwordHeaderSwitch[1] = password; } return passwordHeaderSwitch; } QStringList CliInterface::passwordSwitch(const QString& password) const { if (password.isEmpty()) { return QStringList(); } Q_ASSERT(m_param.contains(PasswordSwitch)); QStringList passwordSwitch = m_param.value(PasswordSwitch).toStringList(); Q_ASSERT(!passwordSwitch.isEmpty() && passwordSwitch.size() <= 2); if (passwordSwitch.size() == 1) { passwordSwitch[0].replace(QLatin1String("$Password"), password); } else { passwordSwitch[1] = password; } return passwordSwitch; } QString CliInterface::compressionLevelSwitch(int level) const { if (level < 0 || level > 9) { return QString(); } Q_ASSERT(m_param.contains(CompressionLevelSwitch)); QString compLevelSwitch = m_param.value(CompressionLevelSwitch).toString(); Q_ASSERT(!compLevelSwitch.isEmpty()); compLevelSwitch.replace(QLatin1String("$CompressionLevel"), QString::number(level)); return compLevelSwitch; } +QString CliInterface::compressionMethodSwitch(const QString &method) const +{ + if (method.isEmpty()) { + return QString(); + } + + Q_ASSERT(m_param.contains(CompressionMethodSwitch)); + QString compMethodSwitch = m_param.value(CompressionMethodSwitch).toString(); + Q_ASSERT(!compMethodSwitch.isEmpty()); + + compMethodSwitch.replace(QLatin1String("$CompressionMethod"), method); + + return compMethodSwitch; +} + QString CliInterface::multiVolumeSwitch(ulong volumeSize) const { // The maximum value we allow in the QDoubleSpinBox is 1000MB. Converted to // KB this is 1024000. if (volumeSize <= 0 || volumeSize > 1024000) { return QString(); } Q_ASSERT(m_param.contains(MultiVolumeSwitch)); QString multiVolumeSwitch = m_param.value(MultiVolumeSwitch).toString(); Q_ASSERT(!multiVolumeSwitch.isEmpty()); multiVolumeSwitch.replace(QLatin1String("$VolumeSize"), QString::number(volumeSize)); return multiVolumeSwitch; } QStringList CliInterface::extractFilesList(const QList &entries) const { QStringList filesList; foreach (const Archive::Entry *e, entries) { filesList << escapeFileName(e->fullPath(true)); } return filesList; } void CliInterface::killProcess(bool emitFinished) { // TODO: Would be good to unit test #304764/#304178. if (!m_process) { return; } m_abortingOperation = !emitFinished; // Give some time for the application to finish gracefully if (!m_process->waitForFinished(5)) { m_process->kill(); // It takes a few hundred ms for the process to be killed. m_process->waitForFinished(1000); } m_abortingOperation = false; } bool CliInterface::passwordQuery() { Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); // There is no process running, so finished() must be emitted manually. emit finished(false); return false; } setPassword(query.password()); return true; } void CliInterface::cleanUp() { qDeleteAll(m_tempAddedFiles); m_tempAddedFiles.clear(); QDir::setCurrent(m_oldWorkingDir); delete m_tempExtractDir; m_tempExtractDir = Q_NULLPTR; delete m_tempAddDir; m_tempAddDir = Q_NULLPTR; } void CliInterface::readStdout(bool handleAll) { //when hacking this function, please remember the following: //- standard output comes in unpredictable chunks, this is why //you can never know if the last part of the output is a complete line or not //- console applications are not really consistent about what //characters they send out (newline, backspace, carriage return, //etc), so keep in mind that this function is supposed to handle //all those special cases and be the lowest common denominator if (m_abortingOperation) return; Q_ASSERT(m_process); if (!m_process->bytesAvailable()) { //if process has no more data, we can just bail out return; } QByteArray dd = m_process->readAllStandardOutput(); m_stdOutData += dd; QList lines = m_stdOutData.split('\n'); //The reason for this check is that archivers often do not end //queries (such as file exists, wrong password) on a new line, but //freeze waiting for input. So we check for errors on the last line in //all cases. // TODO: QLatin1String() might not be the best choice here. // The call to handleLine() at the end of the method uses // QString::fromLocal8Bit(), for example. // TODO: The same check methods are called in handleLine(), this // is suboptimal. bool wrongPasswordMessage = checkForErrorMessage(QLatin1String( lines.last() ), WrongPasswordPatterns); bool foundErrorMessage = (wrongPasswordMessage || checkForErrorMessage(QLatin1String(lines.last()), DiskFullPatterns) || checkForErrorMessage(QLatin1String(lines.last()), ExtractionFailedPatterns) || checkForPasswordPromptMessage(QLatin1String(lines.last())) || checkForErrorMessage(QLatin1String(lines.last()), FileExistsExpression)); if (foundErrorMessage) { handleAll = true; } if (wrongPasswordMessage) { setPassword(QString()); } //this is complex, here's an explanation: //if there is no newline, then there is no guaranteed full line to //handle in the output. The exception is that it is supposed to handle //all the data, OR if there's been an error message found in the //partial data. if (lines.size() == 1 && !handleAll) { return; } if (handleAll) { m_stdOutData.clear(); } else { //because the last line might be incomplete we leave it for now //note, this last line may be an empty string if the stdoutdata ends //with a newline m_stdOutData = lines.takeLast(); } foreach(const QByteArray& line, lines) { if (!line.isEmpty() || (m_listEmptyLines && m_operationMode == List)) { if (!handleLine(QString::fromLocal8Bit(line))) { killProcess(); return; } } } } bool CliInterface::setAddedFiles() { QDir::setCurrent(m_tempAddDir->path()); foreach (const Archive::Entry *file, m_passedFiles) { const QString oldPath = m_tempExtractDir->path() + QLatin1Char('/') + file->fullPath(true); const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + file->name(); if (!QFile::rename(oldPath, newPath)) { return false; } m_tempAddedFiles << new Archive::Entry(Q_NULLPTR, file->name()); } return true; } bool CliInterface::handleLine(const QString& line) { // TODO: This should be implemented by each plugin; the way progress is // shown by each CLI application is subject to a lot of variation. if ((m_operationMode == Extract || m_operationMode == Add) && m_param.contains(CaptureProgress) && m_param.value(CaptureProgress).toBool()) { //read the percentage int pos = line.indexOf(QLatin1Char( '%' )); if (pos > 1) { int percentage = line.midRef(pos - 2, 2).toInt(); emit progress(float(percentage) / 100); return true; } } if (m_operationMode == Extract) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return true; } if (checkForErrorMessage(line, DiskFullPatterns)) { qCWarning(ARK) << "Found disk full message:" << line; emit error(i18nc("@info", "Extraction failed because the disk is full.")); return false; } if (checkForErrorMessage(line, WrongPasswordPatterns)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18nc("@info", "Extraction failed: Incorrect password")); return false; } if (checkForErrorMessage(line, ExtractionFailedPatterns)) { qCWarning(ARK) << "Error in extraction:" << line; emit error(i18n("Extraction failed because of an unexpected error.")); return false; } if (handleFileExistsMessage(line)) { return true; } } if (m_operationMode == List) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return true; } if (checkForErrorMessage(line, WrongPasswordPatterns)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18n("Incorrect password.")); return false; } if (checkForErrorMessage(line, ExtractionFailedPatterns)) { qCWarning(ARK) << "Error in extraction!!"; emit error(i18n("Extraction failed because of an unexpected error.")); return false; } if (checkForErrorMessage(line, CorruptArchivePatterns)) { qCWarning(ARK) << "Archive corrupt"; setCorrupt(true); // Special case: corrupt is not a "fatal" error so we return true here. return true; } if (handleFileExistsMessage(line)) { return true; } return readListLine(line); } if (m_operationMode == Test) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; emit error(i18n("Ark does not currently support testing this archive.")); return false; } if (checkForTestSuccessMessage(line)) { qCDebug(ARK) << "Test successful"; emit testSuccess(); return true; } } return true; } bool CliInterface::checkForPasswordPromptMessage(const QString& line) { const QString passwordPromptPattern(m_param.value(PasswordPromptPattern).toString()); if (passwordPromptPattern.isEmpty()) return false; if (m_passwordPromptPattern.pattern().isEmpty()) { m_passwordPromptPattern.setPattern(m_param.value(PasswordPromptPattern).toString()); } if (m_passwordPromptPattern.match(line).hasMatch()) { return true; } return false; } bool CliInterface::handleFileExistsMessage(const QString& line) { // Check for a filename and store it. foreach (const QString &pattern, m_param.value(FileExistsFileName).toStringList()) { const QRegularExpression rxFileNamePattern(pattern); const QRegularExpressionMatch rxMatch = rxFileNamePattern.match(line); if (rxMatch.hasMatch()) { m_storedFileName = rxMatch.captured(1); qCWarning(ARK) << "Detected existing file:" << m_storedFileName; } } if (!checkForErrorMessage(line, FileExistsExpression)) { return false; } Kerfuffle::OverwriteQuery query(QDir::current().path() + QLatin1Char( '/' ) + m_storedFileName); query.setNoRenameMode(true); emit userQuery(&query); qCDebug(ARK) << "Waiting response"; query.waitForResponse(); qCDebug(ARK) << "Finished response"; QString responseToProcess; const QStringList choices = m_param.value(FileExistsInput).toStringList(); if (query.responseOverwrite()) { responseToProcess = choices.at(0); } else if (query.responseSkip()) { responseToProcess = choices.at(1); } else if (query.responseOverwriteAll()) { responseToProcess = choices.at(2); } else if (query.responseAutoSkip()) { responseToProcess = choices.at(3); } else if (query.responseCancelled()) { if (choices.count() < 5) { // If the program has no way to cancel the extraction, we resort to killing it return doKill(); } responseToProcess = choices.at(4); } Q_ASSERT(!responseToProcess.isEmpty()); responseToProcess += QLatin1Char( '\n' ); writeToProcess(responseToProcess.toLocal8Bit()); return true; } bool CliInterface::checkForErrorMessage(const QString& line, int parameterIndex) { QList patterns; if (m_patternCache.contains(parameterIndex)) { patterns = m_patternCache.value(parameterIndex); } else { if (!m_param.contains(parameterIndex)) { return false; } foreach(const QString& rawPattern, m_param.value(parameterIndex).toStringList()) { patterns << QRegularExpression(rawPattern); } m_patternCache[parameterIndex] = patterns; } foreach(const QRegularExpression& pattern, patterns) { if (pattern.match(line).hasMatch()) { return true; } } return false; } bool CliInterface::checkForTestSuccessMessage(const QString& line) { const QRegularExpression rx(m_param.value(TestPassedPattern).toString()); const QRegularExpressionMatch rxMatch = rx.match(line); if (rxMatch.hasMatch()) { return true; } return false; } bool CliInterface::doKill() { if (m_process) { killProcess(false); return true; } return false; } bool CliInterface::doSuspend() { return false; } bool CliInterface::doResume() { return false; } QString CliInterface::escapeFileName(const QString& fileName) const { return fileName; } QStringList CliInterface::entryPathDestinationPairs(const QList &entriesWithoutChildren, const Archive::Entry *destination) { QStringList pairList; if (entriesWithoutChildren.count() > 1) { foreach (const Archive::Entry *file, entriesWithoutChildren) { pairList << file->fullPath(true) << destination->fullPath() + file->name(); } } else { pairList << entriesWithoutChildren.at(0)->fullPath(true) << destination->fullPath(true); } return pairList; } void CliInterface::writeToProcess(const QByteArray& data) { Q_ASSERT(m_process); Q_ASSERT(!data.isNull()); qCDebug(ARK) << "Writing" << data << "to the process"; #ifdef Q_OS_WIN m_process->write(data); #else m_process->pty()->write(data); #endif } bool CliInterface::addComment(const QString &comment) { cacheParameterList(); m_operationMode = Comment; m_commentTempFile = new QTemporaryFile; if (!m_commentTempFile->open()) { qCWarning(ARK) << "Failed to create temporary file for comment"; emit finished(false); return false; } QTextStream stream(m_commentTempFile); stream << comment << endl; m_commentTempFile->close(); const auto args = substituteCommentVariables(m_param.value(CommentArgs).toStringList(), m_commentTempFile->fileName()); if (!runProcess(m_param.value(AddProgram).toStringList(), args)) { return false; } m_comment = comment; return true; } QString CliInterface::multiVolumeName() const { QString oldSuffix = QMimeDatabase().suffixForFileName(filename()); QString name; foreach (const QString &multiSuffix, m_param.value(MultiVolumeSuffix).toStringList()) { QString newSuffix = multiSuffix; newSuffix.replace(QStringLiteral("$Suffix"), oldSuffix); name = filename().remove(oldSuffix).append(newSuffix); if (QFileInfo::exists(name)) { break; } } return name; } } diff --git a/kerfuffle/cliinterface.h b/kerfuffle/cliinterface.h index e44487ed..d3e41c09 100644 --- a/kerfuffle/cliinterface.h +++ b/kerfuffle/cliinterface.h @@ -1,520 +1,522 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CLIINTERFACE_H #define CLIINTERFACE_H #include "archiveinterface.h" #include "archiveentry.h" #include "kerfuffle_export.h" #include "part/archivemodel.h" #include #include class KProcess; class KPtyProcess; class QDir; class QTemporaryDir; class QTemporaryFile; namespace Kerfuffle { enum CliInterfaceParameters { ///////////////[ COMMON ]///////////// /** * Bool (default false) * Will look for the %-sign in the stdout while working, in the form of * (2%, 14%, 35%, etc etc), and report progress based upon this */ CaptureProgress = 0, /** * QString * Default: empty * A regexp pattern that matches the program's password prompt. */ PasswordPromptPattern, ///////////////[ LIST ]///////////// /** * QStringList * The names to the program that will handle listing of this * archive (eg "rar"). Will be searched for in PATH */ ListProgram, /** * QStringList * The arguments that are passed to the program above for * listing the archive. Special strings that will be * substituted: * $Archive - the path of the archive */ ListArgs, /** * QStringList (default empty) * List of regexp patterns that indicate a corrupt archive. */ CorruptArchivePatterns, ///////////////[ EXTRACT ]///////////// /** * QStringList * The names to the program that will handle extracting of this * archive (eg "rar"). Will be searched for in PATH */ ExtractProgram, /** * QStringList * The arguments that are passed to the program above for * extracting the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be extracted, if any * $PreservePathSwitch - the flag for extracting with full paths * $PasswordSwitch - the switch setting the password. Note that this * will not be inserted unless the listing function has emitted an * entry with the IsPasswordProtected property set to true. */ ExtractArgs, /** * Bool (default false) * When passing directories to the extract program, do not * include trailing slashes * e.g. if the user selected "foo/" and "foo/bar" in the gui, the * paths "foo" and "foo/bar" will be sent to the program. */ NoTrailingSlashes, /** * QStringList * This should be a qstringlist with either two elements. The first * string is what PreservePathSwitch in the ExtractArgs will be replaced * with if PreservePath is True/enabled. The second is for the disabled * case. An empty string means that the argument will not be used in * that case. * Example: for rar, "x" means extract with full paths, and "e" means * extract without full paths. in this case we will use the stringlist * ("x", "e"). Or, for another format that might use the switch * "--extractFull" for preservePaths, and nothing otherwise: we use the * stringlist ("--extractFull", "") */ PreservePathSwitch, /** * QStringList (default empty) * The format of the root node switch. The variable $Password will be * substituted for the password string. NOTE: supplying passwords * through a virtual terminal is not supported (yet?), because this * is not cross platform compatible. As of KDE 4.3 there are no plans to * change this. * Example: ("-p$Password) * or ("--password", "$Password") */ PasswordSwitch, /** * QString * The format of the compression level switch. The variable $CompressionLevel * will be substituted for the level. * Example: ("-mx=$CompressionLevel) */ CompressionLevelSwitch, /** * QStringList * This is a stringlist with regexps, defining how to recognize the last * line in a "File already exists" prompt when extracting. */ FileExistsExpression, /** * QStringList * This is a stringlist with regexps defining how to recognize the line * containing the filename in a "File already exists" prompt when * extracting. It should have one captured string, which is the filename * of the file/folder that already exists. */ FileExistsFileName, /** * int * This sets on what output channel the FileExistsExpression regex * should be applied on, in other words, on what stream the "file * exists" output will appear in. Values accepted: * 0 - Standard error, stderr (default) * 1 - Standard output, stdout */ FileExistsMode, /** * QStringList * The various responses that can be supplied as a response to the * "file exists" prompt. The various items are to be supplied in the * following order: * index 0 - Yes (overwrite) * index 1 - No (skip/do not overwrite) * index 2 - All (overwrite all) * index 3 - Do not overwrite any files (autoskip) * index 4 - Cancel operation */ FileExistsInput, /** * QStringList * Regexp patterns capturing disk is full error messages. */ DiskFullPatterns, ///////////////[ DELETE ]///////////// /** * QStringList * The names to the program that will handle deleting of elements in this * archive format (eg "rar"). Will be searched for in PATH */ DeleteProgram, /** * QStringList * The arguments that are passed to the program above for * deleting from the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be deleted */ DeleteArgs, /** * QStringList * Default: empty * A list of regexp patterns that will cause the extraction to exit * with a general fail message */ ExtractionFailedPatterns, /** * QStringList * Default: empty * A list of regexp patterns that will alert the user that the password * was wrong. */ WrongPasswordPatterns, ///////////////[ ADD ]///////////// /** * QStringList * The names to the program that will handle adding in this * archive format (eg "rar"). Will be searched for in PATH */ AddProgram, /** * QStringList * The arguments that are passed to the program above for * adding to the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be added */ AddArgs, ///////////////[ MOVE ]///////////// /** * QStringList * The names to the program that will handle adding in this * archive format (eg "rar"). Will be searched for in PATH */ MoveProgram, /** * QStringList * The arguments that are passed to the program above for * moving inside the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be moved * $Destinations - new path of each file selected to be moved */ MoveArgs, ///////////////[ ENCRYPT ]///////////// /** * QStringList (default empty) * The variable $Password will be * substituted for the password string used to encrypt the header. * Example (rar plugin): ("-hp$Password") */ PasswordHeaderSwitch, ///////////////[ COMMENT ]///////////// /** * QStringList * The arguments that are passed to AddProgram when adding * a comment. */ CommentArgs, /** * QString * The variable $CommentFile will be substituted for the file * containing the comment. * Example (rar plugin): -z$CommentFile */ CommentSwitch, TestProgram, TestArgs, TestPassedPattern, MultiVolumeSwitch, - MultiVolumeSuffix + MultiVolumeSuffix, + CompressionMethodSwitch }; typedef QHash ParameterList; class KERFUFFLE_EXPORT CliInterface : public ReadWriteArchiveInterface { Q_OBJECT public: OperationMode m_operationMode; explicit CliInterface(QObject *parent, const QVariantList & args); virtual ~CliInterface(); virtual int copyRequiredSignals() const Q_DECL_OVERRIDE; virtual bool list() Q_DECL_OVERRIDE; virtual bool extractFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) Q_DECL_OVERRIDE; virtual bool addFiles(const QList &files, const Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool copyFiles(const QList &files, Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual bool deleteFiles(const QList &files) Q_DECL_OVERRIDE; virtual bool addComment(const QString &comment) Q_DECL_OVERRIDE; virtual bool testArchive() Q_DECL_OVERRIDE; virtual void resetParsing() = 0; virtual ParameterList parameterList() const = 0; virtual bool readListLine(const QString &line) = 0; bool doKill() Q_DECL_OVERRIDE; bool doSuspend() Q_DECL_OVERRIDE; bool doResume() Q_DECL_OVERRIDE; /** * Sets if the listing should include empty lines. * * The default value is false. */ void setListEmptyLines(bool emptyLines); /** * Move all files from @p tmpDir to @p destDir, preserving paths if @p preservePaths is true. * @return Whether the operation has been successful. */ bool moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths); QStringList substituteListVariables(const QStringList &listArgs, const QString &password); QStringList substituteExtractVariables(const QStringList &extractArgs, const QList &entries, bool preservePaths, const QString &password); - QStringList substituteAddVariables(const QStringList &addArgs, const QList &entries, const QString &password, bool encryptHeader, int compLevel, ulong volumeSize); + QStringList substituteAddVariables(const QStringList &addArgs, const QList &entries, const QString &password, bool encryptHeader, int compLevel, ulong volumeSize, QString compMethod); QStringList substituteMoveVariables(const QStringList &moveArgs, const QList &entriesWithoutChildren, const Archive::Entry *destination, const QString &password); QStringList substituteDeleteVariables(const QStringList &deleteArgs, const QList &entries, const QString &password); QStringList substituteCommentVariables(const QStringList &commentArgs, const QString &commentFile); QStringList substituteTestVariables(const QStringList &testArgs, const QString &password); /** * @see ArchiveModel::entryPathsFromDestination */ void setNewMovedFiles(const QList &entries, const Archive::Entry *destination, int entriesWithoutChildren); /** * @return The preserve path switch, according to the @p preservePaths extraction option. */ QString preservePathSwitch(bool preservePaths) const; /** * @return The password header-switch with the given @p password. */ virtual QStringList passwordHeaderSwitch(const QString& password) const; /** * @return The password switch with the given @p password. */ QStringList passwordSwitch(const QString& password) const; /** * @return The compression level switch with the given @p level. */ QString compressionLevelSwitch(int level) const; + virtual QString compressionMethodSwitch(const QString &method) const; QString multiVolumeSwitch(ulong volumeSize) const; /** * @return The list of selected files to extract. */ QStringList extractFilesList(const QList &files) const; QString multiVolumeName() const Q_DECL_OVERRIDE; protected: bool setAddedFiles(); /** * Handles the given @p line. * @return True if the line is ok. False if the line contains/triggers a "fatal" error * or a canceled user query. If false is returned, the caller is supposed to call killProcess(). */ virtual bool handleLine(const QString& line); bool checkForErrorMessage(const QString& line, int parameterIndex); /** * Checks whether a line of the program's output is a password prompt. * * It uses the regular expression in the @c PasswordPromptPattern parameter * for the check. * * @param line A line of the program's output. * * @return @c true if the given @p line is a password prompt, @c false * otherwise. */ bool checkForPasswordPromptMessage(const QString& line); virtual void cacheParameterList(); /** * Run @p programName with the given @p arguments. * * @param programName The program that will be run (not the whole path). * @param arguments A list of arguments that will be passed to the program. * * @return @c true if the program was found and the process was started correctly, * @c false otherwise (in which case finished(false) is emitted). */ bool runProcess(const QStringList& programNames, const QStringList& arguments); /** * Kill the running process. The finished signal is emitted according to @p emitFinished. */ void killProcess(bool emitFinished = true); /** * Ask the password *before* running any process. * @return True if the user supplies a password, false otherwise (in which case finished() is emitted). */ bool passwordQuery(); void cleanUp(); QString m_oldWorkingDir; QTemporaryDir *m_tempExtractDir; QTemporaryDir *m_tempAddDir; OperationMode m_subOperation; QList m_passedFiles; QList m_tempAddedFiles; Archive::Entry *m_passedDestination; CompressionOptions m_passedOptions; ParameterList m_param; #ifdef Q_OS_WIN KProcess *m_process; #else KPtyProcess *m_process; #endif bool m_abortingOperation; protected slots: virtual void readStdout(bool handleAll = false); private: bool handleFileExistsMessage(const QString& filename); bool checkForTestSuccessMessage(const QString& line); /** * Performs any additional escaping and processing on @p fileName * before passing it to the underlying process. * * The default implementation returns @p fileName unchanged. * * @param fileName String to escape. */ virtual QString escapeFileName(const QString &fileName) const; /** * Returns a list of path pairs which will be supplied to rn command. * [ ... ] * Also constructs a list of new entries resulted in moving. * * @param entriesWithoutChildren List of archive entries * @param destination Must be a directory entry if QList contains more that one entry */ QStringList entryPathDestinationPairs(const QList &entriesWithoutChildren, const Archive::Entry *destination); /** * Wrapper around KProcess::write() or KPtyDevice::write(), depending on * the platform. */ void writeToProcess(const QByteArray& data); bool moveDroppedFilesToDest(const QList &files, const QString &finalDest); /** * @return Whether @p dir is an empty directory. */ bool isEmptyDir(const QDir &dir); void cleanUpExtracting(); void finishCopying(bool result); QByteArray m_stdOutData; QRegularExpression m_passwordPromptPattern; QHash > m_patternCache; QList m_removedFiles; QList m_newMovedFiles; int m_exitCode; bool m_listEmptyLines; QString m_storedFileName; CompressionOptions m_compressionOptions; QString m_extractDestDir; QTemporaryDir *m_extractTempDir; QTemporaryFile *m_commentTempFile; QList m_extractedFiles; protected slots: virtual void processFinished(int exitCode, QProcess::ExitStatus exitStatus); private slots: void extractProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); void continueCopying(bool result); }; } #endif /* CLIINTERFACE_H */ diff --git a/kerfuffle/compressionoptionswidget.cpp b/kerfuffle/compressionoptionswidget.cpp index efdc9647..b5dc25bb 100644 --- a/kerfuffle/compressionoptionswidget.cpp +++ b/kerfuffle/compressionoptionswidget.cpp @@ -1,204 +1,239 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "compressionoptionswidget.h" #include "ark_debug.h" #include "archiveformat.h" #include "pluginmanager.h" #include #include #include namespace Kerfuffle { CompressionOptionsWidget::CompressionOptionsWidget(QWidget *parent, const CompressionOptions &opts) : QWidget(parent) , m_opts(opts) { setupUi(this); KColorScheme colorScheme(QPalette::Active, KColorScheme::View); pwdWidget->setBackgroundWarningColor(colorScheme.background(KColorScheme::NegativeBackground).color()); pwdWidget->setPasswordStrengthMeterVisible(false); connect(multiVolumeCheckbox, &QCheckBox::stateChanged, this, &CompressionOptionsWidget::slotMultiVolumeChecked); if (m_opts.contains(QStringLiteral("VolumeSize"))) { multiVolumeCheckbox->setChecked(true); // Convert from kilobytes. volumeSizeSpinbox->setValue(m_opts.value(QStringLiteral("VolumeSize")).toDouble() / 1024); } } CompressionOptions CompressionOptionsWidget::commpressionOptions() const { CompressionOptions opts; opts[QStringLiteral("CompressionLevel")] = compLevelSlider->value(); if (multiVolumeCheckbox->isChecked()) { // Convert to kilobytes. opts[QStringLiteral("VolumeSize")] = QString::number(volumeSize()); } + if (!compMethodComboBox->currentText().isEmpty()) { + opts[QStringLiteral("CompressionMethod")] = compMethodComboBox->currentText(); + } return opts; } int CompressionOptionsWidget::compressionLevel() const { if (compLevelSlider->isEnabled()) { return compLevelSlider->value(); } else { return -1; } } +QString CompressionOptionsWidget::compressionMethod() const +{ + return compMethodComboBox->currentText(); +} + ulong CompressionOptionsWidget::volumeSize() const { if (collapsibleMultiVolume->isEnabled() && multiVolumeCheckbox->isChecked()) { // Convert to kilobytes. return volumeSizeSpinbox->value() * 1024; } else { return 0; } } void CompressionOptionsWidget::setEncryptionVisible(bool visible) { collapsibleEncryption->setVisible(visible); } QString CompressionOptionsWidget::password() const { return pwdWidget->password(); } void CompressionOptionsWidget::updateWidgets() { const KPluginMetaData metadata = PluginManager().preferredPluginFor(m_mimetype)->metaData(); const ArchiveFormat archiveFormat = ArchiveFormat::fromMetadata(m_mimetype, metadata); Q_ASSERT(archiveFormat.isValid()); if (archiveFormat.encryptionType() != Archive::Unencrypted) { collapsibleEncryption->setEnabled(true); collapsibleEncryption->setToolTip(QString()); pwdWidget->setEnabled(true); if (archiveFormat.encryptionType() == Archive::HeaderEncrypted) { encryptHeaderCheckBox->setEnabled(true); encryptHeaderCheckBox->setToolTip(QString()); } else { encryptHeaderCheckBox->setEnabled(false); // Show the tooltip only if the encryption is still enabled. // This is needed because if the new filter is e.g. tar, the whole encryption group gets disabled. if (collapsibleEncryption->isEnabled() && collapsibleEncryption->isExpanded()) { encryptHeaderCheckBox->setToolTip(i18n("Protection of the list of files is not possible with the %1 format.", m_mimetype.comment())); } else { encryptHeaderCheckBox->setToolTip(QString()); } } } else { collapsibleEncryption->setEnabled(false); collapsibleEncryption->setToolTip(i18n("Protection of the archive with password is not possible with the %1 format.", m_mimetype.comment())); pwdWidget->setEnabled(false); encryptHeaderCheckBox->setToolTip(QString()); } - + collapsibleCompression->setEnabled(true); if (archiveFormat.maxCompressionLevel() == 0) { - collapsibleCompression->setEnabled(false); - collapsibleCompression->setToolTip(i18n("It is not possible to set compression level for the %1 format.", + compLevelSlider->setEnabled(false); + lblCompLevel1->setEnabled(false); + lblCompLevel2->setEnabled(false); + lblCompLevel3->setEnabled(false); + compLevelSlider->setToolTip(i18n("It is not possible to set compression level for the %1 format.", m_mimetype.comment())); } else { - collapsibleCompression->setEnabled(true); - collapsibleCompression->setToolTip(QString()); + compLevelSlider->setEnabled(true); + lblCompLevel1->setEnabled(true); + lblCompLevel2->setEnabled(true); + lblCompLevel3->setEnabled(true); + compLevelSlider->setToolTip(QString()); compLevelSlider->setMinimum(archiveFormat.minCompressionLevel()); compLevelSlider->setMaximum(archiveFormat.maxCompressionLevel()); if (m_opts.contains(QStringLiteral("CompressionLevel"))) { compLevelSlider->setValue(m_opts.value(QStringLiteral("CompressionLevel")).toInt()); } else { compLevelSlider->setValue(archiveFormat.defaultCompressionLevel()); } } + if (archiveFormat.compressionMethods().isEmpty()) { + lblCompMethod->setEnabled(false); + compMethodComboBox->setEnabled(false); + compMethodComboBox->setToolTip(i18n("It is not possible to set compression method for the %1 format.", + m_mimetype.comment())); + compMethodComboBox->clear(); + } else { + lblCompMethod->setEnabled(true); + compMethodComboBox->setEnabled(true); + compMethodComboBox->setToolTip(QString()); + compMethodComboBox->clear(); + compMethodComboBox->insertItems(0, archiveFormat.compressionMethods()); + if (m_opts.contains(QStringLiteral("CompressionMethod")) && + compMethodComboBox->findText(m_opts.value(QStringLiteral("CompressionMethod")).toString()) > -1) { + compMethodComboBox->setCurrentText(m_opts.value(QStringLiteral("CompressionMethod")).toString()); + } else { + compMethodComboBox->setCurrentText(archiveFormat.defaultCompressionMethod()); + } + } + collapsibleCompression->setEnabled(compLevelSlider->isEnabled() || compMethodComboBox->isEnabled()); + if (archiveFormat.supportsMultiVolume()) { collapsibleMultiVolume->setEnabled(true); collapsibleMultiVolume->setToolTip(QString()); } else { collapsibleMultiVolume->setEnabled(false); collapsibleMultiVolume->setToolTip(i18n("The %1 format does not support multi-volume archives.", m_mimetype.comment())); } } void CompressionOptionsWidget::setMimeType(const QMimeType &mimeType) { m_mimetype = mimeType; updateWidgets(); } bool CompressionOptionsWidget::isEncryptionAvailable() const { return collapsibleEncryption->isEnabled(); } bool CompressionOptionsWidget::isEncryptionEnabled() const { return isEncryptionAvailable() && collapsibleEncryption->isExpanded(); } bool CompressionOptionsWidget::isHeaderEncryptionAvailable() const { return isEncryptionEnabled() && encryptHeaderCheckBox->isEnabled(); } bool CompressionOptionsWidget::isHeaderEncryptionEnabled() const { return isHeaderEncryptionAvailable() && encryptHeaderCheckBox->isChecked(); } KNewPasswordWidget::PasswordStatus CompressionOptionsWidget::passwordStatus() const { return pwdWidget->passwordStatus(); } void CompressionOptionsWidget::slotMultiVolumeChecked(int state) { if (state == Qt::Checked) { lblVolumeSize->setEnabled(true); volumeSizeSpinbox->setEnabled(true); } else { lblVolumeSize->setEnabled(false); volumeSizeSpinbox->setEnabled(false); } } } diff --git a/kerfuffle/compressionoptionswidget.h b/kerfuffle/compressionoptionswidget.h index 00b95f8c..9d120df2 100644 --- a/kerfuffle/compressionoptionswidget.h +++ b/kerfuffle/compressionoptionswidget.h @@ -1,71 +1,72 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef COMPRESSIONOPTIONSWIDGET_H #define COMPRESSIONOPTIONSWIDGET_H #include "kerfuffle_export.h" #include "archive_kerfuffle.h" #include "ui_compressionoptionswidget.h" #include #include namespace Kerfuffle { class KERFUFFLE_EXPORT CompressionOptionsWidget : public QWidget, public Ui::CompressionOptionsWidget { Q_OBJECT public: explicit CompressionOptionsWidget(QWidget *parent = Q_NULLPTR, const CompressionOptions &opts = QHash()); int compressionLevel() const; + QString compressionMethod() const; ulong volumeSize() const; QString password() const; CompressionOptions commpressionOptions() const; bool isEncryptionAvailable() const; bool isEncryptionEnabled() const; bool isHeaderEncryptionAvailable() const; bool isHeaderEncryptionEnabled() const; KNewPasswordWidget::PasswordStatus passwordStatus() const; void setEncryptionVisible(bool visible); void setMimeType(const QMimeType &mimeType); private: void updateWidgets(); QMimeType m_mimetype; CompressionOptions m_opts; private slots: void slotMultiVolumeChecked(int state); }; } #endif diff --git a/kerfuffle/compressionoptionswidget.ui b/kerfuffle/compressionoptionswidget.ui index 3a17156b..41732c73 100644 --- a/kerfuffle/compressionoptionswidget.ui +++ b/kerfuffle/compressionoptionswidget.ui @@ -1,219 +1,235 @@ CompressionOptionsWidget 0 0 - 401 + 402 90 true Compression false - - + + Min - - + + Max Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter - - + + 1 0 Level: + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + - + 3 0 300 0 9 1 Qt::Horizontal QSlider::TicksBothSides 1 + + + + + + + Method: + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + true 0 0 Password Protection false false false Qt::LeftToRight Ask for password before showing the list of files in the archive true Multi-volume Archive false Create multi-volume archive 0 30 false Volume size: Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter false megabytes 1 0.100000000000000 1000.000000000000000 0.500000000000000 1.000000000000000 - KCollapsibleGroupBox + KNewPasswordWidget QWidget -
kcollapsiblegroupbox.h
+
knewpasswordwidget.h
1
- KNewPasswordWidget + KCollapsibleGroupBox QWidget -
knewpasswordwidget.h
+
kcollapsiblegroupbox.h
1
diff --git a/kerfuffle/createdialog.cpp b/kerfuffle/createdialog.cpp index e4ef8bf8..d42f4d2d 100644 --- a/kerfuffle/createdialog.cpp +++ b/kerfuffle/createdialog.cpp @@ -1,237 +1,242 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Harald Hvaal * Copyright (C) 2009,2011 Raphael Kubo da Costa * Copyright (C) 2015 Elvis Angelaccio * 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 "createdialog.h" #include "archiveformat.h" #include "ark_debug.h" #include "ui_createdialog.h" #include "kerfuffle/archive_kerfuffle.h" #include "mimetypes.h" #include #include #include #include #include #include namespace Kerfuffle { class CreateDialogUI: public QWidget, public Ui::CreateDialog { public: CreateDialogUI(QWidget *parent = 0) : QWidget(parent) { setupUi(this); } }; CreateDialog::CreateDialog(QWidget *parent, const QString &caption, const QUrl &startDir) : QDialog(parent, Qt::Dialog) { qCDebug(ARK) << "CreateDialog loaded"; setWindowTitle(caption); setModal(true); m_supportedMimeTypes = m_pluginManger.supportedWriteMimeTypes(); m_vlayout = new QVBoxLayout(); setLayout(m_vlayout); m_ui = new CreateDialogUI(this); m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(false); m_ui->destFolderUrlRequester->setMode(KFile::Directory); if (startDir.isEmpty()) { m_ui->destFolderUrlRequester->setUrl(QUrl::fromLocalFile(QDir::currentPath() + QLatin1Char('/'))); } else { m_ui->destFolderUrlRequester->setUrl(startDir); } // Populate combobox with mimetypes. foreach (const QString &type, m_supportedMimeTypes) { m_ui->mimeComboBox->addItem(QMimeDatabase().mimeTypeForName(type).comment()); } connect(m_ui->filenameLineEdit, &QLineEdit::textChanged, this, &CreateDialog::slotFileNameEdited); connect(m_ui->buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(m_ui->buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); connect(this, &QDialog::accepted, this, &CreateDialog::slotUpdateDefaultMimeType); connect(m_ui->mimeComboBox, static_cast(&QComboBox::currentIndexChanged), this, &CreateDialog::slotUpdateWidgets); connect(m_ui->mimeComboBox, static_cast(&QComboBox::currentIndexChanged), this, &CreateDialog::slotUpdateFilenameExtension); m_vlayout->addWidget(m_ui); m_ui->optionsWidget->setMimeType(currentMimeType()); loadConfiguration(); layout()->setSizeConstraint(QLayout::SetFixedSize); m_ui->filenameLineEdit->setFocus(); slotUpdateFilenameExtension(m_ui->mimeComboBox->currentIndex()); } void CreateDialog::slotFileNameEdited(const QString &fileName) { const QMimeType mimeFromFileName = QMimeDatabase().mimeTypeForFile(fileName, QMimeDatabase::MatchExtension); if (m_supportedMimeTypes.contains(mimeFromFileName.name())) { setMimeType(mimeFromFileName.name()); } m_ui->buttonBox->button(QDialogButtonBox::Ok)->setEnabled(!fileName.isEmpty()); } void CreateDialog::slotUpdateWidgets(int index) { m_ui->optionsWidget->setMimeType(QMimeDatabase().mimeTypeForName(m_supportedMimeTypes.at(index))); } void CreateDialog::slotUpdateFilenameExtension(int index) { m_ui->chkAddExtension->setText(i18nc("the argument is a file extension (the period is not a typo)", "Automatically add .%1", QMimeDatabase().mimeTypeForName(m_supportedMimeTypes.at(index)).preferredSuffix())); } QUrl CreateDialog::selectedUrl() const { QString fileName = m_ui->filenameLineEdit->text(); QString dir = m_ui->destFolderUrlRequester->url().toLocalFile(); if (m_ui->chkAddExtension->isChecked()) { QString detectedSuffix = QMimeDatabase().suffixForFileName(m_ui->filenameLineEdit->text().trimmed()); if (!currentMimeType().suffixes().contains(detectedSuffix)) { if (!fileName.endsWith(QLatin1Char('.'))) { fileName.append(QLatin1Char('.')); } fileName.append(currentMimeType().preferredSuffix()); } } if (!dir.endsWith(QLatin1Char('/'))) { dir.append(QLatin1Char('/')); } return QUrl::fromLocalFile(dir + fileName); } int CreateDialog::compressionLevel() const { return m_ui->optionsWidget->compressionLevel(); } +QString CreateDialog::compressionMethod() const +{ + return m_ui->optionsWidget->compressionMethod(); +} + ulong CreateDialog::volumeSize() const { return m_ui->optionsWidget->volumeSize(); } QString CreateDialog::password() const { return m_ui->optionsWidget->password(); } bool CreateDialog::isEncryptionAvailable() const { return m_ui->optionsWidget->isEncryptionAvailable(); } bool CreateDialog::isEncryptionEnabled() const { return m_ui->optionsWidget->isEncryptionEnabled(); } bool CreateDialog::isHeaderEncryptionAvailable() const { return m_ui->optionsWidget->isHeaderEncryptionAvailable(); } bool CreateDialog::isHeaderEncryptionEnabled() const { return m_ui->optionsWidget->isHeaderEncryptionEnabled(); } void CreateDialog::accept() { if (!isEncryptionEnabled()) { QDialog::accept(); return; } switch (m_ui->optionsWidget->passwordStatus()) { case KNewPasswordWidget::WeakPassword: case KNewPasswordWidget::StrongPassword: QDialog::accept(); break; case KNewPasswordWidget::PasswordNotVerified: KMessageBox::error(Q_NULLPTR, i18n("The chosen password does not match the given verification password.")); break; default: break; } } void CreateDialog::slotUpdateDefaultMimeType() { m_config.writeEntry("LastMimeType", currentMimeType().name()); } void CreateDialog::loadConfiguration() { m_config = KConfigGroup(KSharedConfig::openConfig()->group("CreateDialog")); QMimeType lastUsedMime = QMimeDatabase().mimeTypeForName(m_config.readEntry("LastMimeType", QStringLiteral("application/x-compressed-tar"))); setMimeType(lastUsedMime.name()); } QMimeType CreateDialog::currentMimeType() const { Q_ASSERT(m_supportedMimeTypes.size() > m_ui->mimeComboBox->currentIndex()); return QMimeDatabase().mimeTypeForName(m_supportedMimeTypes.at(m_ui->mimeComboBox->currentIndex())); } bool CreateDialog::setMimeType(const QString &mimeTypeName) { const QMimeType mimeType = QMimeDatabase().mimeTypeForName(mimeTypeName); int index = m_supportedMimeTypes.indexOf(mimeTypeName); if (index == -1) { return false; } m_ui->mimeComboBox->setCurrentIndex(index); // This is needed to make sure widgets get updated in case the mimetype is already selected. slotUpdateWidgets(index); return true; } } diff --git a/kerfuffle/createdialog.h b/kerfuffle/createdialog.h index e4ad1efd..316f5a07 100644 --- a/kerfuffle/createdialog.h +++ b/kerfuffle/createdialog.h @@ -1,105 +1,106 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Harald Hvaal * Copyright (C) 2009 Raphael Kubo da Costa * Copyright (C) 2015 Elvis Angelaccio * Copyright (C) 2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CREATEDIALOG_H #define CREATEDIALOG_H #include "archive_kerfuffle.h" #include "kerfuffle_export.h" #include "pluginmanager.h" #include #include #include class QUrl; class QVBoxLayout; namespace Kerfuffle { class KERFUFFLE_EXPORT CreateDialog : public QDialog { Q_OBJECT public: explicit CreateDialog(QWidget *parent, const QString &caption, const QUrl &startDir); QUrl selectedUrl() const; QString password() const; QMimeType currentMimeType() const; bool setMimeType(const QString &mimeTypeName); int compressionLevel() const; + QString compressionMethod() const; ulong volumeSize() const; /** * @return Whether the user can encrypt the new archive. */ bool isEncryptionAvailable() const; /** * @return Whether the user has chosen to encrypt the new archive. */ bool isEncryptionEnabled() const; /** * @return Whether the user can encrypt the list of files in the new archive. */ bool isHeaderEncryptionAvailable() const; /** * @return Whether the user has chosen to encrypt the list of files in the new archive. */ bool isHeaderEncryptionEnabled() const; public slots: virtual void accept() Q_DECL_OVERRIDE; private: void loadConfiguration(); class CreateDialogUI *m_ui; QVBoxLayout *m_vlayout; KConfigGroup m_config; QStringList m_supportedMimeTypes; PluginManager m_pluginManger; CompressionOptions m_compOptions; private slots: void slotFileNameEdited(const QString &text); void slotUpdateWidgets(int index); void slotUpdateDefaultMimeType(); void slotUpdateFilenameExtension(int index); }; } #endif diff --git a/part/part.cpp b/part/part.cpp index 85df0d45..2ebcc316 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1,1708 +1,1715 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2009-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "part.h" #include "ark_debug.h" #include "adddialog.h" #include "overwritedialog.h" #include "archiveformat.h" #include "archivemodel.h" #include "archiveview.h" #include "arkviewer.h" #include "dnddbusinterfaceadaptor.h" #include "infopanel.h" #include "jobtracker.h" #include "kerfuffle/extractiondialog.h" #include "kerfuffle/extractionsettingspage.h" #include "kerfuffle/jobs.h" #include "kerfuffle/settings.h" #include "kerfuffle/previewsettingspage.h" #include "kerfuffle/propertiesdialog.h" #include "pluginmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(Factory, "ark_part.json", registerPlugin();) namespace Ark { static quint32 s_instanceCounter = 1; Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList& args) : KParts::ReadWritePart(parent), m_splitter(Q_NULLPTR), m_busy(false), m_jobTracker(Q_NULLPTR) { Q_UNUSED(args) setComponentData(*createAboutData(), false); new DndExtractAdaptor(this); const QString pathName = QStringLiteral("/DndExtract/%1").arg(s_instanceCounter++); if (!QDBusConnection::sessionBus().registerObject(pathName, this)) { qCCritical(ARK) << "Could not register a D-Bus object for drag'n'drop"; } // m_vlayout is needed for later insertion of QMessageWidget QWidget *mainWidget = new QWidget; m_vlayout = new QVBoxLayout; m_model = new ArchiveModel(pathName, this); m_splitter = new QSplitter(Qt::Horizontal, parentWidget); m_view = new ArchiveView; m_infoPanel = new InfoPanel(m_model); // Add widgets for the comment field. m_commentView = new QPlainTextEdit(); m_commentView->setReadOnly(true); m_commentView->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_commentBox = new QGroupBox(i18n("Comment")); m_commentBox->hide(); QVBoxLayout *vbox = new QVBoxLayout; vbox->addWidget(m_commentView); m_commentBox->setLayout(vbox); m_messageWidget = new KMessageWidget(parentWidget); m_messageWidget->hide(); m_commentMsgWidget = new KMessageWidget(); m_commentMsgWidget->setText(i18n("Comment has been modified.")); m_commentMsgWidget->setMessageType(KMessageWidget::Information); m_commentMsgWidget->setCloseButtonVisible(false); m_commentMsgWidget->hide(); QAction *saveAction = new QAction(i18n("Save"), m_commentMsgWidget); m_commentMsgWidget->addAction(saveAction); connect(saveAction, &QAction::triggered, this, &Part::slotAddComment); m_commentBox->layout()->addWidget(m_commentMsgWidget); connect(m_commentView, &QPlainTextEdit::textChanged, this, &Part::slotCommentChanged); setWidget(mainWidget); mainWidget->setLayout(m_vlayout); // Configure the QVBoxLayout and add widgets m_vlayout->setContentsMargins(0,0,0,0); m_vlayout->addWidget(m_messageWidget); m_vlayout->addWidget(m_splitter); // Vertical QSplitter for the file view and comment field. m_commentSplitter = new QSplitter(Qt::Vertical, parentWidget); m_commentSplitter->setOpaqueResize(false); m_commentSplitter->addWidget(m_view); m_commentSplitter->addWidget(m_commentBox); m_commentSplitter->setCollapsible(0, false); // Horizontal QSplitter for the file view and infopanel. m_splitter->addWidget(m_commentSplitter); m_splitter->addWidget(m_infoPanel); // Read settings from config file and show/hide infoPanel. if (!ArkSettings::showInfoPanel()) { m_infoPanel->hide(); } else { m_splitter->setSizes(ArkSettings::splitterSizes()); } setupView(); setupActions(); connect(m_view, &ArchiveView::entryChanged, this, &Part::slotRenameFile); connect(m_model, &ArchiveModel::loadingStarted, this, &Part::slotLoadingStarted); connect(m_model, &ArchiveModel::loadingFinished, this, &Part::slotLoadingFinished); connect(m_model, &ArchiveModel::droppedFiles, this, static_cast(&Part::slotAddFiles)); connect(m_model, &ArchiveModel::error, this, &Part::slotError); connect(m_model, &ArchiveModel::messageWidget, this, &Part::displayMsgWidget); connect(this, &Part::busy, this, &Part::setBusyGui); connect(this, &Part::ready, this, &Part::setReadyGui); connect(this, static_cast(&KParts::ReadOnlyPart::completed), this, &Part::setFileNameFromArchive); m_statusBarExtension = new KParts::StatusBarExtension(this); setXMLFile(QStringLiteral("ark_part.rc")); } Part::~Part() { qDeleteAll(m_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()) { return; } if (m_commentMsgWidget->isHidden() && m_commentView->toPlainText() != m_model->archive()->comment()) { m_commentMsgWidget->animatedShow(); } else if (m_commentMsgWidget->isVisible() && m_commentView->toPlainText() == m_model->archive()->comment()) { m_commentMsgWidget->hide(); } } KAboutData *Part::createAboutData() { return new KAboutData(QStringLiteral("ark"), i18n("ArkPart"), QStringLiteral("3.0")); } void Part::registerJob(KJob* job) { if (!m_jobTracker) { m_jobTracker = new JobTracker(widget()); m_statusBarExtension->addStatusBarItem(m_jobTracker->widget(0), 0, true); m_jobTracker->widget(job)->show(); } m_jobTracker->registerJob(job); emit busy(); connect(job, &KJob::result, this, &Part::ready); } // TODO: KIO::mostLocalHere is used here to resolve some KIO URLs to local // paths (e.g. desktop:/), but more work is needed to support extraction // to non-local destinations. See bugs #189322 and #204323. void Part::extractSelectedFilesTo(const QString& localPath) { if (!m_model) { return; } const QUrl url = QUrl::fromUserInput(localPath, QString()); KIO::StatJob* statJob = nullptr; // Try to resolve the URL to a local path. if (!url.isLocalFile() && !url.scheme().isEmpty()) { statJob = KIO::mostLocalUrl(url); if (!statJob->exec() || statJob->error() != 0) { return; } } const QString destination = statJob ? statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : localPath; delete statJob; // The URL could not be resolved to a local path. if (!url.isLocalFile() && destination.isEmpty()) { qCWarning(ARK) << "Ark cannot extract to non-local destination:" << localPath; KMessageBox::sorry(widget(), xi18nc("@info", "Ark can only extract to local destinations.")); return; } qCDebug(ARK) << "Extract to" << destination; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; options[QStringLiteral("RemoveRootNode")] = true; options[QStringLiteral("DragAndDrop")] = true; // Create and start the ExtractJob. ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())), destination, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } void Part::setupView() { m_view->setContextMenuPolicy(Qt::CustomContextMenu); m_view->setModel(m_model); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::updateActions); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::selectionChanged); connect(m_view, &QTreeView::activated, this, &Part::slotActivated); connect(m_view, &QWidget::customContextMenuRequested, this, &Part::slotShowContextMenu); connect(m_model, &QAbstractItemModel::columnsInserted, this, &Part::adjustColumns); } void Part::slotActivated(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; m_showInfoPanelAction = new KToggleAction(i18nc("@action:inmenu", "Show Information Panel"), this); actionCollection()->addAction(QStringLiteral( "show-infopanel" ), m_showInfoPanelAction); m_showInfoPanelAction->setChecked(ArkSettings::showInfoPanel()); connect(m_showInfoPanelAction, &QAction::triggered, this, &Part::slotToggleInfoPanel); m_saveAsAction = actionCollection()->addAction(KStandardAction::SaveAs, QStringLiteral("ark_file_save_as"), this, SLOT(slotSaveAs())); m_openFileAction = actionCollection()->addAction(QStringLiteral("openfile")); m_openFileAction->setText(i18nc("open a file with external program", "&Open")); m_openFileAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with the associated application")); connect(m_openFileAction, &QAction::triggered, m_signalMapper, static_cast(&QSignalMapper::map)); m_signalMapper->setMapping(m_openFileAction, OpenFile); m_openFileWithAction = actionCollection()->addAction(QStringLiteral("openfilewith")); m_openFileWithAction->setText(i18nc("open a file with external program", "Open &With...")); m_openFileWithAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileWithAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with an external program")); connect(m_openFileWithAction, &QAction::triggered, m_signalMapper, static_cast(&QSignalMapper::map)); m_signalMapper->setMapping(m_openFileWithAction, OpenFileWith); m_previewAction = actionCollection()->addAction(QStringLiteral("preview")); m_previewAction->setText(i18nc("to preview a file inside an archive", "Pre&view")); m_previewAction->setIcon(QIcon::fromTheme(QStringLiteral("document-preview-archive"))); m_previewAction->setToolTip(i18nc("@info:tooltip", "Click to preview the selected file")); actionCollection()->setDefaultShortcut(m_previewAction, Qt::CTRL + Qt::Key_P); connect(m_previewAction, &QAction::triggered, m_signalMapper, static_cast(&QSignalMapper::map)); m_signalMapper->setMapping(m_previewAction, Preview); m_extractArchiveAction = actionCollection()->addAction(QStringLiteral("extract_all")); m_extractArchiveAction->setText(i18nc("@action:inmenu", "E&xtract All")); m_extractArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); m_extractArchiveAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose how to extract all the files in the archive")); actionCollection()->setDefaultShortcut(m_extractArchiveAction, Qt::CTRL + Qt::SHIFT + Qt::Key_E); connect(m_extractArchiveAction, &QAction::triggered, this, &Part::slotExtractArchive); m_extractAction = actionCollection()->addAction(QStringLiteral("extract")); m_extractAction->setText(i18nc("@action:inmenu", "&Extract")); m_extractAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); actionCollection()->setDefaultShortcut(m_extractAction, Qt::CTRL + Qt::Key_E); m_extractAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose to extract either all files or just the selected ones")); connect(m_extractAction, &QAction::triggered, this, &Part::slotShowExtractionDialog); m_addFilesAction = actionCollection()->addAction(QStringLiteral("add")); m_addFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert"))); m_addFilesAction->setText(i18n("Add &Files to...")); m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive")); actionCollection()->setDefaultShortcut(m_addFilesAction, Qt::ALT + Qt::Key_A); connect(m_addFilesAction, &QAction::triggered, this, static_cast(&Part::slotAddFiles)); m_renameFileAction = actionCollection()->addAction(QStringLiteral("rename")); m_renameFileAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-rename"))); m_renameFileAction->setText(i18n("&Rename")); actionCollection()->setDefaultShortcut(m_renameFileAction, Qt::Key_F2); m_renameFileAction->setToolTip(i18nc("@info:tooltip", "Click to rename the selected file")); connect(m_renameFileAction, &QAction::triggered, this, &Part::slotEditFileName); m_deleteFilesAction = actionCollection()->addAction(QStringLiteral("delete")); m_deleteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-remove"))); m_deleteFilesAction->setText(i18n("De&lete")); actionCollection()->setDefaultShortcut(m_deleteFilesAction, Qt::Key_Delete); m_deleteFilesAction->setToolTip(i18nc("@info:tooltip", "Click to delete the selected files")); connect(m_deleteFilesAction, &QAction::triggered, this, &Part::slotDeleteFiles); m_cutFilesAction = actionCollection()->addAction(QStringLiteral("cut")); m_cutFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-cut"))); m_cutFilesAction->setText(i18nc("@action:inmenu", "C&ut")); actionCollection()->setDefaultShortcut(m_cutFilesAction, Qt::CTRL + Qt::Key_X); m_cutFilesAction->setToolTip(i18nc("@info:tooltip", "Click to cut the selected files")); connect(m_cutFilesAction, &QAction::triggered, this, &Part::slotCutFiles); m_copyFilesAction = actionCollection()->addAction(QStringLiteral("copy")); m_copyFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-copy"))); m_copyFilesAction->setText(i18nc("@action:inmenu", "C&opy")); actionCollection()->setDefaultShortcut(m_copyFilesAction, Qt::CTRL + Qt::Key_C); m_copyFilesAction->setToolTip(i18nc("@info:tooltip", "Click to copy the selected files")); connect(m_copyFilesAction, &QAction::triggered, this, &Part::slotCopyFiles); m_pasteFilesAction = actionCollection()->addAction(QStringLiteral("paste")); m_pasteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("edit-paste"))); m_pasteFilesAction->setText(i18nc("@action:inmenu", "Pa&ste")); actionCollection()->setDefaultShortcut(m_pasteFilesAction, Qt::CTRL + Qt::Key_V); m_pasteFilesAction->setToolTip(i18nc("@info:tooltip", "Click to paste the files here")); connect(m_pasteFilesAction, &QAction::triggered, this, static_cast(&Part::slotPasteFiles)); m_propertiesAction = actionCollection()->addAction(QStringLiteral("properties")); m_propertiesAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); m_propertiesAction->setText(i18nc("@action:inmenu", "&Properties")); actionCollection()->setDefaultShortcut(m_propertiesAction, Qt::ALT + Qt::Key_Return); m_propertiesAction->setToolTip(i18nc("@info:tooltip", "Click to see properties for archive")); connect(m_propertiesAction, &QAction::triggered, this, &Part::slotShowProperties); m_editCommentAction = actionCollection()->addAction(QStringLiteral("edit_comment")); m_editCommentAction->setIcon(QIcon::fromTheme(QStringLiteral("document-edit"))); actionCollection()->setDefaultShortcut(m_editCommentAction, Qt::ALT + Qt::Key_C); m_editCommentAction->setToolTip(i18nc("@info:tooltip", "Click to add or edit comment")); connect(m_editCommentAction, &QAction::triggered, this, &Part::slotShowComment); m_testArchiveAction = actionCollection()->addAction(QStringLiteral("test_archive")); m_testArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("checkmark"))); m_testArchiveAction->setText(i18nc("@action:inmenu", "&Test Integrity")); actionCollection()->setDefaultShortcut(m_testArchiveAction, Qt::ALT + Qt::Key_T); m_testArchiveAction->setToolTip(i18nc("@info:tooltip", "Click to test the archive for integrity")); connect(m_testArchiveAction, &QAction::triggered, this, &Part::slotTestArchive); connect(m_signalMapper, static_cast(&QSignalMapper::mapped), this, &Part::slotOpenEntry); updateActions(); updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); } void Part::updateActions() { bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); const Archive::Entry *entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); int selectedEntriesCount = m_view->selectionModel()->selectedRows().count(); // 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 != Q_NULLPTR && entry->property("size").toLongLong() < maxPreviewSize)); const bool isDir = (entry == Q_NULLPTR) ? false : entry->isDir(); m_previewAction->setEnabled(!isBusy() && isPreviewable && !isDir && (selectedEntriesCount == 1)); m_extractArchiveAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_extractAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_saveAsAction->setEnabled(!isBusy() && m_model->rowCount() > 0); m_addFilesAction->setEnabled(!isBusy() && isWritable && !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_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() << m_view->height() * 0.6 << 1); } m_commentView->setFocus(); } void Part::slotAddComment() { CommentJob *job = m_model->archive()->addComment(m_commentView->toPlainText()); if (!job) { return; } registerJob(job); job->start(); m_commentMsgWidget->hide(); if (m_commentView->toPlainText().isEmpty()) { m_commentBox->hide(); } } void Part::slotTestArchive() { TestJob *job = m_model->archive()->testArchive(); if (!job) { return; } registerJob(job); connect(job, &KJob::result, this, &Part::slotTestingDone); job->start(); } void Part::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")); } updateActions(); m_view->setDropsEnabled(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()); } void Part::slotTestingDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else if (static_cast(job)->testSucceeded()) { KMessageBox::information(widget(), i18n("The archive passed the integrity test."), i18n("Test Results")); } else { KMessageBox::error(widget(), i18n("The archive failed the integrity test."), i18n("Test Results")); } } void Part::updateQuickExtractMenu(QAction *extractAction) { if (!extractAction) { return; } QMenu *menu = extractAction->menu(); if (!menu) { menu = new QMenu(); extractAction->setMenu(menu); connect(menu, &QMenu::triggered, this, &Part::slotQuickExtractFiles); // Remember to keep this action's properties as similar to // extractAction's as possible (except where it does not make // sense, such as the text or the shortcut). QAction *extractTo = menu->addAction(i18n("Extract To...")); extractTo->setIcon(extractAction->icon()); extractTo->setToolTip(extractAction->toolTip()); if (extractAction == m_extractArchiveAction) { connect(extractTo, &QAction::triggered, this, &Part::slotExtractArchive); } else { connect(extractTo, &QAction::triggered, this, &Part::slotShowExtractionDialog); } menu->addSeparator(); QAction *header = menu->addAction(i18n("Quick Extract To...")); header->setEnabled(false); header->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); } while (menu->actions().size() > 3) { menu->removeAction(menu->actions().last()); } const KConfigGroup conf(KSharedConfig::openConfig(), "ExtractDialog"); const QStringList dirHistory = conf.readPathEntry("DirHistory", QStringList()); for (int i = 0; i < qMin(10, dirHistory.size()); ++i) { const QString dir = QUrl(dirHistory.value(i)).toString(QUrl::RemoveScheme | QUrl::NormalizePathSegments | QUrl::PreferLocalFile); if (QDir(dir).exists()) { QAction *newAction = menu->addAction(dir); newAction->setData(dir); } } } void Part::slotQuickExtractFiles(QAction *triggeredAction) { // #190507: triggeredAction->data.isNull() means it's the "Extract to..." // action, and we do not want it to run here if (!triggeredAction->data().isNull()) { QString userDestination = triggeredAction->data().toString(); QString finalDestinationDirectory; const QString detectedSubfolder = detectSubfolder(); qCDebug(ARK) << "Detected subfolder" << detectedSubfolder; if (!isSingleFolderArchive()) { if (!userDestination.endsWith(QDir::separator())) { userDestination.append(QDir::separator()); } finalDestinationDirectory = userDestination + detectedSubfolder; QDir(userDestination).mkdir(detectedSubfolder); } else { finalDestinationDirectory = userDestination; } qCDebug(ARK) << "Extracting to:" << finalDestinationDirectory; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())), finalDestinationDirectory, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } } void Part::selectionChanged() { m_infoPanel->setIndexes(m_view->selectionModel()->selectedRows()); } bool Part::openFile() { qCDebug(ARK) << "Attempting to open archive" << localFilePath(); resetGui(); if (!isLocalFileValid()) { return false; } const bool creatingNewArchive = arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true"); if (creatingNewArchive) { createArchive(); } else { loadArchive(); } return true; } bool Part::saveFile() { return true; } bool Part::isBusy() const { return m_busy; } KConfigSkeleton *Part::config() const { return ArkSettings::self(); } QList Part::settingsPages(QWidget *parent) const { QList pages; pages.append(new ExtractionSettingsPage(parent, i18nc("@title:tab", "Extraction Settings"), QStringLiteral("archive-extract"))); pages.append(new PreviewSettingsPage(parent, i18nc("@title:tab", "Preview Settings"), QStringLiteral("document-preview-archive"))); return pages; } bool Part::isLocalFileValid() { const QString localFile = localFilePath(); const QFileInfo localFileInfo(localFile); const bool creatingNewArchive = arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true"); if (localFileInfo.isDir()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "%1 is a directory.", localFile)); return false; } if (creatingNewArchive) { if (localFileInfo.exists()) { if (!confirmAndDelete(localFile)) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Could not overwrite %1. Check whether you have write permission.", localFile)); return false; } } displayMsgWidget(KMessageWidget::Information, xi18nc("@info", "The archive %1 will be created as soon as you add a file.", localFile)); } else { if (!localFileInfo.exists()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 was not found.", localFile)); return false; } if (!localFileInfo.isReadable()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 could not be loaded, as it was not possible to read from it.", localFile)); return false; } } return true; } bool Part::confirmAndDelete(const QString &targetFile) { QFileInfo targetInfo(targetFile); const auto buttonCode = KMessageBox::warningYesNo(widget(), xi18nc("@info", "The archive %1 already exists. Do you wish to overwrite it?", targetInfo.fileName()), i18nc("@title:window", "File Exists"), KGuiItem(i18nc("@action:button", "Overwrite")), KStandardGuiItem::cancel()); if (buttonCode != KMessageBox::Yes || !targetInfo.isWritable()) { return false; } qCDebug(ARK) << "Removing file" << targetFile; return QFile(targetFile).remove(); } void Part::slotLoadingStarted() { m_model->filesToMove.clear(); m_model->filesToCopy.clear(); } void Part::slotLoadingFinished(KJob *job) { if (job->error()) { if (arguments().metaData()[QStringLiteral("createNewArchive")] != QLatin1String("true")) { if (job->error() != KJob::KilledJobError) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Loading the archive %1 failed with the following error:%2", localFilePath(), job->errorString())); } // The file failed to open, so reset the open archive, info panel and caption. m_model->reset(); m_infoPanel->setPrettyFileName(QString()); m_infoPanel->updateWithDefaults(); emit setWindowCaption(QString()); } } m_view->sortByColumn(0, Qt::AscendingOrder); // #303708: expand the first level only when there is just one root folder. // Typical use case: an archive with source files. if (m_view->model()->rowCount() == 1) { m_view->expandToDepth(0); } // After loading all files, resize the columns to fit all fields m_view->header()->resizeSections(QHeaderView::ResizeToContents); // Now we can start accepting drops in the archive view. m_view->setDropsEnabled(true); updateActions(); if (!m_model->archive()) { return; } if (!m_model->archive()->comment().isEmpty()) { m_commentView->setPlainText(m_model->archive()->comment()); slotShowComment(); } else { m_commentView->clear(); m_commentBox->hide(); } if (m_model->rowCount() == 0) { qCWarning(ARK) << "No entry listed by the plugin"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "The archive is empty or Ark could not open its content.")); } else if (m_model->rowCount() == 1) { if (m_model->archive()->mimeType().inherits(QStringLiteral("application/x-cd-image")) && m_model->entryForIndex(m_model->index(0, 0))->fullPath() == QLatin1String("README.TXT")) { qCWarning(ARK) << "Detected ISO image with UDF filesystem"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "Ark does not currently support ISO files with UDF filesystem.")); } } if (arguments().metaData()[QStringLiteral("showExtractDialog")] == QLatin1String("true")) { QTimer::singleShot(0, this, &Part::slotShowExtractionDialog); } } void Part::setReadyGui() { QApplication::restoreOverrideCursor(); m_busy = false; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->hide(); } m_view->setEnabled(true); updateActions(); } void Part::setBusyGui() { QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); m_busy = true; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->show(); } m_view->setEnabled(false); updateActions(); } void Part::setFileNameFromArchive() { const QString prettyName = url().fileName(); m_infoPanel->setPrettyFileName(prettyName); m_infoPanel->updateWithDefaults(); emit setWindowCaption(prettyName); } void Part::slotOpenEntry(int mode) { qCDebug(ARK) << "Opening with mode" << mode; QModelIndex index = m_view->selectionModel()->currentIndex(); Archive::Entry *entry = m_model->entryForIndex(index); // Don't open directories. if (entry->isDir()) { return; } // We don't support opening symlinks. if (!entry->property("link").toString().isEmpty()) { displayMsgWidget(KMessageWidget::Information, i18n("Ark cannot open symlinks.")); return; } // Extract the entry. if (!entry->fullPath().isEmpty()) { m_openFileMode = static_cast(mode); KJob *job = Q_NULLPTR; if (m_openFileMode == Preview) { job = m_model->preview(entry); connect(job, &KJob::result, this, &Part::slotPreviewExtractedEntry); } else { job = (m_openFileMode == OpenFile) ? m_model->open(entry) : m_model->openWith(entry); connect(job, &KJob::result, this, &Part::slotOpenExtractedEntry); } registerJob(job); job->start(); } } void Part::slotOpenExtractedEntry(KJob *job) { if (!job->error()) { OpenJob *openJob = qobject_cast(job); Q_ASSERT(openJob); // Since the user could modify the file (unlike the Preview case), // we'll need to manually delete the temp dir in the Part destructor. m_tmpExtractDirList << openJob->tempDir(); const QString fullName = openJob->validatedFilePath(); bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); // If archive is readonly set temporarily extracted file to readonly as // well so user will be notified if trying to modify and save the file. if (!isWritable) { QFile::setPermissions(fullName, QFileDevice::ReadOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther); } if (isWritable) { m_fileWatcher = new QFileSystemWatcher; connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &Part::slotWatchedFileModified); m_fileWatcher->addPath(fullName); } if (qobject_cast(job)) { const QList urls = {QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile)}; KRun::displayOpenWithDialog(urls, widget()); } else { KRun::runUrl(QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile), QMimeDatabase().mimeTypeForFile(fullName).name(), widget()); } } else if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotPreviewExtractedEntry(KJob *job) { if (!job->error()) { PreviewJob *previewJob = qobject_cast(job); Q_ASSERT(previewJob); 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, Q_NULLPTR, relPath); } // This is needed because some apps, such as Kate, delete and recreate // files when saving. m_fileWatcher->addPath(file); } void Part::slotError(const QString& errorMessage, const QString& details) { if (details.isEmpty()) { KMessageBox::error(widget(), errorMessage); } else { KMessageBox::detailedError(widget(), errorMessage, details); } } bool Part::isSingleFolderArchive() const { return m_model->archive()->isSingleFolder(); } QString Part::detectSubfolder() const { if (!m_model) { return QString(); } return m_model->archive()->subfolderName(); } void Part::slotExtractArchive() { if (m_view->selectionModel()->selectedRows().count() > 0) { m_view->selectionModel()->clear(); } slotShowExtractionDialog(); } void Part::slotShowExtractionDialog() { if (!m_model) { return; } QPointer dialog(new Kerfuffle::ExtractionDialog); dialog.data()->setModal(true); if (m_view->selectionModel()->selectedRows().count() > 0) { dialog.data()->setShowSelectedFiles(true); } dialog.data()->setSingleFolderArchive(isSingleFolderArchive()); dialog.data()->setSubfolder(detectSubfolder()); dialog.data()->setCurrentUrl(QUrl::fromLocalFile(QFileInfo(m_model->archive()->fileName()).absolutePath())); dialog.data()->show(); dialog.data()->restoreWindowSize(); if (dialog.data()->exec()) { updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); QList files; // If the user has chosen to extract only selected entries, fetch these // from the QTreeView. if (!dialog.data()->extractAllFiles()) { files = filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); } qCDebug(ARK) << "Selected " << files; Kerfuffle::ExtractionOptions options; if (dialog.data()->preservePaths()) { options[QStringLiteral("PreservePaths")] = true; } options[QStringLiteral("FollowExtractionDialogSettings")] = true; const QString destinationDirectory = dialog.data()->destinationDirectory().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; } QList Part::filesForIndexes(const QModelIndexList& list) const { QList ret; foreach(const QModelIndex& index, list) { ret << m_model->entryForIndex(index); } return ret; } QList Part::filesAndRootNodesForIndexes(const QModelIndexList& list) const { QList fileList; QStringList fullPathsList; foreach (const QModelIndex& index, list) { // Find the topmost unselected parent. This is done by iterating up // through the directory hierarchy and see if each parent is included // in the selection OR if the parent is already part of list. // The latter is needed for unselected folders which are subfolders of // a selected parent folder. QModelIndex selectionRoot = index.parent(); while (m_view->selectionModel()->isSelected(selectionRoot) || list.contains(selectionRoot)) { selectionRoot = selectionRoot.parent(); } // Fetch the root node for the unselected parent. const QString rootFileName = selectionRoot.isValid() ? m_model->entryForIndex(selectionRoot)->fullPath() : QString(); // Append index with root node to fileList. QModelIndexList alist = QModelIndexList() << index; foreach (Archive::Entry *entry, filesForIndexes(alist)) { const QString fullPath = entry->fullPath(); if (!fullPathsList.contains(fullPath)) { entry->rootNode = rootFileName; fileList.append(entry); fullPathsList.append(fullPath); } } } return fileList; } void Part::slotExtractionDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else { ExtractJob *extractJob = qobject_cast(job); Q_ASSERT(extractJob); const bool followExtractionDialogSettings = extractJob->extractionOptions().value(QStringLiteral("FollowExtractionDialogSettings"), false).toBool(); if (!followExtractionDialogSettings) { return; } if (ArkSettings::openDestinationFolderAfterExtraction()) { qCDebug(ARK) << "Shall open" << extractJob->destinationDirectory(); QUrl destinationDirectory = QUrl::fromLocalFile(extractJob->destinationDirectory()).adjusted(QUrl::NormalizePathSegments); qCDebug(ARK) << "Shall open URL" << destinationDirectory; KRun::runUrl(destinationDirectory, QStringLiteral("inode/directory"), widget()); } if (ArkSettings::closeAfterExtraction()) { emit quit(); } } } void Part::adjustColumns() { m_view->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); } void Part::slotAddFiles(const QStringList& filesToAdd, const Archive::Entry *destination, const QString &relPath) { if (!m_model->archive() || filesToAdd.isEmpty()) { return; } QStringList withChildPaths; foreach (const QString& file, filesToAdd) { m_jobTempEntries.push_back(new Archive::Entry(Q_NULLPTR, file)); if (QFileInfo(file).isDir()) { withChildPaths << file + QLatin1Char('/'); QDirIterator it(file, QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (it.hasNext()) { QString path = it.next(); if (it.fileInfo().isDir()) { path += QLatin1Char('/'); } withChildPaths << path; } } else { withChildPaths << file; } } withChildPaths = ReadOnlyArchiveInterface::entryPathsFromDestination(withChildPaths, destination, 0); QList conflictingEntries; bool error = m_model->conflictingEntries(conflictingEntries, withChildPaths, true); if (conflictingEntries.count() > 0) { QPointer overwriteDialog = new OverwriteDialog(widget(), conflictingEntries, m_model->entryIcons(), error); int ret = overwriteDialog->exec(); delete overwriteDialog; if (ret == QDialog::Rejected) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); return; } } // GlobalWorkDir is used by AddJob and should contain the part of the // absolute path of files to be added that should NOT be included in the // directory structure within the archive. // Example: We add file "/home/user/somedir/somefile.txt" and want the file // to have the relative path within the archive "somedir/somefile.txt". // GlobalWorkDir is then: "/home/user" QString globalWorkDir = filesToAdd.first(); // path represents the path of the file within the archive. This needs to // be removed from globalWorkDir, otherwise the files will be added to the // root of the archive. In the example above, path would be "somedir/". if (!relPath.isEmpty()) { globalWorkDir.remove(relPath); qCDebug(ARK) << "Adding" << filesToAdd << "to" << relPath; } else { qCDebug(ARK) << "Adding " << filesToAdd << ((destination == Q_NULLPTR) ? QString() : QStringLiteral("to ") + destination->fullPath()); } // Remove trailing slash (needed when adding dirs). if (globalWorkDir.right(1) == QLatin1String("/")) { globalWorkDir.chop(1); } CompressionOptions options(m_model->archive()->compressionOptions()); // Now take the absolute path of the parent directory. globalWorkDir = QFileInfo(globalWorkDir).dir().absolutePath(); qCDebug(ARK) << "Detected GlobalWorkDir to be " << globalWorkDir; options[QStringLiteral("GlobalWorkDir")] = globalWorkDir; AddJob *job = m_model->addFiles(m_jobTempEntries, destination, options); if (!job) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); return; } connect(job, &KJob::result, this, &Part::slotAddFilesDone); registerJob(job); job->start(); } void Part::slotAddFiles() { // If compression options are already set, we don't use the values from CreateDialog. CompressionOptions opts; if (m_model->archive()->compressionOptions().isEmpty()) { if (arguments().metaData().contains(QStringLiteral("compressionLevel"))) { opts[QStringLiteral("CompressionLevel")] = arguments().metaData()[QStringLiteral("compressionLevel")]; } + if (arguments().metaData().contains(QStringLiteral("compressionMethod"))) { + opts[QStringLiteral("CompressionMethod")] = arguments().metaData()[QStringLiteral("compressionMethod")]; + } if (arguments().metaData().contains(QStringLiteral("volumeSize"))) { opts[QStringLiteral("VolumeSize")] = arguments().metaData()[QStringLiteral("volumeSize")]; } m_model->archive()->setCompressionOptions(opts); } else { opts = m_model->archive()->compressionOptions(); } + if (m_model->archive()->property("compressionMethods").toStringList().size() == 1) { + opts[QStringLiteral("CompressionMethod")] = m_model->archive()->property("compressionMethods").toStringList().first(); + } + QString dialogTitle = i18nc("@title:window", "Add Files"); const Archive::Entry *destination = Q_NULLPTR; if (m_view->selectionModel()->selectedRows().count() == 1) { destination = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); if (destination->isDir()) { dialogTitle = i18nc("@title:window", "Add Files to %1", destination->fullPath());; } else { destination = Q_NULLPTR; } } qCDebug(ARK) << "Opening AddDialog with opts:" << opts; // #264819: passing widget() as the parent will not work as expected. // KFileDialog will create a KFileWidget, which runs an internal // event loop to stat the given directory. This, in turn, leads to // events being delivered to widget(), which is a QSplitter, which // in turn reimplements childEvent() and will end up calling // QWidget::show() on the KFileDialog (thus showing it in a // non-modal state). // When KFileDialog::exec() is called, the widget is already shown // and nothing happens. QPointer dlg = new AddDialog(widget(), dialogTitle, m_lastUsedAddPath, m_model->archive()->mimeType(), opts); if (dlg->exec() == QDialog::Accepted) { qCDebug(ARK) << "Selected files:" << dlg->selectedFiles(); qCDebug(ARK) << "Options:" << dlg->compressionOptions(); m_model->archive()->setCompressionOptions(dlg->compressionOptions()); slotAddFiles(dlg->selectedFiles(), destination, QString()); } delete dlg; } void Part::slotEditFileName() { QModelIndex currentIndex = m_view->selectionModel()->currentIndex(); currentIndex = (currentIndex.parent().isValid()) ? currentIndex.parent().child(currentIndex.row(), 0) : m_model->index(currentIndex.row(), 0); m_view->openEntryEditor(currentIndex); } void Part::slotCutFiles() { QModelIndexList selectedRows = addChildren(m_view->selectionModel()->selectedRows()); m_model->filesToMove = ArchiveModel::entryMap(filesForIndexes(selectedRows)); qCDebug(ARK) << "Entries marked to cut:" << m_model->filesToMove.values(); m_model->filesToCopy.clear(); foreach (const QModelIndex &row, m_cutIndexes) { m_view->dataChanged(row, row); } m_cutIndexes = selectedRows; foreach (const QModelIndex &row, m_cutIndexes) { m_view->dataChanged(row, row); } updateActions(); } void Part::slotCopyFiles() { m_model->filesToCopy = ArchiveModel::entryMap(filesForIndexes(addChildren(m_view->selectionModel()->selectedRows()))); qCDebug(ARK) << "Entries marked to copy:" << m_model->filesToCopy.values(); foreach (const QModelIndex &row, m_cutIndexes) { m_view->dataChanged(row, row); } m_cutIndexes.clear(); m_model->filesToMove.clear(); updateActions(); } void Part::slotRenameFile(const QString &name) { if (name == QStringLiteral(".") || name == QStringLiteral("..") || name.contains(QLatin1Char('/'))) { QMessageBox messageBox(QMessageBox::Warning, i18n("Invalid filename"), i18n("Filename can't contain slashes and can't be equal to \".\" or \"..\""), QMessageBox::Ok); messageBox.exec(); return; } const Archive::Entry *entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); QList entriesToMove = filesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); m_destination = new Archive::Entry(); const QString &entryPath = entry->fullPath(true); const QString rootPath = entryPath.left(entryPath.count() - entry->name().count()); m_destination->setFullPath(rootPath + name + ((entry->isDir()) ? QLatin1Char('/') : QChar())); slotPasteFiles(entriesToMove, m_destination, 1); } void Part::slotPasteFiles() { m_destination = (m_view->selectionModel()->selectedRows().count() > 0) ? m_model->entryForIndex(m_view->selectionModel()->currentIndex()) : Q_NULLPTR; if (m_destination == Q_NULLPTR) { m_destination = new Archive::Entry(Q_NULLPTR, QString()); } else { m_destination = new Archive::Entry(Q_NULLPTR, m_destination->fullPath()); } if (m_model->filesToMove.count() > 0) { // Changing destination to include new entry path if pasting only 1 entry. QList entriesWithoutChildren = ReadOnlyArchiveInterface::entriesWithoutChildren(m_model->filesToMove.values()); if (entriesWithoutChildren.count() == 1) { const Archive::Entry *entry = entriesWithoutChildren.first(); const QString nameWithSlash = entry->name() + ((entry->isDir()) ? QLatin1Char('/') : QChar()); m_destination->setFullPath(m_destination->fullPath() + nameWithSlash); } foreach (const Archive::Entry *entry, entriesWithoutChildren) { if (entry->isDir() && m_destination->fullPath().startsWith(entry->fullPath())) { QMessageBox messageBox(QMessageBox::Warning, i18n("Moving a folder into itself"), i18n("Folders can't be moved into themselves."), QMessageBox::Ok); messageBox.exec(); delete m_destination; return; } } QList entryList = m_model->filesToMove.values(); slotPasteFiles(entryList, m_destination, entriesWithoutChildren.count()); m_model->filesToMove.clear(); } else { QList entryList = m_model->filesToCopy.values(); slotPasteFiles(entryList, m_destination, 0); m_model->filesToCopy.clear(); } m_cutIndexes.clear(); updateActions(); } void Part::slotPasteFiles(QList &files, Kerfuffle::Archive::Entry *destination, int entriesWithoutChildren) { if (files.isEmpty()) { delete m_destination; return; } QStringList filesPaths = ReadOnlyArchiveInterface::entryFullPaths(files); QStringList newPaths = ReadOnlyArchiveInterface::entryPathsFromDestination(filesPaths, destination, entriesWithoutChildren); if (ArchiveModel::hasDuplicatedEntries(newPaths)) { QMessageBox messageBox(QMessageBox::Warning, i18n("Pasting entries with the same name"), i18n("Entries with the same names can't be pasted to the same destination."), QMessageBox::Ok); messageBox.exec(); delete m_destination; return; } QList conflictingEntries; bool error = m_model->conflictingEntries(conflictingEntries, newPaths, false); if (conflictingEntries.count() != 0) { QPointer overwriteDialog = new OverwriteDialog(widget(), conflictingEntries, m_model->entryIcons(), error); int ret = overwriteDialog->exec(); delete overwriteDialog; if (ret == QDialog::Rejected) { delete m_destination; return; } } if (entriesWithoutChildren > 0) { qCDebug(ARK) << "Moving" << files << "to" << destination; } else { qCDebug(ARK) << "Copying " << files << "to" << destination; } CompressionOptions options(m_model->archive()->compressionOptions()); KJob *job; if (entriesWithoutChildren != 0) { job = m_model->moveFiles(files, destination, options); } else { job = m_model->copyFiles(files, destination, options); } if (job) { connect(job, &KJob::result, this, &Part::slotPasteFilesDone); registerJob(job); job->start(); } else { delete m_destination; } } void Part::slotAddFilesDone(KJob* job) { qDeleteAll(m_jobTempEntries); m_jobTempEntries.clear(); if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else { // Hide the "archive will be created as soon as you add a file" message. m_messageWidget->hide(); // 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(m_view->selectionModel()->selectedRows()))); 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()); } void Part::displayMsgWidget(KMessageWidget::MessageType type, const QString& msg) { // The widget could be already visible, so hide it. m_messageWidget->hide(); m_messageWidget->setText(msg); m_messageWidget->setMessageType(type); m_messageWidget->animatedShow(); } } // namespace Ark #include "part.moc" diff --git a/plugins/cli7zplugin/cliplugin.cpp b/plugins/cli7zplugin/cliplugin.cpp index 1d41973d..c05b3b97 100644 --- a/plugins/cli7zplugin/cliplugin.cpp +++ b/plugins/cli7zplugin/cliplugin.cpp @@ -1,309 +1,334 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/cliinterface.h" #include "kerfuffle/kerfuffle_export.h" #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_cli7z.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList & args) : CliInterface(parent, args) , m_archiveType(ArchiveType7z) , m_parseState(ParseStateTitle) , m_linesComment(0) , m_isFirstInformationEntry(true) { qCDebug(ARK) << "Loaded cli_7z plugin"; } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateTitle; m_comment.clear(); m_numberOfVolumes = 0; } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { //p[CaptureProgress] = true; p[ListProgram] = p[ExtractProgram] = p[DeleteProgram] = p[MoveProgram] = p[AddProgram] = p[TestProgram] = QStringList() << QStringLiteral("7z"); p[ListArgs] = QStringList() << QStringLiteral("l") << QStringLiteral("-slt") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive"); p[ExtractArgs] = QStringList() << QStringLiteral("$PreservePathSwitch") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive") << QStringLiteral("$Files"); p[PreservePathSwitch] = QStringList() << QStringLiteral("x") << QStringLiteral("e"); p[PasswordSwitch] = QStringList() << QStringLiteral("-p$Password"); p[PasswordHeaderSwitch] = QStringList { QStringLiteral("-p$Password"), QStringLiteral("-mhe=on") }; p[WrongPasswordPatterns] = QStringList() << QStringLiteral("Wrong password"); p[CompressionLevelSwitch] = QStringLiteral("-mx=$CompressionLevel"); p[AddArgs] = QStringList() << QStringLiteral("a") << QStringLiteral("-l") << QStringLiteral("$Archive") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$CompressionLevelSwitch") + << QStringLiteral("$CompressionMethodSwitch") << QStringLiteral("$MultiVolumeSwitch") << QStringLiteral("$Files"); p[MoveArgs] = QStringList() << QStringLiteral("rn") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive") << QStringLiteral("$PathPairs"); p[DeleteArgs] = QStringList() << QStringLiteral("d") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive") << QStringLiteral("$Files"); p[TestArgs] = QStringList() << QStringLiteral("t") << QStringLiteral("$Archive") << QStringLiteral("$PasswordSwitch"); p[TestPassedPattern] = QStringLiteral("^Everything is Ok$"); p[FileExistsExpression] = QStringList() << QStringLiteral("^\\(Y\\)es / \\(N\\)o / \\(A\\)lways / \\(S\\)kip all / A\\(u\\)to rename all / \\(Q\\)uit\\? $") << QStringLiteral("^\\? \\(Y\\)es / \\(N\\)o / \\(A\\)lways / \\(S\\)kip all / A\\(u\\)to rename all / \\(Q\\)uit\\? $"); p[FileExistsFileName] = QStringList() << QStringLiteral("^file \\./(.*)$") << QStringLiteral("^ Path: \\./(.*)$"); p[FileExistsInput] = QStringList() << QStringLiteral("Y") //overwrite << QStringLiteral("N") //skip << QStringLiteral("A") //overwrite all << QStringLiteral("S") //autoskip << QStringLiteral("Q"); //cancel p[PasswordPromptPattern] = QStringLiteral("Enter password \\(will not be echoed\\)"); p[ExtractionFailedPatterns] = QStringList() << QStringLiteral("ERROR: E_FAIL") << QStringLiteral("Open ERROR: Can not open the file as \\[7z\\] archive"); p[CorruptArchivePatterns] = QStringList() << QStringLiteral("Unexpected end of archive") << QStringLiteral("Headers Error"); p[DiskFullPatterns] = QStringList() << QStringLiteral("No space left on device"); p[MultiVolumeSwitch] = QStringLiteral("-v$VolumeSizek"); p[MultiVolumeSuffix] = QStringList() << QStringLiteral("$Suffix.001"); + QMap compMethodMap; + compMethodMap[QStringLiteral("zip")] = QStringLiteral("-mm=$CompressionMethod"); + compMethodMap[QStringLiteral("7z")] = QStringLiteral("-m0=$CompressionMethod"); + p[CompressionMethodSwitch] = compMethodMap; } return p; } bool CliPlugin::readListLine(const QString& line) { static const QLatin1String archiveInfoDelimiter1("--"); // 7z 9.13+ static const QLatin1String archiveInfoDelimiter2("----"); // 7z 9.04 static const QLatin1String entryInfoDelimiter("----------"); const QRegularExpression rxComment(QStringLiteral("Comment = .+$")); if (m_parseState == ParseStateTitle) { const QRegularExpression rxVersionLine(QStringLiteral("^p7zip Version ([\\d\\.]+) .*$")); QRegularExpressionMatch matchVersion = rxVersionLine.match(line); if (matchVersion.hasMatch()) { m_parseState = ParseStateHeader; const QString p7zipVersion = matchVersion.captured(1); qCDebug(ARK) << "p7zip version" << p7zipVersion << "detected"; } } else if (m_parseState == ParseStateHeader) { if (line.startsWith(QStringLiteral("Listing archive:"))) { qCDebug(ARK) << "Archive name: " << line.right(line.size() - 16).trimmed(); } else if ((line == archiveInfoDelimiter1) || (line == archiveInfoDelimiter2)) { m_parseState = ParseStateArchiveInformation; } else if (line.contains(QStringLiteral("Error: "))) { qCWarning(ARK) << line.mid(7); } } else if (m_parseState == ParseStateArchiveInformation) { if (line == entryInfoDelimiter) { m_parseState = ParseStateEntryInformation; } else if (line.startsWith(QStringLiteral("Type = "))) { const QString type = line.mid(7).trimmed(); qCDebug(ARK) << "Archive type: " << type; if (type == QLatin1String("7z")) { m_archiveType = ArchiveType7z; } else if (type == QLatin1String("bzip2")) { m_archiveType = ArchiveTypeBZip2; } else if (type == QLatin1String("gzip")) { m_archiveType = ArchiveTypeGZip; } else if (type == QLatin1String("xz")) { m_archiveType = ArchiveTypeXz; } else if (type == QLatin1String("tar")) { m_archiveType = ArchiveTypeTar; } else if (type == QLatin1String("zip")) { m_archiveType = ArchiveTypeZip; } else if (type == QLatin1String("Rar")) { m_archiveType = ArchiveTypeRar; } else if (type == QLatin1String("Split")) { setMultiVolume(true); } else { // Should not happen qCWarning(ARK) << "Unsupported archive type"; return false; } } else if (line.startsWith(QStringLiteral("Volumes = "))) { m_numberOfVolumes = line.section(QLatin1Char('='), 1).trimmed().toInt(); } else if (line.startsWith(QStringLiteral("Method = "))) { QStringList methods = line.section(QLatin1Char('='), 1).trimmed().split(QLatin1Char(' '), QString::SkipEmptyParts); // LZMA methods are output with some trailing numbers by 7z representing dictionary/block sizes. // We are not interested in these, so remove them. QMutableListIterator i(methods); while (i.hasNext()) { QString m = i.next(); if (m.startsWith(QLatin1String("LZMA2"))) { m = m.left(5); } else if (m.startsWith(QLatin1String("LZMA"))) { m = m.left(4); } i.setValue(m); } emit compressionMethodFound(methods); } else if (rxComment.match(line).hasMatch()) { m_parseState = ParseStateComment; m_comment.append(line.section(QLatin1Char('='), 1) + QLatin1Char('\n')); } } else if (m_parseState == ParseStateComment) { if (line == entryInfoDelimiter) { m_parseState = ParseStateEntryInformation; if (!m_comment.trimmed().isEmpty()) { m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } } else if (m_parseState == ParseStateEntryInformation) { if (m_isFirstInformationEntry) { m_isFirstInformationEntry = false; m_currentArchiveEntry = new Archive::Entry(); m_currentArchiveEntry->compressedSizeIsSet = false; } if (line.startsWith(QStringLiteral("Path = "))) { const QString entryFilename = QDir::fromNativeSeparators(line.mid(7).trimmed()); m_currentArchiveEntry->setProperty("fullPath", entryFilename); } else if (line.startsWith(QStringLiteral("Size = "))) { m_currentArchiveEntry->setProperty("size", line.mid(7).trimmed()); } else if (line.startsWith(QStringLiteral("Packed Size = "))) { // #236696: 7z files only show a single Packed Size value // corresponding to the whole archive. if (m_archiveType != ArchiveType7z) { m_currentArchiveEntry->compressedSizeIsSet = true; m_currentArchiveEntry->setProperty("compressedSize", line.mid(14).trimmed()); } } else if (line.startsWith(QStringLiteral("Modified = "))) { m_currentArchiveEntry->setProperty("timestamp", QDateTime::fromString(line.mid(11).trimmed(), QStringLiteral("yyyy-MM-dd hh:mm:ss"))); } else if (line.startsWith(QStringLiteral("Attributes = "))) { const QString attributes = line.mid(13).trimmed(); const bool isDirectory = attributes.startsWith(QLatin1Char('D')); m_currentArchiveEntry->setProperty("isDirectory", isDirectory); if (isDirectory) { const QString directoryName = m_currentArchiveEntry->fullPath(); if (!directoryName.endsWith(QLatin1Char('/'))) { const bool isPasswordProtected = (line.at(12) == QLatin1Char('+')); m_currentArchiveEntry->setProperty("fullPath", QString(directoryName + QLatin1Char('/'))); m_currentArchiveEntry->setProperty("isPasswordProtected", isPasswordProtected); } } m_currentArchiveEntry->setProperty("permissions", attributes.mid(1)); } else if (line.startsWith(QStringLiteral("CRC = "))) { m_currentArchiveEntry->setProperty("CRC", line.mid(6).trimmed()); } else if (line.startsWith(QStringLiteral("Method = "))) { m_currentArchiveEntry->setProperty("method", line.mid(9).trimmed()); // For zip archives we need to check method for each entry. if (m_archiveType == ArchiveTypeZip) { QString method = line.mid(9).trimmed(); if (method == QLatin1String("xz")) { method = QStringLiteral("XZ"); } if (!m_compressionMethods.contains(method)) { m_compressionMethods.append(method); emit compressionMethodFound(m_compressionMethods); } } } else if (line.startsWith(QStringLiteral("Encrypted = ")) && line.size() >= 13) { m_currentArchiveEntry->setProperty("isPasswordProtected", line.at(12) == QLatin1Char('+')); } else if (line.startsWith(QStringLiteral("Block = ")) || line.startsWith(QStringLiteral("Version = "))) { m_isFirstInformationEntry = true; if (!m_currentArchiveEntry->fullPath().isEmpty()) { emit entry(m_currentArchiveEntry); } else { delete m_currentArchiveEntry; } m_currentArchiveEntry = Q_NULLPTR; } } return true; } QStringList CliPlugin::passwordHeaderSwitch(const QString& password) const { if (password.isEmpty()) { return QStringList(); } Q_ASSERT(m_param.contains(PasswordHeaderSwitch)); QStringList passwordHeaderSwitch = m_param.value(PasswordHeaderSwitch).toStringList(); Q_ASSERT(!passwordHeaderSwitch.isEmpty() && passwordHeaderSwitch.size() == 2); passwordHeaderSwitch[0].replace(QLatin1String("$Password"), password); return passwordHeaderSwitch; } +QString CliPlugin::compressionMethodSwitch(const QString &method) const +{ + if (method.isEmpty()) { + return QString(); + } + + Q_ASSERT(!filename().isEmpty()); + Q_ASSERT(m_param.contains(CompressionMethodSwitch)); + + QMap switches = m_param.value(CompressionMethodSwitch).toMap(); + Q_ASSERT(!switches.isEmpty()); + + QString compMethodSwitch; + + compMethodSwitch = switches[QFileInfo(filename()).suffix().toLower()].toString(); + compMethodSwitch.replace(QLatin1String("$CompressionMethod"), method); + + return compMethodSwitch; +} + #include "cliplugin.moc" diff --git a/plugins/cli7zplugin/cliplugin.h b/plugins/cli7zplugin/cliplugin.h index 27c33c50..c2b89201 100644 --- a/plugins/cli7zplugin/cliplugin.h +++ b/plugins/cli7zplugin/cliplugin.h @@ -1,73 +1,74 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2010 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "kerfuffle/cliinterface.h" #include "kerfuffle/archiveentry.h" class CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList & args); virtual ~CliPlugin(); virtual void resetParsing() Q_DECL_OVERRIDE; virtual Kerfuffle::ParameterList parameterList() const Q_DECL_OVERRIDE; virtual bool readListLine(const QString &line) Q_DECL_OVERRIDE; /** * @return The password header-switch with the given @p password. */ virtual QStringList passwordHeaderSwitch(const QString& password) const Q_DECL_OVERRIDE; + virtual QString compressionMethodSwitch(const QString &method) const Q_DECL_OVERRIDE; private: enum ArchiveType { ArchiveType7z = 0, ArchiveTypeBZip2, ArchiveTypeGZip, ArchiveTypeXz, ArchiveTypeTar, ArchiveTypeZip, ArchiveTypeRar } m_archiveType; enum ParseState { ParseStateTitle = 0, ParseStateHeader, ParseStateArchiveInformation, ParseStateComment, ParseStateEntryInformation } m_parseState; int m_linesComment; Kerfuffle::Archive::Entry *m_currentArchiveEntry; bool m_isFirstInformationEntry; QStringList m_compressionMethods; }; #endif // CLIPLUGIN_H diff --git a/plugins/cli7zplugin/kerfuffle_cli7z.json.cmake b/plugins/cli7zplugin/kerfuffle_cli7z.json.cmake index 30344415..d4d6bad0 100644 --- a/plugins/cli7zplugin/kerfuffle_cli7z.json.cmake +++ b/plugins/cli7zplugin/kerfuffle_cli7z.json.cmake @@ -1,65 +1,83 @@ { "KPlugin": { "Id": "kerfuffle_cli7z", "MimeTypes": [ "@SUPPORTED_MIMETYPES@" ], "Name": "7zip archive plugin", "Name[ca@valencia]": "Connector per arxius 7zip", "Name[ca]": "Connector per arxius 7zip", "Name[cs]": "Modul pro archiv 7zip", "Name[de]": "7zip-Archiv-Modul", "Name[es]": "Complemento de archivo 7zip", "Name[et]": "7zip arhiivi plugin", "Name[fi]": "7zip-pakkaustuki", - "Name[fr]": "Module externe d'archive « 7zip »", + "Name[fr]": "Module externe d'archive « 7zip »", "Name[gl]": "Complemento de arquivo de 7zip", "Name[he]": "תוסף ארכיוני 7zip", "Name[it]": "Estensione per archivi 7zip", "Name[nb]": "Programtillegg for 7zip-arkiv", "Name[nl]": "7zip-archiefplug-in", "Name[nn]": "7zip-arkivtillegg", "Name[pl]": "Wtyczka archiwów 7zip", "Name[pt]": "'Plugin' de pacotes 7zip", "Name[pt_BR]": "Plugin de arquivos 7zip", "Name[ru]": "Поддержка архивов 7zip", "Name[sk]": "Modul 7zip archívu", "Name[sl]": "Vstavek za arhive 7zip", "Name[sr@ijekavian]": "Прикључак 7зип архива", "Name[sr@ijekavianlatin]": "Priključak 7zip arhiva", "Name[sr@latin]": "Priključak 7zip arhiva", "Name[sr]": "Прикључак 7зип архива", "Name[sv]": "Insticksprogram för 7zip-arkiv", "Name[uk]": "Додаток для архівів 7zip", "Name[x-test]": "xx7zip archive pluginxx", "Name[zh_CN]": "7zip 归档插件", "ServiceTypes": [ "Kerfuffle/Plugin" ], "Version": "@KDE_APPLICATIONS_VERSION@" }, "X-KDE-Kerfuffle-ReadOnlyExecutables": [ "7z" ], "X-KDE-Kerfuffle-ReadWrite": true, "X-KDE-Kerfuffle-ReadWriteExecutables": [ "7z" ], "X-KDE-Priority": 180, "application/x-7z-compressed": { "CompressionLevelDefault": 5, "CompressionLevelMax": 9, "CompressionLevelMin": 0, "HeaderEncryption": true, "SupportsMultiVolume": true, - "SupportsTesting": true + "SupportsTesting": true, + "CompressionMethods": [ + "BZip2", + "Copy", + "Deflate", + "LZMA", + "LZMA2", + "PPMd" + ], + "CompressionMethodDefault": "LZMA2" }, "application/zip": { "CompressionLevelDefault": 5, "CompressionLevelMax": 9, "CompressionLevelMin": 0, "Encryption": true, "SupportsMultiVolume": true, - "SupportsTesting": true + "SupportsTesting": true, + "CompressionMethods": [ + "BZip2", + "Copy", + "Deflate", + "Deflate64", + "LZMA", + "PPMd" + ], + "CompressionMethodDefault": "Deflate" } -} \ No newline at end of file +} diff --git a/plugins/clirarplugin/cliplugin.cpp b/plugins/clirarplugin/cliplugin.cpp index ac13a43f..3bf786bb 100644 --- a/plugins/clirarplugin/cliplugin.cpp +++ b/plugins/clirarplugin/cliplugin.cpp @@ -1,549 +1,569 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2010-2011,2014 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/archiveentry.h" #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_clirar.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList& args) : CliInterface(parent, args) , m_parseState(ParseStateTitle) , m_isUnrar5(false) , m_isPasswordProtected(false) , m_isSolid(false) , m_remainingIgnoreLines(1) //The first line of UNRAR output is empty. , m_linesComment(0) { qCDebug(ARK) << "Loaded cli_rar plugin"; // Empty lines are needed for parsing output of unrar. setListEmptyLines(true); } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateTitle; m_remainingIgnoreLines = 1; m_unrarVersion.clear(); m_comment.clear(); m_numberOfVolumes = 0; } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { p[CaptureProgress] = true; p[ListProgram] = p[ExtractProgram] = p[TestProgram] = QStringList() << QStringLiteral( "unrar" ); p[DeleteProgram] = p[MoveProgram] = p[AddProgram] = QStringList() << QStringLiteral( "rar" ); p[ListArgs] = QStringList() << QStringLiteral("vt") << QStringLiteral("-v") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive"); p[ExtractArgs] = QStringList() << QStringLiteral( "-kb" ) << QStringLiteral( "-p-" ) << QStringLiteral( "$PreservePathSwitch" ) << QStringLiteral( "$PasswordSwitch" ) << QStringLiteral( "$Archive" ) << QStringLiteral( "$Files" ); p[PreservePathSwitch] = QStringList() << QStringLiteral( "x" ) << QStringLiteral( "e" ); p[PasswordSwitch] = QStringList() << QStringLiteral( "-p$Password" ); p[PasswordHeaderSwitch] = QStringList() << QStringLiteral("-hp$Password"); p[CompressionLevelSwitch] = QStringLiteral("-m$CompressionLevel"); p[MultiVolumeSwitch] = QStringLiteral("-v$VolumeSizek"); p[DeleteArgs] = QStringList() << QStringLiteral( "d" ) << QStringLiteral( "$PasswordSwitch" ) << QStringLiteral( "$Archive" ) << QStringLiteral( "$Files" ); p[FileExistsExpression] = QStringList() << QStringLiteral("^\\[Y\\]es, \\[N\\]o, \\[A\\]ll, n\\[E\\]ver, \\[R\\]ename, \\[Q\\]uit $"); p[FileExistsFileName] = QStringList() << QStringLiteral("^(.+) already exists. Overwrite it") // unrar 3 & 4 << QStringLiteral("^Would you like to replace the existing file (.+)$"); // unrar 5 p[FileExistsInput] = QStringList() << QStringLiteral( "Y" ) //overwrite << QStringLiteral( "N" ) //skip << QStringLiteral( "A" ) //overwrite all << QStringLiteral( "E" ) //autoskip << QStringLiteral( "Q" ); //cancel p[AddArgs] = QStringList() << QStringLiteral( "a" ) << QStringLiteral( "$Archive" ) << QStringLiteral("$PasswordSwitch") << QStringLiteral("$CompressionLevelSwitch") + << QStringLiteral("$CompressionMethodSwitch") << QStringLiteral("$MultiVolumeSwitch") << QStringLiteral( "$Files" ); p[MoveArgs] = QStringList() << QStringLiteral( "rn" ) << QStringLiteral( "$PasswordSwitch" ) << QStringLiteral( "$Archive" ) << QStringLiteral( "$PathPairs" ); p[PasswordPromptPattern] = QLatin1String("Enter password \\(will not be echoed\\) for"); p[WrongPasswordPatterns] = QStringList() << QStringLiteral("password incorrect") << QStringLiteral("wrong password"); p[ExtractionFailedPatterns] = QStringList() << QStringLiteral( "CRC failed" ) << QStringLiteral( "Cannot find volume" ); p[CorruptArchivePatterns] = QStringList() << QStringLiteral("Unexpected end of archive") << QStringLiteral("the file header is corrupt"); p[DiskFullPatterns] = QStringList() << QStringLiteral("No space left on device"); p[CommentArgs] = QStringList() << QStringLiteral("c") << QStringLiteral("$CommentSwitch") << QStringLiteral("$Archive"); p[CommentSwitch] = QStringLiteral("-z$CommentFile"); p[TestArgs] = QStringList() << QStringLiteral("t") << QStringLiteral("$Archive") << QStringLiteral("$PasswordSwitch"); p[TestPassedPattern] = QStringLiteral("^All OK$"); // rar will sometimes create multi-volume archives where first volume is // called name.part1.rar and other times name.part01.rar. p[MultiVolumeSuffix] = QStringList() << QStringLiteral("part01.$Suffix") << QStringLiteral("part1.$Suffix"); + p[CompressionMethodSwitch] = QStringLiteral("-ma$CompressionMethod"); } return p; } bool CliPlugin::readListLine(const QString &line) { // Ignore number of lines corresponding to m_remainingIgnoreLines. if (m_remainingIgnoreLines > 0) { --m_remainingIgnoreLines; return true; } // Parse the title line, which contains the version of unrar. if (m_parseState == ParseStateTitle) { QRegularExpression rxVersionLine(QStringLiteral("^UNRAR (\\d+\\.\\d+)( beta \\d)? .*$")); QRegularExpressionMatch matchVersion = rxVersionLine.match(line); if (matchVersion.hasMatch()) { m_parseState = ParseStateComment; m_unrarVersion = matchVersion.captured(1); qCDebug(ARK) << "UNRAR version" << m_unrarVersion << "detected"; if (m_unrarVersion.toFloat() >= 5) { m_isUnrar5 = true; qCDebug(ARK) << "Using UNRAR 5 parser"; } else { qCDebug(ARK) << "Using UNRAR 4 parser"; } } else { // If the second line doesn't contain an UNRAR title, something // is wrong. qCCritical(ARK) << "Failed to detect UNRAR output."; return false; } // Or see what version of unrar we are dealing with and call specific // handler functions. } else if (m_isUnrar5) { return handleUnrar5Line(line); } else { return handleUnrar4Line(line); } return true; } bool CliPlugin::handleUnrar5Line(const QString &line) { // Parses the comment field. if (m_parseState == ParseStateComment) { // RegExp matching end of comment field. // FIXME: Comment itself could also contain the Archive path string here. QRegularExpression rxCommentEnd(QStringLiteral("^Archive: .+$")); if (rxCommentEnd.match(line).hasMatch()) { m_parseState = ParseStateHeader; m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; if (!m_comment.isEmpty()) { qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } return true; } // Parses the header, which is whatever is between the comment field // and the entries. else if (m_parseState == ParseStateHeader) { // "Details: " indicates end of header. if (line.startsWith(QStringLiteral("Details: "))) { ignoreLines(1, ParseStateEntryDetails); if (line.contains(QLatin1String("volume"))) { m_numberOfVolumes++; if (!isMultiVolume()) { setMultiVolume(true); qCDebug(ARK) << "Multi-volume archive detected"; } } if (line.contains(QLatin1String("solid")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } if (line.contains(QLatin1String("RAR 4"))) { emit compressionMethodFound(QStringList{QStringLiteral("RAR4")}); } else if (line.contains(QLatin1String("RAR 5"))) { emit compressionMethodFound(QStringList{QStringLiteral("RAR5")}); } } return true; } // Parses the entry details for each entry. else if (m_parseState == ParseStateEntryDetails) { // For multi-volume archives there is a header between the entries in // each volume. if (line.startsWith(QLatin1String("Archive: "))) { m_parseState = ParseStateHeader; return true; // Empty line indicates end of entry. } else if (line.trimmed().isEmpty() && !m_unrar5Details.isEmpty()) { handleUnrar5Entry(); } else { // All detail lines should contain a colon. if (!line.contains(QLatin1Char(':'))) { qCWarning(ARK) << "Unrecognized line:" << line; return true; } // The details are on separate lines, so we store them in the QHash // m_unrar5Details. m_unrar5Details.insert(line.section(QLatin1Char(':'), 0, 0).trimmed().toLower(), line.section(QLatin1Char(':'), 1).trimmed()); } return true; } return true; } void CliPlugin::handleUnrar5Entry() { Archive::Entry *e = new Archive::Entry(); QString compressionRatio = m_unrar5Details.value(QStringLiteral("ratio")); compressionRatio.chop(1); // Remove the '%' e->setProperty("ratio", compressionRatio); QString time = m_unrar5Details.value(QStringLiteral("mtime")); QDateTime ts = QDateTime::fromString(time, QStringLiteral("yyyy-MM-dd HH:mm:ss,zzz")); e->setProperty("timestamp", ts); bool isDirectory = (m_unrar5Details.value(QStringLiteral("type")) == QLatin1String("Directory")); e->setProperty("isDirectory", isDirectory); if (isDirectory && !m_unrar5Details.value(QStringLiteral("name")).endsWith(QLatin1Char('/'))) { m_unrar5Details[QStringLiteral("name")] += QLatin1Char('/'); } QString compression = m_unrar5Details.value(QStringLiteral("compression")); int optionPos = compression.indexOf(QLatin1Char('-')); if (optionPos != -1) { e->setProperty("method", compression.mid(optionPos)); e->setProperty("version", compression.left(optionPos).trimmed()); } else { // No method specified. e->setProperty("method", QStringLiteral("")); e->setProperty("version", compression); } m_isPasswordProtected = m_unrar5Details.value(QStringLiteral("flags")).contains(QStringLiteral("encrypted")); e->setProperty("isPasswordProtected", m_isPasswordProtected); e->setProperty("fullPath", m_unrar5Details.value(QStringLiteral("name"))); e->setProperty("size", m_unrar5Details.value(QStringLiteral("size"))); e->setProperty("compressedSize", m_unrar5Details.value(QStringLiteral("packed size"))); e->setProperty("permissions", m_unrar5Details.value(QStringLiteral("attributes"))); e->setProperty("CRC", m_unrar5Details.value(QStringLiteral("crc32"))); if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { e->setProperty("link", m_unrar5Details.value(QStringLiteral("target"))); } m_unrar5Details.clear(); emit entry(e); } bool CliPlugin::handleUnrar4Line(const QString &line) { // Parses the comment field. if (m_parseState == ParseStateComment) { // RegExp matching end of comment field. // FIXME: Comment itself could also contain the Archive path string here. QRegularExpression rxCommentEnd(QStringLiteral("^(Solid archive|Archive|Volume) .+$")); // unrar 4 outputs the following string when opening v5 RAR archives. if (line == QLatin1String("Unsupported archive format. Please update RAR to a newer version.")) { emit error(i18n("Your unrar executable is version %1, which is too old to handle this archive. Please update to a more recent version.", m_unrarVersion)); return false; } // unrar 3 reports a non-RAR archive when opening v5 RAR archives. if (line.endsWith(QLatin1String(" is not RAR archive"))) { emit error(i18n("Unrar reported a non-RAR archive. The installed unrar version (%1) is old. Try updating your unrar.", m_unrarVersion)); return false; } // If we reach this point, then we can be sure that it's not a RAR5 // archive, so assume RAR4. emit compressionMethodFound(QStringList{QStringLiteral("RAR4")}); if (rxCommentEnd.match(line).hasMatch()) { if (line.startsWith(QLatin1String("Volume "))) { m_numberOfVolumes++; if (!isMultiVolume()) { setMultiVolume(true); qCDebug(ARK) << "Multi-volume archive detected"; } } if (line.startsWith(QLatin1String("Solid archive")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } m_parseState = ParseStateHeader; m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; if (!m_comment.isEmpty()) { qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } return true; } // Parses the header, which is whatever is between the comment field // and the entries. else if (m_parseState == ParseStateHeader) { // Horizontal line indicates end of header. if (line.startsWith(QStringLiteral("--------------------"))) { m_parseState = ParseStateEntryFileName; } else if (line.startsWith(QLatin1String("Volume "))) { m_numberOfVolumes++; } return true; } // Parses the entry name, which is on the first line of each entry. else if (m_parseState == ParseStateEntryFileName) { // Ignore empty lines. if (line.trimmed().isEmpty()) { return true; } // Three types of subHeaders can be displayed for unrar 3 and 4. // STM has 4 lines, RR has 3, and CMT has lines corresponding to // length of comment field +3. We ignore the subheaders. QRegularExpression rxSubHeader(QStringLiteral("^Data header type: (CMT|STM|RR)$")); QRegularExpressionMatch matchSubHeader = rxSubHeader.match(line); if (matchSubHeader.hasMatch()) { qCDebug(ARK) << "SubHeader of type" << matchSubHeader.captured(1) << "found"; if (matchSubHeader.captured(1) == QLatin1String("STM")) { ignoreLines(4, ParseStateEntryFileName); } else if (matchSubHeader.captured(1) == QLatin1String("CMT")) { ignoreLines(m_linesComment + 3, ParseStateEntryFileName); } else if (matchSubHeader.captured(1) == QLatin1String("RR")) { ignoreLines(3, ParseStateEntryFileName); } return true; } // The entries list ends with a horizontal line, followed by a // single summary line or, for multi-volume archives, another header. if (line.startsWith(QStringLiteral("-----------------"))) { m_parseState = ParseStateHeader; return true; // Encrypted files are marked with an asterisk. } else if (line.startsWith(QLatin1Char('*'))) { m_isPasswordProtected = true; m_unrar4Details.append(QString(line.trimmed()).remove(0, 1)); //Remove the asterisk // Entry names always start at the second position, so a line not // starting with a space is not an entry name. } else if (!line.startsWith(QLatin1Char(' '))) { qCWarning(ARK) << "Unrecognized line:" << line; return true; // If we reach this, then we can assume the line is an entry name, so // save it, and move on to the rest of the entry details. } else { m_unrar4Details.append(line.trimmed()); } m_parseState = ParseStateEntryDetails; return true; } // Parses the remainder of the entry details for each entry. else if (m_parseState == ParseStateEntryDetails) { // If the line following an entry name is empty, we did something // wrong. Q_ASSERT(!line.trimmed().isEmpty()); // If we reach a horizontal line, then the previous line was not an // entry name, so go back to header. if (line.startsWith(QStringLiteral("-----------------"))) { m_parseState = ParseStateHeader; return true; } // In unrar 3 and 4 the details are on a single line, so we // pass a QStringList containing the details. We need to store // it due to symlinks (see below). m_unrar4Details.append(line.split(QLatin1Char(' '), QString::SkipEmptyParts)); // The details line contains 9 fields, so m_unrar4Details // should now contain 9 + the filename = 10 strings. If not, this is // not an archive entry. if (m_unrar4Details.size() != 10) { m_parseState = ParseStateHeader; return true; } // When unrar 3 and 4 list a symlink, they output an extra line // containing the link target. The extra line is output after // the line we ignore, so we first need to ignore one line. if (m_unrar4Details.at(6).startsWith(QLatin1Char('l'))) { ignoreLines(1, ParseStateLinkTarget); return true; } else { handleUnrar4Entry(); } // Unrar 3 & 4 show a third line for each entry, which contains // three details: Host OS, Solid, and Old. We can ignore this // line. ignoreLines(1, ParseStateEntryFileName); return true; } // Parses a symlink target. else if (m_parseState == ParseStateLinkTarget) { m_unrar4Details.append(QString(line).remove(QStringLiteral("-->")).trimmed()); handleUnrar4Entry(); m_parseState = ParseStateEntryFileName; return true; } return true; } void CliPlugin::handleUnrar4Entry() { Archive::Entry *e = new Archive::Entry(Q_NULLPTR); QDateTime ts = QDateTime::fromString(QString(m_unrar4Details.at(4) + QLatin1Char(' ') + m_unrar4Details.at(5)), QStringLiteral("dd-MM-yy hh:mm")); // Unrar 3 & 4 output dates with a 2-digit year but QDateTime takes it as // 19??. Let's take 1950 as cut-off; similar to KDateTime. if (ts.date().year() < 1950) { ts = ts.addYears(100); } e->setProperty("timestamp", ts); bool isDirectory = ((m_unrar4Details.at(6).at(0) == QLatin1Char('d')) || (m_unrar4Details.at(6).at(1) == QLatin1Char('D'))); e->setProperty("isDirectory", isDirectory); if (isDirectory && !m_unrar4Details.at(0).endsWith(QLatin1Char('/'))) { m_unrar4Details[0] += QLatin1Char('/'); } // Unrar reports the ratio as ((compressed size * 100) / size); // we consider ratio as (100 * ((size - compressed size) / size)). // If the archive is a multivolume archive, a string indicating // whether the archive's position in the volume is displayed // instead of the compression ratio. QString compressionRatio = m_unrar4Details.at(3); if ((compressionRatio == QStringLiteral("<--")) || (compressionRatio == QStringLiteral("<->")) || (compressionRatio == QStringLiteral("-->"))) { compressionRatio = QLatin1Char('0'); } else { compressionRatio.chop(1); // Remove the '%' } e->setProperty("ratio", compressionRatio); // TODO: // - Permissions differ depending on the system the entry was added // to the archive. e->setProperty("fullPath", m_unrar4Details.at(0)); e->setProperty("size", m_unrar4Details.at(1)); e->setProperty("compressedSize", m_unrar4Details.at(2)); e->setProperty("permissions", m_unrar4Details.at(6)); e->setProperty("CRC", m_unrar4Details.at(7)); e->setProperty("method", m_unrar4Details.at(8)); e->setProperty("version", m_unrar4Details.at(9)); e->setProperty("isPasswordProtected", m_isPasswordProtected); if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { e->setProperty("link", m_unrar4Details.at(10)); } m_unrar4Details.clear(); emit entry(e); } void CliPlugin::ignoreLines(int lines, ParseState nextState) { m_remainingIgnoreLines = lines; m_parseState = nextState; } +QString CliPlugin::compressionMethodSwitch(const QString &method) const +{ + if (method.isEmpty()) { + return QString(); + } + + Q_ASSERT(m_param.contains(CompressionMethodSwitch)); + QString compMethodSwitch = m_param.value(CompressionMethodSwitch).toString(); + Q_ASSERT(!compMethodSwitch.isEmpty()); + compMethodSwitch.replace(QLatin1String("$CompressionMethod"), method); + + // This is needed for because the user-visible strings are different from the + // ones needed by the switch (e.g. RAR4 vs 4). + compMethodSwitch.remove(QLatin1String("RAR")); + + return compMethodSwitch; +} + #include "cliplugin.moc" diff --git a/plugins/clirarplugin/cliplugin.h b/plugins/clirarplugin/cliplugin.h index 7340e6f3..6cc30f76 100644 --- a/plugins/clirarplugin/cliplugin.h +++ b/plugins/clirarplugin/cliplugin.h @@ -1,70 +1,72 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2010 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "kerfuffle/cliinterface.h" class CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList &args); virtual ~CliPlugin(); virtual void resetParsing() Q_DECL_OVERRIDE; virtual Kerfuffle::ParameterList parameterList() const Q_DECL_OVERRIDE; virtual bool readListLine(const QString &line) Q_DECL_OVERRIDE; + virtual QString compressionMethodSwitch(const QString &method) const Q_DECL_OVERRIDE; + private: enum ParseState { ParseStateTitle = 0, ParseStateComment, ParseStateHeader, ParseStateEntryFileName, ParseStateEntryDetails, ParseStateLinkTarget } m_parseState; bool handleUnrar5Line(const QString &line); void handleUnrar5Entry(); bool handleUnrar4Line(const QString &line); void handleUnrar4Entry(); void ignoreLines(int lines, ParseState nextState); QStringList m_unrar4Details; QHash m_unrar5Details; QString m_unrarVersion; bool m_isUnrar5; bool m_isPasswordProtected; bool m_isSolid; int m_remainingIgnoreLines; int m_linesComment; }; #endif // CLIPLUGIN_H diff --git a/plugins/clirarplugin/kerfuffle_clirar.json.cmake b/plugins/clirarplugin/kerfuffle_clirar.json.cmake index 9abd05c2..77717b06 100644 --- a/plugins/clirarplugin/kerfuffle_clirar.json.cmake +++ b/plugins/clirarplugin/kerfuffle_clirar.json.cmake @@ -1,68 +1,78 @@ { "KPlugin": { "Id": "kerfuffle_clirar", "MimeTypes": [ "@SUPPORTED_MIMETYPES@" ], "Name": "RAR archive plugin", "Name[ca@valencia]": "Connector per arxius RAR", "Name[ca]": "Connector per arxius RAR", "Name[cs]": "Modul pro archiv RAR", "Name[de]": "RAR-Archiv-Modul", "Name[es]": "Complemento de archivo RAR", "Name[et]": "RAR-arhiivi plugin", "Name[fi]": "RAR-pakkaustuki", - "Name[fr]": "Module externe d'archive « RAR »", + "Name[fr]": "Module externe d'archive « RAR »", "Name[gl]": "Complemento de arquivo RAR", "Name[he]": "תוסף ארכיוני RAR", "Name[it]": "Estensione per archivi RAR", "Name[ja]": "RAR アーカイブ用プラグイン", "Name[nb]": "Programtillegg for RAR-arkiv", "Name[nl]": "RAR-archiefplug-in", "Name[nn]": "RAR-arkivtillegg", "Name[pl]": "Wtyczka archiwów RAR", "Name[pt]": "'Plugin' de pacotes RAR", "Name[pt_BR]": "Plugin de arquivos RAR", "Name[ru]": "Поддержка архивов RAR", "Name[sk]": "Modul RAR archívu", "Name[sl]": "Vstavek za arhive RAR", "Name[sr@ijekavian]": "Прикључак РАР архива", "Name[sr@ijekavianlatin]": "Priključak RAR arhiva", "Name[sr@latin]": "Priključak RAR arhiva", "Name[sr]": "Прикључак РАР архива", "Name[sv]": "Insticksprogram för RAR-arkiv", "Name[uk]": "Додаток для архівів RAR", "Name[x-test]": "xxRAR archive pluginxx", "Name[zh_CN]": "RAR 归档插件", "ServiceTypes": [ "Kerfuffle/Plugin" ], "Version": "@KDE_APPLICATIONS_VERSION@" }, "X-KDE-Kerfuffle-ReadOnlyExecutables": [ "unrar" ], "X-KDE-Kerfuffle-ReadWrite": true, "X-KDE-Kerfuffle-ReadWriteExecutables": [ "rar" ], "X-KDE-Priority": 120, "application/vnd.rar": { "CompressionLevelDefault": 3, "CompressionLevelMax": 5, "CompressionLevelMin": 0, "HeaderEncryption": true, "SupportsMultiVolume": true, "SupportsTesting": true, - "SupportsWriteComment": true + "SupportsWriteComment": true, + "CompressionMethods": [ + "RAR4", + "RAR5" + ], + "CompressionMethodDefault": "RAR4" }, "application/x-rar": { "CompressionLevelDefault": 3, "CompressionLevelMax": 5, "CompressionLevelMin": 0, "HeaderEncryption": true, "SupportsMultiVolume": true, "SupportsTesting": true, - "SupportsWriteComment": true + "SupportsWriteComment": true, + "CompressionMethods": [ + "RAR4", + "RAR5" + ], + "CompressionMethodDefault": "RAR4" } -} \ No newline at end of file +} diff --git a/plugins/clizipplugin/cliplugin.cpp b/plugins/clizipplugin/cliplugin.cpp index 29bc81d3..0a8f3816 100644 --- a/plugins/clizipplugin/cliplugin.cpp +++ b/plugins/clizipplugin/cliplugin.cpp @@ -1,305 +1,323 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/cliinterface.h" #include "kerfuffle/kerfuffle_export.h" #include "kerfuffle/archiveentry.h" #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_clizip.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList & args) : CliInterface(parent, args) , m_parseState(ParseStateHeader) , m_linesComment(0) { qCDebug(ARK) << "Loaded cli_zip plugin"; } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateHeader; m_tempComment.clear(); m_comment.clear(); } // #208091: infozip applies special meanings to some characters, so we // need to escape them with backslashes.see match.c in // infozip's source code QString CliPlugin::escapeFileName(const QString &fileName) const { const QString escapedCharacters(QStringLiteral("[]*?^-\\!")); QString quoted; const int len = fileName.length(); const QLatin1Char backslash('\\'); quoted.reserve(len * 2); for (int i = 0; i < len; ++i) { if (escapedCharacters.contains(fileName.at(i))) { quoted.append(backslash); } quoted.append(fileName.at(i)); } return quoted; } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { p[CaptureProgress] = false; p[ListProgram] = QStringList() << QStringLiteral("zipinfo"); p[ExtractProgram] = p[TestProgram] = QStringList() << QStringLiteral("unzip"); p[DeleteProgram] = p[AddProgram] = QStringList() << QStringLiteral("zip"); p[ListArgs] = QStringList() << QStringLiteral("-l") << QStringLiteral("-T") << QStringLiteral("-z") << QStringLiteral("$Archive"); p[ExtractArgs] = QStringList() << QStringLiteral("$PreservePathSwitch") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive") << QStringLiteral("$Files"); p[PreservePathSwitch] = QStringList() << QStringLiteral("") << QStringLiteral("-j"); p[PasswordSwitch] = QStringList() << QStringLiteral("-P$Password"); p[CompressionLevelSwitch] = QStringLiteral("-$CompressionLevel"); p[DeleteArgs] = QStringList() << QStringLiteral("-d") << QStringLiteral("$Archive") << QStringLiteral("$Files"); p[FileExistsExpression] = QStringList() << QStringLiteral("^replace (.+)\\? \\[y\\]es, \\[n\\]o, \\[A\\]ll, \\[N\\]one, \\[r\\]ename: $"); p[FileExistsFileName] = QStringList() << p[FileExistsExpression].toString(); p[FileExistsInput] = QStringList() << QStringLiteral("y") //overwrite << QStringLiteral("n") //skip << QStringLiteral("A") //overwrite all << QStringLiteral("N"); //autoskip p[AddArgs] = QStringList() << QStringLiteral("-r") << QStringLiteral("$Archive") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$CompressionLevelSwitch") + << QStringLiteral("$CompressionMethodSwitch") << QStringLiteral("$Files"); p[PasswordPromptPattern] = QStringLiteral(" password: "); p[WrongPasswordPatterns] = QStringList() << QStringLiteral("incorrect password"); p[ExtractionFailedPatterns] = QStringList() << QStringLiteral("unsupported compression method"); p[CorruptArchivePatterns] = QStringList() << QStringLiteral("End-of-central-directory signature not found"); p[DiskFullPatterns] = QStringList() << QStringLiteral("write error \\(disk full\\?\\)") << QStringLiteral("No space left on device"); p[TestArgs] = QStringList() << QStringLiteral("-t") << QStringLiteral("$Archive") << QStringLiteral("$PasswordSwitch"); p[TestPassedPattern] = QStringLiteral("^No errors detected in compressed data of "); + p[CompressionMethodSwitch] = QStringLiteral("-Z$CompressionMethod"); } return p; } bool CliPlugin::readListLine(const QString &line) { static const QRegularExpression entryPattern(QStringLiteral( "^(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\d{8}).(\\d{6})\\s+(.+)$") ); // RegExp to identify the line preceding comments. const QRegularExpression commentPattern(QStringLiteral("^Archive: .*$")); // RegExp to identify the line following comments. const QRegularExpression commentEndPattern(QStringLiteral("^Zip file size: .*$")); switch (m_parseState) { case ParseStateHeader: if (commentPattern.match(line).hasMatch()) { m_parseState = ParseStateComment; } else if (commentEndPattern.match(line).hasMatch()){ m_parseState = ParseStateEntry; } break; case ParseStateComment: if (commentEndPattern.match(line).hasMatch()) { m_parseState = ParseStateEntry; if (!m_tempComment.trimmed().isEmpty()) { m_comment = m_tempComment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_tempComment.append(line + QLatin1Char('\n')); } case ParseStateEntry: QRegularExpressionMatch rxMatch = entryPattern.match(line); if (rxMatch.hasMatch()) { Archive::Entry *e = new Archive::Entry(); e->setProperty("permissions", rxMatch.captured(1)); // #280354: infozip may not show the right attributes for a given directory, so an entry // ending with '/' is actually more reliable than 'd' bein in the attributes. e->setProperty("isDirectory", rxMatch.captured(10).endsWith(QLatin1Char('/'))); e->setProperty("size", rxMatch.captured(4)); QString status = rxMatch.captured(5); if (status[0].isUpper()) { e->setProperty("isPasswordProtected", true); } e->setProperty("compressedSize", rxMatch.captured(6).toInt()); e->setProperty("method", rxMatch.captured(7)); QString method = convertCompressionMethod(rxMatch.captured(7)); if (!m_compressionMethods.contains(method)) { m_compressionMethods.append(method); emit compressionMethodFound(m_compressionMethods); } const QDateTime ts(QDate::fromString(rxMatch.captured(8), QStringLiteral("yyyyMMdd")), QTime::fromString(rxMatch.captured(9), QStringLiteral("hhmmss"))); e->setProperty("timestamp", ts); e->setProperty("fullPath", rxMatch.captured(10)); emit entry(e); } break; } return true; } bool CliPlugin::moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions &options) { m_oldWorkingDir = QDir::currentPath(); m_tempExtractDir = new QTemporaryDir(); m_tempAddDir = new QTemporaryDir(); QDir::setCurrent(m_tempExtractDir->path()); m_passedFiles = files; m_passedDestination = destination; m_passedOptions = options; m_subOperation = Extract; connect(this, &CliPlugin::finished, this, &CliPlugin::continueMoving); return extractFiles(files, QDir::currentPath(), options); } int CliPlugin::moveRequiredSignals() const { return 4; } void CliPlugin::continueMoving(bool result) { if (!result) { finishMoving(false); return; } switch (m_subOperation) { case Extract: m_subOperation = Delete; if (!deleteFiles(m_passedFiles)) { finishMoving(false); } break; case Delete: m_subOperation = Add; if (!setMovingAddedFiles() || !addFiles(m_tempAddedFiles, m_passedDestination, m_passedOptions)) { finishMoving(false); } break; case Add: finishMoving(true); break; default: Q_ASSERT(false); } } bool CliPlugin::setMovingAddedFiles() { m_passedFiles = entriesWithoutChildren(m_passedFiles); // If there are more files being moved than 1, we have destination as a destination folder, // otherwise it's new entry full path. if (m_passedFiles.count() > 1) { return setAddedFiles(); } QDir::setCurrent(m_tempAddDir->path()); const Archive::Entry *file = m_passedFiles.at(0); const QString oldPath = m_tempExtractDir->path() + QLatin1Char('/') + file->fullPath(true); const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + m_passedDestination->name(); if (!QFile::rename(oldPath, newPath)) { return false; } m_tempAddedFiles << new Archive::Entry(Q_NULLPTR, m_passedDestination->name()); // We have to exclude file name from destination path in order to pass it to addFiles method. const QString destinationPath = m_passedDestination->fullPath(); int destinationLength = destinationPath.count(); bool iteratedChar = false; do { destinationLength--; if (destinationPath.at(destinationLength) != QLatin1Char('/')) { iteratedChar = true; } } while (destinationLength > 0 && !(iteratedChar && destinationPath.at(destinationLength) == QLatin1Char('/'))); m_passedDestination->setProperty("fullPath", destinationPath.left(destinationLength + 1)); return true; } void CliPlugin::finishMoving(bool result) { disconnect(this, &CliPlugin::finished, this, &CliPlugin::continueMoving); emit progress(1.0); emit finished(result); cleanUp(); } +QString CliPlugin::compressionMethodSwitch(const QString &method) const +{ + if (method.isEmpty()) { + return QString(); + } + + Q_ASSERT(m_param.contains(CompressionMethodSwitch)); + QString compMethodSwitch = m_param.value(CompressionMethodSwitch).toString(); + Q_ASSERT(!compMethodSwitch.isEmpty()); + + // We use capitalization of methods in UI, but CLI requires lowercase. + compMethodSwitch.replace(QLatin1String("$CompressionMethod"), method.toLower()); + + return compMethodSwitch; +} + QString CliPlugin::convertCompressionMethod(const QString &method) { if (method == QLatin1String("stor")) { return QStringLiteral("Store"); } else if (method.startsWith(QLatin1String("def"))) { return QStringLiteral("Deflate"); } else if (method == QLatin1String("bzp2")) { return QStringLiteral("BZip2"); } return method; } #include "cliplugin.moc" diff --git a/plugins/clizipplugin/cliplugin.h b/plugins/clizipplugin/cliplugin.h index 2d44b075..aacd16c9 100644 --- a/plugins/clizipplugin/cliplugin.h +++ b/plugins/clizipplugin/cliplugin.h @@ -1,66 +1,68 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "kerfuffle/cliinterface.h" #include using namespace Kerfuffle; class KERFUFFLE_EXPORT CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList &args); virtual ~CliPlugin(); virtual void resetParsing() Q_DECL_OVERRIDE; virtual QString escapeFileName(const QString &fileName) const Q_DECL_OVERRIDE; virtual Kerfuffle::ParameterList parameterList() const Q_DECL_OVERRIDE; virtual bool readListLine(const QString &line) Q_DECL_OVERRIDE; virtual bool moveFiles(const QList &files, Archive::Entry *destination, const CompressionOptions& options) Q_DECL_OVERRIDE; virtual int moveRequiredSignals() const Q_DECL_OVERRIDE; + virtual QString compressionMethodSwitch(const QString &method) const Q_DECL_OVERRIDE; + private slots: void continueMoving(bool result); private: bool setMovingAddedFiles(); void finishMoving(bool result); QString convertCompressionMethod(const QString &method); enum ParseState { ParseStateHeader = 0, ParseStateComment, ParseStateEntry } m_parseState; int m_linesComment; QString m_tempComment; QStringList m_compressionMethods; }; #endif // CLIPLUGIN_H diff --git a/plugins/clizipplugin/kerfuffle_clizip.json.cmake b/plugins/clizipplugin/kerfuffle_clizip.json.cmake index 57cbc6dc..bddfb5bb 100644 --- a/plugins/clizipplugin/kerfuffle_clizip.json.cmake +++ b/plugins/clizipplugin/kerfuffle_clizip.json.cmake @@ -1,64 +1,70 @@ { "KPlugin": { "Id": "kerfuffle_clizip", "MimeTypes": [ "@SUPPORTED_MIMETYPES@" ], "Name": "ZIP archive plugin", "Name[ca@valencia]": "Connector per arxius ZIP", "Name[ca]": "Connector per arxius ZIP", "Name[cs]": "Modul pro archiv ZIP", "Name[de]": "ZIP-Archiv-Modul", "Name[es]": "Complemento de archivo ZIP", "Name[et]": "ZIP-arhiivi plugin", "Name[fi]": "ZIP-pakkaustuki", - "Name[fr]": "Module externe d'archive « zip »", + "Name[fr]": "Module externe d'archive « zip »", "Name[gl]": "Complemento de arquivo ZIP", "Name[he]": "תוסף ארכיוני ZIP", "Name[it]": "Estensione per archivi ZIP", "Name[nb]": "Programtillegg for ZIP-arkiv", "Name[nl]": "ZIP-archiefplug-in", "Name[nn]": "ZIP-arkivtillegg", "Name[pl]": "Wtyczka archiwów ZIP", "Name[pt]": "'Plugin' de pacotes ZIP", "Name[pt_BR]": "Plugin de arquivos ZIP", "Name[ru]": "Поддержка архивов ZIP", "Name[sk]": "Modul ZIP archívu", "Name[sl]": "Vstavek za arhive ZIP", "Name[sr@ijekavian]": "Прикључак ЗИП архива", "Name[sr@ijekavianlatin]": "Priključak ZIP arhiva", "Name[sr@latin]": "Priključak ZIP arhiva", "Name[sr]": "Прикључак ЗИП архива", "Name[sv]": "Insticksprogram för ZIP-arkiv", "Name[uk]": "Додаток для архівів ZIP", "Name[x-test]": "xxZIP archive pluginxx", "Name[zh_CN]": "ZIP 归档插件", "ServiceTypes": [ "Kerfuffle/Plugin" ], "Version": "@KDE_APPLICATIONS_VERSION@" }, "X-KDE-Kerfuffle-ReadOnlyExecutables": [ "zipinfo", "unzip" ], "X-KDE-Kerfuffle-ReadWrite": true, "X-KDE-Kerfuffle-ReadWriteExecutables": [ "zip" ], "X-KDE-Priority": 160, "application/x-java-archive": { "CompressionLevelDefault": 6, "CompressionLevelMax": 9, "CompressionLevelMin": 0, "Encryption": true, "SupportsTesting": true }, "application/zip": { "CompressionLevelDefault": 6, "CompressionLevelMax": 9, "CompressionLevelMin": 0, "Encryption": true, - "SupportsTesting": true + "SupportsTesting": true, + "CompressionMethods": [ + "BZip2", + "Deflate", + "Store" + ], + "CompressionMethodDefault": "deflate" } -} \ No newline at end of file +}