diff --git a/app/main.cpp b/app/main.cpp index 9ef965bc..79276205 100644 --- a/app/main.cpp +++ b/app/main.cpp @@ -1,348 +1,347 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2015-2017 Ragnar Thomsen * * 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 "ark_version.h" #include "ark_debug.h" #include "mainwindow.h" #include "batchextract.h" #include "addtoarchive.h" #include "pluginmanager.h" #include #include -#include #include #include #include #include #include #include #include using Kerfuffle::AddToArchive; class OpenFileEventHandler : public QObject { Q_OBJECT public: OpenFileEventHandler(QApplication *parent, MainWindow *w) : QObject(parent) , m_window(w) { parent->installEventFilter(this); } bool eventFilter(QObject *obj, QEvent *event) override { if (event->type() == QEvent::FileOpen) { QFileOpenEvent *openEvent = static_cast(event); qCDebug(ARK) << "File open event:" << openEvent->url() << "for window" << m_window; m_window->openUrl(openEvent->url()); return true; } return QObject::eventFilter(obj, event); } private: MainWindow *m_window; }; int main(int argc, char **argv) { QCoreApplication::setAttribute(Qt::AA_ShareOpenGLContexts); // Required for the webengine part. QApplication application(argc, argv); /** * enable high dpi support */ application.setAttribute(Qt::AA_UseHighDpiPixmaps, true); KCrash::initialize(); // Debug output can be turned on here: //QLoggingCategory::setFilterRules(QStringLiteral("ark.debug = true")); Kdelibs4ConfigMigrator migrate(QStringLiteral("ark")); migrate.setConfigFiles(QStringList() << QStringLiteral("arkrc")); migrate.setUiFiles(QStringList() << QStringLiteral("arkuirc")); migrate.migrate(); KLocalizedString::setApplicationDomain("ark"); KAboutData aboutData(QStringLiteral("ark"), i18n("Ark"), QStringLiteral(ARK_VERSION_STRING), i18n("KDE Archiving tool"), KAboutLicense::GPL, i18n("(c) 1997-2019, The Ark Developers"), QString(), QStringLiteral("https://utils.kde.org/projects/ark") ); aboutData.setOrganizationDomain("kde.org"); aboutData.addAuthor(i18n("Elvis Angelaccio"), i18n("Maintainer"), QStringLiteral("elvis.angelaccio@kde.org")); aboutData.addAuthor(i18n("Ragnar Thomsen"), i18n("Maintainer, KF5 port"), QStringLiteral("rthomsen6@gmail.com")); aboutData.addAuthor(i18n("Raphael Kubo da Costa"), i18n("Former Maintainer"), QStringLiteral("rakuco@FreeBSD.org")); aboutData.addAuthor(i18n("Harald Hvaal"), i18n("Former Maintainer"), QStringLiteral("haraldhv@stud.ntnu.no")); aboutData.addAuthor(i18n("Henrique Pinto"), i18n("Former Maintainer"), QStringLiteral("henrique.pinto@kdemail.net")); aboutData.addAuthor(i18n("Helio Chissini de Castro"), i18n("Former maintainer"), QStringLiteral("helio@kde.org")); aboutData.addAuthor(i18n("Georg Robbers"), QString(), QStringLiteral("Georg.Robbers@urz.uni-hd.de")); aboutData.addAuthor(i18n("Roberto Selbach Teixeira"), QString(), QStringLiteral("maragato@kde.org")); aboutData.addAuthor(i18n("Francois-Xavier Duranceau"), QString(), QStringLiteral("duranceau@kde.org")); aboutData.addAuthor(i18n("Emily Ezust (Corel Corporation)"), QString(), QStringLiteral("emilye@corel.com")); aboutData.addAuthor(i18n("Michael Jarrett (Corel Corporation)"), QString(), QStringLiteral("michaelj@corel.com")); aboutData.addAuthor(i18n("Robert Palmbos"), QString(), QStringLiteral("palm9744@kettering.edu")); aboutData.addCredit(i18n("Vladyslav Batyrenko"), i18n("Advanced editing functionalities"), QString(), QStringLiteral("https://mvlabat.github.io/ark-gsoc-2016/")); aboutData.addCredit(i18n("Bryce Corkins"), i18n("Icons"), QStringLiteral("dbryce@attglobal.net")); aboutData.addCredit(i18n("Liam Smit"), i18n("Ideas, help with the icons"), QStringLiteral("smitty@absamail.co.za")); aboutData.addCredit(i18n("Andrew Smith"), i18n("bkisofs code"), QString(), QStringLiteral("http://littlesvr.ca/misc/contactandrew.php")); KAboutData::setApplicationData(aboutData); application.setWindowIcon(QIcon::fromTheme(QStringLiteral("ark"), application.windowIcon())); QCommandLineParser parser; // Url to open. parser.addPositionalArgument(QStringLiteral("[urls]"), i18n("URLs to open.")); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("d") << QStringLiteral("dialog"), i18n("Show a dialog for specifying the options for the operation (extract/add)"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("o") << QStringLiteral("destination"), i18n("Destination folder to extract to. Defaults to current path if not specified."), QStringLiteral("directory"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("O") << QStringLiteral("opendestination"), i18n("Open destination folder after extraction."))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("c") << QStringLiteral("add"), i18n("Query the user for an archive filename and add specified files to it. Quit when finished."))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("t") << QStringLiteral("add-to"), i18n("Add the specified files to 'filename'. Create archive if it does not exist. Quit when finished."), QStringLiteral("filename"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("p") << QStringLiteral("changetofirstpath"), i18n("Change the current dir to the first entry and add all other entries relative to this one."))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("f") << QStringLiteral("autofilename"), i18n("Automatically choose a filename, with the selected suffix (for example rar, tar.gz, zip or any other supported types)"), QStringLiteral("suffix"))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("b") << QStringLiteral("batch"), i18n("Use the batch interface instead of the usual dialog. This option is implied if more than one url is specified."))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("e") << QStringLiteral("autodestination"), i18n("The destination argument will be set to the path of the first file supplied."))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("a") << QStringLiteral("autosubfolder"), i18n("Archive contents will be read, and if detected to not be a single folder archive, a subfolder with the name of the archive will be created."))); parser.addOption(QCommandLineOption(QStringList() << QStringLiteral("m") << QStringLiteral("mimetypes"), i18n("List supported MIME types."))); aboutData.setupCommandLine(&parser); // Do the command line parsing. parser.process(application); // Handle standard options. aboutData.processCommandLine(&parser); // This is needed to prevent Dolphin from freezing when opening an archive. KDBusService dbusService(KDBusService::Multiple | KDBusService::NoExitOnFailure); // Session restoring. if (application.isSessionRestored()) { if (!KMainWindow::canBeRestored(1)) { return -1; } MainWindow* window = new MainWindow; window->restore(1); if (!window->loadPart()) { delete window; return -1; } } else { // New ark window (no restored session). // Open any given URLs. const QStringList urls = parser.positionalArguments(); if (parser.isSet(QStringLiteral("add")) || parser.isSet(QStringLiteral("add-to"))) { AddToArchive *addToArchiveJob = new AddToArchive(&application); application.setQuitOnLastWindowClosed(false); QObject::connect(addToArchiveJob, &KJob::result, &application, &QCoreApplication::quit, Qt::QueuedConnection); if (parser.isSet(QStringLiteral("changetofirstpath"))) { qCDebug(ARK) << "Setting changetofirstpath"; addToArchiveJob->setChangeToFirstPath(true); } if (parser.isSet(QStringLiteral("add-to"))) { qCDebug(ARK) << "Setting filename to" << parser.value(QStringLiteral("add-to")); addToArchiveJob->setFilename(QUrl::fromUserInput(parser.value(QStringLiteral("add-to")), QString(), QUrl::AssumeLocalFile)); } if (parser.isSet(QStringLiteral("autofilename"))) { qCDebug(ARK) << "Setting autofilename to" << parser.value(QStringLiteral("autofilename")); addToArchiveJob->setAutoFilenameSuffix(parser.value(QStringLiteral("autofilename"))); } for (int i = 0; i < urls.count(); ++i) { //TODO: use the returned value here? qCDebug(ARK) << "Adding url" << QUrl::fromUserInput(urls.at(i), QString(), QUrl::AssumeLocalFile); addToArchiveJob->addInput(QUrl::fromUserInput(urls.at(i), QString(), QUrl::AssumeLocalFile)); } if (parser.isSet(QStringLiteral("dialog"))) { qCDebug(ARK) << "Using kerfuffle to open add dialog"; if (!addToArchiveJob->showAddDialog()) { return 0; } } addToArchiveJob->start(); } else if (parser.isSet(QStringLiteral("batch"))) { if (urls.isEmpty()) { qCDebug(ARK) << "No urls to be extracted were provided."; parser.showHelp(-1); } BatchExtract *batchJob = new BatchExtract(&application); application.setQuitOnLastWindowClosed(false); QObject::connect(batchJob, &KJob::result, &application, &QCoreApplication::quit, Qt::QueuedConnection); for (int i = 0; i < urls.count(); ++i) { qCDebug(ARK) << "Adding url" << QUrl::fromUserInput(urls.at(i), QString(), QUrl::AssumeLocalFile); batchJob->addInput(QUrl::fromUserInput(urls.at(i), QString(), QUrl::AssumeLocalFile)); } if (parser.isSet(QStringLiteral("autosubfolder"))) { qCDebug(ARK) << "Setting autosubfolder"; batchJob->setAutoSubfolder(true); } if (parser.isSet(QStringLiteral("autodestination"))) { QString autopath = QFileInfo(QUrl::fromUserInput(urls.at(0), QString(), QUrl::AssumeLocalFile).path()).path(); qCDebug(ARK) << "By autodestination, setting path to " << autopath; batchJob->setDestinationFolder(autopath); } if (parser.isSet(QStringLiteral("destination"))) { qCDebug(ARK) << "Setting destination to " << parser.value(QStringLiteral("destination")); batchJob->setDestinationFolder(parser.value(QStringLiteral("destination"))); } if (parser.isSet(QStringLiteral("opendestination"))) { qCDebug(ARK) << "Setting opendestination"; batchJob->setOpenDestinationAfterExtraction(true); } if (parser.isSet(QStringLiteral("dialog"))) { qCDebug(ARK) << "Opening extraction dialog"; if (!batchJob->showExtractDialog()) { return 0; } } batchJob->start(); } else if (parser.isSet(QStringLiteral("mimetypes"))) { Kerfuffle::PluginManager pluginManager; const auto mimeTypes = pluginManager.supportedMimeTypes(); QTextStream cout(stdout); for (const auto &mimeType : mimeTypes) { cout << mimeType << '\n'; } return 0; } else { MainWindow *window = new MainWindow; if (!window->loadPart()) { // if loading the part fails delete window; return -1; } if (!urls.isEmpty()) { qCDebug(ARK) << "Trying to open" << QUrl::fromUserInput(urls.at(0), QString(), QUrl::AssumeLocalFile); if (parser.isSet(QStringLiteral("dialog"))) { window->setShowExtractDialog(true); } window->openUrl(QUrl::fromUserInput(urls.at(0), QString(), QUrl::AssumeLocalFile)); } new OpenFileEventHandler(&application, window); window->show(); } } qCDebug(ARK) << "Entering application loop"; return application.exec(); } #include "main.moc" diff --git a/app/mainwindow.cpp b/app/mainwindow.cpp index 51211997..6ccb154f 100644 --- a/app/mainwindow.cpp +++ b/app/mainwindow.cpp @@ -1,356 +1,354 @@ /* * 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 "settingsdialog.h" #include "settingspage.h" #include "pluginmanager.h" #include "interface.h" #include #include #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); // Ark doesn't provide a fullscreen mode; remove the corresponding window button setWindowFlags(windowFlags() & ~Qt::WindowFullscreenButtonHint); } MainWindow::~MainWindow() { if (m_recentFilesAction) { m_recentFilesAction->saveEntries(KSharedConfig::openConfig()->group("Recent Files")); } guiFactory()->removeClient(m_part); delete m_part; m_part = nullptr; } void MainWindow::dragEnterEvent(QDragEnterEvent * event) { qCDebug(ARK) << event; Interface *iface = qobject_cast(m_part); if (iface->isBusy()) { return; } const bool partAcceptsDrops = !m_part->url().isEmpty() && m_part->isReadWrite(); if (!event->source() && isValidArchiveDrag(event->mimeData()) && !partAcceptsDrops) { event->acceptProposedAction(); } return; } void MainWindow::dropEvent(QDropEvent * event) { qCDebug(ARK) << event; Interface *iface = qobject_cast(m_part); if (iface->isBusy()) { return; } if ((event->source() == nullptr) && (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) << event; Interface *iface = qobject_cast(m_part); if (iface->isBusy()) { return; } if ((event->source() == nullptr) && (isValidArchiveDrag(event->mimeData()))) { event->acceptProposedAction(); } } bool MainWindow::loadPart() { KPluginFactory *factory = 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)) : 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()); setXMLFile(QStringLiteral("arkui.rc")); setupGUI(ToolBar | Keys | Save); createGUI(m_part); statusBar()->hide(); connect(m_part, SIGNAL(ready()), this, SLOT(updateActions())); connect(m_part, SIGNAL(quit()), this, SLOT(quit())); // #365200: this will disable m_recentFilesAction, while openUrl() will enable it. // So updateActions() needs to be called after openUrl() returns. connect(m_part, SIGNAL(busy()), this, SLOT(updateActions()), Qt::QueuedConnection); connect(m_part, QOverload<>::of(&KParts::ReadOnlyPart::completed), this, &MainWindow::addPartUrl); updateActions(); return true; } void MainWindow::setupActions() { m_newAction = KStandardAction::openNew(this, &MainWindow::newArchive, this); actionCollection()->addAction(QStringLiteral("ark_file_new"), m_newAction); m_openAction = KStandardAction::open(this, &MainWindow::openArchive, this); actionCollection()->addAction(QStringLiteral("ark_file_open"), m_openAction); auto quitAction = KStandardAction::quit(this, &MainWindow::quit, this); actionCollection()->addAction(QStringLiteral("ark_quit"), quitAction); m_recentFilesAction = KStandardAction::openRecent(this, &MainWindow::openUrl, this); 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, &MainWindow::showSettings, actionCollection()); } void MainWindow::updateActions() { Interface *iface = qobject_cast(m_part); Kerfuffle::PluginManager pluginManager; m_newAction->setEnabled(!iface->isBusy() && !pluginManager.availableWritePlugins().isEmpty()); 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(Kerfuffle::PluginManager::SortByComment)); dlg->setFileMode(QFileDialog::ExistingFile); dlg->setAcceptMode(QFileDialog::AcceptOpen); connect(dlg, &QDialog::finished, this, [this, dlg](int result) { if (result == QDialog::Accepted) { openUrl(dlg->selectedUrls().at(0)); } dlg->deleteLater(); }); dlg->open(); } void MainWindow::openUrl(const QUrl& url) { if (url.isEmpty()) { return; } m_part->setArguments(m_openArgs); m_part->openUrl(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. const auto topLevelWidgets = qApp->topLevelWidgets(); for (QWidget *widget : topLevelWidgets) { if (widget->isVisible()) { widget->close(); } } KParts::MainWindow::closeEvent(event); } void MainWindow::quit() { close(); } void MainWindow::showSettings() { if (KConfigDialog::showDialog(QStringLiteral("settings"))) { return; } Interface *iface = qobject_cast(m_part); Q_ASSERT(iface); auto dialog = new Kerfuffle::SettingsDialog(this, QStringLiteral("settings"), iface->config()); const auto pages = iface->settingsPages(this); for (Kerfuffle::SettingsPage *page : pages) { dialog->addPage(page, page->name(), page->iconName()); connect(dialog, &KConfigDialog::settingsChanged, page, &Kerfuffle::SettingsPage::slotSettingsChanged); connect(dialog, &Kerfuffle::SettingsDialog::defaultsButtonClicked, page, &Kerfuffle::SettingsPage::slotDefaultsButtonClicked); } // Hide the icons list if only one page has been added. dialog->setFaceType(KPageDialog::Auto); dialog->setModal(true); connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::writeSettings); connect(dialog, &KConfigDialog::settingsChanged, this, &MainWindow::updateActions); dialog->show(); } void MainWindow::writeSettings() { Interface *iface = qobject_cast(m_part); Q_ASSERT(iface); iface->config()->save(); } void MainWindow::addPartUrl() { m_recentFilesAction->addUrl(m_part->url()); } 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( 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(); } if (!dialog.data()->encryptionMethod().isEmpty()) { m_openArgs.metaData()[QStringLiteral("encryptionMethod")] = dialog.data()->encryptionMethod(); } 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/adddialogtest.cpp b/autotests/kerfuffle/adddialogtest.cpp index c60ca3c5..aa7c7068 100644 --- a/autotests/kerfuffle/adddialogtest.cpp +++ b/autotests/kerfuffle/adddialogtest.cpp @@ -1,176 +1,174 @@ /* * 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 "adddialog.h" #include "archiveformat.h" #include "pluginmanager.h" #include #include -#include -#include #include #include using namespace Kerfuffle; class AddDialogTest : public QObject { Q_OBJECT private Q_SLOTS: void initTestCase(); void testBasicWidgets_data(); void testBasicWidgets(); private: PluginManager m_pluginManager; }; void AddDialogTest::initTestCase() { // Avoid a runtime dependency on KLauncher. qputenv("KDE_FORK_SLAVES", "yes"); } void AddDialogTest::testBasicWidgets_data() { QTest::addColumn("mimeType"); QTest::addColumn("supportsCompLevel"); QTest::addColumn("initialCompLevel"); QTest::addColumn("changeToCompLevel"); QTest::newRow("tar") << QStringLiteral("application/x-tar") << false << -1 << -1; QTest::newRow("targzip") << QStringLiteral("application/x-compressed-tar") << true << 3 << 7; QTest::newRow("tarbzip") << QStringLiteral("application/x-bzip-compressed-tar") << true << 3 << 7; QTest::newRow("tarZ") << QStringLiteral("application/x-tarz") << false << -1 << -1; QTest::newRow("tarxz") << QStringLiteral("application/x-xz-compressed-tar") << true << 3 << 7; QTest::newRow("tarlzma") << QStringLiteral("application/x-lzma-compressed-tar") << true << 3 << 7; QTest::newRow("tarlzip") << QStringLiteral("application/x-lzip-compressed-tar") << true << 3 << 7; const auto writeMimeTypes = m_pluginManager.supportedWriteMimeTypes(); if (writeMimeTypes.contains(QStringLiteral("application/zip"))) { QTest::newRow("zip") << QStringLiteral("application/zip") << true << 3 << 7; } else { qDebug() << "zip format not available, skipping test."; } if (writeMimeTypes.contains(QStringLiteral("application/x-7z-compressed"))) { QTest::newRow("7z") << QStringLiteral("application/x-7z-compressed") << true << 3 << 7; } else { qDebug() << "7z format not available, skipping test."; } if (writeMimeTypes.contains(QStringLiteral("application/vnd.rar"))) { QTest::newRow("rar") << QStringLiteral("application/vnd.rar") << true << 2 << 5; } else { qDebug() << "rar format not available, skipping test."; } if (writeMimeTypes.contains(QStringLiteral("application/x-lrzip-compressed-tar"))) { QTest::newRow("tarlrzip") << QStringLiteral("application/x-lrzip-compressed-tar") << true << 3 << 7; } else { qDebug() << "tar.lrzip format not available, skipping test."; } if (writeMimeTypes.contains(QStringLiteral("application/x-tzo"))) { QTest::newRow("tarlzop") << QStringLiteral("application/x-tzo") << true << 3 << 7; } else { qDebug() << "tar.lzo format not available, skipping test."; } } void AddDialogTest::testBasicWidgets() { QFETCH(QString, mimeType); const QMimeType mime = QMimeDatabase().mimeTypeForName(mimeType); QFETCH(bool, supportsCompLevel); QFETCH(int, initialCompLevel); QFETCH(int, changeToCompLevel); AddDialog *dialog = new AddDialog(nullptr, QString(), QUrl(), mime); dialog->slotOpenOptions(); auto collapsibleCompression = dialog->optionsDialog->findChild(QStringLiteral("collapsibleCompression")); QVERIFY(collapsibleCompression); const KPluginMetaData metadata = PluginManager().preferredPluginFor(mime)->metaData(); const ArchiveFormat archiveFormat = ArchiveFormat::fromMetadata(mime, metadata); QVERIFY(archiveFormat.isValid()); if (archiveFormat.defaultCompressionLevel() > 0 && supportsCompLevel) { // Test that collapsiblegroupbox is enabled for mimetypes that support compression levels. QVERIFY(collapsibleCompression->isEnabled()); auto compLevelSlider = dialog->optionsDialog->findChild(QStringLiteral("compLevelSlider")); QVERIFY(compLevelSlider); // Test that min/max of slider are correct. QCOMPARE(compLevelSlider->minimum(), archiveFormat.minCompressionLevel()); QCOMPARE(compLevelSlider->maximum(), archiveFormat.maxCompressionLevel()); // Test that the slider is set to default compression level. QCOMPARE(compLevelSlider->value(), archiveFormat.defaultCompressionLevel()); // Set the compression level slider. compLevelSlider->setValue(changeToCompLevel); } else { // Test that collapsiblegroupbox is disabled for mimetypes that don't support compression levels. QVERIFY(!collapsibleCompression->isEnabled()); } dialog->optionsDialog->accept(); dialog->accept(); if (archiveFormat.defaultCompressionLevel() > 0 && supportsCompLevel) { // Test that the value set by slider is exported from AddDialog. QCOMPARE(dialog->compressionOptions().compressionLevel(), changeToCompLevel); } // Test that passing a compression level in ctor works. CompressionOptions opts; opts.setCompressionLevel(initialCompLevel); dialog = new AddDialog(nullptr, QString(), QUrl(), mime, opts); dialog->slotOpenOptions(); if (archiveFormat.defaultCompressionLevel() > 0 && supportsCompLevel) { auto compLevelSlider = dialog->optionsDialog->findChild(QStringLiteral("compLevelSlider")); QVERIFY(compLevelSlider); // Test that slider is set to the compression level supplied in ctor. QCOMPARE(compLevelSlider->value(), initialCompLevel); } dialog->optionsDialog->accept(); dialog->accept(); } QTEST_MAIN(AddDialogTest) #include "adddialogtest.moc" diff --git a/autotests/kerfuffle/jsonparser.cpp b/autotests/kerfuffle/jsonparser.cpp index 899cc13c..5976ee33 100644 --- a/autotests/kerfuffle/jsonparser.cpp +++ b/autotests/kerfuffle/jsonparser.cpp @@ -1,83 +1,82 @@ /* * Copyright (c) 2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "jsonparser.h" #include "archiveinterface.h" #include #include -#include JSONParser::JSONParser() { } JSONParser::~JSONParser() { } JSONParser::JSONArchive JSONParser::parse(QIODevice *json) { QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(json->readAll(), &error); if (error.error != QJsonParseError::NoError) { qDebug() << "Parse error: " << error.errorString(); return JSONParser::JSONArchive(); } return createJSONArchive(jsonDoc.toVariant()); } JSONParser::JSONArchive JSONParser::createJSONArchive(const QVariant &json) { JSONParser::JSONArchive archive; const auto jsonList = json.toList(); for (const QVariant &entry : jsonList) { const QVariantMap entryMap = entry.toMap(); if (!entryMap.contains(QStringLiteral("fullPath"))) { continue; } Kerfuffle::Archive::Entry *e = new Kerfuffle::Archive::Entry(); QVariantMap::const_iterator entryIterator = entryMap.constBegin(); for (; entryIterator != entryMap.constEnd(); ++entryIterator) { const QByteArray key = entryIterator.key().toUtf8(); if (e->property(key.constData()).isValid()) { e->setProperty(key.constData(), entryIterator.value()); } else { qDebug() << entryIterator.key() << "is not a valid entry key"; } } const QString fullPath = entryMap[QStringLiteral("fullPath")].toString(); archive[fullPath] = e; } return archive; } diff --git a/autotests/kerfuffle/movetest.cpp b/autotests/kerfuffle/movetest.cpp index ff5dc262..c7e09e71 100644 --- a/autotests/kerfuffle/movetest.cpp +++ b/autotests/kerfuffle/movetest.cpp @@ -1,268 +1,267 @@ /* * 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 "abstractaddtest.h" #include "archiveentry.h" #include "jobs.h" #include "testhelper.h" -#include #include using namespace Kerfuffle; class MoveTest : public AbstractAddTest { Q_OBJECT public: MoveTest() : AbstractAddTest() {} private Q_SLOTS: void testMoving_data(); void testMoving(); }; QTEST_GUILESS_MAIN(MoveTest) void MoveTest::testMoving_data() { QTest::addColumn("archiveName"); QTest::addColumn("plugin"); QTest::addColumn>("targetEntries"); QTest::addColumn("destination"); QTest::addColumn("expectedNewPaths"); QTest::addColumn("numberOfEntries"); setupRows(QStringLiteral("replace a single file"), QStringLiteral("test"), QVector { new Archive::Entry(this, QStringLiteral("a.txt")), }, new Archive::Entry(this, QStringLiteral("empty_dir/a.txt")), QStringList { QStringLiteral("empty_dir/a.txt") }, 13); setupRows(QStringLiteral("replace several files"), QStringLiteral("test"), QVector { new Archive::Entry(this, QStringLiteral("a.txt")), new Archive::Entry(this, QStringLiteral("b.txt")) }, new Archive::Entry(this, QStringLiteral("empty_dir/")), QStringList { QStringLiteral("empty_dir/a.txt"), QStringLiteral("empty_dir/b.txt") }, 13); setupRows(QStringLiteral("replace a root directory"), QStringLiteral("test"), QVector { new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")), new Archive::Entry(this, QStringLiteral("dir1/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/b.txt")) }, new Archive::Entry(this, QStringLiteral("empty_dir/")), QStringList { QStringLiteral("empty_dir/a.txt"), QStringLiteral("empty_dir/b.txt"), QStringLiteral("empty_dir/dir/"), QStringLiteral("empty_dir/dir/a.txt"), QStringLiteral("empty_dir/dir/b.txt") }, 13); setupRows(QStringLiteral("replace a root directory 2"), QStringLiteral("test"), QVector { new Archive::Entry(this, QStringLiteral("dir2/dir/")), new Archive::Entry(this, QStringLiteral("dir2/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir2/dir/b.txt")) }, new Archive::Entry(this, QStringLiteral("empty_dir/dir/")), QStringList { QStringLiteral("empty_dir/dir/"), QStringLiteral("empty_dir/dir/a.txt"), QStringLiteral("empty_dir/dir/b.txt") }, 13); setupRows(QStringLiteral("replace a directory"), QStringLiteral("test"), QVector { new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")) }, new Archive::Entry(this, QStringLiteral("empty_dir/dir/")), QStringList { QStringLiteral("empty_dir/dir/"), QStringLiteral("empty_dir/dir/a.txt"), QStringLiteral("empty_dir/dir/b.txt") }, 13); setupRows(QStringLiteral("replace several directories"), QStringLiteral("test"), QVector { new Archive::Entry(this, QStringLiteral("dir1/")), new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")), new Archive::Entry(this, QStringLiteral("dir1/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/b.txt")), new Archive::Entry(this, QStringLiteral("dir2/")), new Archive::Entry(this, QStringLiteral("dir2/dir/")), new Archive::Entry(this, QStringLiteral("dir2/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir2/dir/b.txt")) }, new Archive::Entry(this, QStringLiteral("empty_dir/")), QStringList { QStringLiteral("empty_dir/dir1/"), QStringLiteral("empty_dir/dir1/a.txt"), QStringLiteral("empty_dir/dir1/b.txt"), QStringLiteral("empty_dir/dir1/dir/"), QStringLiteral("empty_dir/dir1/dir/a.txt"), QStringLiteral("empty_dir/dir1/dir/b.txt"), QStringLiteral("empty_dir/dir2/"), QStringLiteral("empty_dir/dir2/dir/"), QStringLiteral("empty_dir/dir2/dir/a.txt"), QStringLiteral("empty_dir/dir2/dir/b.txt") }, 13); setupRows(QStringLiteral("replace several entries"), QStringLiteral("test"), QVector { new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")), new Archive::Entry(this, QStringLiteral("dir1/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/b.txt")) }, new Archive::Entry(this, QStringLiteral("empty_dir/")), QStringList { QStringLiteral("empty_dir/dir/"), QStringLiteral("empty_dir/dir/a.txt"), QStringLiteral("empty_dir/dir/b.txt"), QStringLiteral("empty_dir/a.txt"), QStringLiteral("empty_dir/b.txt") }, 13); setupRows(QStringLiteral("move a directory to the root"), QStringLiteral("test"), QVector { new Archive::Entry(this, QStringLiteral("dir1/dir/")), new Archive::Entry(this, QStringLiteral("dir1/dir/a.txt")), new Archive::Entry(this, QStringLiteral("dir1/dir/b.txt")) }, new Archive::Entry(this, QStringLiteral("dir/")), QStringList { QStringLiteral("dir/"), QStringLiteral("dir/a.txt"), QStringLiteral("dir/b.txt") }, 13); setupRows(QStringLiteral("rename a file in the root (bug #368530)"), QStringLiteral("test"), QVector { new Archive::Entry(this, QStringLiteral("a.txt")) }, new Archive::Entry(this, QStringLiteral("new-name.txt")), QStringList { QStringLiteral("new-name.txt") }, 13); } void MoveTest::testMoving() { QTemporaryDir temporaryDir; QFETCH(QString, archiveName); const QString archivePath = temporaryDir.path() + QLatin1Char('/') + archiveName; QVERIFY(QFile::copy(QFINDTESTDATA(QStringLiteral("data/") + archiveName), archivePath)); QFETCH(Plugin*, plugin); QVERIFY(plugin); auto loadJob = Archive::load(archivePath, plugin); QVERIFY(loadJob); loadJob->setAutoDelete(false); TestHelper::startAndWaitForResult(loadJob); auto archive = loadJob->archive(); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not find a plugin to handle the archive. Skipping test.", SkipSingle); } QFETCH(QVector, targetEntries); QFETCH(Archive::Entry*, destination); QFETCH(QStringList, expectedNewPaths); // Retrieve current paths in the archive. QStringList oldPaths = getEntryPaths(archive); // Check that the entries to be moved are in the archive. for (const auto entry : qAsConst(targetEntries)) { QVERIFY(oldPaths.contains(entry->fullPath())); } // Check that the expected paths (after the MoveJob) are not in the archive. for (const auto &expectedPath : qAsConst(expectedNewPaths)) { QVERIFY(!oldPaths.contains(expectedPath)); } CompressionOptions options; options.setGlobalWorkDir(QFINDTESTDATA("data")); MoveJob *moveJob = archive->moveFiles(targetEntries, destination, options); TestHelper::startAndWaitForResult(moveJob); // Retrieve the resulting paths. QStringList newPaths = getEntryPaths(archive); // Check that the expected paths are now in the archive. for (const auto &path : qAsConst(expectedNewPaths)) { QVERIFY(newPaths.contains(path)); } // Check that the target paths are no longer in the archive. for (const auto entry : qAsConst(targetEntries)) { QVERIFY(!newPaths.contains(entry->fullPath())); } QFETCH(uint, numberOfEntries); QCOMPARE(archive->numberOfEntries(), numberOfEntries); loadJob->deleteLater(); archive->deleteLater(); } #include "movetest.moc" diff --git a/autotests/testhelper/abstractaddtest.cpp b/autotests/testhelper/abstractaddtest.cpp index 67b6680c..e6b400bc 100644 --- a/autotests/testhelper/abstractaddtest.cpp +++ b/autotests/testhelper/abstractaddtest.cpp @@ -1,67 +1,66 @@ /* * Copyright (c) 2016 Vladyslav Batyrenko * 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 "abstractaddtest.h" #include "jobs.h" #include "testhelper.h" -#include #include #include using namespace Kerfuffle; QStringList AbstractAddTest::getEntryPaths(Archive *archive) { QStringList paths; auto loadJob = Archive::load(archive->fileName()); QObject::connect(loadJob, &Job::newEntry, [&paths](Archive::Entry* entry) { paths << entry->fullPath(); }); TestHelper::startAndWaitForResult(loadJob); return paths; } void AbstractAddTest::setupRows(const QString &testName, const QString &archiveName, const QVector &targetEntries, Archive::Entry *destination, const QStringList &expectedNewPaths, uint numberOfEntries) const { // Repeat the same test case for each format and for each plugin supporting the format. const QStringList formats = TestHelper::testFormats(); for (const QString &format : formats) { const QString filename = QStringLiteral("%1.%2").arg(archiveName, format); const auto mime = QMimeDatabase().mimeTypeForFile(filename, QMimeDatabase::MatchExtension); const auto plugins = m_pluginManager.preferredWritePluginsFor(mime); for (const auto plugin : plugins) { QTest::newRow(QStringLiteral("%1 (%2, %3)").arg(testName, format, plugin->metaData().pluginId()).toUtf8().constData()) << filename << plugin << targetEntries << destination << expectedNewPaths << numberOfEntries; } } } diff --git a/kerfuffle/adddialog.cpp b/kerfuffle/adddialog.cpp index a86c6263..bec7a55a 100644 --- a/kerfuffle/adddialog.cpp +++ b/kerfuffle/adddialog.cpp @@ -1,125 +1,124 @@ /* * 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 "adddialog.h" #include "ark_debug.h" #include "archiveformat.h" #include "compressionoptionswidget.h" #include "mimetypes.h" #include -#include #include #include #include namespace Kerfuffle { AddDialog::AddDialog(QWidget *parent, const QString &title, const QUrl &startDir, const QMimeType &mimeType, const CompressionOptions &opts) : QDialog(parent, Qt::Dialog) , m_mimeType(mimeType) , m_compOptions(opts) { qCDebug(ARK) << "AddDialog loaded with options:" << m_compOptions; setWindowTitle(title); QVBoxLayout *vlayout = new QVBoxLayout(this); m_fileWidget = new KFileWidget(startDir, this); vlayout->addWidget(m_fileWidget); QPushButton *optionsButton = new QPushButton(QIcon::fromTheme(QStringLiteral("settings-configure")), i18n("Advanced Options")); optionsButton->setSizePolicy(QSizePolicy::Fixed, QSizePolicy::Fixed); m_fileWidget->setCustomWidget(optionsButton); connect(optionsButton, &QPushButton::clicked, this, &AddDialog::slotOpenOptions); m_fileWidget->setMode(KFile::Files | KFile::Directory | KFile::LocalOnly | KFile::ExistingOnly); m_fileWidget->setOperationMode(KFileWidget::Opening); m_fileWidget->okButton()->setText(i18nc("@action:button", "Add")); m_fileWidget->okButton()->show(); connect(m_fileWidget->okButton(), &QPushButton::clicked, m_fileWidget, &KFileWidget::slotOk); connect(m_fileWidget, &KFileWidget::accepted, m_fileWidget, &KFileWidget::accept); connect(m_fileWidget, &KFileWidget::accepted, this, &QDialog::accept); m_fileWidget->cancelButton()->show(); connect(m_fileWidget->cancelButton(), &QPushButton::clicked, this, &QDialog::reject); } AddDialog::~AddDialog() { } QStringList AddDialog::selectedFiles() const { return m_fileWidget->selectedFiles(); } CompressionOptions AddDialog::compressionOptions() const { qCDebug(ARK) << "Returning with options:" << m_compOptions; return m_compOptions; } void AddDialog::slotOpenOptions() { optionsDialog = new QDialog(this); QVBoxLayout *vlayout = new QVBoxLayout(optionsDialog); optionsDialog->setWindowTitle(i18n("Advanced Options")); CompressionOptionsWidget *optionsWidget = new CompressionOptionsWidget(optionsDialog, m_compOptions); optionsWidget->setMimeType(m_mimeType); optionsWidget->setEncryptionVisible(false); optionsWidget->collapsibleMultiVolume->setVisible(false); optionsWidget->collapsibleCompression->expand(); vlayout->addWidget(optionsWidget); QDialogButtonBox *buttonBox = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, optionsDialog); vlayout->addWidget(buttonBox); connect(buttonBox, &QDialogButtonBox::accepted, optionsDialog, &QDialog::accept); connect(buttonBox, &QDialogButtonBox::rejected, optionsDialog, &QDialog::reject); optionsDialog->layout()->setSizeConstraint(QLayout::SetFixedSize); connect(optionsDialog, &QDialog::finished, this, [this, optionsWidget](int result) { if (result == QDialog::Accepted) { m_compOptions = optionsWidget->commpressionOptions(); } optionsDialog->deleteLater(); }); optionsDialog->open(); } } diff --git a/kerfuffle/addtoarchive.cpp b/kerfuffle/addtoarchive.cpp index 5c0c29ce..1e895ee0 100644 --- a/kerfuffle/addtoarchive.cpp +++ b/kerfuffle/addtoarchive.cpp @@ -1,266 +1,265 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Harald Hvaal * Copyright (C) 2009 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "addtoarchive.h" #include "archiveentry.h" #include "archive_kerfuffle.h" #include "ark_debug.h" #include "createdialog.h" #include "jobs.h" -#include #include #include #include #include #include #include #include #include #include namespace Kerfuffle { AddToArchive::AddToArchive(QObject *parent) : KJob(parent) , m_changeToFirstPath(false) , m_enableHeaderEncryption(false) { } AddToArchive::~AddToArchive() { } void AddToArchive::setAutoFilenameSuffix(const QString& suffix) { m_autoFilenameSuffix = suffix; } void AddToArchive::setChangeToFirstPath(bool value) { m_changeToFirstPath = value; } void AddToArchive::setFilename(const QUrl &path) { m_filename = path.toLocalFile(); } void AddToArchive::setMimeType(const QString & mimeType) { m_mimeType = mimeType; } void AddToArchive::setPassword(const QString &password) { m_password = password; } void AddToArchive::setHeaderEncryptionEnabled(bool enabled) { m_enableHeaderEncryption = enabled; } bool AddToArchive::showAddDialog() { qCDebug(ARK) << "Opening add dialog"; if (m_filename.isEmpty()) { m_filename = detectBaseName(m_entries); } QPointer dialog = new Kerfuffle::CreateDialog( nullptr, // parent i18n("Compress to Archive"), // caption QUrl::fromLocalFile(QFileInfo(m_filename).path())); // startDir dialog->setFileName(QFileInfo(m_filename).fileName()); bool ret = dialog.data()->exec(); if (ret) { qCDebug(ARK) << "CreateDialog returned URL:" << dialog.data()->selectedUrl().toString(); qCDebug(ARK) << "CreateDialog returned mime:" << dialog.data()->currentMimeType().name(); setFilename(dialog.data()->selectedUrl()); setMimeType(dialog.data()->currentMimeType().name()); setPassword(dialog.data()->password()); setHeaderEncryptionEnabled(dialog.data()->isHeaderEncryptionEnabled()); m_options.setCompressionLevel(dialog.data()->compressionLevel()); m_options.setCompressionMethod(dialog.data()->compressionMethod()); m_options.setEncryptionMethod(dialog.data()->encryptionMethod()); m_options.setVolumeSize(dialog.data()->volumeSize()); } delete dialog.data(); return ret; } bool AddToArchive::addInput(const QUrl &url) { Archive::Entry *entry = new Archive::Entry(this); entry->setFullPath(url.toLocalFile()); m_entries << entry; if (m_firstPath.isEmpty()) { QString firstEntry = url.toLocalFile(); m_firstPath = QFileInfo(firstEntry).dir().absolutePath(); } return true; } void AddToArchive::start() { qCDebug(ARK) << "Starting job"; QTimer::singleShot(0, this, &AddToArchive::slotStartJob); } bool AddToArchive::doKill() { return m_createJob && m_createJob->kill(); } void AddToArchive::slotStartJob() { if (m_entries.isEmpty()) { KMessageBox::error(nullptr, i18n("No input files were given.")); emitResult(); return; } if (m_filename.isEmpty()) { if (m_autoFilenameSuffix.isEmpty()) { KMessageBox::error(nullptr, xi18n("You need to either supply a filename for the archive or a suffix (such as rar, tar.gz) with the --autofilename argument.")); emitResult(); return; } if (m_firstPath.isEmpty()) { qCWarning(ARK) << "Weird, this should not happen. no firstpath defined. aborting"; emitResult(); return; } detectFileName(); } if (m_changeToFirstPath) { if (m_firstPath.isEmpty()) { qCWarning(ARK) << "Weird, this should not happen. no firstpath defined. aborting"; emitResult(); return; } const QDir stripDir(m_firstPath); for (Archive::Entry *entry : qAsConst(m_entries)) { entry->setFullPath(stripDir.absoluteFilePath(entry->fullPath())); } qCDebug(ARK) << "Setting GlobalWorkDir to " << stripDir.path(); m_options.setGlobalWorkDir(stripDir.path()); } m_createJob = Archive::create(m_filename, m_mimeType, m_entries, m_options, this); if (!m_password.isEmpty()) { m_createJob->enableEncryption(m_password, m_enableHeaderEncryption); } KIO::getJobTracker()->registerJob(m_createJob); connect(m_createJob, &KJob::result, this, &AddToArchive::slotFinished); m_createJob->start(); } void AddToArchive::detectFileName() { const QString base = detectBaseName(m_entries); const QString suffix = !m_autoFilenameSuffix.isEmpty() ? QLatin1Char( '.' ) + m_autoFilenameSuffix : QString(); QString finalName = base + suffix; //if file already exists, append a number to the base until it doesn't //exist int appendNumber = 0; while (QFileInfo::exists(finalName)) { ++appendNumber; finalName = base + QLatin1Char( '_' ) + QString::number(appendNumber) + suffix; } qCDebug(ARK) << "Autoset filename to" << finalName; m_filename = finalName; } void AddToArchive::slotFinished(KJob *job) { qCDebug(ARK) << "job finished"; if (job->error() && !job->errorString().isEmpty()) { KMessageBox::error(nullptr, job->errorString()); } emitResult(); } QString AddToArchive::detectBaseName(const QVector &entries) const { QFileInfo fileInfo = QFileInfo(entries.first()->fullPath()); QDir parentDir = fileInfo.dir(); QString base = parentDir.absolutePath() + QLatin1Char('/'); if (entries.size() > 1) { if (!parentDir.isRoot()) { // Use directory name for the new archive. base += parentDir.dirName(); } } else { // Strip filename of its extension, but only if present (see #362690). if (!QMimeDatabase().mimeTypeForFile(fileInfo.fileName(), QMimeDatabase::MatchExtension).isDefault()) { base += fileInfo.completeBaseName(); } else { base += fileInfo.fileName(); } } // Special case for compressed tar archives. if (base.right(4).toUpper() == QLatin1String(".TAR")) { base.chop(4); } if (base.endsWith(QLatin1Char('/'))) { base.chop(1); } return base; } } diff --git a/kerfuffle/addtoarchive.h b/kerfuffle/addtoarchive.h index cc0b4a3a..a8dd44b4 100644 --- a/kerfuffle/addtoarchive.h +++ b/kerfuffle/addtoarchive.h @@ -1,100 +1,99 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Harald Hvaal * Copyright (C) 2009 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ADDTOARCHIVE_H #define ADDTOARCHIVE_H #include "kerfuffle_export.h" #include "archive_kerfuffle.h" #include #include -#include /** * Compresses all input files into an archive. * * This is a job class that creates a compressed archive * with all the given input files. * * It provides the functionality for the --add command-line * option, and does not need the GUI to be running. * * @author Harald Hvaal */ namespace Kerfuffle { class KERFUFFLE_EXPORT AddToArchive : public KJob { Q_OBJECT public: explicit AddToArchive(QObject *parent = nullptr); ~AddToArchive() override; bool showAddDialog(); void setPreservePaths(bool value); void setChangeToFirstPath(bool value); QString detectBaseName(const QVector &entries) const; public Q_SLOTS: bool addInput(const QUrl &url); void setAutoFilenameSuffix(const QString& suffix); void setFilename(const QUrl &path); void setMimeType(const QString & mimeType); void setPassword(const QString &password); void setHeaderEncryptionEnabled(bool enabled); void start() override; protected: bool doKill() override; private Q_SLOTS: void slotFinished(KJob*); void slotStartJob(); private: void detectFileName(); CompressionOptions m_options; CreateJob *m_createJob; QString m_filename; QString m_strippedPath; QString m_autoFilenameSuffix; QString m_firstPath; QString m_mimeType; QString m_password; QVector m_entries; bool m_changeToFirstPath; bool m_enableHeaderEncryption; }; } #endif // ADDTOARCHIVE_H diff --git a/kerfuffle/archive_kerfuffle.cpp b/kerfuffle/archive_kerfuffle.cpp index d6184d2e..d5f5db8f 100644 --- a/kerfuffle/archive_kerfuffle.cpp +++ b/kerfuffle/archive_kerfuffle.cpp @@ -1,545 +1,544 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008 Harald Hvaal * Copyright (c) 2009-2011 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "archive_kerfuffle.h" #include "ark_debug.h" #include "archiveinterface.h" #include "jobs.h" #include "mimetypes.h" #include "pluginmanager.h" -#include #include #include #include #include namespace Kerfuffle { Archive *Archive::create(const QString &fileName, QObject *parent) { return create(fileName, QString(), parent); } Archive *Archive::create(const QString &fileName, const QString &fixedMimeType, QObject *parent) { qCDebug(ARK) << "Going to create archive" << fileName; PluginManager pluginManager; const QMimeType mimeType = fixedMimeType.isEmpty() ? determineMimeType(fileName) : QMimeDatabase().mimeTypeForName(fixedMimeType); const QVector offers = pluginManager.preferredPluginsFor(mimeType); if (offers.isEmpty()) { qCCritical(ARK) << "Could not find a plugin to handle" << fileName; return new Archive(NoPlugin, parent); } Archive *archive = nullptr; for (Plugin *plugin : offers) { archive = create(fileName, plugin, parent); // Use the first valid plugin, according to the priority sorting. if (archive->isValid()) { return archive; } } qCCritical(ARK) << "Failed to find a usable plugin for" << fileName; return archive; } Archive *Archive::create(const QString &fileName, Plugin *plugin, QObject *parent) { Q_ASSERT(plugin); qCDebug(ARK) << "Checking plugin" << plugin->metaData().pluginId(); KPluginFactory *factory = KPluginLoader(plugin->metaData().fileName()).factory(); if (!factory) { qCWarning(ARK) << "Invalid plugin factory for" << plugin->metaData().pluginId(); return new Archive(FailedPlugin, parent); } const QVariantList args = {QVariant(QFileInfo(fileName).absoluteFilePath()), QVariant().fromValue(plugin->metaData())}; ReadOnlyArchiveInterface *iface = factory->create(nullptr, args); if (!iface) { qCWarning(ARK) << "Could not create plugin instance" << plugin->metaData().pluginId(); return new Archive(FailedPlugin, parent); } if (!plugin->isValid()) { qCDebug(ARK) << "Cannot use plugin" << plugin->metaData().pluginId() << "- check whether" << plugin->readOnlyExecutables() << "are installed."; return new Archive(FailedPlugin, parent); } qCDebug(ARK) << "Successfully loaded plugin" << plugin->metaData().pluginId(); return new Archive(iface, !plugin->isReadWrite(), parent); } BatchExtractJob *Archive::batchExtract(const QString &fileName, const QString &destination, bool autoSubfolder, bool preservePaths, QObject *parent) { auto loadJob = load(fileName, parent); auto batchJob = new BatchExtractJob(loadJob, destination, autoSubfolder, preservePaths); return batchJob; } CreateJob *Archive::create(const QString &fileName, const QString &mimeType, const QVector &entries, const CompressionOptions &options, QObject *parent) { auto archive = create(fileName, mimeType, parent); auto createJob = new CreateJob(archive, entries, options); return createJob; } Archive *Archive::createEmpty(const QString &fileName, const QString &mimeType, QObject *parent) { auto archive = create(fileName, mimeType, parent); Q_ASSERT(archive->isEmpty()); return archive; } LoadJob *Archive::load(const QString &fileName, QObject *parent) { return load(fileName, QString(), parent); } LoadJob *Archive::load(const QString &fileName, const QString &mimeType, QObject *parent) { auto archive = create(fileName, mimeType, parent); auto loadJob = new LoadJob(archive); return loadJob; } LoadJob *Archive::load(const QString &fileName, Plugin *plugin, QObject *parent) { auto archive = create(fileName, plugin, parent); auto loadJob = new LoadJob(archive); return loadJob; } Archive::Archive(ArchiveError errorCode, QObject *parent) : QObject(parent) , m_iface(nullptr) , m_error(errorCode) { qCDebug(ARK) << "Created archive instance with error"; } Archive::Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent) : QObject(parent) , m_iface(archiveInterface) , m_isReadOnly(isReadOnly) , m_isSingleFolder(false) , m_isMultiVolume(false) , m_extractedFilesSize(0) , m_error(NoError) , m_encryptionType(Unencrypted) { qCDebug(ARK) << "Created archive instance"; Q_ASSERT(m_iface); m_iface->setParent(this); connect(m_iface, &ReadOnlyArchiveInterface::compressionMethodFound, this, &Archive::onCompressionMethodFound); connect(m_iface, &ReadOnlyArchiveInterface::encryptionMethodFound, this, &Archive::onEncryptionMethodFound); } void Archive::onCompressionMethodFound(const QString &method) { QStringList methods = property("compressionMethods").toStringList(); if (!methods.contains(method) && method != QLatin1String("Store")) { methods.append(method); } methods.sort(); setProperty("compressionMethods", methods); } void Archive::onEncryptionMethodFound(const QString &method) { QStringList methods = property("encryptionMethods").toStringList(); if (!methods.contains(method)) { methods.append(method); } methods.sort(); setProperty("encryptionMethods", methods); } Archive::~Archive() { } QString Archive::completeBaseName() const { const QString suffix = QFileInfo(fileName()).suffix(); QString base = QFileInfo(fileName()).completeBaseName(); // Special case for compressed tar archives. if (base.right(4).toUpper() == QLatin1String(".TAR")) { base.chop(4); // Multi-volume 7z's are named name.7z.001. } else if (base.right(3).toUpper() == QLatin1String(".7Z")) { base.chop(3); // Multi-volume zip's are named name.zip.001. } else if (base.right(4).toUpper() == QLatin1String(".ZIP")) { base.chop(4); // For multivolume rar's we want to remove the ".partNNN" suffix. } else if (suffix.toUpper() == QLatin1String("RAR")) { base.remove(QRegularExpression(QStringLiteral("\\.part[0-9]{1,3}$"))); } return base; } QString Archive::fileName() const { return isValid() ? m_iface->filename() : QString(); } QString Archive::comment() const { return isValid() ? m_iface->comment() : QString(); } CommentJob* Archive::addComment(const QString &comment) { if (!isValid()) { return nullptr; } qCDebug(ARK) << "Going to add comment:" << comment; Q_ASSERT(!isReadOnly()); CommentJob *job = new CommentJob(comment, static_cast(m_iface)); return job; } TestJob* Archive::testArchive() { if (!isValid()) { return nullptr; } qCDebug(ARK) << "Going to test archive"; TestJob *job = new TestJob(m_iface); return job; } QMimeType Archive::mimeType() { if (!isValid()) { return QMimeType(); } if (!m_mimeType.isValid()) { m_mimeType = determineMimeType(fileName()); } return m_mimeType; } bool Archive::isEmpty() const { return (numberOfEntries() == 0); } bool Archive::isReadOnly() const { return isValid() ? (m_iface->isReadOnly() || m_isReadOnly || (isMultiVolume() && (numberOfEntries() > 0))) : false; } bool Archive::isSingleFile() const { // If the only entry is a folder, isSingleFolder() is true. return numberOfEntries() == 1 && !isSingleFolder(); } bool Archive::isSingleFolder() const { if (!isValid()) { return false; } return m_isSingleFolder; } bool Archive::hasComment() const { return isValid() ? !comment().isEmpty() : false; } bool Archive::isMultiVolume() const { if (!isValid()) { return false; } return m_iface->isMultiVolume(); } void Archive::setMultiVolume(bool value) { m_iface->setMultiVolume(value); } int Archive::numberOfVolumes() const { return m_iface->numberOfVolumes(); } Archive::EncryptionType Archive::encryptionType() const { if (!isValid()) { return Unencrypted; } return m_encryptionType; } QString Archive::password() const { return m_iface->password(); } uint Archive::numberOfEntries() const { if (!isValid()) { return 0; } return m_iface->numberOfEntries(); } qulonglong Archive::unpackedSize() const { if (!isValid()) { return 0; } return m_extractedFilesSize; } qulonglong Archive::packedSize() const { return isValid() ? static_cast(QFileInfo(fileName()).size()) : 0; } QString Archive::subfolderName() const { if (!isValid()) { return QString(); } return m_subfolderName; } bool Archive::isValid() const { return m_iface && (m_error == NoError); } ArchiveError Archive::error() const { return m_error; } DeleteJob* Archive::deleteFiles(QVector &entries) { if (!isValid()) { return nullptr; } qCDebug(ARK) << "Going to delete" << entries.size() << "entries"; if (m_iface->isReadOnly()) { return nullptr; } DeleteJob *newJob = new DeleteJob(entries, static_cast(m_iface)); return newJob; } AddJob* Archive::addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options) { if (!isValid()) { return nullptr; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions.setEncryptedArchiveHint(true); } qCDebug(ARK) << "Going to add files" << files << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); AddJob *newJob = new AddJob(files, destination, newOptions, static_cast(m_iface)); connect(newJob, &AddJob::result, this, &Archive::onAddFinished); return newJob; } MoveJob* Archive::moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options) { if (!isValid()) { return nullptr; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions.setEncryptedArchiveHint(true); } qCDebug(ARK) << "Going to move files" << files << "to destination" << destination << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); MoveJob *newJob = new MoveJob(files, destination, newOptions, static_cast(m_iface)); return newJob; } CopyJob* Archive::copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { if (!isValid()) { return nullptr; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions.setEncryptedArchiveHint(true); } qCDebug(ARK) << "Going to copy files" << files << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); CopyJob *newJob = new CopyJob(files, destination, newOptions, static_cast(m_iface)); return newJob; } ExtractJob* Archive::extractFiles(const QVector &files, const QString &destinationDir, const ExtractionOptions &options) { if (!isValid()) { return nullptr; } ExtractionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions.setEncryptedArchiveHint(true); } ExtractJob *newJob = new ExtractJob(files, destinationDir, newOptions, m_iface); return newJob; } PreviewJob *Archive::preview(Archive::Entry *entry) { if (!isValid()) { return nullptr; } PreviewJob *job = new PreviewJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } OpenJob *Archive::open(Archive::Entry *entry) { if (!isValid()) { return nullptr; } OpenJob *job = new OpenJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } OpenWithJob *Archive::openWith(Archive::Entry *entry) { if (!isValid()) { return nullptr; } OpenWithJob *job = new OpenWithJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } void Archive::encrypt(const QString &password, bool encryptHeader) { if (!isValid()) { return; } m_iface->setPassword(password); m_iface->setHeaderEncryptionEnabled(encryptHeader); m_encryptionType = encryptHeader ? HeaderEncrypted : Encrypted; } void Archive::onAddFinished(KJob* job) { //if the archive was previously a single folder archive and an add job //has successfully finished, then it is no longer a single folder //archive (for the current implementation, which does not allow adding //folders/files other places than the root. //TODO: handle the case of creating a new file and singlefolderarchive //then. if (m_isSingleFolder && !job->error()) { m_isSingleFolder = false; } } void Archive::onUserQuery(Query* query) { query->execute(); } QString Archive::multiVolumeName() const { return m_iface->multiVolumeName(); } ReadOnlyArchiveInterface *Archive::interface() { return m_iface; } bool Archive::hasMultipleTopLevelEntries() const { return !isSingleFile() && !isSingleFolder(); } } // namespace Kerfuffle diff --git a/kerfuffle/archive_kerfuffle.h b/kerfuffle/archive_kerfuffle.h index 578b5ce2..95e11f11 100644 --- a/kerfuffle/archive_kerfuffle.h +++ b/kerfuffle/archive_kerfuffle.h @@ -1,249 +1,248 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008 Harald Hvaal * Copyright (c) 2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVE_H #define ARCHIVE_H #include "kerfuffle_export.h" #include "options.h" #include #include #include #include -#include namespace Kerfuffle { class LoadJob; class BatchExtractJob; class CreateJob; class ExtractJob; class DeleteJob; class AddJob; class MoveJob; class CopyJob; class CommentJob; class TestJob; class OpenJob; class OpenWithJob; class Plugin; class PreviewJob; class Query; class ReadOnlyArchiveInterface; enum ArchiveError { NoError = 0, NoPlugin, FailedPlugin }; class KERFUFFLE_EXPORT Archive : public QObject { Q_OBJECT /** * Complete base name, without the "tar" extension (if any). */ Q_PROPERTY(QString completeBaseName READ completeBaseName CONSTANT) Q_PROPERTY(QString fileName READ fileName CONSTANT) Q_PROPERTY(QString comment READ comment CONSTANT) Q_PROPERTY(QMimeType mimeType READ mimeType CONSTANT) Q_PROPERTY(bool isEmpty READ isEmpty) Q_PROPERTY(bool isReadOnly READ isReadOnly CONSTANT) Q_PROPERTY(bool isSingleFile READ isSingleFile) Q_PROPERTY(bool isSingleFolder MEMBER m_isSingleFolder READ isSingleFolder) Q_PROPERTY(bool isMultiVolume READ isMultiVolume WRITE setMultiVolume) Q_PROPERTY(bool numberOfVolumes READ numberOfVolumes) Q_PROPERTY(EncryptionType encryptionType MEMBER m_encryptionType READ encryptionType) Q_PROPERTY(uint numberOfEntries READ numberOfEntries) Q_PROPERTY(qulonglong unpackedSize MEMBER m_extractedFilesSize READ unpackedSize) Q_PROPERTY(qulonglong packedSize READ packedSize) Q_PROPERTY(QString subfolderName MEMBER m_subfolderName READ subfolderName) Q_PROPERTY(QString password READ password) Q_PROPERTY(QStringList compressionMethods MEMBER m_compressionMethods) Q_PROPERTY(QStringList encryptionMethods MEMBER m_encryptionMethods) public: enum EncryptionType { Unencrypted, Encrypted, HeaderEncrypted }; Q_ENUM(EncryptionType) class Entry; QString completeBaseName() const; QString fileName() const; QString comment() const; QMimeType mimeType(); bool isEmpty() const; bool isReadOnly() const; bool isSingleFile() const; bool isSingleFolder() const; bool isMultiVolume() const; void setMultiVolume(bool value); bool hasComment() const; int numberOfVolumes() const; EncryptionType encryptionType() const; QString password() const; uint numberOfEntries() const; qulonglong unpackedSize() const; qulonglong packedSize() const; QString subfolderName() const; QString multiVolumeName() const; ReadOnlyArchiveInterface *interface(); /** * @return Whether the archive has more than one top-level entry. */ bool hasMultipleTopLevelEntries() const; /** * @return Batch extraction job for @p filename to @p destination. * @param autoSubfolder Whether the job will extract into a subfolder. * @param preservePaths Whether the job will preserve paths. * @param parent The parent for the archive. */ static BatchExtractJob *batchExtract(const QString &fileName, const QString &destination, bool autoSubfolder, bool preservePaths, QObject *parent = nullptr); /** * @return Job to create an archive for the given @p entries. * @param fileName The name of the new archive. * @param mimeType The mimetype of the new archive. */ static CreateJob* create(const QString &fileName, const QString &mimeType, const QVector &entries, const CompressionOptions& options, QObject *parent = nullptr); /** * @return An empty archive with name @p fileName, mimetype @p mimeType and @p parent as parent. */ static Archive *createEmpty(const QString &fileName, const QString &mimeType, QObject *parent = nullptr); /** * @return Job to load the archive @p fileName. * @param parent The parent of the archive that will be loaded. */ static LoadJob* load(const QString &fileName, QObject *parent = nullptr); /** * @return Job to load the archive @p fileName with mimetype @p mimeType. * @param parent The parent of the archive that will be loaded. */ static LoadJob* load(const QString &fileName, const QString &mimeType, QObject *parent = nullptr); /** * @return Job to load the archive @p fileName by using @p plugin. * @param parent The parent of the archive that will be loaded. */ static LoadJob* load(const QString &fileName, Plugin *plugin, QObject *parent = nullptr); ~Archive() override; ArchiveError error() const; bool isValid() const; DeleteJob* deleteFiles(QVector &entries); CommentJob* addComment(const QString &comment); TestJob* testArchive(); AddJob* addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options = CompressionOptions()); /** * Renames or moves entries within the archive. * * @param files All the renamed or moved files and their child entries (for renaming a directory too). * @param destination New entry name (for renaming) or destination folder (for moving). * If ReadOnlyArchiveInterface::entriesWithoutChildren(files).count() returns 1, then it's renaming, * so you must specify the resulted entry name, even if it's not going to be changed. * Otherwise (if count is more than 1) it's moving, so destination must contain only targeted folder path * or be empty, if moving to the root. */ MoveJob* moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options = CompressionOptions()); /** * Copies entries within the archive. * * @param files All the renamed or moved files and their child entries (for renaming a directory too). * @param destination Destination path. It must contain only targeted folder path or be empty, * if copying to the root. */ CopyJob* copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions& options = CompressionOptions()); ExtractJob* extractFiles(const QVector &files, const QString &destinationDir, const ExtractionOptions &options = ExtractionOptions()); PreviewJob* preview(Archive::Entry *entry); OpenJob* open(Archive::Entry *entry); OpenWithJob* openWith(Archive::Entry *entry); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ void encrypt(const QString &password, bool encryptHeader); private Q_SLOTS: void onAddFinished(KJob*); void onUserQuery(Kerfuffle::Query*); void onCompressionMethodFound(const QString &method); void onEncryptionMethodFound(const QString &method); private: Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent = nullptr); Archive(ArchiveError errorCode, QObject *parent = nullptr); static Archive *create(const QString &fileName, QObject *parent = nullptr); static Archive *create(const QString &fileName, const QString &fixedMimeType, QObject *parent = nullptr); /** * Create an archive instance from a given @p plugin. * @param fileName The name of the archive. * @return A valid archive if the plugin could be loaded, an invalid one otherwise (with the FailedPlugin error set). */ static Archive *create(const QString &fileName, Plugin *plugin, QObject *parent = nullptr); ReadOnlyArchiveInterface *m_iface; bool m_isReadOnly; bool m_isSingleFolder; bool m_isMultiVolume; QString m_subfolderName; qulonglong m_extractedFilesSize; ArchiveError m_error; EncryptionType m_encryptionType; QMimeType m_mimeType; QStringList m_compressionMethods; QStringList m_encryptionMethods; }; } // namespace Kerfuffle Q_DECLARE_METATYPE(KPluginMetaData) #endif // ARCHIVE_H diff --git a/kerfuffle/archiveentry.h b/kerfuffle/archiveentry.h index ac43c152..88e1d1d9 100644 --- a/kerfuffle/archiveentry.h +++ b/kerfuffle/archiveentry.h @@ -1,135 +1,134 @@ /* * 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 ARCHIVEENTRY_H #define ARCHIVEENTRY_H #include "archive_kerfuffle.h" #include -#include namespace Kerfuffle { enum PathFormat { NoTrailingSlash, WithTrailingSlash }; class Archive::Entry : public QObject { Q_OBJECT /** * Meta data related to one entry in a compressed archive. * * When creating a plugin, information about every single entry in * an archive is contained in an ArchiveEntry, and metadata * is set with the entries in this enum. * * Please notice that not all archive formats support all the properties * below, so set those that are available. */ Q_PROPERTY(QString fullPath MEMBER m_fullPath WRITE setFullPath) Q_PROPERTY(QString name READ name) Q_PROPERTY(QString permissions MEMBER m_permissions) Q_PROPERTY(QString owner MEMBER m_owner) Q_PROPERTY(QString group MEMBER m_group) Q_PROPERTY(qulonglong size MEMBER m_size) Q_PROPERTY(qulonglong compressedSize MEMBER m_compressedSize) Q_PROPERTY(QString link MEMBER m_link) Q_PROPERTY(QString ratio MEMBER m_ratio) Q_PROPERTY(QString CRC MEMBER m_CRC) Q_PROPERTY(QString method MEMBER m_method) Q_PROPERTY(QString version MEMBER m_version) Q_PROPERTY(QDateTime timestamp MEMBER m_timestamp) Q_PROPERTY(bool isDirectory MEMBER m_isDirectory WRITE setIsDirectory) Q_PROPERTY(bool isPasswordProtected MEMBER m_isPasswordProtected) public: explicit Entry(QObject *parent = nullptr, const QString &fullPath = {}, const QString &rootNode = {}); ~Entry() override; void copyMetaData(const Archive::Entry *sourceEntry); QVector entries(); const QVector entries() const; void setEntryAt(int index, Entry *value); void appendEntry(Entry *entry); void removeEntryAt(int index); Entry *getParent() const; void setParent(Entry *parent); void setFullPath(const QString &fullPath); QString fullPath(PathFormat format = WithTrailingSlash) const; QString name() const; void setIsDirectory(const bool isDirectory); bool isDir() const; int row() const; Entry *find(const QString &name) const; Entry *findByPath(const QStringList & pieces, int index = 0) const; /** * Fills @p dirs and @p files with the number of directories and files * in the entry (both will be 0 if the entry is not a directory). */ void countChildren(uint &dirs, uint &files) const; bool operator==(const Archive::Entry &right) const; public: QString rootNode; bool compressedSizeIsSet; private: QVector m_entries; QString m_name; Entry *m_parent; QString m_fullPath; QString m_permissions; QString m_owner; QString m_group; qulonglong m_size; qulonglong m_compressedSize; QString m_link; QString m_ratio; QString m_CRC; QString m_method; QString m_version; QDateTime m_timestamp; bool m_isDirectory; bool m_isPasswordProtected; }; QDebug KERFUFFLE_EXPORT operator<<(QDebug d, const Kerfuffle::Archive::Entry &entry); QDebug KERFUFFLE_EXPORT operator<<(QDebug d, const Kerfuffle::Archive::Entry *entry); } Q_DECLARE_METATYPE(Kerfuffle::Archive::Entry*) #endif //ARCHIVEENTRY_H diff --git a/kerfuffle/archiveinterface.cpp b/kerfuffle/archiveinterface.cpp index 0e986bd3..d52a1452 100644 --- a/kerfuffle/archiveinterface.cpp +++ b/kerfuffle/archiveinterface.cpp @@ -1,294 +1,293 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "archiveinterface.h" #include "ark_debug.h" #include "mimetypes.h" -#include #include #include namespace Kerfuffle { ReadOnlyArchiveInterface::ReadOnlyArchiveInterface(QObject *parent, const QVariantList & args) : QObject(parent) , m_numberOfVolumes(0) , m_numberOfEntries(0) , m_waitForFinishedSignal(false) , m_isHeaderEncryptionEnabled(false) , m_isCorrupt(false) , m_isMultiVolume(false) { Q_ASSERT(args.size() >= 2); qCDebug(ARK) << "Created read-only interface for" << args.first().toString(); m_filename = args.first().toString(); m_mimetype = determineMimeType(m_filename); connect(this, &ReadOnlyArchiveInterface::entry, this, &ReadOnlyArchiveInterface::onEntry); m_metaData = args.at(1).value(); } ReadOnlyArchiveInterface::~ReadOnlyArchiveInterface() { } void ReadOnlyArchiveInterface::onEntry(Archive::Entry *archiveEntry) { Q_UNUSED(archiveEntry) m_numberOfEntries++; } QString ReadOnlyArchiveInterface::filename() const { return m_filename; } QString ReadOnlyArchiveInterface::comment() const { return m_comment; } bool ReadOnlyArchiveInterface::isReadOnly() const { return true; } bool ReadOnlyArchiveInterface::open() { return true; } void ReadOnlyArchiveInterface::setPassword(const QString &password) { m_password = password; } void ReadOnlyArchiveInterface::setHeaderEncryptionEnabled(bool enabled) { m_isHeaderEncryptionEnabled = enabled; } QString ReadOnlyArchiveInterface::password() const { return m_password; } bool ReadOnlyArchiveInterface::doKill() { //default implementation return false; } void ReadOnlyArchiveInterface::setCorrupt(bool isCorrupt) { m_isCorrupt = isCorrupt; } bool ReadOnlyArchiveInterface::isCorrupt() const { return m_isCorrupt; } bool ReadOnlyArchiveInterface::isMultiVolume() const { return m_isMultiVolume; } void ReadOnlyArchiveInterface::setMultiVolume(bool value) { m_isMultiVolume = value; } int ReadOnlyArchiveInterface::numberOfVolumes() const { return m_numberOfVolumes; } QString ReadOnlyArchiveInterface::multiVolumeName() const { return filename(); } ReadWriteArchiveInterface::ReadWriteArchiveInterface(QObject *parent, const QVariantList &args) : ReadOnlyArchiveInterface(parent, args) { qCDebug(ARK) << "Created read-write interface for" << args.first().toString(); connect(this, &ReadWriteArchiveInterface::entryRemoved, this, &ReadWriteArchiveInterface::onEntryRemoved); } ReadWriteArchiveInterface::~ReadWriteArchiveInterface() { } bool ReadOnlyArchiveInterface::waitForFinishedSignal() { return m_waitForFinishedSignal; } int ReadOnlyArchiveInterface::moveRequiredSignals() const { return 1; } int ReadOnlyArchiveInterface::copyRequiredSignals() const { return 1; } void ReadOnlyArchiveInterface::setWaitForFinishedSignal(bool value) { m_waitForFinishedSignal = value; } QStringList ReadOnlyArchiveInterface::entryFullPaths(const QVector &entries, PathFormat format) { QStringList filesList; for (const Archive::Entry *file : entries) { filesList << file->fullPath(format); } return filesList; } QVector ReadOnlyArchiveInterface::entriesWithoutChildren(const QVector &entries) { // QMap is easy way to get entries sorted by their fullPath. QMap sortedEntries; for (Archive::Entry *entry : entries) { sortedEntries.insert(entry->fullPath(), entry); } QVector filteredEntries; QString lastFolder; for (Archive::Entry *entry : qAsConst(sortedEntries)) { if (lastFolder.count() > 0 && entry->fullPath().startsWith(lastFolder)) { continue; } lastFolder = (entry->fullPath().right(1) == QLatin1String("/")) ? entry->fullPath() : QString(); filteredEntries << entry; } return filteredEntries; } QStringList ReadOnlyArchiveInterface::entryPathsFromDestination(QStringList entries, const Archive::Entry *destination, int entriesWithoutChildren) { QStringList paths = QStringList(); entries.sort(); QString lastFolder; const QString destinationPath = (destination == nullptr) ? QString() : destination->fullPath(); QString newPath; int nameLength = 0; for (const QString &entryPath : qAsConst(entries)) { if (lastFolder.count() > 0 && entryPath.startsWith(lastFolder)) { // Replace last moved or copied folder path with destination path. int charsCount = entryPath.count() - lastFolder.count(); if (entriesWithoutChildren != 1) { charsCount += nameLength; } newPath = destinationPath + entryPath.right(charsCount); } else { const QString name = entryPath.split(QLatin1Char('/'), QString::SkipEmptyParts).last(); if (entriesWithoutChildren != 1) { newPath = destinationPath + name; if (entryPath.right(1) == QLatin1String("/")) { newPath += QLatin1Char('/'); } } else { // If the mode is set to Move and there is only one passed file in the list, // we have to use destination as newPath. newPath = destinationPath; } if (entryPath.right(1) == QLatin1String("/")) { nameLength = name.count() + 1; // plus slash lastFolder = entryPath; } else { nameLength = 0; lastFolder = QString(); } } paths << newPath; } return paths; } bool ReadOnlyArchiveInterface::isHeaderEncryptionEnabled() const { return m_isHeaderEncryptionEnabled; } QMimeType ReadOnlyArchiveInterface::mimetype() const { return m_mimetype; } bool ReadOnlyArchiveInterface::hasBatchExtractionProgress() const { return false; } bool ReadOnlyArchiveInterface::isLocked() const { return false; } bool ReadWriteArchiveInterface::isReadOnly() const { if (isLocked()) { return true; } // We set corrupt archives to read-only to avoid add/delete actions, that // are likely to fail anyway. if (isCorrupt()) { return true; } QFileInfo fileInfo(filename()); if (fileInfo.exists()) { return !fileInfo.isWritable(); } else { return !fileInfo.dir().exists(); // TODO: Should also check if we can create a file in that directory } } uint ReadOnlyArchiveInterface::numberOfEntries() const { return m_numberOfEntries; } void ReadWriteArchiveInterface::onEntryRemoved(const QString &path) { Q_UNUSED(path) m_numberOfEntries--; } } // namespace Kerfuffle diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp index e481b0f6..19a3c13a 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,1148 +1,1144 @@ /* * 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 +#include namespace Kerfuffle { CliInterface::CliInterface(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args) { //because this interface uses the event loop setWaitForFinishedSignal(true); if (QMetaType::type("QProcess::ExitStatus") == 0) { qRegisterMetaType("QProcess::ExitStatus"); } m_cliProps = new CliProperties(this, m_metaData, mimetype()); } CliInterface::~CliInterface() { Q_ASSERT(!m_process); } void CliInterface::setListEmptyLines(bool emptyLines) { m_listEmptyLines = emptyLines; } int CliInterface::copyRequiredSignals() const { return 2; } bool CliInterface::list() { resetParsing(); m_operationMode = List; m_numberOfEntries = 0; // To compute progress. m_archiveSizeOnDisk = static_cast(QFileInfo(filename()).size()); connect(this, &ReadOnlyArchiveInterface::entry, this, &CliInterface::onEntry); return runProcess(m_cliProps->property("listProgram").toString(), m_cliProps->listArgs(filename(), password())); } bool CliInterface::extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) { qCDebug(ARK) << "destination directory:" << destinationDirectory; m_operationMode = Extract; m_extractionOptions = options; m_extractedFiles = files; m_extractDestDir = destinationDirectory; if (!m_cliProps->property("passwordSwitch").toStringList().isEmpty() && options.encryptedArchiveHint() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } QUrl destDir = QUrl(destinationDirectory); m_oldWorkingDirExtraction = QDir::currentPath(); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); const bool useTmpExtractDir = options.isDragAndDropEnabled() || options.alwaysUseTempDir(); if (useTmpExtractDir) { // Create an hidden temp folder in the current directory. m_extractTempDir.reset(new QTemporaryDir(QStringLiteral(".%1-").arg(QCoreApplication::applicationName()))); 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; } destDir = QUrl(m_extractTempDir->path()); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); } return runProcess(m_cliProps->property("extractProgram").toString(), m_cliProps->extractArgs(filename(), extractFilesList(files), options.preservePaths(), password())); } bool CliInterface::addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd) { Q_UNUSED(numberOfEntriesToAdd) m_operationMode = Add; QVector filesToPass = QVector(); // 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 == nullptr) ? QString() : destination->fullPath(); qCDebug(ARK) << "Adding" << files.count() << "file(s) to destination:" << destinationPath; if (!destinationPath.isEmpty()) { m_extractTempDir.reset(new QTemporaryDir()); const QString absoluteDestinationPath = m_extractTempDir->path() + QLatin1Char('/') + destinationPath; QDir qDir; qDir.mkpath(absoluteDestinationPath); QObject *preservedParent = nullptr; for (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 == nullptr) { preservedParent = file->parent(); } const QString filePath = QDir::currentPath() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); const QString newFilePath = absoluteDestinationPath + file->fullPath(NoTrailingSlash); if (QFile::link(filePath, newFilePath)) { qCDebug(ARK) << "Symlink's created:" << filePath << newFilePath; } else { qCDebug(ARK) << "Can't create symlink" << filePath << newFilePath; emit finished(false); 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 (!m_cliProps->property("passwordSwitch").toString().isEmpty() && options.encryptedArchiveHint() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } return runProcess(m_cliProps->property("addProgram").toString(), m_cliProps->addArgs(filename(), entryFullPaths(filesToPass, NoTrailingSlash), password(), isHeaderEncryptionEnabled(), options.compressionLevel(), options.compressionMethod(), options.encryptionMethod(), options.volumeSize())); } bool CliInterface::moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(options); m_operationMode = Move; m_removedFiles = files; QVector withoutChildren = entriesWithoutChildren(files); setNewMovedFiles(files, destination, withoutChildren.count()); return runProcess(m_cliProps->property("moveProgram").toString(), m_cliProps->moveArgs(filename(), withoutChildren, destination, password())); } bool CliInterface::copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { m_oldWorkingDir = QDir::currentPath(); m_tempWorkingDir.reset(new QTemporaryDir()); m_tempAddDir.reset(new QTemporaryDir()); QDir::setCurrent(m_tempWorkingDir->path()); m_passedFiles = files; m_passedDestination = destination; m_passedOptions = options; m_numberOfEntries = 0; m_subOperation = Extract; connect(this, &CliInterface::finished, this, &CliInterface::continueCopying); return extractFiles(files, QDir::currentPath(), ExtractionOptions()); } bool CliInterface::deleteFiles(const QVector &files) { m_operationMode = Delete; m_removedFiles = files; return runProcess(m_cliProps->property("deleteProgram").toString(), m_cliProps->deleteArgs(filename(), files, password())); } bool CliInterface::testArchive() { resetParsing(); m_operationMode = Test; return runProcess(m_cliProps->property("testProgram").toString(), m_cliProps->testArgs(filename(), password())); } bool CliInterface::runProcess(const QString& programName, const QStringList& arguments) { Q_ASSERT(!m_process); QString programPath = QStandardPaths::findExecutable(programName); if (programPath.isEmpty()) { emit error(xi18nc("@info", "Failed to locate program %1 on disk.", programName)); 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, QOverload::of(&QProcess::finished), this, &CliInterface::extractProcessFinished); } else { connect(m_process, QOverload::of(&QProcess::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 = nullptr; } // #193908 - #222392 // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_operationMode == Delete || m_operationMode == Move) { const QStringList removedFullPaths = entryFullPaths(m_removedFiles); for (const QString &fullPath : removedFullPaths) { emit entryRemoved(fullPath); } for (Archive::Entry *e : qAsConst(m_newMovedFiles)) { emit entry(e); } m_newMovedFiles.clear(); } if (m_operationMode == Add && !isMultiVolume()) { list(); } else if (m_operationMode == List && isCorrupt()) { Kerfuffle::LoadCorruptQuery query(filename()); query.execute(); 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 = nullptr; } // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_extractionOptions.alwaysUseTempDir()) { // 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_extractionOptions.isDragAndDropEnabled()) { if (!moveToDestination(QDir::current(), QDir(m_extractDestDir), m_extractionOptions.preservePaths())) { 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_extractionOptions.isDragAndDropEnabled()) { const bool droppedFilesMoved = moveDroppedFilesToDest(m_extractedFiles, m_extractDestDir); if (!droppedFilesMoved) { cleanUpExtracting(); return; } cleanUpExtracting(); } // #395939: make sure we *always* restore the old working dir. restoreWorkingDirExtraction(); 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 QVector &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; for (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); query.execute(); 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()) { emit cancelled(); emit finished(false); 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."; 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())); emit finished(false); 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() { restoreWorkingDirExtraction(); m_extractTempDir.reset(); } void CliInterface::restoreWorkingDirExtraction() { if (m_oldWorkingDirExtraction.isEmpty()) { return; } if (!QDir::setCurrent(m_oldWorkingDirExtraction)) { qCWarning(ARK) << "Failed to restore old working directory:" << m_oldWorkingDirExtraction; } else { m_oldWorkingDirExtraction.clear(); } } 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); query.execute(); 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; } void CliInterface::setNewMovedFiles(const QVector &entries, const Archive::Entry *destination, int entriesWithoutChildren) { m_newMovedFiles.clear(); QMap entryMap; for (const Archive::Entry* entry : entries) { entryMap.insert(entry->fullPath(), entry); } QString lastFolder; QString newPath; int nameLength = 0; for (const Archive::Entry* entry : qAsConst(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(NoTrailingSlash); } 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(nullptr); newEntry->copyMetaData(entry); newEntry->setFullPath(newPath); m_newMovedFiles << newEntry; } } QStringList CliInterface::extractFilesList(const QVector &entries) const { QStringList filesList; for (const Archive::Entry *e : entries) { filesList << escapeFileName(e->fullPath(NoTrailingSlash)); } 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()); query.execute(); 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); m_tempWorkingDir.reset(); m_tempAddDir.reset(); } 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 = isWrongPasswordMsg(QLatin1String(lines.last())); bool foundErrorMessage = (wrongPasswordMessage || isDiskFullMsg(QLatin1String(lines.last())) || isFileExistsMsg(QLatin1String(lines.last()))) || isPasswordPrompt(QLatin1String(lines.last())); 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(); } for (const QByteArray& line : qAsConst(lines)) { if (!line.isEmpty() || (m_listEmptyLines && m_operationMode == List)) { if (!handleLine(QString::fromLocal8Bit(line))) { killProcess(); return; } } } } bool CliInterface::setAddedFiles() { QDir::setCurrent(m_tempAddDir->path()); for (const Archive::Entry *file : qAsConst(m_passedFiles)) { const QString oldPath = m_tempWorkingDir->path() + QLatin1Char('/') + file->fullPath(NoTrailingSlash); const QString newPath = m_tempAddDir->path() + QLatin1Char('/') + file->name(); if (!QFile::rename(oldPath, newPath)) { return false; } m_tempAddedFiles << new Archive::Entry(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_cliProps->property("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 (isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); query.execute(); if (query.responseCancelled()) { emit cancelled(); return false; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return true; } if (isDiskFullMsg(line)) { qCWarning(ARK) << "Found disk full message:" << line; emit error(i18nc("@info", "Extraction failed because the disk is full.")); return false; } if (isWrongPasswordMsg(line)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18nc("@info", "Extraction failed: Incorrect password")); return false; } if (handleFileExistsMessage(line)) { return true; } return readExtractLine(line); } if (m_operationMode == List) { if (isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); query.execute(); if (query.responseCancelled()) { emit cancelled(); return false; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return true; } if (isWrongPasswordMsg(line)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18n("Incorrect password.")); return false; } if (isCorruptArchiveMsg(line)) { qCWarning(ARK) << "Archive corrupt"; setCorrupt(true); // Special case: corrupt is not a "fatal" error so we return true here. return true; } return readListLine(line); } if (m_operationMode == Delete) { return readDeleteLine(line); } if (m_operationMode == Test) { if (isPasswordPrompt(line)) { qCDebug(ARK) << "Found a password prompt"; emit error(i18n("Ark does not currently support testing this archive.")); return false; } if (m_cliProps->isTestPassedMsg(line)) { qCDebug(ARK) << "Test successful"; emit testSuccess(); return true; } } return true; } bool CliInterface::readDeleteLine(const QString &line) { Q_UNUSED(line); return true; } bool CliInterface::handleFileExistsMessage(const QString& line) { // Check for a filename and store it. if (isFileExistsFileName(line)) { const QStringList fileExistsFileNameRegExp = m_cliProps->property("fileExistsFileNameRegExp").toStringList(); for (const QString &pattern : fileExistsFileNameRegExp) { 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 (!isFileExistsMsg(line)) { return false; } Kerfuffle::OverwriteQuery query(QDir::current().path() + QLatin1Char( '/' ) + m_storedFileName); query.setNoRenameMode(true); query.execute(); QString responseToProcess; const QStringList choices = m_cliProps->property("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()) { emit cancelled(); 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::doKill() { if (m_process) { killProcess(false); return true; } return false; } QString CliInterface::escapeFileName(const QString& fileName) const { return fileName; } QStringList CliInterface::entryPathDestinationPairs(const QVector &entriesWithoutChildren, const Archive::Entry *destination) { QStringList pairList; if (entriesWithoutChildren.count() > 1) { for (const Archive::Entry *file : entriesWithoutChildren) { pairList << file->fullPath(NoTrailingSlash) << destination->fullPath() + file->name(); } } else { pairList << entriesWithoutChildren.at(0)->fullPath(NoTrailingSlash) << destination->fullPath(NoTrailingSlash); } 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) { m_operationMode = Comment; m_commentTempFile.reset(new QTemporaryFile()); if (!m_commentTempFile->open()) { qCWarning(ARK) << "Failed to create temporary file for comment"; emit finished(false); return false; } QTextStream stream(m_commentTempFile.data()); stream << comment << endl; m_commentTempFile->close(); if (!runProcess(m_cliProps->property("addProgram").toString(), m_cliProps->commentArgs(filename(), m_commentTempFile->fileName()))) { return false; } m_comment = comment; return true; } QString CliInterface::multiVolumeName() const { QString oldSuffix = QMimeDatabase().suffixForFileName(filename()); QString name; const QStringList multiVolumeSuffix = m_cliProps->property("multiVolumeSuffix").toStringList(); for (const QString &multiSuffix : multiVolumeSuffix) { QString newSuffix = multiSuffix; newSuffix.replace(QStringLiteral("$Suffix"), oldSuffix); name = filename().remove(oldSuffix).append(newSuffix); if (QFileInfo::exists(name)) { break; } } return name; } CliProperties *CliInterface::cliProperties() const { return m_cliProps; } void CliInterface::onEntry(Archive::Entry *archiveEntry) { if (archiveEntry->compressedSizeIsSet) { m_listedSize += archiveEntry->property("compressedSize").toULongLong(); if (m_listedSize <= m_archiveSizeOnDisk) { emit progress(float(m_listedSize)/float(m_archiveSizeOnDisk)); } else { // In case summed compressed size exceeds archive size on disk. emit progress(1); } } } bool CliInterface::isPasswordPrompt(const QString &line) { Q_UNUSED(line); return false; } bool CliInterface::isWrongPasswordMsg(const QString &line) { Q_UNUSED(line); return false; } bool CliInterface::isCorruptArchiveMsg(const QString &line) { Q_UNUSED(line); return false; } bool CliInterface::isDiskFullMsg(const QString &line) { Q_UNUSED(line); return false; } bool CliInterface::isFileExistsMsg(const QString &line) { Q_UNUSED(line); return false; } bool CliInterface::isFileExistsFileName(const QString &line) { Q_UNUSED(line); return false; } } diff --git a/kerfuffle/cliproperties.cpp b/kerfuffle/cliproperties.cpp index fe21b925..35534321 100644 --- a/kerfuffle/cliproperties.cpp +++ b/kerfuffle/cliproperties.cpp @@ -1,304 +1,304 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "cliproperties.h" #include "ark_debug.h" #include "archiveformat.h" #include "pluginmanager.h" - +#include namespace Kerfuffle { CliProperties::CliProperties(QObject *parent, const KPluginMetaData &metaData, const QMimeType &archiveType) : QObject(parent) , m_mimeType(archiveType) , m_metaData(metaData) { } QStringList CliProperties::addArgs(const QString &archive, const QStringList &files, const QString &password, bool headerEncryption, int compressionLevel, const QString &compressionMethod, const QString &encryptionMethod, ulong volumeSize) { if (!encryptionMethod.isEmpty()) { Q_ASSERT(!password.isEmpty()); } QStringList args; for (const QString &s : qAsConst(m_addSwitch)) { args << s; } if (!password.isEmpty()) { args << substitutePasswordSwitch(password, headerEncryption); } if (compressionLevel > -1) { args << substituteCompressionLevelSwitch(compressionLevel); } if (!compressionMethod.isEmpty()) { args << substituteCompressionMethodSwitch(compressionMethod); } if (!encryptionMethod.isEmpty()) { args << substituteEncryptionMethodSwitch(encryptionMethod); } if (volumeSize > 0) { args << substituteMultiVolumeSwitch(volumeSize); } args << archive; args << files; args.removeAll(QString()); return args; } QStringList CliProperties::commentArgs(const QString &archive, const QString &commentfile) { QStringList args; const QStringList commentSwitches = substituteCommentSwitch(commentfile); for (const QString &s : commentSwitches) { args << s; } args << archive; args.removeAll(QString()); return args; } QStringList CliProperties::deleteArgs(const QString &archive, const QVector &files, const QString &password) { QStringList args; args << m_deleteSwitch; if (!password.isEmpty()) { args << substitutePasswordSwitch(password); } args << archive; for (const Archive::Entry *e : files) { args << e->fullPath(NoTrailingSlash); } args.removeAll(QString()); return args; } QStringList CliProperties::extractArgs(const QString &archive, const QStringList &files, bool preservePaths, const QString &password) { QStringList args; if (preservePaths && !m_extractSwitch.isEmpty()) { args << m_extractSwitch; } else if (!preservePaths && !m_extractSwitchNoPreserve.isEmpty()) { args << m_extractSwitchNoPreserve; } if (!password.isEmpty()) { args << substitutePasswordSwitch(password); } args << archive; args << files; args.removeAll(QString()); return args; } QStringList CliProperties::listArgs(const QString &archive, const QString &password) { QStringList args; for (const QString &s : qAsConst(m_listSwitch)) { args << s; } const auto encryptionType = ArchiveFormat::fromMetadata(m_mimeType, m_metaData).encryptionType(); if (!password.isEmpty() && encryptionType == Archive::EncryptionType::HeaderEncrypted) { args << substitutePasswordSwitch(password); } args << archive; args.removeAll(QString()); return args; } QStringList CliProperties::moveArgs(const QString &archive, const QVector &entries, Archive::Entry *destination, const QString &password) { QStringList args; args << m_moveSwitch; if (!password.isEmpty()) { args << substitutePasswordSwitch(password); } args << archive; if (entries.count() > 1) { for (const Archive::Entry *file : entries) { args << file->fullPath(NoTrailingSlash) << destination->fullPath() + file->name(); } } else { args << entries.at(0)->fullPath(NoTrailingSlash) << destination->fullPath(NoTrailingSlash); } args.removeAll(QString()); return args; } QStringList CliProperties::testArgs(const QString &archive, const QString &password) { QStringList args; for (const QString &s : qAsConst(m_testSwitch)) { args << s; } if (!password.isEmpty()) { args << substitutePasswordSwitch(password); } args << archive; args.removeAll(QString()); return args; } QStringList CliProperties::substituteCommentSwitch(const QString &commentfile) const { Q_ASSERT(!commentfile.isEmpty()); Q_ASSERT(ArchiveFormat::fromMetadata(m_mimeType, m_metaData).supportsWriteComment()); QStringList commentSwitches = m_commentSwitch; Q_ASSERT(!commentSwitches.isEmpty()); QMutableListIterator i(commentSwitches); while (i.hasNext()) { i.next(); i.value().replace(QLatin1String("$CommentFile"), commentfile); } return commentSwitches; } QStringList CliProperties::substitutePasswordSwitch(const QString &password, bool headerEnc) const { if (password.isEmpty()) { return QStringList(); } Archive::EncryptionType encryptionType = ArchiveFormat::fromMetadata(m_mimeType, m_metaData).encryptionType(); Q_ASSERT(encryptionType != Archive::EncryptionType::Unencrypted); QStringList passwordSwitch; if (headerEnc) { passwordSwitch = m_passwordSwitchHeaderEnc; } else { passwordSwitch = m_passwordSwitch; } Q_ASSERT(!passwordSwitch.isEmpty()); QMutableListIterator i(passwordSwitch); while (i.hasNext()) { i.next(); i.value().replace(QLatin1String("$Password"), password); } return passwordSwitch; } QString CliProperties::substituteCompressionLevelSwitch(int level) const { if (level < 0 || level > 9) { return QString(); } Q_ASSERT(ArchiveFormat::fromMetadata(m_mimeType, m_metaData).maxCompressionLevel() != -1); QString compLevelSwitch = m_compressionLevelSwitch; Q_ASSERT(!compLevelSwitch.isEmpty()); compLevelSwitch.replace(QLatin1String("$CompressionLevel"), QString::number(level)); return compLevelSwitch; } QString CliProperties::substituteCompressionMethodSwitch(const QString &method) const { if (method.isEmpty()) { return QString(); } Q_ASSERT(!ArchiveFormat::fromMetadata(m_mimeType, m_metaData).compressionMethods().isEmpty()); QString compMethodSwitch = m_compressionMethodSwitch[m_mimeType.name()].toString(); Q_ASSERT(!compMethodSwitch.isEmpty()); QString cliMethod = ArchiveFormat::fromMetadata(m_mimeType, m_metaData).compressionMethods().value(method).toString(); compMethodSwitch.replace(QLatin1String("$CompressionMethod"), cliMethod); return compMethodSwitch; } QString CliProperties::substituteEncryptionMethodSwitch(const QString &method) const { if (method.isEmpty()) { return QString(); } const ArchiveFormat format = ArchiveFormat::fromMetadata(m_mimeType, m_metaData); Q_ASSERT(!format.encryptionMethods().isEmpty()); QString encMethodSwitch = m_encryptionMethodSwitch[m_mimeType.name()].toString(); if (encMethodSwitch.isEmpty()) { return QString(); } Q_ASSERT(format.encryptionMethods().contains(method)); encMethodSwitch.replace(QLatin1String("$EncryptionMethod"), method); return encMethodSwitch; } QString CliProperties::substituteMultiVolumeSwitch(ulong volumeSize) const { // The maximum value we allow in the QDoubleSpinBox is 1,000,000MB. Converted to // KB this is 1,024,000,000. if (volumeSize <= 0 || volumeSize > 1024000000) { return QString(); } Q_ASSERT(ArchiveFormat::fromMetadata(m_mimeType, m_metaData).supportsMultiVolume()); QString multiVolumeSwitch = m_multiVolumeSwitch; Q_ASSERT(!multiVolumeSwitch.isEmpty()); multiVolumeSwitch.replace(QLatin1String("$VolumeSize"), QString::number(volumeSize)); return multiVolumeSwitch; } bool CliProperties::isTestPassedMsg(const QString &line) { for (const QString &rx : qAsConst(m_testPassedPatterns)) { if (QRegularExpression(rx).match(line).hasMatch()) { return true; } } return false; } } diff --git a/kerfuffle/cliproperties.h b/kerfuffle/cliproperties.h index 9a33e36d..5400cfaf 100644 --- a/kerfuffle/cliproperties.h +++ b/kerfuffle/cliproperties.h @@ -1,137 +1,136 @@ /* * 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 CLIPROPERTIES_H #define CLIPROPERTIES_H #include "archiveinterface.h" #include "kerfuffle_export.h" -#include namespace Kerfuffle { class KERFUFFLE_EXPORT CliProperties: public QObject { Q_OBJECT Q_PROPERTY(QString addProgram MEMBER m_addProgram) Q_PROPERTY(QString deleteProgram MEMBER m_deleteProgram) Q_PROPERTY(QString extractProgram MEMBER m_extractProgram) Q_PROPERTY(QString listProgram MEMBER m_listProgram) Q_PROPERTY(QString moveProgram MEMBER m_moveProgram) Q_PROPERTY(QString testProgram MEMBER m_testProgram) Q_PROPERTY(QStringList addSwitch MEMBER m_addSwitch) Q_PROPERTY(QStringList commentSwitch MEMBER m_commentSwitch) Q_PROPERTY(QString deleteSwitch MEMBER m_deleteSwitch) Q_PROPERTY(QStringList extractSwitch MEMBER m_extractSwitch) Q_PROPERTY(QStringList extractSwitchNoPreserve MEMBER m_extractSwitchNoPreserve) Q_PROPERTY(QStringList listSwitch MEMBER m_listSwitch) Q_PROPERTY(QString moveSwitch MEMBER m_moveSwitch) Q_PROPERTY(QStringList testSwitch MEMBER m_testSwitch) Q_PROPERTY(QStringList passwordSwitch MEMBER m_passwordSwitch) Q_PROPERTY(QStringList passwordSwitchHeaderEnc MEMBER m_passwordSwitchHeaderEnc) Q_PROPERTY(QString compressionLevelSwitch MEMBER m_compressionLevelSwitch) Q_PROPERTY(QHash compressionMethodSwitch MEMBER m_compressionMethodSwitch) Q_PROPERTY(QHash encryptionMethodSwitch MEMBER m_encryptionMethodSwitch) Q_PROPERTY(QString multiVolumeSwitch MEMBER m_multiVolumeSwitch) Q_PROPERTY(QStringList testPassedPatterns MEMBER m_testPassedPatterns) Q_PROPERTY(QStringList fileExistsFileNameRegExp MEMBER m_fileExistsFileNameRegExp) Q_PROPERTY(QStringList fileExistsInput MEMBER m_fileExistsInput) Q_PROPERTY(QStringList multiVolumeSuffix MEMBER m_multiVolumeSuffix) Q_PROPERTY(bool captureProgress MEMBER m_captureProgress) public: explicit CliProperties(QObject *parent, const KPluginMetaData &metaData, const QMimeType &archiveType); QStringList addArgs(const QString &archive, const QStringList &files, const QString &password, bool headerEncryption, int compressionLevel, const QString &compressionMethod, const QString &encryptionMethod, ulong volumeSize); QStringList commentArgs(const QString &archive, const QString &commentfile); QStringList deleteArgs(const QString &archive, const QVector &files, const QString &password); QStringList extractArgs(const QString &archive, const QStringList &files, bool preservePaths, const QString &password); QStringList listArgs(const QString &archive, const QString &password); QStringList moveArgs(const QString &archive, const QVector &entries, Archive::Entry *destination, const QString &password); QStringList testArgs(const QString &archive, const QString &password); bool isTestPassedMsg(const QString &line); private: QStringList substituteCommentSwitch(const QString &commentfile) const; QStringList substitutePasswordSwitch(const QString &password, bool headerEnc = false) const; QString substituteCompressionLevelSwitch(int level) const; QString substituteCompressionMethodSwitch(const QString &method) const; QString substituteEncryptionMethodSwitch(const QString &method) const; QString substituteMultiVolumeSwitch(ulong volumeSize) const; QString m_addProgram; QString m_deleteProgram; QString m_extractProgram; QString m_listProgram; QString m_moveProgram; QString m_testProgram; QStringList m_addSwitch; QStringList m_commentSwitch; QString m_deleteSwitch; QStringList m_extractSwitch; QStringList m_extractSwitchNoPreserve; QStringList m_listSwitch; QString m_moveSwitch; QStringList m_testSwitch; QStringList m_passwordSwitch; QStringList m_passwordSwitchHeaderEnc; QString m_compressionLevelSwitch; QHash m_compressionMethodSwitch; QHash m_encryptionMethodSwitch; QString m_multiVolumeSwitch; QStringList m_testPassedPatterns; QStringList m_fileExistsFileNameRegExp; QStringList m_fileExistsInput; QStringList m_multiVolumeSuffix; bool m_captureProgress = false; QMimeType m_mimeType; KPluginMetaData m_metaData; }; } #endif /* CLIPROPERTIES_H */ diff --git a/kerfuffle/createdialog.cpp b/kerfuffle/createdialog.cpp index 2c1d8e9b..c74ed698 100644 --- a/kerfuffle/createdialog.cpp +++ b/kerfuffle/createdialog.cpp @@ -1,262 +1,260 @@ /* * 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 "mimetypes.h" #include #include -#include -#include #include #include namespace Kerfuffle { class CreateDialogUI: public QWidget, public Ui::CreateDialog { Q_OBJECT public: CreateDialogUI(QWidget *parent = nullptr) : QWidget(parent) { setupUi(this); } }; CreateDialog::CreateDialog(QWidget *parent, const QString &caption, const QUrl &startDir) : QDialog(parent, Qt::Dialog) { setWindowTitle(caption); setModal(true); m_supportedMimeTypes = m_pluginManger.supportedWriteMimeTypes(PluginManager::SortByComment); 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. for (const QString &type : qAsConst(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, QOverload::of(&QComboBox::currentIndexChanged), this, &CreateDialog::slotUpdateWidgets); connect(m_ui->mimeComboBox, QOverload::of(&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::setFileName(const QString &fileName) { m_ui->filenameLineEdit->setText(fileName); const QString detectedSuffix = QMimeDatabase().suffixForFileName(fileName); if (currentMimeType().suffixes().contains(detectedSuffix)) { m_ui->filenameLineEdit->setSelection(0, fileName.length() - detectedSuffix.length() - 1); } else { m_ui->filenameLineEdit->selectAll(); } } 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 (dir.trimmed().endsWith(QLatin1Char('/'))) { dir = dir.trimmed(); } 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(); } QString CreateDialog::encryptionMethod() const { return m_ui->optionsWidget->encryptionMethod(); } 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(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) { 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; } } #include "createdialog.moc" diff --git a/kerfuffle/pluginmanager.cpp b/kerfuffle/pluginmanager.cpp index cf0f5293..60bff387 100644 --- a/kerfuffle/pluginmanager.cpp +++ b/kerfuffle/pluginmanager.cpp @@ -1,303 +1,302 @@ /* * ark -- archiver for the KDE project * * 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 "pluginmanager.h" #include "ark_debug.h" #include "settings.h" -#include #include #include #include #include #include #include #include #include namespace Kerfuffle { PluginManager::PluginManager(QObject *parent) : QObject(parent) { loadPlugins(); } QVector PluginManager::installedPlugins() const { return m_plugins; } QVector PluginManager::availablePlugins() const { QVector availablePlugins; for (Plugin *plugin : qAsConst(m_plugins)) { if (plugin->isValid()) { availablePlugins << plugin; } } return availablePlugins; } QVector PluginManager::availableWritePlugins() const { QVector availableWritePlugins; const auto plugins = availablePlugins(); for (Plugin *plugin : plugins) { if (plugin->isReadWrite()) { availableWritePlugins << plugin; } } return availableWritePlugins; } QVector PluginManager::enabledPlugins() const { QVector enabledPlugins; for (Plugin *plugin : qAsConst(m_plugins)) { if (plugin->isEnabled()) { enabledPlugins << plugin; } } return enabledPlugins; } QVector PluginManager::preferredPluginsFor(const QMimeType &mimeType) { const auto mimeName = mimeType.name(); if (m_preferredPluginsCache.contains(mimeName)) { return m_preferredPluginsCache.value(mimeName); } const auto plugins = preferredPluginsFor(mimeType, false); m_preferredPluginsCache.insert(mimeName, plugins); return plugins; } QVector PluginManager::preferredWritePluginsFor(const QMimeType &mimeType) const { return preferredPluginsFor(mimeType, true); } Plugin *PluginManager::preferredPluginFor(const QMimeType &mimeType) { const QVector preferredPlugins = preferredPluginsFor(mimeType); return preferredPlugins.isEmpty() ? new Plugin() : preferredPlugins.first(); } Plugin *PluginManager::preferredWritePluginFor(const QMimeType &mimeType) const { const QVector preferredWritePlugins = preferredWritePluginsFor(mimeType); return preferredWritePlugins.isEmpty() ? new Plugin() : preferredWritePlugins.first(); } QStringList PluginManager::supportedMimeTypes(MimeSortingMode mode) const { QSet supported; QMimeDatabase db; const auto plugins = availablePlugins(); for (Plugin *plugin : plugins) { const auto mimeTypes = plugin->metaData().mimeTypes(); for (const auto& mimeType : mimeTypes) { if (db.mimeTypeForName(mimeType).isValid()) { supported.insert(mimeType); } } } // Remove entry for lrzipped tar if lrzip executable not found in path. if (QStandardPaths::findExecutable(QStringLiteral("lrzip")).isEmpty()) { supported.remove(QStringLiteral("application/x-lrzip-compressed-tar")); } // Remove entry for lz4-compressed tar if lz4 executable not found in path. if (QStandardPaths::findExecutable(QStringLiteral("lz4")).isEmpty()) { supported.remove(QStringLiteral("application/x-lz4-compressed-tar")); } // Remove entry for lzo-compressed tar if libarchive not linked against lzo and lzop executable not found in path. if (!libarchiveHasLzo() && QStandardPaths::findExecutable(QStringLiteral("lzop")).isEmpty()) { supported.remove(QStringLiteral("application/x-tzo")); } if (mode == SortByComment) { return sortByComment(supported); } return supported.toList(); } QStringList PluginManager::supportedWriteMimeTypes(MimeSortingMode mode) const { QSet supported; QMimeDatabase db; const auto plugins = availableWritePlugins(); for (Plugin *plugin : plugins) { const auto mimeTypes = plugin->metaData().mimeTypes(); for (const auto& mimeType : mimeTypes) { if (db.mimeTypeForName(mimeType).isValid()) { supported.insert(mimeType); } } } // Remove entry for lrzipped tar if lrzip executable not found in path. if (QStandardPaths::findExecutable(QStringLiteral("lrzip")).isEmpty()) { supported.remove(QStringLiteral("application/x-lrzip-compressed-tar")); } // Remove entry for lz4-compressed tar if lz4 executable not found in path. if (QStandardPaths::findExecutable(QStringLiteral("lz4")).isEmpty()) { supported.remove(QStringLiteral("application/x-lz4-compressed-tar")); } // Remove entry for lzo-compressed tar if libarchive not linked against lzo and lzop executable not found in path. if (!libarchiveHasLzo() && QStandardPaths::findExecutable(QStringLiteral("lzop")).isEmpty()) { supported.remove(QStringLiteral("application/x-tzo")); } if (mode == SortByComment) { return sortByComment(supported); } return supported.toList(); } QVector PluginManager::filterBy(const QVector &plugins, const QMimeType &mimeType) const { const bool supportedMime = supportedMimeTypes().contains(mimeType.name()); QVector filteredPlugins; for (Plugin *plugin : plugins) { if (!supportedMime) { // Check whether the mimetype inherits from a supported mimetype. const QStringList mimeTypes = plugin->metaData().mimeTypes(); for (const QString &mime : mimeTypes) { if (mimeType.inherits(mime)) { filteredPlugins << plugin; } } } else if (plugin->metaData().mimeTypes().contains(mimeType.name())) { filteredPlugins << plugin; } } return filteredPlugins; } void PluginManager::loadPlugins() { const QVector plugins = KPluginLoader::findPlugins(QStringLiteral("kerfuffle")); QSet addedPlugins; for (const KPluginMetaData &metaData : plugins) { const auto pluginId = metaData.pluginId(); // Filter out duplicate plugins. if (addedPlugins.contains(pluginId)) { continue; } Plugin *plugin = new Plugin(this, metaData); plugin->setEnabled(!ArkSettings::disabledPlugins().contains(pluginId)); addedPlugins << pluginId; m_plugins << plugin; } } QVector PluginManager::preferredPluginsFor(const QMimeType &mimeType, bool readWrite) const { QVector preferredPlugins = filterBy((readWrite ? availableWritePlugins() : availablePlugins()), mimeType); std::sort(preferredPlugins.begin(), preferredPlugins.end(), [](Plugin* p1, Plugin* p2) { return p1->priority() > p2->priority(); }); return preferredPlugins; } QStringList PluginManager::sortByComment(const QSet &mimeTypes) { QMap map; // Initialize the QMap to sort by comment. for (const QString &mimeType : mimeTypes) { QMimeType mime(QMimeDatabase().mimeTypeForName(mimeType)); map[mime.comment().toLower()] = mime.name(); } // Convert to sorted QStringList. QStringList sortedMimeTypes; for (const QString &value : qAsConst(map)) { sortedMimeTypes << value; } return sortedMimeTypes; } bool PluginManager::libarchiveHasLzo() { // Step 1: look for the libarchive plugin, which is built against libarchive. const QString pluginPath = []() { const QStringList paths = QCoreApplication::libraryPaths(); for (const QString &path : paths) { const QString pluginPath = QStringLiteral("%1/kerfuffle/kerfuffle_libarchive.so").arg(path); if (QFileInfo::exists(pluginPath)) { return pluginPath; } } return QString(); }(); // Step 2: process the libarchive plugin dependencies to figure out the absolute libarchive path. QProcess dependencyTool; QStringList args; #ifdef DEPENDENCY_TOOL_ARGS args << QStringLiteral(DEPENDENCY_TOOL_ARGS); #endif dependencyTool.setProgram(QStringLiteral(DEPENDENCY_TOOL)); dependencyTool.setArguments(args + QStringList(pluginPath)); dependencyTool.start(); dependencyTool.waitForFinished(); const QString output = QString::fromUtf8(dependencyTool.readAllStandardOutput()); QRegularExpression regex(QStringLiteral("/.*/libarchive.so|/.*/libarchive.*.dylib")); if (!regex.match(output).hasMatch()) { return false; } // Step 3: check whether libarchive links against liblzo. const QStringList libarchivePath(regex.match(output).captured(0)); dependencyTool.setArguments(args + libarchivePath); dependencyTool.start(); dependencyTool.waitForFinished(); return dependencyTool.readAllStandardOutput().contains(QByteArrayLiteral("lzo")); } } diff --git a/part/archivemodel.cpp b/part/archivemodel.cpp index 37963372..83641168 100644 --- a/part/archivemodel.cpp +++ b/part/archivemodel.cpp @@ -1,914 +1,915 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2010-2012 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "archivemodel.h" #include "ark_debug.h" #include "jobs.h" #include #include +#include #include #include #include #include #include using namespace Kerfuffle; // Used to speed up the loading of large archives. static Archive::Entry *s_previousMatch = nullptr; Q_GLOBAL_STATIC(QStringList, s_previousPieces) ArchiveModel::ArchiveModel(const QString &dbusPathName, QObject *parent) : QAbstractItemModel(parent) , m_dbusPathName(dbusPathName) , m_numberOfFiles(0) , m_numberOfFolders(0) , m_fileEntryListed(false) { initRootEntry(); // Mappings between column indexes and entry properties. m_propertiesMap = { { FullPath, "fullPath" }, { Size, "size" }, { CompressedSize, "compressedSize" }, { Permissions, "permissions" }, { Owner, "owner" }, { Group, "group" }, { Ratio, "ratio" }, { CRC, "CRC" }, { Method, "method" }, { Version, "version" }, { Timestamp, "timestamp" }, }; } ArchiveModel::~ArchiveModel() { } QVariant ArchiveModel::data(const QModelIndex &index, int role) const { if (index.isValid()) { Archive::Entry *entry = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: { // TODO: complete the columns. int column = m_showColumns.at(index.column()); switch (column) { case FullPath: return entry->name(); case Size: if (entry->isDir()) { uint dirs; uint files; entry->countChildren(dirs, files); return KIO::itemsSummaryString(dirs + files, files, dirs, 0, false); } else if (!entry->property("link").toString().isEmpty()) { return QVariant(); } else { return KIO::convertSize(entry->property("size").toULongLong()); } case CompressedSize: if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { qulonglong compressedSize = entry->property("compressedSize").toULongLong(); if (compressedSize != 0) { return KIO::convertSize(compressedSize); } else { return QVariant(); } } case Ratio: // TODO: Use entry->metaData()[Ratio] when available. if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { qulonglong compressedSize = entry->property("compressedSize").toULongLong(); qulonglong size = entry->property("size").toULongLong(); if (compressedSize == 0 || size == 0) { return QVariant(); } else { int ratio = int(100 * ((double)size - compressedSize) / size); return QString(QString::number(ratio) + QStringLiteral(" %")); } } case Timestamp: { const QDateTime timeStamp = entry->property("timestamp").toDateTime(); return QLocale().toString(timeStamp, QLocale::ShortFormat); } default: return entry->property(m_propertiesMap[column].constData()); } } case Qt::DecorationRole: if (index.column() == 0) { const Archive::Entry *e = static_cast(index.internalPointer()); QIcon::Mode mode = (filesToMove.contains(e->fullPath())) ? QIcon::Disabled : QIcon::Normal; return m_entryIcons.value(e->fullPath(NoTrailingSlash)).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small), mode); } return QVariant(); case Qt::FontRole: { QFont f; f.setItalic(entry->property("isPasswordProtected").toBool()); return f; } default: return QVariant(); } } return QVariant(); } Qt::ItemFlags ArchiveModel::flags(const QModelIndex &index) const { Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); if (index.isValid()) { return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | defaultFlags; } return nullptr; } QVariant ArchiveModel::headerData(int section, Qt::Orientation, int role) const { if (role == Qt::DisplayRole) { if (section >= m_showColumns.size()) { qCDebug(ARK) << "WEIRD: showColumns.size = " << m_showColumns.size() << " and section = " << section; return QVariant(); } int columnId = m_showColumns.at(section); switch (columnId) { case FullPath: return i18nc("Name of a file inside an archive", "Name"); case Size: return i18nc("Uncompressed size of a file inside an archive", "Size"); case CompressedSize: return i18nc("Compressed size of a file inside an archive", "Compressed"); case Ratio: return i18nc("Compression rate of file", "Rate"); case Owner: return i18nc("File's owner username", "Owner"); case Group: return i18nc("File's group", "Group"); case Permissions: return i18nc("File permissions", "Mode"); case CRC: return i18nc("CRC hash code", "CRC"); case Method: return i18nc("Compression method", "Method"); case Version: // TODO: what exactly is a file version? return i18nc("File version", "Version"); case Timestamp: return i18nc("Timestamp", "Date"); default: return i18nc("Unnamed column", "??"); } } return QVariant(); } QModelIndex ArchiveModel::index(int row, int column, const QModelIndex &parent) const { if (hasIndex(row, column, parent)) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : m_rootEntry.data(); Q_ASSERT(parentEntry->isDir()); const Archive::Entry *item = parentEntry->entries().value(row, nullptr); if (item != nullptr) { return createIndex(row, column, const_cast(item)); } } return QModelIndex(); } QModelIndex ArchiveModel::parent(const QModelIndex &index) const { if (index.isValid()) { Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); if (item->getParent() && (item->getParent() != m_rootEntry.data())) { return createIndex(item->getParent()->row(), 0, item->getParent()); } } return QModelIndex(); } Archive::Entry *ArchiveModel::entryForIndex(const QModelIndex &index) { if (index.isValid()) { Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); return item; } return nullptr; } int ArchiveModel::rowCount(const QModelIndex &parent) const { if (parent.column() <= 0) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : m_rootEntry.data(); if (parentEntry && parentEntry->isDir()) { return parentEntry->entries().count(); } } return 0; } int ArchiveModel::columnCount(const QModelIndex &parent) const { Q_UNUSED(parent) return m_showColumns.size(); } Qt::DropActions ArchiveModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } QStringList ArchiveModel::mimeTypes() const { QStringList types; // MIME types we accept for dragging (eg. Dolphin -> Ark). types << QStringLiteral("text/uri-list") << QStringLiteral("text/plain") << QStringLiteral("text/x-moz-url"); // MIME types we accept for dropping (eg. Ark -> Dolphin). types << QStringLiteral("application/x-kde-ark-dndextract-service") << QStringLiteral("application/x-kde-ark-dndextract-path"); return types; } QMimeData *ArchiveModel::mimeData(const QModelIndexList &indexes) const { Q_UNUSED(indexes) QMimeData *mimeData = new QMimeData; mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-service"), QDBusConnection::sessionBus().baseService().toUtf8()); mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-path"), m_dbusPathName.toUtf8()); return mimeData; } bool ArchiveModel::dropMimeData(const QMimeData *data, Qt::DropAction action, int row, int column, const QModelIndex &parent) { Q_UNUSED(action) if (!data->hasUrls()) { return false; } if (archive()->isReadOnly() || (archive()->encryptionType() != Archive::Unencrypted && archive()->password().isEmpty())) { emit messageWidget(KMessageWidget::Error, i18n("Adding files is not supported for this archive.")); return false; } QStringList paths; const auto urls = data->urls(); for (const QUrl &url : urls) { paths << url.toLocalFile(); } const Archive::Entry *entry = nullptr; QModelIndex droppedOnto = index(row, column, parent); if (droppedOnto.isValid()) { entry = entryForIndex(droppedOnto); if (!entry->isDir()) { entry = entry->getParent(); } } emit droppedFiles(paths, entry); return true; } // For a rationale, see bugs #194241, #241967 and #355839 QString ArchiveModel::cleanFileName(const QString& fileName) { // Skip entries with filename "/" or "//" or "." // "." is present in ISO files. QRegularExpression pattern(QStringLiteral("/+|\\.")); QRegularExpressionMatch match; if (fileName.contains(pattern, &match) && match.captured() == fileName) { qCDebug(ARK) << "Skipping entry with filename" << fileName; return QString(); } else if (fileName.startsWith(QLatin1String("./"))) { return fileName.mid(2); } return fileName; } void ArchiveModel::initRootEntry() { m_rootEntry.reset(new Archive::Entry()); m_rootEntry->setProperty("isDirectory", true); } Archive::Entry *ArchiveModel::parentFor(const Archive::Entry *entry, InsertBehaviour behaviour) { QStringList pieces = entry->fullPath().split(QLatin1Char('/'), QString::SkipEmptyParts); if (pieces.isEmpty()) { return nullptr; } pieces.removeLast(); // Used to speed up loading of large archives. if (s_previousMatch) { // The number of path elements must be the same for the shortcut // to work. if (s_previousPieces->count() == pieces.count()) { bool equal = true; // Check if all pieces match. for (int i = 0; i < s_previousPieces->count(); ++i) { if (s_previousPieces->at(i) != pieces.at(i)) { equal = false; break; } } // If match return it. if (equal) { return s_previousMatch; } } } Archive::Entry *parent = m_rootEntry.data(); for (const QString &piece : qAsConst(pieces)) { Archive::Entry *entry = parent->find(piece); if (!entry) { // Directory entry will be traversed later (that happens for some archive formats, 7z for instance). // We have to create one before, in order to construct tree from its children, // and then delete the existing one (see ArchiveModel::newEntry). entry = new Archive::Entry(parent); entry->setProperty("fullPath", (parent == m_rootEntry.data()) ? QString(piece + QLatin1Char('/')) : QString(parent->fullPath(WithTrailingSlash) + piece + QLatin1Char('/'))); entry->setProperty("isDirectory", true); insertEntry(entry, behaviour); } if (!entry->isDir()) { Archive::Entry *e = new Archive::Entry(parent); e->copyMetaData(entry); // Maybe we have both a file and a directory of the same name. // We avoid removing previous entries unless necessary. insertEntry(e, behaviour); } parent = entry; } s_previousMatch = parent; *s_previousPieces = pieces; return parent; } QModelIndex ArchiveModel::indexForEntry(Archive::Entry *entry) { Q_ASSERT(entry); if (entry != m_rootEntry.data()) { Q_ASSERT(entry->getParent()); Q_ASSERT(entry->getParent()->isDir()); return createIndex(entry->row(), 0, entry); } return QModelIndex(); } void ArchiveModel::slotEntryRemoved(const QString & path) { const QString entryFileName(cleanFileName(path)); if (entryFileName.isEmpty()) { return; } Archive::Entry *entry = m_rootEntry->findByPath(entryFileName.split(QLatin1Char('/'), QString::SkipEmptyParts)); if (entry) { Archive::Entry *parent = entry->getParent(); QModelIndex index = indexForEntry(entry); Q_UNUSED(index); beginRemoveRows(indexForEntry(parent), entry->row(), entry->row()); m_entryIcons.remove(parent->entries().at(entry->row())->fullPath(NoTrailingSlash)); parent->removeEntryAt(entry->row()); endRemoveRows(); } } void ArchiveModel::slotUserQuery(Kerfuffle::Query *query) { query->execute(); } void ArchiveModel::slotNewEntry(Archive::Entry *entry) { newEntry(entry, NotifyViews); } void ArchiveModel::slotListEntry(Archive::Entry *entry) { newEntry(entry, DoNotNotifyViews); } void ArchiveModel::newEntry(Archive::Entry *receivedEntry, InsertBehaviour behaviour) { if (receivedEntry->fullPath().isEmpty()) { qCDebug(ARK) << "Weird, received empty entry (no filename) - skipping"; return; } // If there are no columns registered, then populate columns from entry. If the first entry // is a directory we check again for the first file entry to ensure all relevent columms are shown. if (m_showColumns.isEmpty() || !m_fileEntryListed) { QList toInsert; const auto size = receivedEntry->property("size").toULongLong(); const auto compressedSize = receivedEntry->property("compressedSize").toULongLong(); for (auto i = m_propertiesMap.begin(); i != m_propertiesMap.end(); ++i) { // Singlefile plugin doesn't report the uncompressed size. if (i.key() == Size && size == 0 && compressedSize > 0) { continue; } if (!receivedEntry->property(i.value().constData()).toString().isEmpty()) { if (i.key() != CompressedSize || receivedEntry->compressedSizeIsSet) { if (!m_showColumns.contains(i.key())) { toInsert << i.key(); } } } } if (behaviour == NotifyViews) { beginInsertColumns(QModelIndex(), 0, toInsert.size() - 1); } m_showColumns << toInsert; if (behaviour == NotifyViews) { endInsertColumns(); } m_fileEntryListed = !receivedEntry->isDir(); } // #194241: Filenames such as "./file" should be displayed as "file" // #241967: Entries called "/" should be ignored // #355839: Entries called "//" should be ignored QString entryFileName = cleanFileName(receivedEntry->fullPath()); if (entryFileName.isEmpty()) { // The entry contains only "." or "./" return; } receivedEntry->setProperty("fullPath", entryFileName); // For some archive formats (e.g. AppImage and RPM) paths of folders do not // contain a trailing slash, so we append it. if (receivedEntry->property("isDirectory").toBool() && !receivedEntry->property("fullPath").toString().endsWith(QLatin1Char('/'))) { receivedEntry->setProperty("fullPath", QString(receivedEntry->property("fullPath").toString() + QLatin1Char('/'))); qCDebug(ARK) << "Trailing slash appended to entry:" << receivedEntry->property("fullPath"); } // Skip already created entries. Archive::Entry *existing = m_rootEntry->findByPath(entryFileName.split(QLatin1Char('/'))); if (existing) { existing->setProperty("fullPath", entryFileName); // Multi-volume files are repeated at least in RAR archives. // In that case, we need to sum the compressed size for each volume qulonglong currentCompressedSize = existing->property("compressedSize").toULongLong(); existing->setProperty("compressedSize", currentCompressedSize + receivedEntry->property("compressedSize").toULongLong()); return; } // Find parent entry, creating missing directory Archive::Entry's in the process. Archive::Entry *parent = parentFor(receivedEntry, behaviour); // Create an Archive::Entry. const QStringList path = entryFileName.split(QLatin1Char('/'), QString::SkipEmptyParts); Archive::Entry *entry = parent->find(path.last()); if (entry) { entry->copyMetaData(receivedEntry); entry->setProperty("fullPath", entryFileName); } else { receivedEntry->setParent(parent); insertEntry(receivedEntry, behaviour); } } void ArchiveModel::slotLoadingFinished(KJob *job) { std::sort(m_showColumns.begin(), m_showColumns.end()); if (!job->error()) { qCDebug(ARK) << "Showing columns: " << m_showColumns; m_archive.reset(qobject_cast(job)->archive()); beginResetModel(); endResetModel(); } emit loadingFinished(job); } void ArchiveModel::insertEntry(Archive::Entry *entry, InsertBehaviour behaviour) { Q_ASSERT(entry); Archive::Entry *parent = entry->getParent(); Q_ASSERT(parent); if (behaviour == NotifyViews) { beginInsertRows(indexForEntry(parent), parent->entries().count(), parent->entries().count()); } parent->appendEntry(entry); if (behaviour == NotifyViews) { endInsertRows(); } // Save an icon for each newly added entry. QMimeDatabase db; QIcon icon; entry->isDir() ? icon = QIcon::fromTheme(db.mimeTypeForName(QStringLiteral("inode/directory")).iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small)) : icon = QIcon::fromTheme(db.mimeTypeForFile(entry->fullPath()).iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small)); m_entryIcons.insert(entry->fullPath(NoTrailingSlash), icon); } Kerfuffle::Archive* ArchiveModel::archive() const { return m_archive.data(); } void ArchiveModel::reset() { m_archive.reset(nullptr); s_previousMatch = nullptr; s_previousPieces->clear(); initRootEntry(); // TODO: make sure if it's ok to not have calls to beginRemoveColumns here m_showColumns.clear(); beginResetModel(); endResetModel(); } void ArchiveModel::createEmptyArchive(const QString &path, const QString &mimeType, QObject *parent) { reset(); m_archive.reset(Archive::createEmpty(path, mimeType, parent)); } KJob *ArchiveModel::loadArchive(const QString &path, const QString &mimeType, QObject *parent) { reset(); auto loadJob = Archive::load(path, mimeType, parent); connect(loadJob, &KJob::result, this, &ArchiveModel::slotLoadingFinished); connect(loadJob, &Job::newEntry, this, &ArchiveModel::slotListEntry); connect(loadJob, &Job::userQuery, this, &ArchiveModel::slotUserQuery); emit loadingStarted(); return loadJob; } ExtractJob* ArchiveModel::extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { QVector files({file}); return extractFiles(files, destinationDir, options); } ExtractJob* ArchiveModel::extractFiles(const QVector& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { Q_ASSERT(m_archive); ExtractJob *newJob = m_archive->extractFiles(files, destinationDir, options); connect(newJob, &ExtractJob::userQuery, this, &ArchiveModel::slotUserQuery); return newJob; } Kerfuffle::PreviewJob *ArchiveModel::preview(Archive::Entry *file) const { Q_ASSERT(m_archive); PreviewJob *job = m_archive->preview(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } OpenJob *ArchiveModel::open(Archive::Entry *file) const { Q_ASSERT(m_archive); OpenJob *job = m_archive->open(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } OpenWithJob *ArchiveModel::openWith(Archive::Entry *file) const { Q_ASSERT(m_archive); OpenWithJob *job = m_archive->openWith(file); connect(job, &Job::userQuery, this, &ArchiveModel::slotUserQuery); return job; } AddJob* ArchiveModel::addFiles(QVector &entries, const Archive::Entry *destination, const CompressionOptions& options) { if (!m_archive) { return nullptr; } if (!m_archive->isReadOnly()) { AddJob *job = m_archive->addFiles(entries, destination, options); connect(job, &AddJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &AddJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return nullptr; } Kerfuffle::MoveJob *ArchiveModel::moveFiles(QVector &entries, Archive::Entry *destination, const CompressionOptions &options) { if (!m_archive) { return nullptr; } if (!m_archive->isReadOnly()) { MoveJob *job = m_archive->moveFiles(entries, destination, options); connect(job, &MoveJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &MoveJob::userQuery, this, &ArchiveModel::slotUserQuery); connect(job, &MoveJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved); connect(job, &MoveJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs); return job; } return nullptr; } Kerfuffle::CopyJob *ArchiveModel::copyFiles(QVector &entries, Archive::Entry *destination, const CompressionOptions &options) { if (!m_archive) { return nullptr; } if (!m_archive->isReadOnly()) { CopyJob *job = m_archive->copyFiles(entries, destination, options); connect(job, &CopyJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &CopyJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return nullptr; } DeleteJob* ArchiveModel::deleteFiles(QVector entries) { Q_ASSERT(m_archive); if (!m_archive->isReadOnly()) { DeleteJob *job = m_archive->deleteFiles(entries); connect(job, &DeleteJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved); connect(job, &DeleteJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs); connect(job, &DeleteJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return nullptr; } void ArchiveModel::encryptArchive(const QString &password, bool encryptHeader) { if (!m_archive) { return; } m_archive->encrypt(password, encryptHeader); } bool ArchiveModel::conflictingEntries(QList &conflictingEntries, const QStringList &entries, bool allowMerging) const { bool error = false; // We can't accept destination as an argument, because it can be a new entry path for renaming. const Archive::Entry *destination; { QStringList destinationParts = entries.first().split(QLatin1Char('/'), QString::SkipEmptyParts); destinationParts.removeLast(); if (destinationParts.count() > 0) { destination = m_rootEntry->findByPath(destinationParts); } else { destination = m_rootEntry.data(); } } const Archive::Entry *lastDirEntry = destination; QString skippedDirPath; for (const QString &entry : entries) { if (skippedDirPath.count() > 0 && entry.startsWith(skippedDirPath)) { continue; } else { skippedDirPath.clear(); } while (!entry.startsWith(lastDirEntry->fullPath())) { lastDirEntry = lastDirEntry->getParent(); } bool isDir = entry.right(1) == QLatin1String("/"); const Archive::Entry *archiveEntry = lastDirEntry->find(entry.split(QLatin1Char('/'), QString::SkipEmptyParts).last()); if (archiveEntry != nullptr) { if (archiveEntry->isDir() != isDir || !allowMerging) { if (isDir) { skippedDirPath = lastDirEntry->fullPath(); } if (!error) { conflictingEntries.clear(); error = true; } conflictingEntries << archiveEntry; } else { if (isDir) { lastDirEntry = archiveEntry; } else if (!error) { conflictingEntries << archiveEntry; } } } else if (isDir) { skippedDirPath = entry; } } return error; } bool ArchiveModel::hasDuplicatedEntries(const QStringList &entries) { QStringList tempList; for (const QString &entry : entries) { if (tempList.contains(entry)) { return true; } tempList << entry; } return false; } QMap ArchiveModel::entryMap(const QVector &entries) { QMap map; for (Archive::Entry *entry : entries) { map.insert(entry->fullPath(), entry); } return map; } const QHash ArchiveModel::entryIcons() const { return m_entryIcons; } void ArchiveModel::slotCleanupEmptyDirs() { QList queue; QList nodesToDelete; // Add root nodes. for (int i = 0; i < rowCount(); ++i) { queue.append(QPersistentModelIndex(index(i, 0))); } // Breadth-first traverse. while (!queue.isEmpty()) { QPersistentModelIndex node = queue.takeFirst(); Archive::Entry *entry = entryForIndex(node); if (!hasChildren(node)) { if (entry->fullPath().isEmpty()) { nodesToDelete << node; } } else { for (int i = 0; i < rowCount(node); ++i) { queue.append(QPersistentModelIndex(index(i, 0, node))); } } } for (const QPersistentModelIndex& node : qAsConst(nodesToDelete)) { Archive::Entry *rawEntry = static_cast(node.internalPointer()); qCDebug(ARK) << "Delete with parent entries " << rawEntry->getParent()->entries() << " and row " << rawEntry->row(); beginRemoveRows(parent(node), rawEntry->row(), rawEntry->row()); m_entryIcons.remove(rawEntry->getParent()->entries().at(rawEntry->row())->fullPath(NoTrailingSlash)); rawEntry->getParent()->removeEntryAt(rawEntry->row()); endRemoveRows(); } } void ArchiveModel::countEntriesAndSize() { // This function is used to count the number of folders/files and // the total compressed size. This is needed for PropertiesDialog // to update the corresponding values after adding/deleting files. // When ArchiveModel has been properly fixed, this code can likely // be removed. m_numberOfFiles = 0; m_numberOfFolders = 0; m_uncompressedSize = 0; QElapsedTimer timer; timer.start(); traverseAndCountDirNode(m_rootEntry.data()); qCDebug(ARK) << "Time to count entries and size:" << timer.elapsed() << "ms"; } void ArchiveModel::traverseAndCountDirNode(Archive::Entry *dir) { const auto entries = dir->entries(); for (Archive::Entry *entry : entries) { if (entry->isDir()) { traverseAndCountDirNode(entry); m_numberOfFolders++; } else { m_numberOfFiles++; m_uncompressedSize += entry->property("size").toULongLong(); } } } qulonglong ArchiveModel::numberOfFiles() const { return m_numberOfFiles; } qulonglong ArchiveModel::numberOfFolders() const { return m_numberOfFolders; } qulonglong ArchiveModel::uncompressedSize() const { return m_uncompressedSize; } QList ArchiveModel::shownColumns() const { return m_showColumns; } QMap ArchiveModel::propertiesMap() const { return m_propertiesMap; } diff --git a/part/archiveview.cpp b/part/archiveview.cpp index 8c6f7d63..1a0387bc 100644 --- a/part/archiveview.cpp +++ b/part/archiveview.cpp @@ -1,180 +1,179 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008-2009 Harald Hvaal * Copyright (c) 2016 Vladyslav Batyrenko * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "archiveview.h" #include "ark_debug.h" #include #include -#include #include #include #include #include ArchiveView::ArchiveView(QWidget *parent) : QTreeView(parent) { setSelectionMode(QAbstractItemView::ExtendedSelection); setSizePolicy(QSizePolicy::Expanding, QSizePolicy::Expanding); setAlternatingRowColors(true); setAnimated(true); setAllColumnsShowFocus(true); setSortingEnabled(true); setDragEnabled(true); setDropIndicatorShown(true); // #368807: drops must be initially disabled, otherwise they will override the MainWindow's ones. // They will be enabled in Part::slotLoadingFinished(). setDropsEnabled(false); header()->setSectionResizeMode(QHeaderView::ResizeToContents); } void ArchiveView::startDrag(Qt::DropActions supportedActions) { //only start the drag if it's over the filename column. this allows dragging selection in //tree/detail view if (currentIndex().column() != 0) { return; } QTreeView::startDrag(supportedActions); } void ArchiveView::expandIfSingleFolder() { if (model()->rowCount() == 1) { expandToDepth(0); } } void ArchiveView::setDropsEnabled(bool enabled) { setAcceptDrops(enabled); setDragDropMode(enabled ? QAbstractItemView::DragDrop : QAbstractItemView::DragOnly); } void ArchiveView::dragEnterEvent(QDragEnterEvent * event) { //TODO: if no model, trigger some mechanism to create one automatically! qCDebug(ARK) << event; if (event->source() == this) { //we don't support internal drops yet. return; } QTreeView::dragEnterEvent(event); } void ArchiveView::dropEvent(QDropEvent * event) { qCDebug(ARK) << event; if (event->source() == this) { //we don't support internal drops yet. return; } QTreeView::dropEvent(event); } void ArchiveView::dragMoveEvent(QDragMoveEvent * event) { qCDebug(ARK) << event; if (event->source() == this) { //we don't support internal drops yet. return; } QTreeView::dragMoveEvent(event); if (event->mimeData()->hasFormat(QStringLiteral("text/uri-list"))) { event->acceptProposedAction(); } } bool ArchiveView::eventFilter(QObject *object, QEvent *event) { if (object == m_entryEditor && event->type() == QEvent::KeyPress) { QKeyEvent *keyEvent = static_cast(event); if (keyEvent->key() == Qt::Key_Escape) { closeEntryEditor(); return true; } } return false; } void ArchiveView::mouseReleaseEvent(QMouseEvent *event) { if (m_editorIndex.isValid()) { closeEntryEditor(); } else { QTreeView::mouseReleaseEvent(event); } } void ArchiveView::keyPressEvent(QKeyEvent *event) { if (m_editorIndex.isValid()) { switch (event->key()) { case Qt::Key_Return: case Qt::Key_Enter: { QLineEdit* editor = static_cast(indexWidget(m_editorIndex)); emit entryChanged(editor->text()); closeEntryEditor(); break; } default: QTreeView::keyPressEvent(event); } } else { QTreeView::keyPressEvent(event); } } void ArchiveView::renameSelectedEntry() { QModelIndex currentIndex = selectionModel()->currentIndex(); currentIndex = (currentIndex.parent().isValid()) ? currentIndex.parent().child(currentIndex.row(), 0) : model()->index(currentIndex.row(), 0); openEntryEditor(currentIndex); } void ArchiveView::openEntryEditor(const QModelIndex &index) { m_editorIndex = index; openPersistentEditor(index); m_entryEditor = static_cast(indexWidget(m_editorIndex)); m_entryEditor->installEventFilter(this); m_entryEditor->setText(index.data().toString()); m_entryEditor->setFocus(Qt::OtherFocusReason); m_entryEditor->selectAll(); } void ArchiveView::closeEntryEditor() { m_entryEditor->removeEventFilter(this); closePersistentEditor(m_editorIndex); m_editorIndex = QModelIndex(); } diff --git a/part/infopanel.cpp b/part/infopanel.cpp index 3ad9c3dd..cd262153 100644 --- a/part/infopanel.cpp +++ b/part/infopanel.cpp @@ -1,218 +1,219 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "infopanel.h" #include "archiveentry.h" #include #include +#include #include #include #include using namespace Kerfuffle; static QPixmap getDesktopIconForName(const QString& name) { return QIcon::fromTheme(name).pixmap(IconSize(KIconLoader::Desktop), IconSize(KIconLoader::Desktop)); } InfoPanel::InfoPanel(ArchiveModel *model, QWidget *parent) : QFrame(parent), m_model(model) { setupUi(this); // Make the file name font bigger than the rest QFont fnt = fileName->font(); if (fnt.pointSize() > -1) { fnt.setPointSize(fnt.pointSize() + 1); } else { fnt.setPixelSize(fnt.pixelSize() + 3); } fileName->setFont(fnt); updateWithDefaults(); } InfoPanel::~InfoPanel() { } void InfoPanel::updateWithDefaults() { iconLabel->setPixmap(getDesktopIconForName(QStringLiteral("utilities-file-archiver"))); const QString currentFileName = prettyFileName(); if (currentFileName.isEmpty()) { fileName->setText(i18n("No archive loaded")); } else { fileName->setText(currentFileName); } additionalInfo->setText(QString()); hideMetaData(); } QString InfoPanel::prettyFileName() const { if (m_prettyFileName.isEmpty()) { if (m_model->archive()) { QFileInfo fileInfo(m_model->archive()->fileName()); return fileInfo.fileName(); } } return m_prettyFileName; } void InfoPanel::setPrettyFileName(const QString& fileName) { m_prettyFileName = fileName; } void InfoPanel::setIndex(const QModelIndex& index) { if (!index.isValid()) { updateWithDefaults(); } else { const Archive::Entry *entry = m_model->entryForIndex(index); if (!entry) { return; } QMimeDatabase db; QMimeType mimeType; if (entry->isDir()) { mimeType = db.mimeTypeForName(QStringLiteral("inode/directory")); } else { mimeType = db.mimeTypeForFile(entry->fullPath(), QMimeDatabase::MatchExtension); } iconLabel->setPixmap(getDesktopIconForName(mimeType.iconName())); if (entry->isDir()) { uint dirs; uint files; entry->countChildren(dirs, files); additionalInfo->setText(KIO::itemsSummaryString(dirs + files, files, dirs, 0, false)); } else if (!entry->property("link").toString().isEmpty()) { additionalInfo->setText(i18n("Symbolic Link")); } else { if (entry->property("size") != 0) { additionalInfo->setText(KIO::convertSize(entry->property("size").toULongLong())); } else { additionalInfo->setText(i18n("Unknown size")); } } const QStringList nameParts = entry->fullPath().split(QLatin1Char( '/' ), QString::SkipEmptyParts); const QString name = (nameParts.count() > 0) ? nameParts.last() : entry->fullPath(); fileName->setText(name); showMetaDataFor(index); } } void InfoPanel::setIndexes(const QModelIndexList &list) { if (list.size() == 0) { setIndex(QModelIndex()); } else if (list.size() == 1) { setIndex(list[ 0 ]); } else { iconLabel->setPixmap(getDesktopIconForName(QStringLiteral("utilities-file-archiver"))); fileName->setText(i18np("One file selected", "%1 files selected", list.size())); quint64 totalSize = 0; for (const QModelIndex& index : list) { const Archive::Entry *entry = m_model->entryForIndex(index); totalSize += entry->property("size").toULongLong(); } additionalInfo->setText(KIO::convertSize(totalSize)); hideMetaData(); } } void InfoPanel::showMetaData() { m_separator->show(); m_metaDataWidget->show(); } void InfoPanel::hideMetaData() { m_separator->hide(); m_metaDataWidget->hide(); } void InfoPanel::showMetaDataFor(const QModelIndex &index) { showMetaData(); const Archive::Entry *entry = m_model->entryForIndex(index); QMimeDatabase db; QMimeType mimeType; if (entry->isDir()) { mimeType = db.mimeTypeForName(QStringLiteral("inode/directory")); } else { mimeType = db.mimeTypeForFile(entry->fullPath(), QMimeDatabase::MatchExtension); } m_typeValueLabel->setText(mimeType.comment()); if (!entry->property("owner").toString().isEmpty()) { m_ownerLabel->show(); m_ownerValueLabel->show(); m_ownerValueLabel->setText(entry->property("owner").toString()); } else { m_ownerLabel->hide(); m_ownerValueLabel->hide(); } if (!entry->property("group").toString().isEmpty()) { m_groupLabel->show(); m_groupValueLabel->show(); m_groupValueLabel->setText(entry->property("group").toString()); } else { m_groupLabel->hide(); m_groupValueLabel->hide(); } if (!entry->property("link").toString().isEmpty()) { m_targetLabel->show(); m_targetValueLabel->show(); m_targetValueLabel->setText(entry->property("link").toString()); } else { m_targetLabel->hide(); m_targetValueLabel->hide(); } if (entry->property("isPasswordProtected").toBool()) { m_passwordLabel->show(); m_passwordValueLabel->show(); } else { m_passwordLabel->hide(); m_passwordValueLabel->hide(); } } diff --git a/part/overwritedialog.cpp b/part/overwritedialog.cpp index 94e03fef..0987afdd 100644 --- a/part/overwritedialog.cpp +++ b/part/overwritedialog.cpp @@ -1,65 +1,67 @@ /* * 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 "overwritedialog.h" +#include + using namespace Kerfuffle; OverwriteDialog::OverwriteDialog(QWidget *parent, const QList &entries, const QHash &icons, bool error) : QDialog(parent) , m_buttonBox(QDialogButtonBox::Cancel, Qt::Horizontal) { m_vBoxLayout.addLayout(&m_messageLayout); m_vBoxLayout.addWidget(&m_entriesList); m_vBoxLayout.addWidget(&m_buttonBox); m_messageLayout.addWidget(&m_messageIcon); m_messageLayout.addWidget(&m_messageText); m_messageIcon.setPixmap(QIcon::fromTheme(QStringLiteral("dialog-warning")).pixmap(QSize(64, 64))); if (error) { m_messageText.setText(i18n("Files with the following paths already exist. Remove them if you really want to overwrite.")); } else { m_messageText.setText(i18n("Files with the following paths already exist. Do you want to continue overwriting them?")); m_buttonBox.addButton(QDialogButtonBox::Ok); } connect(&m_buttonBox, &QDialogButtonBox::accepted, this, &QDialog::accept); connect(&m_buttonBox, &QDialogButtonBox::rejected, this, &QDialog::reject); for (const Archive::Entry *entry : entries) { QListWidgetItem *item = new QListWidgetItem(icons.value(entry->fullPath(NoTrailingSlash)), entry->fullPath(NoTrailingSlash)); m_entriesList.addItem(item); } setLayout(&m_vBoxLayout); setFixedSize(window()->sizeHint()); } OverwriteDialog::~OverwriteDialog() { } diff --git a/part/overwritedialog.h b/part/overwritedialog.h index ba539532..a1e4c0e9 100644 --- a/part/overwritedialog.h +++ b/part/overwritedialog.h @@ -1,60 +1,58 @@ /* * 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 OVERWRITEDIALOG_H #define OVERWRITEDIALOG_H #include "archiveentry.h" -#include -#include #include #include #include #include #include #include class OverwriteDialog : public QDialog { Q_OBJECT public: explicit OverwriteDialog(QWidget *parent, const QList &entries, const QHash &icons, bool error = false); ~OverwriteDialog() override; private: QVBoxLayout m_vBoxLayout; QHBoxLayout m_messageLayout; QLabel m_messageIcon; QLabel m_messageText; QListWidget m_entriesList; QDialogButtonBox m_buttonBox; }; #endif diff --git a/plugins/cliplugin-example/cliplugin.cpp b/plugins/cliplugin-example/cliplugin.cpp index ad037bd4..e2d591bb 100644 --- a/plugins/cliplugin-example/cliplugin.cpp +++ b/plugins/cliplugin-example/cliplugin.cpp @@ -1,149 +1,148 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Claudio Bantaloukas * Copyright (C) 2007 Henrique Pinto * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/archiveentry.h" #include "kerfuffle/kerfuffle_export.h" -#include #include #include #include K_PLUGIN_CLASS_WITH_JSON(CliPlugin, "kerfuffle_cli.json") CliPlugin::CliPlugin(QObject *parent, const QVariantList &args) : CliInterface(parent, args), m_isFirstLine(true), m_incontent(false), m_isPasswordProtected(false) { qCDebug(ARK) << "Loaded cli-example plugin"; } CliPlugin::~CliPlugin() { } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { p[CaptureProgress] = true; p[ListProgram] = p[ExtractProgram] = p[DeleteProgram] = p[AddProgram] = QLatin1String("rar"); p[ListArgs] = QStringList() << QLatin1String("v") << QLatin1String("-c-") << QLatin1String("$Archive"); p[ExtractArgs] = QStringList() << QLatin1String("-p-") << QLatin1String("$PreservePathSwitch") << QLatin1String("$PasswordSwitch") << QLatin1String("$Archive") << QLatin1String("$Files"); p[PreservePathSwitch] = QStringList() << QLatin1String("x") << QLatin1String("e"); p[PasswordSwitch] = QStringList() << QLatin1String("-p$Password"); p[DeleteArgs] = QStringList() << QLatin1String("d") << QLatin1String("$Archive") << QLatin1String("$Files"); p[FileExistsExpression] = QLatin1String("^(.+) already exists. Overwrite it"); p[FileExistsInput] = QStringList() << QLatin1String("Y") //overwrite << QLatin1String("N") //skip << QLatin1String("A") //overwrite all << QLatin1String("E") //autoskip << QLatin1String("Q") //cancel ; p[AddArgs] = QStringList() << QLatin1String("a") << QLatin1String("$Archive") << QLatin1String("$Files"); p[WrongPasswordPatterns] = QStringList() << QLatin1String("password incorrect"); p[ExtractionFailedPatterns] = QStringList() << QLatin1String("CRC failed"); } return p; } bool CliPlugin::readListLine(const QString &line) { const QString m_headerString = QLatin1String("-----------------------------------------"); // skip the heading if (!m_incontent) { if (line.startsWith(m_headerString)) { m_incontent = true; } return true; } // catch final line if (line.startsWith(m_headerString)) { m_incontent = false; return true; } // rar gives one line for the filename and a line after it with some file properties if (m_isFirstLine) { m_entryFilename = line.trimmed(); //m_entryFilename.chop(1); // handle newline if (!m_entryFilename.isEmpty() && m_entryFilename.at(0) == QLatin1Char('*')) { m_isPasswordProtected = true; m_entryFilename.remove(0, 1); // and the spaces in front } else m_isPasswordProtected = false; m_isFirstLine = false; return true; } QStringList fileprops = line.split(QLatin1Char(' '), QString::SkipEmptyParts); m_entryFilename = QDir::fromNativeSeparators(m_entryFilename); bool isDirectory = (bool)(fileprops[ 5 ].contains(QLatin1Char('d'), Qt::CaseInsensitive)); QDateTime ts(QDate::fromString(fileprops[ 3 ], QLatin1String("dd-MM-yy")), QTime::fromString(fileprops[ 4 ], QLatin1String("hh:mm"))); // rar output date with 2 digit year but QDate takes is as 19?? // let's take 1950 is cut-off; similar to KDateTime if (ts.date().year() < 1950) { ts = ts.addYears(100); } if (isDirectory && !m_entryFilename.endsWith(QLatin1Char('/'))) { m_entryFilename += QLatin1Char('/'); } qCDebug(ARK) << m_entryFilename << " : " << fileprops; Archive::Entry *e = new Archive::Entry(); e->setProperty("fullPath", m_entryFilename); e->setProperty("size", fileprops[ 0 ]); e->setProperty("compressedSize", fileprops[ 1 ]); e->setProperty("ratio", fileprops[ 2 ]); e->setProperty("timestamp", ts); e->setProperty("isDirectory", isDirectory); e->setProperty("permissions", fileprops[ 5 ].remove(0, 1)); e->setProperty("CRC", fileprops[ 6 ]); e->setProperty("method", fileprops[ 7 ]); e->setProperty("version", fileprops[ 8 ]); e->setProperty("ssPasswordProtected", m_isPasswordProtected); qCDebug(ARK) << "Added entry: " << e; emit entry(e); m_isFirstLine = true; return true; } #include "cliplugin.moc" diff --git a/plugins/libarchive/libarchiveplugin.cpp b/plugins/libarchive/libarchiveplugin.cpp index 6a1df53b..7e444557 100644 --- a/plugins/libarchive/libarchiveplugin.cpp +++ b/plugins/libarchive/libarchiveplugin.cpp @@ -1,577 +1,578 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2010 Raphael Kubo da Costa * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "libarchiveplugin.h" #include "ark_debug.h" #include "queries.h" #include -#include #include +#include +#include #include LibarchivePlugin::LibarchivePlugin(QObject *parent, const QVariantList &args) : ReadWriteArchiveInterface(parent, args) , m_archiveReadDisk(archive_read_disk_new()) , m_cachedArchiveEntryCount(0) , m_emitNoEntries(false) , m_extractedFilesSize(0) { qCDebug(ARK) << "Initializing libarchive plugin"; archive_read_disk_set_standard_lookup(m_archiveReadDisk.data()); connect(this, &ReadOnlyArchiveInterface::error, this, &LibarchivePlugin::slotRestoreWorkingDir); connect(this, &ReadOnlyArchiveInterface::cancelled, this, &LibarchivePlugin::slotRestoreWorkingDir); } LibarchivePlugin::~LibarchivePlugin() { for (const auto e : qAsConst(m_emittedEntries)) { // Entries might be passed to pending slots, so we just schedule their deletion. e->deleteLater(); } } bool LibarchivePlugin::list() { qCDebug(ARK) << "Listing archive contents"; if (!initializeReader()) { return false; } qDebug(ARK) << "Detected compression filter:" << archive_filter_name(m_archiveReader.data(), 0); QString compMethod = convertCompressionName(QString::fromUtf8(archive_filter_name(m_archiveReader.data(), 0))); if (!compMethod.isEmpty()) { emit compressionMethodFound(compMethod); } m_cachedArchiveEntryCount = 0; m_extractedFilesSize = 0; m_numberOfEntries = 0; auto compressedArchiveSize = QFileInfo(filename()).size(); struct archive_entry *aentry; int result = ARCHIVE_RETRY; bool firstEntry = true; while (!QThread::currentThread()->isInterruptionRequested() && (result = archive_read_next_header(m_archiveReader.data(), &aentry)) == ARCHIVE_OK) { if (firstEntry) { qDebug(ARK) << "Detected format for first entry:" << archive_format_name(m_archiveReader.data()); firstEntry = false; } if (!m_emitNoEntries) { emitEntryFromArchiveEntry(aentry); } m_extractedFilesSize += (qlonglong)archive_entry_size(aentry); emit progress(float(archive_filter_bytes(m_archiveReader.data(), -1))/float(compressedArchiveSize)); m_cachedArchiveEntryCount++; archive_read_data_skip(m_archiveReader.data()); } if (result != ARCHIVE_EOF) { qCWarning(ARK) << "Could not read until the end of the archive:" << QLatin1String(archive_error_string(m_archiveReader.data())); return false; } return archive_read_close(m_archiveReader.data()) == ARCHIVE_OK; } bool LibarchivePlugin::addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions &options, uint numberOfEntriesToAdd) { Q_UNUSED(files) Q_UNUSED(destination) Q_UNUSED(options) Q_UNUSED(numberOfEntriesToAdd) return false; } bool LibarchivePlugin::moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(files) Q_UNUSED(destination) Q_UNUSED(options) return false; } bool LibarchivePlugin::copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) { Q_UNUSED(files) Q_UNUSED(destination) Q_UNUSED(options) return false; } bool LibarchivePlugin::deleteFiles(const QVector &files) { Q_UNUSED(files) return false; } bool LibarchivePlugin::addComment(const QString &comment) { Q_UNUSED(comment) return false; } bool LibarchivePlugin::testArchive() { return false; } bool LibarchivePlugin::hasBatchExtractionProgress() const { return true; } bool LibarchivePlugin::doKill() { return false; } bool LibarchivePlugin::extractFiles(const QVector &files, const QString &destinationDirectory, const ExtractionOptions &options) { if (!initializeReader()) { return false; } ArchiveWrite writer(archive_write_disk_new()); if (!writer.data()) { return false; } archive_write_disk_set_options(writer.data(), extractionFlags()); int totalEntriesCount = 0; const bool extractAll = files.isEmpty(); if (extractAll) { if (!m_cachedArchiveEntryCount) { emit progress(0); //TODO: once information progress has been implemented, send //feedback here that the archive is being read qCDebug(ARK) << "For getting progress information, the archive will be listed once"; m_emitNoEntries = true; list(); m_emitNoEntries = false; } totalEntriesCount = m_cachedArchiveEntryCount; } else { totalEntriesCount = files.size(); } qCDebug(ARK) << "Going to extract" << totalEntriesCount << "entries"; qCDebug(ARK) << "Changing current directory to " << destinationDirectory; m_oldWorkingDir = QDir::currentPath(); QDir::setCurrent(destinationDirectory); // Initialize variables. const bool preservePaths = options.preservePaths(); const bool removeRootNode = options.isDragAndDropEnabled(); bool overwriteAll = false; // Whether to overwrite all files bool skipAll = false; // Whether to skip all files bool dontPromptErrors = false; // Whether to prompt for errors m_currentExtractedFilesSize = 0; int extractedEntriesCount = 0; int progressEntryCount = 0; struct archive_entry *entry; QString fileBeingRenamed; // To avoid traversing the entire archive when extracting a limited set of // entries, we maintain a list of remaining entries and stop when it's empty. const QStringList fullPaths = entryFullPaths(files); QStringList remainingFiles = entryFullPaths(files); // Iterate through all entries in archive. while (!QThread::currentThread()->isInterruptionRequested() && (archive_read_next_header(m_archiveReader.data(), &entry) == ARCHIVE_OK)) { if (!extractAll && remainingFiles.isEmpty()) { break; } fileBeingRenamed.clear(); int index = -1; // Retry with renamed entry, fire an overwrite query again // if the new entry also exists. retry: const bool entryIsDir = S_ISDIR(archive_entry_mode(entry)); // Skip directories if not preserving paths. if (!preservePaths && entryIsDir) { archive_read_data_skip(m_archiveReader.data()); continue; } // entryName is the name inside the archive, full path QString entryName = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry))); // Some archive types e.g. AppImage prepend all entries with "./" so remove this part. if (entryName.startsWith(QLatin1String("./"))) { entryName.remove(0, 2); } // Static libraries (*.a) contain the two entries "/" and "//". // We just skip these to allow extracting this archive type. if (entryName == QLatin1String("/") || entryName == QLatin1String("//")) { archive_read_data_skip(m_archiveReader.data()); continue; } // For now we just can't handle absolute filenames in a tar archive. // TODO: find out what to do here!! if (entryName.startsWith(QLatin1Char( '/' ))) { emit error(i18n("This archive contains archive entries with absolute paths, " "which are not supported by Ark.")); return false; } // Should the entry be extracted? if (extractAll || remainingFiles.contains(entryName) || entryName == fileBeingRenamed) { // Find the index of entry. if (entryName != fileBeingRenamed) { index = fullPaths.indexOf(entryName); } if (!extractAll && index == -1) { // If entry is not found in files, skip entry. continue; } // entryFI is the fileinfo pointing to where the file will be // written from the archive. QFileInfo entryFI(entryName); //qCDebug(ARK) << "setting path to " << archive_entry_pathname( entry ); const QString fileWithoutPath(entryFI.fileName()); // If we DON'T preserve paths, we cut the path and set the entryFI // fileinfo to the one without the path. if (!preservePaths) { // Empty filenames (ie dirs) should have been skipped already, // so asserting. Q_ASSERT(!fileWithoutPath.isEmpty()); archive_entry_copy_pathname(entry, QFile::encodeName(fileWithoutPath).constData()); entryFI = QFileInfo(fileWithoutPath); // OR, if the file has a rootNode attached, remove it from file path. } else if (!extractAll && removeRootNode && entryName != fileBeingRenamed) { const QString &rootNode = files.at(index)->rootNode; if (!rootNode.isEmpty()) { const QString truncatedFilename(entryName.remove(entryName.indexOf(rootNode), rootNode.size())); archive_entry_copy_pathname(entry, QFile::encodeName(truncatedFilename).constData()); entryFI = QFileInfo(truncatedFilename); } } // Check if the file about to be written already exists. if (!entryIsDir && entryFI.exists()) { if (skipAll) { archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); continue; } else if (!overwriteAll && !skipAll) { Kerfuffle::OverwriteQuery query(entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); break; } else if (query.responseSkip()) { archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); continue; } else if (query.responseAutoSkip()) { archive_read_data_skip(m_archiveReader.data()); archive_entry_clear(entry); skipAll = true; continue; } else if (query.responseRename()) { const QString newName(query.newFilename()); fileBeingRenamed = newName; archive_entry_copy_pathname(entry, QFile::encodeName(newName).constData()); goto retry; } else if (query.responseOverwriteAll()) { overwriteAll = true; } } } // If there is an already existing directory. if (entryIsDir && entryFI.exists()) { if (entryFI.isWritable()) { qCWarning(ARK) << "Warning, existing, but writable dir"; } else { qCWarning(ARK) << "Warning, existing, but non-writable dir. skipping"; archive_entry_clear(entry); archive_read_data_skip(m_archiveReader.data()); continue; } } // Write the entry header and check return value. const int returnCode = archive_write_header(writer.data(), entry); switch (returnCode) { case ARCHIVE_OK: // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(entryName, m_archiveReader.data(), writer.data(), (extractAll && m_extractedFilesSize)); break; case ARCHIVE_FAILED: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); // If they user previously decided to ignore future errors, // don't bother prompting again. if (!dontPromptErrors) { // Ask the user if he wants to continue extraction despite an error for this entry. Kerfuffle::ContinueExtractionQuery query(QLatin1String(archive_error_string(writer.data())), entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } dontPromptErrors = query.dontAskAgain(); } break; case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); emit error(i18nc("@info", "Fatal error, extraction aborted.")); return false; default: qCDebug(ARK) << "archive_write_header() returned" << returnCode << "which will be ignored."; break; } // If we only partially extract the archive and the number of // archive entries is available we use a simple progress based on // number of items extracted. if (!extractAll && m_cachedArchiveEntryCount) { ++progressEntryCount; emit progress(float(progressEntryCount) / totalEntriesCount); } extractedEntriesCount++; remainingFiles.removeOne(entryName); } else { // Archive entry not among selected files, skip it. archive_read_data_skip(m_archiveReader.data()); } } qCDebug(ARK) << "Extracted" << extractedEntriesCount << "entries"; slotRestoreWorkingDir(); return archive_read_close(m_archiveReader.data()) == ARCHIVE_OK; } bool LibarchivePlugin::initializeReader() { m_archiveReader.reset(archive_read_new()); if (!(m_archiveReader.data())) { emit error(i18n("The archive reader could not be initialized.")); return false; } if (archive_read_support_filter_all(m_archiveReader.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(m_archiveReader.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(m_archiveReader.data(), QFile::encodeName(filename()).constData(), 10240) != ARCHIVE_OK) { qCWarning(ARK) << "Could not open the archive:" << archive_error_string(m_archiveReader.data()); emit error(i18nc("@info", "Archive corrupted or insufficient permissions.")); return false; } return true; } void LibarchivePlugin::emitEntryFromArchiveEntry(struct archive_entry *aentry) { auto e = new Archive::Entry(); #ifdef Q_OS_WIN e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromUtf16((ushort*)archive_entry_pathname_w(aentry)))); #else e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromWCharArray(archive_entry_pathname_w(aentry)))); #endif const QString owner = QString::fromLatin1(archive_entry_uname(aentry)); if (!owner.isEmpty()) { e->setProperty("owner", owner); } const QString group = QString::fromLatin1(archive_entry_gname(aentry)); if (!group.isEmpty()) { e->setProperty("group", group); } e->compressedSizeIsSet = false; e->setProperty("size", (qlonglong)archive_entry_size(aentry)); e->setProperty("isDirectory", S_ISDIR(archive_entry_mode(aentry))); if (archive_entry_symlink(aentry)) { e->setProperty("link", QLatin1String( archive_entry_symlink(aentry) )); } auto time = static_cast(archive_entry_mtime(aentry)); e->setProperty("timestamp", QDateTime::fromTime_t(time)); emit entry(e); m_emittedEntries << e; } int LibarchivePlugin::extractionFlags() const { int result = ARCHIVE_EXTRACT_TIME; result |= ARCHIVE_EXTRACT_SECURE_NODOTDOT; // TODO: Don't use arksettings here /*if ( ArkSettings::preservePerms() ) { result &= ARCHIVE_EXTRACT_PERM; } if ( !ArkSettings::extractOverwrite() ) { result &= ARCHIVE_EXTRACT_NO_OVERWRITE; }*/ return result; } void LibarchivePlugin::copyData(const QString& filename, struct archive *dest, bool partialprogress) { char buff[10240]; QFile file(filename); if (!file.open(QIODevice::ReadOnly)) { return; } auto readBytes = file.read(buff, sizeof(buff)); while (readBytes > 0 && !QThread::currentThread()->isInterruptionRequested()) { archive_write_data(dest, buff, static_cast(readBytes)); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while writing" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = file.read(buff, sizeof(buff)); } file.close(); } void LibarchivePlugin::copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress) { char buff[10240]; auto readBytes = archive_read_data(source, buff, sizeof(buff)); while (readBytes > 0 && !QThread::currentThread()->isInterruptionRequested()) { archive_write_data(dest, buff, static_cast(readBytes)); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while extracting" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = archive_read_data(source, buff, sizeof(buff)); } } void LibarchivePlugin::slotRestoreWorkingDir() { if (m_oldWorkingDir.isEmpty()) { return; } if (!QDir::setCurrent(m_oldWorkingDir)) { qCWarning(ARK) << "Failed to restore old working directory:" << m_oldWorkingDir; } else { m_oldWorkingDir.clear(); } } QString LibarchivePlugin::convertCompressionName(const QString &method) { if (method == QLatin1String("gzip")) { return QStringLiteral("GZip"); } else if (method == QLatin1String("bzip2")) { return QStringLiteral("BZip2"); } else if (method == QLatin1String("xz")) { return QStringLiteral("XZ"); } else if (method == QLatin1String("compress (.Z)")) { return QStringLiteral("Compress"); } else if (method == QLatin1String("lrzip")) { return QStringLiteral("LRZip"); } else if (method == QLatin1String("lzip")) { return QStringLiteral("LZip"); } else if (method == QLatin1String("lz4")) { return QStringLiteral("LZ4"); } else if (method == QLatin1String("lzop")) { return QStringLiteral("lzop"); } else if (method == QLatin1String("lzma")) { return QStringLiteral("LZMA"); } else if (method == QLatin1String("zstd")) { return QStringLiteral("Zstandard"); } return QString(); } diff --git a/plugins/libarchive/readwritelibarchiveplugin.h b/plugins/libarchive/readwritelibarchiveplugin.h index fec91695..bbdd1915 100644 --- a/plugins/libarchive/readwritelibarchiveplugin.h +++ b/plugins/libarchive/readwritelibarchiveplugin.h @@ -1,96 +1,95 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2016 Vladyslav Batyrenko * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef READWRITELIBARCHIVEPLUGIN_H #define READWRITELIBARCHIVEPLUGIN_H #include "libarchiveplugin.h" -#include #include #include using namespace Kerfuffle; class ReadWriteLibarchivePlugin : public LibarchivePlugin { Q_OBJECT public: explicit ReadWriteLibarchivePlugin(QObject *parent, const QVariantList &args); ~ReadWriteLibarchivePlugin() override; bool addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions &options, uint numberOfEntriesToAdd = 0) override; bool moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) override; bool copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) override; bool deleteFiles(const QVector &files) override; protected: bool initializeWriter(const bool creatingNewFile = false, const CompressionOptions &options = CompressionOptions()); bool initializeWriterFilters(); bool initializeNewFileWriterFilters(const CompressionOptions &options); void finish(const bool isSuccessful); private: /** * Processes all the existing entries and does manipulations to them * based on the OperationMode (Add/Move/Copy/Delete). * * @param entriesCounter Counter of added/moved/copied/deleted entries. * * @return bool indicating whether the operation was successful. */ bool processOldEntries(uint &entriesCounter, OperationMode mode, uint totalCount); /** * Writes entry being read into memory. * * @return bool indicating whether the operation was successful. */ bool writeEntry(struct archive_entry *entry); /** * Writes entry from physical disk. * * @return bool indicating whether the operation was successful. */ bool writeFile(const QString &relativeName, const QString &destination); QSaveFile m_tempFile; ArchiveWrite m_archiveWriter; // New added files by addFiles methods. It's assigned to m_filesPaths // and then is used by processOldEntries method (in Add mode) for skipping already written entries. QStringList m_writtenFiles; // Passed argument from job which is used by processOldEntries method. QStringList m_filesPaths; int m_entriesWithoutChildren = 0; const Archive::Entry *m_destination = nullptr; }; #endif // READWRITELIBARCHIVEPLUGIN_H diff --git a/plugins/libzipplugin/libzipplugin.h b/plugins/libzipplugin/libzipplugin.h index 3bd2331e..87dbdab3 100644 --- a/plugins/libzipplugin/libzipplugin.h +++ b/plugins/libzipplugin/libzipplugin.h @@ -1,70 +1,69 @@ /* * Copyright (c) 2017 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef LIBZIPPLUGIN_H #define LIBZIPPLUGIN_H #include "archiveinterface.h" -#include #include using namespace Kerfuffle; class LibzipPlugin : public ReadWriteArchiveInterface { Q_OBJECT public: explicit LibzipPlugin(QObject *parent, const QVariantList& args); ~LibzipPlugin() override; bool list() override; bool doKill() override; bool extractFiles(const QVector &files, const QString& destinationDirectory, const ExtractionOptions& options) override; bool addFiles(const QVector &files, const Archive::Entry *destination, const CompressionOptions& options, uint numberOfEntriesToAdd = 0) override; bool deleteFiles(const QVector &files) override; bool moveFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) override; bool copyFiles(const QVector &files, Archive::Entry *destination, const CompressionOptions &options) override; bool addComment(const QString& comment) override; bool testArchive() override; private: bool extractEntry(zip_t *archive, const QString &entry, const QString &rootNode, const QString &destDir, bool preservePaths, bool removeRootNode); bool writeEntry(zip_t *archive, const QString &entry, const Archive::Entry* destination, const CompressionOptions& options, bool isDir = false); bool emitEntryForIndex(zip_t *archive, qlonglong index); void emitProgress(double percentage); QString permissionsToString(const mode_t &perm); static void progressCallback(zip_t *, double progress, void *that); QVector m_emittedEntries; bool m_overwriteAll; bool m_skipAll; bool m_listAfterAdd; }; #endif // LIBZIPPLUGIN_H