diff --git a/app/batchextract.cpp b/app/batchextract.cpp index 7c56f310..112793d6 100644 --- a/app/batchextract.cpp +++ b/app/batchextract.cpp @@ -1,302 +1,302 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Harald Hvaal * Copyright (C) 2009-2010 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "batchextract.h" #include "ark_debug.h" #include "kerfuffle/archive_kerfuffle.h" #include "kerfuffle/extractiondialog.h" #include "kerfuffle/jobs.h" #include "kerfuffle/queries.h" #include #include #include #include #include #include #include #include #include #include BatchExtract::BatchExtract(QObject* parent) : KCompositeJob(parent), m_autoSubfolder(false), m_preservePaths(true), m_openDestinationAfterExtraction(false) { setCapabilities(KJob::Killable); connect(this, &KJob::result, this, &BatchExtract::showFailedFiles); } BatchExtract::~BatchExtract() { if (!m_inputs.isEmpty()) { KIO::getJobTracker()->unregisterJob(this); } } void BatchExtract::addExtraction(Kerfuffle::Archive* archive) { QString destination = destinationFolder(); const bool isSingleFolderRPM = (archive->isSingleFolderArchive() && (archive->mimeType().name() == QLatin1String("application/x-rpm"))); if ((autoSubfolder()) && (!archive->isSingleFolderArchive() || isSingleFolderRPM)) { const QDir d(destination); QString subfolderName = archive->subfolderName(); // Special case for single folder RPM archives. // We don't want the autodetected folder to have a meaningless "usr" name. if (isSingleFolderRPM && subfolderName == QStringLiteral("usr")) { qCDebug(ARK) << "Detected single folder RPM archive. Using archive basename as subfolder name"; subfolderName = QFileInfo(archive->fileName()).completeBaseName(); } if (d.exists(subfolderName)) { subfolderName = KIO::suggestName(QUrl::fromUserInput(destination, QString(), QUrl::AssumeLocalFile), subfolderName); } d.mkdir(subfolderName); destination += QLatin1Char( '/' ) + subfolderName; } Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = preservePaths(); - Kerfuffle::ExtractJob *job = archive->copyFiles(QVariantList(), destination, options); + Kerfuffle::ExtractJob *job = archive->copyFiles(QList(), destination, options); qCDebug(ARK) << QString(QStringLiteral("Registering job from archive %1, to %2, preservePaths %3")).arg(archive->fileName(), destination, QString::number(preservePaths())); addSubjob(job); m_fileNames[job] = qMakePair(archive->fileName(), destination); connect(job, SIGNAL(percent(KJob*,ulong)), this, SLOT(forwardProgress(KJob*,ulong))); connect(job, &Kerfuffle::Job::userQuery, this, &BatchExtract::slotUserQuery); } void BatchExtract::slotUserQuery(Kerfuffle::Query *query) { query->execute(); } bool BatchExtract::autoSubfolder() const { return m_autoSubfolder; } void BatchExtract::setAutoSubfolder(bool value) { m_autoSubfolder = value; } void BatchExtract::start() { QTimer::singleShot(0, this, &BatchExtract::slotStartJob); } void BatchExtract::slotStartJob() { // If none of the archives could be loaded, there is no subjob to run if (m_inputs.isEmpty()) { emitResult(); return; } foreach(Kerfuffle::Archive *archive, m_inputs) { addExtraction(archive); } KIO::getJobTracker()->registerJob(this); emit description(this, i18n("Extracting Files"), qMakePair(i18n("Source archive"), m_fileNames.value(subjobs().at(0)).first), qMakePair(i18n("Destination"), m_fileNames.value(subjobs().at(0)).second) ); m_initialJobCount = subjobs().size(); qCDebug(ARK) << "Starting first job"; subjobs().at(0)->start(); } void BatchExtract::showFailedFiles() { if (!m_failedFiles.isEmpty()) { KMessageBox::informationList(0, i18n("The following files could not be extracted:"), m_failedFiles); } } void BatchExtract::slotResult(KJob *job) { // TODO: The user must be informed about which file caused the error, and that the other files // in the queue will not be extracted. if (job->error()) { qCDebug(ARK) << "There was en error:" << job->error() << ", errorText:" << job->errorText(); setErrorText(job->errorText()); setError(job->error()); removeSubjob(job); if (job->error() != KJob::KilledJobError) { KMessageBox::error(NULL, job->errorText().isEmpty() ? i18n("There was an error during extraction.") : job->errorText()); } emitResult(); return; } else { removeSubjob(job); } if (!hasSubjobs()) { if (openDestinationAfterExtraction()) { QUrl destination(destinationFolder()); destination.setPath(QDir::cleanPath(destination.path())); KRun::runUrl(destination, QStringLiteral("inode/directory"), 0); } qCDebug(ARK) << "Finished, emitting the result"; emitResult(); } else { qCDebug(ARK) << "Starting the next job"; emit description(this, i18n("Extracting Files"), qMakePair(i18n("Source archive"), m_fileNames.value(subjobs().at(0)).first), qMakePair(i18n("Destination"), m_fileNames.value(subjobs().at(0)).second) ); subjobs().at(0)->start(); } } void BatchExtract::forwardProgress(KJob *job, unsigned long percent) { Q_UNUSED(job) int jobPart = 100 / m_initialJobCount; setPercent(jobPart *(m_initialJobCount - subjobs().size()) + percent / m_initialJobCount); } bool BatchExtract::addInput(const QUrl& url) { qCDebug(ARK) << "Adding archive" << url.toDisplayString(QUrl::PreferLocalFile); Kerfuffle::Archive *archive = Kerfuffle::Archive::create(url.toDisplayString(QUrl::PreferLocalFile), this); Q_ASSERT(archive); if (!QFileInfo::exists(url.toDisplayString(QUrl::PreferLocalFile))) { m_failedFiles.append(url.fileName()); return false; } m_inputs.append(archive); return true; } bool BatchExtract::openDestinationAfterExtraction() const { return m_openDestinationAfterExtraction; } bool BatchExtract::preservePaths() const { return m_preservePaths; } QString BatchExtract::destinationFolder() const { if (m_destinationFolder.isEmpty()) { return QDir::currentPath(); } else { return m_destinationFolder; } } void BatchExtract::setDestinationFolder(const QString& folder) { if (QFileInfo(folder).isDir()) { m_destinationFolder = folder; } } void BatchExtract::setOpenDestinationAfterExtraction(bool value) { m_openDestinationAfterExtraction = value; } void BatchExtract::setPreservePaths(bool value) { m_preservePaths = value; } bool BatchExtract::showExtractDialog() { QPointer dialog = new Kerfuffle::ExtractionDialog; if (m_inputs.size() > 1) { dialog.data()->batchModeOption(); } dialog.data()->setModal(true); dialog.data()->setAutoSubfolder(autoSubfolder()); dialog.data()->setCurrentUrl(QUrl::fromUserInput(destinationFolder(), QString(), QUrl::AssumeLocalFile)); dialog.data()->setPreservePaths(preservePaths()); if (m_inputs.size() == 1) { if (m_inputs.at(0)->isSingleFolderArchive()) { dialog.data()->setSingleFolderArchive(true); } dialog.data()->setSubfolder(m_inputs.at(0)->subfolderName()); } if (!dialog.data()->exec()) { delete dialog.data(); return false; } setAutoSubfolder(dialog.data()->autoSubfolders()); setDestinationFolder(dialog.data()->destinationDirectory().toDisplayString(QUrl::PreferLocalFile)); setOpenDestinationAfterExtraction(dialog.data()->openDestinationAfterExtraction()); setPreservePaths(dialog.data()->preservePaths()); delete dialog.data(); return true; } diff --git a/autotests/kerfuffle/archivetest.cpp b/autotests/kerfuffle/archivetest.cpp index 2ff88207..c0db49a8 100644 --- a/autotests/kerfuffle/archivetest.cpp +++ b/autotests/kerfuffle/archivetest.cpp @@ -1,570 +1,571 @@ /* * Copyright (c) 2010-2011 Raphael Kubo da Costa * Copyright (c) 2016 Elvis Angelaccio * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "kerfuffle/archive_kerfuffle.h" #include "kerfuffle/jobs.h" #include #include #include using namespace Kerfuffle; class ArchiveTest : public QObject { Q_OBJECT private Q_SLOTS: void testProperties_data(); void testProperties(); void testExtraction_data(); void testExtraction(); void testCreateEncryptedArchive(); }; QTEST_GUILESS_MAIN(ArchiveTest) void ArchiveTest::testProperties_data() { QTest::addColumn("archivePath"); QTest::addColumn("expectedBaseName"); QTest::addColumn("isReadOnly"); QTest::addColumn("canFallbackOnReadOnly"); QTest::addColumn("isSingleFolder"); QTest::addColumn("expectedEncryptionType"); QTest::addColumn("expectedSubfolderName"); // Test non-existent tar archive. QTest::newRow("non-existent tar archive") << QStringLiteral("/tmp/foo.tar.gz") << QStringLiteral("foo") << false << false << false << Archive::Unencrypted << QString(); // Test non-archive file QTest::newRow("not an archive") << QStringLiteral("/tmp/foo.pdf") << QString() << false << false << false << Archive::Unencrypted << QString(); // Test dummy source code tarball. QTest::newRow("dummy source code tarball") << QFINDTESTDATA("data/code-x.y.z.tar.gz") << QStringLiteral("code-x.y.z") << false << false << true << Archive::Unencrypted << QStringLiteral("awesome_project"); QTest::newRow("simple compressed tar archive") << QFINDTESTDATA("data/simplearchive.tar.gz") << QStringLiteral("simplearchive") << false << false << false << Archive::Unencrypted << QStringLiteral("simplearchive"); QTest::newRow("encrypted zip, single entry") << QFINDTESTDATA("data/archivetest_encrypted.zip") << QStringLiteral("archivetest_encrypted") << false << true << false << Archive::Encrypted << QStringLiteral("archivetest_encrypted"); QTest::newRow("simple zip, one unencrypted entry") << QFINDTESTDATA("data/archivetest_unencrypted.zip") << QStringLiteral("archivetest_unencrypted") << false << true << false << Archive::Unencrypted << QStringLiteral("archivetest_unencrypted"); QTest::newRow("rpm archive, no single folder") << QFINDTESTDATA("data/wget.rpm") << QStringLiteral("wget") << true << false << false << Archive::Unencrypted << QStringLiteral("wget"); QTest::newRow("bzip2-compressed tarball") << QFINDTESTDATA("data/simplearchive.tar.bz2") << QStringLiteral("simplearchive") << false << false << false << Archive::Unencrypted << QStringLiteral("simplearchive"); QTest::newRow("xz-compressed tarball") << QFINDTESTDATA("data/simplearchive.tar.xz") << QStringLiteral("simplearchive") << false << false << false << Archive::Unencrypted << QStringLiteral("simplearchive"); QTest::newRow("lzma-compressed tarball") << QFINDTESTDATA("data/simplearchive.tar.lzma") << QStringLiteral("simplearchive") << false << false << false << Archive::Unencrypted << QStringLiteral("simplearchive"); QTest::newRow("compress (.Z) tarball") << QFINDTESTDATA("data/simplearchive.tar.Z") << QStringLiteral("simplearchive") << false << false << false << Archive::Unencrypted << QStringLiteral("simplearchive"); QTest::newRow("lzipped tarball") << QFINDTESTDATA("data/simplearchive.tar.lz") << QStringLiteral("simplearchive") << false << false << false << Archive::Unencrypted << QStringLiteral("simplearchive"); QTest::newRow("lzop-compressed tarball") << QFINDTESTDATA("data/simplearchive.tar.lzo") << QStringLiteral("simplearchive") << false << false << false << Archive::Unencrypted << QStringLiteral("simplearchive"); // Only run test for lrzipped tar if lrzip executable is found in path. if (!QStandardPaths::findExecutable(QStringLiteral("lrzip")).isEmpty()) { QTest::newRow("lrzipped tarball") << QFINDTESTDATA("data/simplearchive.tar.lrz") << QStringLiteral("simplearchive") << false << false << false << Archive::Unencrypted << QStringLiteral("simplearchive"); } else { qDebug() << "lrzip executable not found in path. Skipping lrzip test."; } // Only run test for lz4-compressed tar if lz4 executable is found in path. if (!QStandardPaths::findExecutable(QStringLiteral("lz4")).isEmpty()) { QTest::newRow("lz4-compressed tarball") << QFINDTESTDATA("data/simplearchive.tar.lz4") << QStringLiteral("simplearchive") << false << false << false << Archive::Unencrypted << QStringLiteral("simplearchive"); } else { qDebug() << "lz4 executable not found in path. Skipping lz4 test."; } QTest::newRow("xar archive") << QFINDTESTDATA("data/simplearchive.xar") << QStringLiteral("simplearchive") << true << false << false << Archive::Unencrypted << QStringLiteral("simplearchive"); QTest::newRow("mimetype child of application/zip") << QFINDTESTDATA("data/test.odt") << QStringLiteral("test") << false << true << false << Archive::Unencrypted << QStringLiteral("test"); } void ArchiveTest::testProperties() { QFETCH(QString, archivePath); Archive *archive = Archive::create(archivePath, this); QVERIFY(archive); if (!archive->isValid()) { QVERIFY(archive->fileName().isEmpty()); QVERIFY(!archive->hasComment()); QVERIFY(archive->error() != NoError); QSKIP("Could not find a plugin to handle the archive. Skipping test.", SkipSingle); } QFETCH(QString, expectedBaseName); QCOMPARE(archive->completeBaseName(), expectedBaseName); QFETCH(bool, isReadOnly); QFETCH(bool, canFallbackOnReadOnly); // If the plugin supports fallback on read-only mode, we cannot be sure at this point // if the archive is going to be read-write or read-only. if (!canFallbackOnReadOnly) { QCOMPARE(archive->isReadOnly(), isReadOnly); } QFETCH(bool, isSingleFolder); QCOMPARE(archive->isSingleFolderArchive(), isSingleFolder); QFETCH(Archive::EncryptionType, expectedEncryptionType); QCOMPARE(archive->encryptionType(), expectedEncryptionType); QFETCH(QString, expectedSubfolderName); QCOMPARE(archive->subfolderName(), expectedSubfolderName); archive->deleteLater(); } void ArchiveTest::testExtraction_data() { QTest::addColumn("archivePath"); - QTest::addColumn("entriesToExtract"); + QTest::addColumn>("entriesToExtract"); QTest::addColumn("extractionOptions"); QTest::addColumn("expectedExtractedEntriesCount"); ExtractionOptions optionsPreservePaths; optionsPreservePaths[QStringLiteral("PreservePaths")] = true; ExtractionOptions dragAndDropOptions = optionsPreservePaths; dragAndDropOptions[QStringLiteral("DragAndDrop")] = true; dragAndDropOptions[QStringLiteral("RemoveRootNode")] = true; QString archivePath = QFINDTESTDATA("data/simplearchive.tar.gz"); QTest::newRow("extract the whole simplearchive.tar.gz") << archivePath - << QVariantList() + << QList() << optionsPreservePaths << 4; + Archive::Entry *e1 = new Archive::Entry(Q_NULLPTR); archivePath = QFINDTESTDATA("data/simplearchive.tar.gz"); QTest::newRow("extract selected entries from a tar.gz, without paths") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << ExtractionOptions() << 2; archivePath = QFINDTESTDATA("data/simplearchive.tar.gz"); QTest::newRow("extract selected entries from a tar.gz, preserve paths") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << optionsPreservePaths << 3; archivePath = QFINDTESTDATA("data/simplearchive.tar.gz"); QTest::newRow("extract selected entries from a tar.gz, drag-and-drop") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())), - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir/"))) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()), + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir/")) } << dragAndDropOptions << 2; archivePath = QFINDTESTDATA("data/one_toplevel_folder.zip"); QTest::newRow("extract the whole one_toplevel_folder.zip") << archivePath - << QVariantList() + << QList() << optionsPreservePaths << 9; archivePath = QFINDTESTDATA("data/one_toplevel_folder.zip"); QTest::newRow("extract selected entries from a zip, without paths") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/test2.txt"), QStringLiteral("A"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B"))) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/test2.txt"), QStringLiteral("A")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B")) } << ExtractionOptions() << 2; archivePath = QFINDTESTDATA("data/one_toplevel_folder.zip"); QTest::newRow("extract selected entries from a zip, preserve paths") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/test2.txt"), QStringLiteral("A"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B"))) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/test2.txt"), QStringLiteral("A")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B")) } << optionsPreservePaths << 4; archivePath = QFINDTESTDATA("data/one_toplevel_folder.zip"); QTest::newRow("extract selected entries from a zip, drag-and-drop") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/test2.txt"), QStringLiteral("A/"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/C/"), QStringLiteral("A/B/"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/C/test1.txt"), QStringLiteral("A/B/"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/C/test2.txt"), QStringLiteral("A/B/"))) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/test2.txt"), QStringLiteral("A/")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/B/C/"), QStringLiteral("A/B/")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/B/C/test1.txt"), QStringLiteral("A/B/")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/B/C/test2.txt"), QStringLiteral("A/B/")) } << dragAndDropOptions << 4; archivePath = QFINDTESTDATA("data/one_toplevel_folder.7z"); QTest::newRow("extract the whole one_toplevel_folder.7z") << archivePath - << QVariantList() + << QList() << optionsPreservePaths << 9; archivePath = QFINDTESTDATA("data/one_toplevel_folder.7z"); QTest::newRow("extract selected entries from a 7z, without paths") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/test2.txt"), QStringLiteral("A"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B"))) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/test2.txt"), QStringLiteral("A")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B")) } << ExtractionOptions() << 2; archivePath = QFINDTESTDATA("data/one_toplevel_folder.7z"); QTest::newRow("extract selected entries from a 7z, preserve paths") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/test2.txt"), QStringLiteral("A"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B"))) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/test2.txt"), QStringLiteral("A")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B")) } << optionsPreservePaths << 4; archivePath = QFINDTESTDATA("data/one_toplevel_folder.7z"); QTest::newRow("extract selected entries from a 7z, drag-and-drop") << archivePath - << QVariantList {QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/test2.txt"), QStringLiteral("A/B/")))} + << QList {new Archive::Entry(Q_NULLPTR, QStringLiteral("A/B/test2.txt"), QStringLiteral("A/B/"))} << dragAndDropOptions << 1; archivePath = QFINDTESTDATA("data/empty_folders.zip"); QTest::newRow("zip with empty folders") << archivePath - << QVariantList() + << QList() << optionsPreservePaths << 5; archivePath = QFINDTESTDATA("data/empty_folders.tar.gz"); QTest::newRow("tar with empty folders") << archivePath - << QVariantList() + << QList() << optionsPreservePaths << 5; archivePath = QFINDTESTDATA("data/simplearchive.tar.bz2"); QTest::newRow("extract selected entries from a bzip2-compressed tarball without path") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("file3.txt"), QString())), - QVariant::fromValue(fileRootNodePair(QStringLiteral("dir2/file22.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("file3.txt"), QString()), + new Archive::Entry(Q_NULLPTR, QStringLiteral("dir2/file22.txt"), QString()) } << ExtractionOptions() << 2; archivePath = QFINDTESTDATA("data/simplearchive.tar.bz2"); QTest::newRow("extract all entries from a bzip2-compressed tarball with path") << archivePath - << QVariantList() + << QList() << optionsPreservePaths << 7; archivePath = QFINDTESTDATA("data/simplearchive.tar.xz"); QTest::newRow("extract selected entries from a xz-compressed tarball without path") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("file3.txt"), QString())), - QVariant::fromValue(fileRootNodePair(QStringLiteral("dir2/file22.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("file3.txt"), QString()), + new Archive::Entry(Q_NULLPTR, QStringLiteral("dir2/file22.txt"), QString()) } << ExtractionOptions() << 2; archivePath = QFINDTESTDATA("data/simplearchive.tar.xz"); QTest::newRow("extract all entries from a xz-compressed tarball with path") << archivePath - << QVariantList() + << QList() << optionsPreservePaths << 7; archivePath = QFINDTESTDATA("data/simplearchive.tar.lzma"); QTest::newRow("extract selected entries from a lzma-compressed tarball without path") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("file3.txt"), QString())), - QVariant::fromValue(fileRootNodePair(QStringLiteral("dir2/file22.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("file3.txt"), QString()), + new Archive::Entry(Q_NULLPTR, QStringLiteral("dir2/file22.txt"), QString()) } << ExtractionOptions() << 2; archivePath = QFINDTESTDATA("data/simplearchive.tar.lzma"); QTest::newRow("extract all entries from a lzma-compressed tarball with path") << archivePath - << QVariantList() + << QList() << optionsPreservePaths << 7; archivePath = QFINDTESTDATA("data/simplearchive.tar.Z"); QTest::newRow("extract selected entries from a compress (.Z)-compressed tarball without path") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("file3.txt"), QString())), - QVariant::fromValue(fileRootNodePair(QStringLiteral("dir2/file22.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("file3.txt"), QString()), + new Archive::Entry(Q_NULLPTR, QStringLiteral("dir2/file22.txt"), QString()) } << ExtractionOptions() << 2; archivePath = QFINDTESTDATA("data/simplearchive.tar.Z"); QTest::newRow("extract all entries from a compress (.Z)-compressed tarball with path") << archivePath - << QVariantList() + << QList() << optionsPreservePaths << 7; archivePath = QFINDTESTDATA("data/simplearchive.tar.lz"); QTest::newRow("extract selected entries from a lzipped tarball without path") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("file3.txt"), QString())), - QVariant::fromValue(fileRootNodePair(QStringLiteral("dir2/file22.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("file3.txt"), QString()), + new Archive::Entry(Q_NULLPTR, QStringLiteral("dir2/file22.txt"), QString()) } << ExtractionOptions() << 2; archivePath = QFINDTESTDATA("data/simplearchive.tar.lz"); QTest::newRow("extract all entries from a lzipped tarball with path") << archivePath - << QVariantList() + << QList() << optionsPreservePaths << 7; archivePath = QFINDTESTDATA("data/simplearchive.tar.lzo"); QTest::newRow("extract selected entries from a lzop-compressed tarball without path") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("file3.txt"), QString())), - QVariant::fromValue(fileRootNodePair(QStringLiteral("dir2/file22.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("file3.txt"), QString()), + new Archive::Entry(Q_NULLPTR, QStringLiteral("dir2/file22.txt"), QString()) } << ExtractionOptions() << 2; archivePath = QFINDTESTDATA("data/simplearchive.tar.lzo"); QTest::newRow("extract all entries from a lzop-compressed tarball with path") << archivePath - << QVariantList() + << QList() << optionsPreservePaths << 7; // Only run test for lrzipped tar if lrzip executable is found in path. if (!QStandardPaths::findExecutable(QStringLiteral("lrzip")).isEmpty()) { archivePath = QFINDTESTDATA("data/simplearchive.tar.lrz"); QTest::newRow("extract selected entries from a lrzip-compressed tarball without path") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("file3.txt"), QString())), - QVariant::fromValue(fileRootNodePair(QStringLiteral("dir2/file22.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("file3.txt"), QString()), + new Archive::Entry(Q_NULLPTR, QStringLiteral("dir2/file22.txt"), QString()) } << ExtractionOptions() << 2; archivePath = QFINDTESTDATA("data/simplearchive.tar.lrz"); QTest::newRow("extract all entries from a lrzip-compressed tarball with path") << archivePath - << QVariantList() + << QList() << optionsPreservePaths << 7; } else { qDebug() << "lrzip executable not found in path. Skipping lrzip test."; } // Only run test for lz4-compressed tar if lz4 executable is found in path. if (!QStandardPaths::findExecutable(QStringLiteral("lz4")).isEmpty()) { archivePath = QFINDTESTDATA("data/simplearchive.tar.lz4"); QTest::newRow("extract selected entries from a lz4-compressed tarball without path") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("file3.txt"), QString())), - QVariant::fromValue(fileRootNodePair(QStringLiteral("dir2/file22.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("file3.txt"), QString()), + new Archive::Entry(Q_NULLPTR, QStringLiteral("dir2/file22.txt"), QString()) } << ExtractionOptions() << 2; archivePath = QFINDTESTDATA("data/simplearchive.tar.lz4"); QTest::newRow("extract all entries from a lz4-compressed tarball with path") << archivePath - << QVariantList() + << QList() << optionsPreservePaths << 7; } else { qDebug() << "lz4 executable not found in path. Skipping lz4 test."; } archivePath = QFINDTESTDATA("data/simplearchive.xar"); QTest::newRow("extract selected entries from a xar archive without path") << archivePath - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("dir1/file11.txt"), QString())), - QVariant::fromValue(fileRootNodePair(QStringLiteral("file4.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("dir1/file11.txt"), QString()), + new Archive::Entry(Q_NULLPTR, QStringLiteral("file4.txt"), QString()) } << ExtractionOptions() << 2; archivePath = QFINDTESTDATA("data/simplearchive.xar"); QTest::newRow("extract all entries from a xar archive with path") << archivePath - << QVariantList() + << QList() << optionsPreservePaths << 6; } void ArchiveTest::testExtraction() { QFETCH(QString, archivePath); Archive *archive = Archive::create(archivePath, this); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not find a plugin to handle the archive. Skipping test.", SkipSingle); } QTemporaryDir destDir; if (!destDir.isValid()) { QSKIP("Could not create a temporary directory for extraction. Skipping test.", SkipSingle); } - QFETCH(QVariantList, entriesToExtract); + QFETCH(QList, entriesToExtract); QFETCH(ExtractionOptions, extractionOptions); auto extractionJob = archive->copyFiles(entriesToExtract, destDir.path(), extractionOptions); QEventLoop eventLoop(this); connect(extractionJob, &KJob::result, &eventLoop, &QEventLoop::quit); extractionJob->start(); eventLoop.exec(); // krazy:exclude=crashy QFETCH(int, expectedExtractedEntriesCount); int extractedEntriesCount = 0; QDirIterator dirIt(destDir.path(), QDir::AllEntries | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (dirIt.hasNext()) { extractedEntriesCount++; dirIt.next(); } QCOMPARE(extractedEntriesCount, expectedExtractedEntriesCount); archive->deleteLater(); } void ArchiveTest::testCreateEncryptedArchive() { Archive *archive = Archive::create(QStringLiteral("foo.zip")); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not find a plugin to handle the archive. Skipping test.", SkipSingle); } QCOMPARE(archive->encryptionType(), Archive::Unencrypted); archive->encrypt(QStringLiteral("1234"), false); QCOMPARE(archive->encryptionType(), Archive::Encrypted); archive->deleteLater(); } #include "archivetest.moc" diff --git a/autotests/kerfuffle/data/archive-deepsinglehierarchy.json b/autotests/kerfuffle/data/archive-deepsinglehierarchy.json index 9aa8c0c1..c6f6a639 100644 --- a/autotests/kerfuffle/data/archive-deepsinglehierarchy.json +++ b/autotests/kerfuffle/data/archive-deepsinglehierarchy.json @@ -1,20 +1,20 @@ [ { - "fileName": "aDir/", + "fullPath": "aDir/", "isDirectory": true }, { - "fileName": "aDir/b.txt" + "fullPath": "aDir/b.txt" }, { - "fileName": "aDir/aDirInside/", + "fullPath": "aDir/aDirInside/", "isDirectory": true }, { - "fileName": "aDir/aDirInside/anotherDir/", + "fullPath": "aDir/aDirInside/anotherDir/", "isDirectory": true }, { - "fileName": "aDir/aDirInside/anotherDir/file.txt" + "fullPath": "aDir/aDirInside/anotherDir/file.txt" } ] diff --git a/autotests/kerfuffle/data/archive-emptysinglefolder.json b/autotests/kerfuffle/data/archive-emptysinglefolder.json index e6c95a20..46d5451d 100644 --- a/autotests/kerfuffle/data/archive-emptysinglefolder.json +++ b/autotests/kerfuffle/data/archive-emptysinglefolder.json @@ -1,6 +1,6 @@ [ { - "fileName": "aDir/", + "fullPath": "aDir/", "isDirectory": true } ] diff --git a/autotests/kerfuffle/data/archive-malicious.json b/autotests/kerfuffle/data/archive-malicious.json index d576a578..696e001d 100644 --- a/autotests/kerfuffle/data/archive-malicious.json +++ b/autotests/kerfuffle/data/archive-malicious.json @@ -1,24 +1,24 @@ [ { - "FileName": "aDir/", - "IsDirectory": true + "fullPath": "aDir/", + "isDirectory": true }, { - "FileName": "aDir/b.txt" + "fullPath": "aDir/b.txt" }, { - "FileName": "anotherDir/", - "IsDirectory": true + "fullPath": "anotherDir/", + "isDirectory": true }, { - "FileName": "anotherDir/..", - "IsDirectory": true + "fullPath": "anotherDir/..", + "isDirectory": true }, { - "FileName": "anotherDir/../..", - "IsDirectory": true + "fullPath": "anotherDir/../..", + "isDirectory": true }, { - "FileName": "anotherDir/../../file.txt" + "fullPath": "anotherDir/../../file.txt" } ] diff --git a/autotests/kerfuffle/data/archive-multiplefolders.json b/autotests/kerfuffle/data/archive-multiplefolders.json index 47b0caf0..c5ee946d 100644 --- a/autotests/kerfuffle/data/archive-multiplefolders.json +++ b/autotests/kerfuffle/data/archive-multiplefolders.json @@ -1,16 +1,16 @@ [ { - "fileName": "aDir/", + "fullPath": "aDir/", "isDirectory": true }, { - "fileName": "aDir/b.txt" + "fullPath": "aDir/b.txt" }, { - "fileName": "anotherDir/", + "fullPath": "anotherDir/", "isDirectory": true }, { - "fileName": "anotherDir/file.txt" + "fullPath": "anotherDir/file.txt" } ] diff --git a/autotests/kerfuffle/data/archive-nodir-manyfiles.json b/autotests/kerfuffle/data/archive-nodir-manyfiles.json index 9f89f946..c8afc566 100644 --- a/autotests/kerfuffle/data/archive-nodir-manyfiles.json +++ b/autotests/kerfuffle/data/archive-nodir-manyfiles.json @@ -1,8 +1,8 @@ [ { - "fileName": "a.txt" + "fullPath": "a.txt" }, { - "fileName": "file.txt" + "fullPath": "file.txt" } ] diff --git a/autotests/kerfuffle/data/archive-onetopfolder.json b/autotests/kerfuffle/data/archive-onetopfolder.json index ca025e9f..e9c557b3 100644 --- a/autotests/kerfuffle/data/archive-onetopfolder.json +++ b/autotests/kerfuffle/data/archive-onetopfolder.json @@ -1,9 +1,9 @@ [ { - "fileName": "aDir/", + "fullPath": "aDir/", "isDirectory": true }, { - "fileName": "aDir/b.txt" + "fullPath": "aDir/b.txt" } ] diff --git a/autotests/kerfuffle/data/archive-password.json b/autotests/kerfuffle/data/archive-password.json index 73fd1c1c..520b7133 100644 --- a/autotests/kerfuffle/data/archive-password.json +++ b/autotests/kerfuffle/data/archive-password.json @@ -1,13 +1,13 @@ [ { - "fileName": "foo.txt", + "fullPath": "foo.txt", "isPasswordProtected": true }, { - "fileName": "bar.txt" + "fullPath": "bar.txt" }, { - "fileName": "aDirectory/", + "fullPath": "aDirectory/", "isDirectory": true } ] diff --git a/autotests/kerfuffle/data/archive-singlefile.json b/autotests/kerfuffle/data/archive-singlefile.json index 8573c439..df1f4a55 100644 --- a/autotests/kerfuffle/data/archive-singlefile.json +++ b/autotests/kerfuffle/data/archive-singlefile.json @@ -1,5 +1,5 @@ [ { - "fileName": "a.txt" + "fullPath": "a.txt" } ] \ No newline at end of file diff --git a/autotests/kerfuffle/data/archive-unorderedsinglefolder.json b/autotests/kerfuffle/data/archive-unorderedsinglefolder.json index 60ce54dd..6cf288b2 100644 --- a/autotests/kerfuffle/data/archive-unorderedsinglefolder.json +++ b/autotests/kerfuffle/data/archive-unorderedsinglefolder.json @@ -1,16 +1,16 @@ [ { - "fileName": "aDir/anotherDir/bar.txt" + "fullPath": "aDir/anotherDir/bar.txt" }, { - "fileName": "aDir/foo.txt" + "fullPath": "aDir/foo.txt" }, { - "fileName": "aDir/anotherDir/", + "fullPath": "aDir/anotherDir/", "isDirectory": true }, { - "fileName": "aDir/", + "fullPath": "aDir/", "isDirectory": true } ] diff --git a/autotests/kerfuffle/data/archive001.json b/autotests/kerfuffle/data/archive001.json index 3efc5583..0a6130a6 100644 --- a/autotests/kerfuffle/data/archive001.json +++ b/autotests/kerfuffle/data/archive001.json @@ -1,15 +1,15 @@ [ { - "fileName": "a.txt" + "fullPath": "a.txt" }, { - "fileName": "aDir/", + "fullPath": "aDir/", "isDirectory": true }, { - "fileName": "aDir/b.txt" + "fullPath": "aDir/b.txt" }, { - "fileName": "c.txt" + "fullPath": "c.txt" } ] diff --git a/autotests/kerfuffle/data/archive002.json b/autotests/kerfuffle/data/archive002.json index c4019196..f81b4339 100644 --- a/autotests/kerfuffle/data/archive002.json +++ b/autotests/kerfuffle/data/archive002.json @@ -1,18 +1,18 @@ [ { - "fileName": "a.txt", + "fullPath": "a.txt", "size": 5 }, { - "fileName": "aDir/", + "fullPath": "aDir/", "isDirectory": true }, { - "fileName": "aDir/b.txt", + "fullPath": "aDir/b.txt", "size": 954 }, { - "fileName": "c.txt", + "fullPath": "c.txt", "size": 45000 } ] diff --git a/autotests/kerfuffle/jobstest.cpp b/autotests/kerfuffle/jobstest.cpp index d9dd0c93..0fe81dac 100644 --- a/autotests/kerfuffle/jobstest.cpp +++ b/autotests/kerfuffle/jobstest.cpp @@ -1,367 +1,385 @@ /* * Copyright (c) 2010-2011 Raphael Kubo da Costa * Copyright (c) 2016 Elvis Angelaccio * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "jsonarchiveinterface.h" #include "kerfuffle/jobs.h" #include "kerfuffle/archiveentry.h" #include #include #include using namespace Kerfuffle; class JobsTest : public QObject { Q_OBJECT public: JobsTest(); protected Q_SLOTS: void init(); void slotNewEntry(Archive::Entry *entry); private Q_SLOTS: // ListJob-related tests void testListJob_data(); void testListJob(); // ExtractJob-related tests void testExtractJobAccessors(); void testTempExtractJob(); // DeleteJob-related tests void testRemoveEntries_data(); void testRemoveEntries(); // AddJob-related tests void testAddEntries_data(); void testAddEntries(); private: JSONArchiveInterface *createArchiveInterface(const QString& filePath); QList listEntries(JSONArchiveInterface *iface); void startAndWaitForResult(KJob *job); QList m_entries; QEventLoop m_eventLoop; }; QTEST_GUILESS_MAIN(JobsTest) JobsTest::JobsTest() : QObject(Q_NULLPTR) , m_eventLoop(this) { } void JobsTest::init() { m_entries.clear(); } void JobsTest::slotNewEntry(Archive::Entry *entry) { m_entries.append(entry); } JSONArchiveInterface *JobsTest::createArchiveInterface(const QString& filePath) { JSONArchiveInterface *iface = new JSONArchiveInterface(this, {filePath}); if (!iface->open()) { qDebug() << "Could not open" << filePath; return Q_NULLPTR; } return iface; } QList JobsTest::listEntries(JSONArchiveInterface *iface) { m_entries.clear(); ListJob *listJob = new ListJob(iface); connect(listJob, &Job::newEntry, this, &JobsTest::slotNewEntry); startAndWaitForResult(listJob); return m_entries; } void JobsTest::startAndWaitForResult(KJob *job) { connect(job, &KJob::result, &m_eventLoop, &QEventLoop::quit); job->start(); m_eventLoop.exec(); } void JobsTest::testListJob_data() { QTest::addColumn("jsonArchive"); QTest::addColumn("expectedExtractedFilesSize"); QTest::addColumn("isPasswordProtected"); QTest::addColumn("isSingleFolder"); QTest::addColumn("expectedEntryNames"); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << 0LL << false << false << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")}; QTest::newRow("archive002.json") << QFINDTESTDATA("data/archive002.json") << 45959LL << false << false << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")}; QTest::newRow("archive-deepsinglehierarchy.json") << QFINDTESTDATA("data/archive-deepsinglehierarchy.json") << 0LL << false << true << QStringList { // Depth-first order! QStringLiteral("aDir/"), QStringLiteral("aDir/aDirInside/"), QStringLiteral("aDir/aDirInside/anotherDir/"), QStringLiteral("aDir/aDirInside/anotherDir/file.txt"), QStringLiteral("aDir/b.txt") }; QTest::newRow("archive-multiplefolders.json") << QFINDTESTDATA("data/archive-multiplefolders.json") << 0LL << false << false << QStringList {QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("anotherDir/"), QStringLiteral("anotherDir/file.txt")}; QTest::newRow("archive-nodir-manyfiles.json") << QFINDTESTDATA("data/archive-nodir-manyfiles.json") << 0LL << false << false << QStringList {QStringLiteral("a.txt"), QStringLiteral("file.txt")}; QTest::newRow("archive-onetopfolder.json") << QFINDTESTDATA("data/archive-onetopfolder.json") << 0LL << false << true << QStringList {QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt")}; QTest::newRow("archive-password.json") << QFINDTESTDATA("data/archive-password.json") << 0LL << true << false // Possibly unexpected behavior of listing: // 1. Directories are listed before files, if they are empty! // 2. Files are sorted alphabetically. << QStringList {QStringLiteral("aDirectory/"), QStringLiteral("bar.txt"), QStringLiteral("foo.txt")}; QTest::newRow("archive-singlefile.json") << QFINDTESTDATA("data/archive-singlefile.json") << 0LL << false << false << QStringList {QStringLiteral("a.txt")}; QTest::newRow("archive-emptysinglefolder.json") << QFINDTESTDATA("data/archive-emptysinglefolder.json") << 0LL << false << true << QStringList {QStringLiteral("aDir/")}; QTest::newRow("archive-unorderedsinglefolder.json") << QFINDTESTDATA("data/archive-unorderedsinglefolder.json") << 0LL << false << true << QStringList { QStringLiteral("aDir/"), QStringLiteral("aDir/anotherDir/"), QStringLiteral("aDir/anotherDir/bar.txt"), QStringLiteral("aDir/foo.txt") }; } void JobsTest::testListJob() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); ListJob *listJob = new ListJob(iface); listJob->setAutoDelete(false); startAndWaitForResult(listJob); QFETCH(qlonglong, expectedExtractedFilesSize); QCOMPARE(listJob->extractedFilesSize(), expectedExtractedFilesSize); QFETCH(bool, isPasswordProtected); QCOMPARE(listJob->isPasswordProtected(), isPasswordProtected); QFETCH(bool, isSingleFolder); QCOMPARE(listJob->isSingleFolderArchive(), isSingleFolder); QFETCH(QStringList, expectedEntryNames); auto archiveEntries = listEntries(iface); QCOMPARE(archiveEntries.size(), expectedEntryNames.size()); for (int i = 0; i < archiveEntries.size(); i++) { - QCOMPARE(archiveEntries.at(i)->property("fileName").toString(), expectedEntryNames.at(i)); + QCOMPARE(archiveEntries.at(i)->property("fullPath").toString(), expectedEntryNames.at(i)); } listJob->deleteLater(); } void JobsTest::testExtractJobAccessors() { JSONArchiveInterface *iface = createArchiveInterface(QFINDTESTDATA("data/archive001.json")); - ExtractJob *job = new ExtractJob(QVariantList(), QStringLiteral("/tmp/some-dir"), ExtractionOptions(), iface); + ExtractJob *job = new ExtractJob(QList(), QStringLiteral("/tmp/some-dir"), ExtractionOptions(), iface); ExtractionOptions defaultOptions; defaultOptions[QStringLiteral("PreservePaths")] = false; QCOMPARE(job->destinationDirectory(), QLatin1String("/tmp/some-dir")); QCOMPARE(job->extractionOptions(), defaultOptions); job->setAutoDelete(false); startAndWaitForResult(job); QCOMPARE(job->destinationDirectory(), QLatin1String("/tmp/some-dir")); QCOMPARE(job->extractionOptions(), defaultOptions); delete job; ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; options[QStringLiteral("foo")] = QLatin1String("bar"); options[QStringLiteral("pi")] = 3.14f; - job = new ExtractJob(QVariantList(), QStringLiteral("/root"), options, iface); + job = new ExtractJob(QList(), QStringLiteral("/root"), options, iface); QCOMPARE(job->destinationDirectory(), QLatin1String("/root")); QCOMPARE(job->extractionOptions(), options); job->setAutoDelete(false); startAndWaitForResult(job); QCOMPARE(job->destinationDirectory(), QLatin1String("/root")); QCOMPARE(job->extractionOptions(), options); delete job; } void JobsTest::testTempExtractJob() { JSONArchiveInterface *iface = createArchiveInterface(QFINDTESTDATA("data/archive-malicious.json")); - PreviewJob *job = new PreviewJob(QStringLiteral("anotherDir/../../file.txt"), false, iface); + PreviewJob *job = new PreviewJob(new Archive::Entry(Q_NULLPTR, QStringLiteral("anotherDir/../../file.txt")), false, iface); QVERIFY(job->validatedFilePath().endsWith(QLatin1String("anotherDir/file.txt"))); QVERIFY(job->extractionOptions()[QStringLiteral("PreservePaths")].toBool()); job->setAutoDelete(false); startAndWaitForResult(job); QVERIFY(job->validatedFilePath().endsWith(QLatin1String("anotherDir/file.txt"))); QVERIFY(job->extractionOptions()[QStringLiteral("PreservePaths")].toBool()); delete job; } void JobsTest::testRemoveEntries_data() { QTest::addColumn("jsonArchive"); - QTest::addColumn("entries"); - QTest::addColumn("entriesToDelete"); + QTest::addColumn>("entries"); + QTest::addColumn>("entriesToDelete"); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") - << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} - << QVariantList {QStringLiteral("c.txt")}; + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("a.txt")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt")) + } + << QList {new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"))}; QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") - << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} - << QVariantList {QStringLiteral("a.txt"), QStringLiteral("c.txt")}; + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("a.txt")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt")) + } + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("a.txt")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt")) + }; // Error test: if we delete non-existent entries, the archive must not change. QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") - << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} - << QVariantList {QStringLiteral("foo.txt")}; + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("a.txt")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt")) + } + << QList {new Archive::Entry(Q_NULLPTR, QStringLiteral("foo.txt"))}; } void JobsTest::testRemoveEntries() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); - QFETCH(QStringList, entries); - QFETCH(QVariantList, entriesToDelete); + QFETCH(QList, entries); + QFETCH(QList, entriesToDelete); - QStringList expectedRemainingEntries; - Q_FOREACH (const QString& entry, entries) { + QList expectedRemainingEntries; + Q_FOREACH (Archive::Entry *entry, entries) { if (!entriesToDelete.contains(entry)) { expectedRemainingEntries.append(entry); } } DeleteJob *deleteJob = new DeleteJob(entriesToDelete, iface); startAndWaitForResult(deleteJob); auto remainingEntries = listEntries(iface); QCOMPARE(remainingEntries.size(), expectedRemainingEntries.size()); for (int i = 0; i < remainingEntries.size(); i++) { - QCOMPARE(remainingEntries.at(i)->property("fileName").toString(), expectedRemainingEntries.at(i)); + QCOMPARE(remainingEntries.at(i), expectedRemainingEntries.at(i)); } iface->deleteLater(); } void JobsTest::testAddEntries_data() { QTest::addColumn("jsonArchive"); QTest::addColumn("originalEntries"); QTest::addColumn("entriesToAdd"); QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} << QStringList {QStringLiteral("foo.txt")}; QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} << QStringList {QStringLiteral("foo.txt"), QStringLiteral("bar.txt")}; // Error test: if we add an already existent entry, the archive must not change. QTest::newRow("archive001.json") << QFINDTESTDATA("data/archive001.json") << QStringList {QStringLiteral("a.txt"), QStringLiteral("aDir/"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt")} << QStringList {QStringLiteral("c.txt")}; } void JobsTest::testAddEntries() { QFETCH(QString, jsonArchive); JSONArchiveInterface *iface = createArchiveInterface(jsonArchive); QVERIFY(iface); - QFETCH(QStringList, originalEntries); + QFETCH(QList, originalEntries); auto currentEntries = listEntries(iface); QCOMPARE(currentEntries.size(), originalEntries.size()); - QFETCH(QStringList, entriesToAdd); + QFETCH(QList, entriesToAdd); AddJob *addJob = new AddJob(entriesToAdd, CompressionOptions(), iface); startAndWaitForResult(addJob); currentEntries = listEntries(iface); int expectedEntriesCount = originalEntries.size(); - Q_FOREACH (const QString& entry, entriesToAdd) { + Q_FOREACH (Archive::Entry *entry, entriesToAdd) { if (!originalEntries.contains(entry)) { expectedEntriesCount++; } } QCOMPARE(currentEntries.size(), expectedEntriesCount); iface->deleteLater(); } #include "jobstest.moc" diff --git a/autotests/kerfuffle/jsonarchiveinterface.cpp b/autotests/kerfuffle/jsonarchiveinterface.cpp index 07b6d1ca..a5b5f3b4 100644 --- a/autotests/kerfuffle/jsonarchiveinterface.cpp +++ b/autotests/kerfuffle/jsonarchiveinterface.cpp @@ -1,116 +1,117 @@ /* * Copyright (c) 2010-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "jsonarchiveinterface.h" #include #include "kerfuffle/archiveentry.h" JSONArchiveInterface::JSONArchiveInterface(QObject *parent, const QVariantList& args) : Kerfuffle::ReadWriteArchiveInterface(parent, args) { } JSONArchiveInterface::~JSONArchiveInterface() { } bool JSONArchiveInterface::list() { JSONParser::JSONArchive::const_iterator it = m_archive.constBegin(); for (; it != m_archive.constEnd(); ++it) { emit entry(*it); } return true; } bool JSONArchiveInterface::open() { QFile file(filename()); if (!file.exists()) { return false; } if (!file.open(QIODevice::ReadOnly | QIODevice::Text)) { return false; } m_archive = JSONParser::parse(&file); return !m_archive.isEmpty(); } -bool JSONArchiveInterface::addFiles(const QStringList& files, const Kerfuffle::CompressionOptions& options) +bool JSONArchiveInterface::addFiles(const QList &files, const Kerfuffle::CompressionOptions& options) { Q_UNUSED(options) - foreach (const QString& file, files) { - if (m_archive.contains(file)) { + foreach (const Kerfuffle::Archive::Entry *entry, files) { + const QString &path = entry->property("fullPath").toString(); + if (m_archive.contains(path)) { return false; } Kerfuffle::Archive::Entry *e = new Kerfuffle::Archive::Entry(NULL); - e->setProperty("fileName", file); + e->setProperty("fullPath", path); - m_archive[file] = e; + m_archive[path] = e; } return true; } -bool JSONArchiveInterface::copyFiles(const QList& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) +bool JSONArchiveInterface::copyFiles(const QList& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) { Q_UNUSED(files) Q_UNUSED(destinationDirectory) Q_UNUSED(options) return true; } -bool JSONArchiveInterface::deleteFiles(const QList& files) +bool JSONArchiveInterface::deleteFiles(const QList& files) { - foreach (const QVariant& file, files) { - const QString fileName = file.toString(); + foreach (const Kerfuffle::Archive::Entry *file, files) { + const QString &fileName = file->property("fullPath").toString(); if (m_archive.contains(fileName)) { m_archive.remove(fileName); emit entryRemoved(fileName); } } return true; } bool JSONArchiveInterface::addComment(const QString& comment) { Q_UNUSED(comment) return true; } bool JSONArchiveInterface::testArchive() { return true; } diff --git a/autotests/kerfuffle/jsonarchiveinterface.h b/autotests/kerfuffle/jsonarchiveinterface.h index 49d2c28e..aaacce69 100644 --- a/autotests/kerfuffle/jsonarchiveinterface.h +++ b/autotests/kerfuffle/jsonarchiveinterface.h @@ -1,68 +1,68 @@ /* * Copyright (c) 2010-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef JSONARCHIVEINTERFACE_H #define JSONARCHIVEINTERFACE_H #include "jsonparser.h" #include "kerfuffle/archiveinterface.h" #include "kerfuffle/archive_kerfuffle.h" /** * A dummy archive interface used by our test cases. * * It reads a JSON file which defines the contents of the archive. * For the file format description, see the documentation for @c JSONParser. * * The file's content is read to memory when open() is called and the archive * is then closed. This means that this class never changes the file's content * on disk, and entry addition or deletion do not change the original file. * * @sa JSONParser * * @author Raphael Kubo da Costa */ class JSONArchiveInterface : public Kerfuffle::ReadWriteArchiveInterface { Q_OBJECT public: explicit JSONArchiveInterface(QObject *parent, const QVariantList& args); virtual ~JSONArchiveInterface(); virtual bool list() Q_DECL_OVERRIDE; virtual bool open() Q_DECL_OVERRIDE; - virtual bool addFiles(const QStringList& files, const Kerfuffle::CompressionOptions& options) Q_DECL_OVERRIDE; - virtual bool copyFiles(const QList& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) Q_DECL_OVERRIDE; - virtual bool deleteFiles(const QList& files) Q_DECL_OVERRIDE; + virtual bool addFiles(const QList& files, const Kerfuffle::CompressionOptions& options) Q_DECL_OVERRIDE; + virtual bool copyFiles(const QList& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) Q_DECL_OVERRIDE; + virtual bool deleteFiles(const QList& files) Q_DECL_OVERRIDE; virtual bool addComment(const QString& comment) Q_DECL_OVERRIDE; virtual bool testArchive() Q_DECL_OVERRIDE; private: JSONParser::JSONArchive m_archive; }; #endif diff --git a/autotests/kerfuffle/jsonparser.cpp b/autotests/kerfuffle/jsonparser.cpp index f9e5751d..de4393a5 100644 --- a/autotests/kerfuffle/jsonparser.cpp +++ b/autotests/kerfuffle/jsonparser.cpp @@ -1,83 +1,83 @@ /* * 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 "kerfuffle/archiveinterface.h" #include "kerfuffle/archiveentry.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; foreach (const QVariant &entry, json.toList()) { const QVariantMap entryMap = entry.toMap(); - if (!entryMap.contains(QStringLiteral("fileName"))) { + if (!entryMap.contains(QStringLiteral("fullPath"))) { continue; } Kerfuffle::Archive::Entry *e = new Kerfuffle::Archive::Entry(Q_NULLPTR); QVariantMap::const_iterator entryIterator = entryMap.constBegin(); for (; entryIterator != entryMap.constEnd(); ++entryIterator) { const char *key = entryIterator.key().toStdString().c_str(); if (e->property(key).isValid()) { e->setProperty(key, entryIterator.value()); } else { qDebug() << entryIterator.key() << "is not a valid entry key"; } } - const QString fileName = entryMap[QStringLiteral("fileName")].toString(); - archive[fileName] = e; + const QString fullPath = entryMap[QStringLiteral("fullPath")].toString(); + archive[fullPath] = e; } return archive; } diff --git a/autotests/kerfuffle/jsonparser.h b/autotests/kerfuffle/jsonparser.h index 8f02ff81..dc0e80ae 100644 --- a/autotests/kerfuffle/jsonparser.h +++ b/autotests/kerfuffle/jsonparser.h @@ -1,80 +1,80 @@ /* * 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 JSONPARSER_H #define JSONPARSER_H #include "kerfuffle/archive_kerfuffle.h" #include #include /** * Simple parser which reads JSON files and creates @c ArchiveEntry objects * from it. * * The JSON file is expected to follow a specific format that describes an * archive read by Kerfuffle. * * The format consists of a list of dictionaries whose keys are values from the * EntryMetaDataType enum. The only required key for each entry is FileName; * other values which are omitted for each entry are assumed to be 0 or false. * * Example file: * @code * [ - * { "FileName": "foo", "IsPasswordProtected": true }, - * { "FileName": "aDir/", "IsDirectory": true } + * { "fullPath": "foo", "IsPasswordProtected": true }, + * { "fullPath": "aDir/", "IsDirectory": true } * ] * @endcode * * @author Raphael Kubo da Costa */ class JSONParser { public: typedef QMap JSONArchive; ~JSONParser(); static JSONArchive parse(QIODevice *json); private: JSONParser(); /** * Parses each entry in the QVariant obtained from parsing a JSON file and * creates a @c JSONArchive from them. * - * If an entry does not have a "FileName" key, it is ignored. Keys which do + * If an entry does not have a "fullPath" key, it is ignored. Keys which do * not correspond to a value in the EntryMetaDataType enum are ignored. * * @return A new @c JSONArchive corresponding to the parsed JSON file. If a * parsing error occurs, it is empty. */ static JSONArchive createJSONArchive(const QVariant &json); }; #endif // JSONPARSER_H diff --git a/autotests/plugins/cli7zplugin/cli7ztest.cpp b/autotests/plugins/cli7zplugin/cli7ztest.cpp index 4c9ecab2..e1162e94 100644 --- a/autotests/plugins/cli7zplugin/cli7ztest.cpp +++ b/autotests/plugins/cli7zplugin/cli7ztest.cpp @@ -1,380 +1,380 @@ /* * Copyright (c) 2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "cli7ztest.h" #include #include #include #include #include QTEST_GUILESS_MAIN(Cli7zTest) using namespace Kerfuffle; void Cli7zTest::initTestCase() { m_plugin = new Plugin(this); foreach (Plugin *plugin, m_pluginManger.availablePlugins()) { if (plugin->metaData().pluginId() == QStringLiteral("kerfuffle_cli7z")) { m_plugin = plugin; return; } } } void Cli7zTest::testArchive_data() { QTest::addColumn("archivePath"); QTest::addColumn("expectedFileName"); QTest::addColumn("isReadOnly"); QTest::addColumn("isSingleFolder"); QTest::addColumn("expectedEncryptionType"); QTest::addColumn("expectedSubfolderName"); QString archivePath = QFINDTESTDATA("data/one_toplevel_folder.7z"); QTest::newRow("archive with one top-level folder") << archivePath << QFileInfo(archivePath).fileName() << false << true << Archive::Unencrypted << QStringLiteral("A"); } void Cli7zTest::testArchive() { if (!m_plugin->isValid()) { QSKIP("cli7z plugin not available. Skipping test.", SkipSingle); } QFETCH(QString, archivePath); Archive *archive = Archive::create(archivePath, m_plugin, this); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not load the cli7z plugin. Skipping test.", SkipSingle); } QFETCH(QString, expectedFileName); QCOMPARE(QFileInfo(archive->fileName()).fileName(), expectedFileName); QFETCH(bool, isReadOnly); QCOMPARE(archive->isReadOnly(), isReadOnly); QFETCH(bool, isSingleFolder); QCOMPARE(archive->isSingleFolderArchive(), isSingleFolder); QFETCH(Archive::EncryptionType, expectedEncryptionType); QCOMPARE(archive->encryptionType(), expectedEncryptionType); QFETCH(QString, expectedSubfolderName); QCOMPARE(archive->subfolderName(), expectedSubfolderName); } void Cli7zTest::testList_data() { QTest::addColumn("outputTextFile"); QTest::addColumn("expectedEntriesCount"); // Index of some entry to be tested. QTest::addColumn("someEntryIndex"); // Entry metadata. QTest::addColumn("expectedName"); QTest::addColumn("isDirectory"); QTest::addColumn("isPasswordProtected"); QTest::addColumn("expectedSize"); QTest::addColumn("expectedTimestamp"); // p7zip version 15.14 tests QTest::newRow("normal-file-1514") << QFINDTESTDATA("data/archive-with-symlink-1514.txt") << 10 << 4 << QStringLiteral("testarchive/dir2/file2.txt") << false << false << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); QTest::newRow("encrypted-1514") << QFINDTESTDATA("data/archive-encrypted-1514.txt") << 9 << 3 << QStringLiteral("testarchive/dir1/file1.txt") << false << true << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); // p7zip version 15.09 tests QTest::newRow("normal-file-1509") << QFINDTESTDATA("data/archive-with-symlink-1509.txt") << 10 << 4 << QStringLiteral("testarchive/dir2/file2.txt") << false << false << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); QTest::newRow("encrypted-1509") << QFINDTESTDATA("data/archive-encrypted-1509.txt") << 9 << 3 << QStringLiteral("testarchive/dir1/file1.txt") << false << true << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); // p7zip version 9.38.1 tests QTest::newRow("normal-file-9381") << QFINDTESTDATA("data/archive-with-symlink-9381.txt") << 10 << 4 << QStringLiteral("testarchive/dir2/file2.txt") << false << false << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); QTest::newRow("encrypted-9381") << QFINDTESTDATA("data/archive-encrypted-9381.txt") << 9 << 3 << QStringLiteral("testarchive/dir1/file1.txt") << false << true << (qulonglong) 32 << QStringLiteral("2015-05-17T19:41:48"); } void Cli7zTest::testList() { CliPlugin *plugin = new CliPlugin(this, {QStringLiteral("dummy.7z")}); QSignalSpy signalSpy(plugin, SIGNAL(entry(ArchiveEntry))); QFETCH(QString, outputTextFile); QFETCH(int, expectedEntriesCount); QFile outputText(outputTextFile); QVERIFY(outputText.open(QIODevice::ReadOnly)); QTextStream outputStream(&outputText); while (!outputStream.atEnd()) { const QString line(outputStream.readLine()); QVERIFY(plugin->readListLine(line)); } QCOMPARE(signalSpy.count(), expectedEntriesCount); QFETCH(int, someEntryIndex); QVERIFY(someEntryIndex < signalSpy.count()); Archive::Entry *entry = signalSpy.at(someEntryIndex).at(0).value(); QFETCH(QString, expectedName); - QCOMPARE(entry->property("fileName").toString(), expectedName); + QCOMPARE(entry->property("fullPath").toString(), expectedName); QFETCH(bool, isDirectory); QCOMPARE(entry->isDir(), isDirectory); QFETCH(bool, isPasswordProtected); QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected); QFETCH(qulonglong, expectedSize); QCOMPARE(entry->property("size").toULongLong(), expectedSize); QFETCH(QString, expectedTimestamp); QCOMPARE(entry->property("timestamp").toString(), expectedTimestamp); plugin->deleteLater(); } void Cli7zTest::testListArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.7z") << QString() << QStringList { QStringLiteral("l"), QStringLiteral("-slt"), QStringLiteral("/tmp/foo.7z") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.7z") << QStringLiteral("1234") << QStringList { QStringLiteral("l"), QStringLiteral("-slt"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.7z") }; } void Cli7zTest::testListArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList listArgs = { QStringLiteral("l"), QStringLiteral("-slt"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$Archive") }; QFETCH(QString, password); const auto replacedArgs = plugin->substituteListVariables(listArgs, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void Cli7zTest::testAddArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("encryptHeader"); QTest::addColumn("compressionLevel"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.7z") << QString() << false << 5 << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("-mx=5") }; QTest::newRow("encrypted") << QStringLiteral("/tmp/foo.7z") << QStringLiteral("1234") << false << 5 << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("-p1234"), QStringLiteral("-mx=5") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.7z") << QStringLiteral("1234") << true << 5 << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("-p1234"), QStringLiteral("-mhe=on"), QStringLiteral("-mx=5") }; } void Cli7zTest::testAddArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList addArgs = { QStringLiteral("a"), QStringLiteral("$Archive"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$CompressionLevelSwitch"), QStringLiteral("$Files") }; QFETCH(QString, password); QFETCH(bool, encryptHeader); QFETCH(int, compressionLevel); QStringList replacedArgs = plugin->substituteAddVariables(addArgs, {}, password, encryptHeader, compressionLevel); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void Cli7zTest::testExtractArgs_data() { QTest::addColumn("archiveName"); - QTest::addColumn("files"); + QTest::addColumn>("files"); QTest::addColumn("preservePaths"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("preserve paths, encrypted") << QStringLiteral("/tmp/foo.7z") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << true << QStringLiteral("1234") << QStringList { QStringLiteral("x"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("preserve paths, unencrypted") << QStringLiteral("/tmp/foo.7z") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << true << QString() << QStringList { QStringLiteral("x"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, encrypted") << QStringLiteral("/tmp/foo.7z") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << false << QStringLiteral("1234") << QStringList { QStringLiteral("e"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, unencrypted") << QStringLiteral("/tmp/foo.7z") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << false << QString() << QStringList { QStringLiteral("e"), QStringLiteral("/tmp/foo.7z"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; } void Cli7zTest::testExtractArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList extractArgs = { QStringLiteral("$PreservePathSwitch"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$Archive"), QStringLiteral("$Files") }; - QFETCH(QVariantList, files); + QFETCH(QList, files); QFETCH(bool, preservePaths); QFETCH(QString, password); QStringList replacedArgs = plugin->substituteCopyVariables(extractArgs, files, preservePaths, password); QVERIFY(replacedArgs.size() >= extractArgs.size()); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } diff --git a/autotests/plugins/clirarplugin/clirartest.cpp b/autotests/plugins/clirarplugin/clirartest.cpp index 47b78c04..c6245c27 100644 --- a/autotests/plugins/clirarplugin/clirartest.cpp +++ b/autotests/plugins/clirarplugin/clirartest.cpp @@ -1,424 +1,424 @@ /* * Copyright (c) 2011,2014 Raphael Kubo da Costa * Copyright (c) 2015,2016 Ragnar Thomsen * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "clirartest.h" #include "kerfuffle/archiveentry.h" #include #include #include #include #include QTEST_GUILESS_MAIN(CliRarTest) using namespace Kerfuffle; void CliRarTest::initTestCase() { m_plugin = new Plugin(this); foreach (Plugin *plugin, m_pluginManger.availablePlugins()) { if (plugin->metaData().pluginId() == QStringLiteral("kerfuffle_clirar")) { m_plugin = plugin; return; } } } void CliRarTest::testArchive_data() { QTest::addColumn("archivePath"); QTest::addColumn("expectedFileName"); QTest::addColumn("isReadOnly"); QTest::addColumn("isSingleFolder"); QTest::addColumn("expectedEncryptionType"); QTest::addColumn("expectedSubfolderName"); QString archivePath = QFINDTESTDATA("data/one_toplevel_folder.rar"); QTest::newRow("archive with one top-level folder") << archivePath << QFileInfo(archivePath).fileName() << false << true << Archive::Unencrypted << QStringLiteral("A"); } void CliRarTest::testArchive() { if (!m_plugin->isValid()) { QSKIP("clirar plugin not available. Skipping test.", SkipSingle); } QFETCH(QString, archivePath); Archive *archive = Archive::create(archivePath, m_plugin, this); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not load the clirar plugin. Skipping test.", SkipSingle); } QFETCH(QString, expectedFileName); QCOMPARE(QFileInfo(archive->fileName()).fileName(), expectedFileName); QFETCH(bool, isReadOnly); QCOMPARE(archive->isReadOnly(), isReadOnly); QFETCH(bool, isSingleFolder); QCOMPARE(archive->isSingleFolderArchive(), isSingleFolder); QFETCH(Archive::EncryptionType, expectedEncryptionType); QCOMPARE(archive->encryptionType(), expectedEncryptionType); QFETCH(QString, expectedSubfolderName); QCOMPARE(archive->subfolderName(), expectedSubfolderName); } void CliRarTest::testList_data() { QTest::addColumn("outputTextFile"); QTest::addColumn("expectedEntriesCount"); // Index of some entry to be tested. QTest::addColumn("someEntryIndex"); // Entry metadata. QTest::addColumn("expectedName"); QTest::addColumn("isDirectory"); QTest::addColumn("isPasswordProtected"); QTest::addColumn("symlinkTarget"); QTest::addColumn("expectedSize"); QTest::addColumn("expectedCompressedSize"); QTest::addColumn("expectedTimestamp"); // Unrar 5 tests QTest::newRow("normal-file-unrar5") << QFINDTESTDATA("data/archive-with-symlink-unrar5.txt") << 8 << 2 << QStringLiteral("rartest/file2.txt") << false << false << QString() << (qulonglong) 14 << (qulonglong) 23 << QStringLiteral("2016-03-21T08:57:36"); QTest::newRow("symlink-unrar5") << QFINDTESTDATA("data/archive-with-symlink-unrar5.txt") << 8 << 3 << QStringLiteral("rartest/linktofile1.txt") << false << false << QStringLiteral("file1.txt") << (qulonglong) 9 << (qulonglong) 9 << QStringLiteral("2016-03-21T08:58:16"); QTest::newRow("encrypted-unrar5") << QFINDTESTDATA("data/archive-encrypted-unrar5.txt") << 7 << 2 << QStringLiteral("rartest/file2.txt") << false << true << QString() << (qulonglong) 14 << (qulonglong) 32 << QStringLiteral("2016-03-21T17:03:36"); QTest::newRow("recovery-record-unrar5") << QFINDTESTDATA("data/archive-recovery-record-unrar5.txt") << 3 << 0 << QStringLiteral("file1.txt") << false << false << QString() << (qulonglong) 32 << (qulonglong) 33 << QStringLiteral("2015-07-26T19:04:38"); QTest::newRow("corrupt-archive-unrar5") << QFINDTESTDATA("data/archive-corrupt-file-header-unrar5.txt") << 8 << 6 << QStringLiteral("dir1/") << true << false << QString() << (qulonglong) 0 << (qulonglong) 0 << QStringLiteral("2015-05-14T01:45:24"); // Unrar 4 tests QTest::newRow("normal-file-unrar4") << QFINDTESTDATA("data/archive-with-symlink-unrar4.txt") << 8 << 2 << QStringLiteral("rartest/file2.txt") << false << false << QString() << (qulonglong) 14 << (qulonglong) 23 << QStringLiteral("2016-03-21T08:57:00"); QTest::newRow("symlink-unrar4") << QFINDTESTDATA("data/archive-with-symlink-unrar4.txt") << 8 << 3 << QStringLiteral("rartest/linktofile1.txt") << false << false << QStringLiteral("file1.txt") << (qulonglong) 9 << (qulonglong) 9 << QStringLiteral("2016-03-21T08:58:00"); QTest::newRow("encrypted-unrar4") << QFINDTESTDATA("data/archive-encrypted-unrar4.txt") << 7 << 2 << QStringLiteral("rartest/file2.txt") << false << true << QString() << (qulonglong) 14 << (qulonglong) 32 << QStringLiteral("2016-03-21T17:03:00"); QTest::newRow("recovery-record-unrar4") << QFINDTESTDATA("data/archive-recovery-record-unrar4.txt") << 3 << 0 << QStringLiteral("file1.txt") << false << false << QString() << (qulonglong) 32 << (qulonglong) 33 << QStringLiteral("2015-07-26T19:04:00"); QTest::newRow("corrupt-archive-unrar4") << QFINDTESTDATA("data/archive-corrupt-file-header-unrar4.txt") << 8 << 6 << QStringLiteral("dir1/") << true << false << QString() << (qulonglong) 0 << (qulonglong) 0 << QStringLiteral("2015-05-14T01:45:00"); /* * Check that the plugin will not crash when reading corrupted archives, which * have lines such as "Unexpected end of archive" or "??? - the file header is * corrupt" instead of a file name and the header string after it. * * See bug 262857 and commit 2042997013432cdc6974f5b26d39893a21e21011. */ QTest::newRow("corrupt-archive-unrar3") << QFINDTESTDATA("data/archive-corrupt-file-header-unrar3.txt") << 1 << 0 << QStringLiteral("some-file.ext") << false << false << QString() << (qulonglong) 732522496 << (qulonglong) 14851208 << QStringLiteral("2010-10-29T20:47:00"); } void CliRarTest::testList() { CliPlugin *rarPlugin = new CliPlugin(this, {QStringLiteral("dummy.rar")}); QSignalSpy signalSpy(rarPlugin, SIGNAL(entry(ArchiveEntry))); QFETCH(QString, outputTextFile); QFETCH(int, expectedEntriesCount); QFile outputText(outputTextFile); QVERIFY(outputText.open(QIODevice::ReadOnly)); QTextStream outputStream(&outputText); while (!outputStream.atEnd()) { const QString line(outputStream.readLine()); QVERIFY(rarPlugin->readListLine(line)); } QCOMPARE(signalSpy.count(), expectedEntriesCount); QFETCH(int, someEntryIndex); QVERIFY(someEntryIndex < signalSpy.count()); Archive::Entry *entry = signalSpy.at(someEntryIndex).at(0).value(); QFETCH(QString, expectedName); - QCOMPARE(entry->property("fileName").toString(), expectedName); + QCOMPARE(entry->property("fullPath").toString(), expectedName); QFETCH(bool, isDirectory); QCOMPARE(entry->isDir(), isDirectory); QFETCH(bool, isPasswordProtected); QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected); QFETCH(QString, symlinkTarget); QCOMPARE(entry->property("link").toString(), symlinkTarget); QFETCH(qulonglong, expectedSize); QCOMPARE(entry->property("size").toULongLong(), expectedSize); QFETCH(qulonglong, expectedCompressedSize); QCOMPARE(entry->property("compressedSize").toULongLong(), expectedCompressedSize); QFETCH(QString, expectedTimestamp); QCOMPARE(entry->property("timestamp").toString(), expectedTimestamp); rarPlugin->deleteLater(); } void CliRarTest::testListArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.rar") << QString() << QStringList { QStringLiteral("vt"), QStringLiteral("-v"), QStringLiteral("/tmp/foo.rar") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.rar") << QStringLiteral("1234") << QStringList { QStringLiteral("vt"), QStringLiteral("-v"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.rar") }; } void CliRarTest::testListArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList listArgs = { QStringLiteral("vt"), QStringLiteral("-v"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$Archive") }; QFETCH(QString, password); const auto replacedArgs = plugin->substituteListVariables(listArgs, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void CliRarTest::testAddArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("encryptHeader"); QTest::addColumn("compressionLevel"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.rar") << QString() << false << 3 << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("-m3") }; QTest::newRow("encrypted") << QStringLiteral("/tmp/foo.rar") << QStringLiteral("1234") << false << 3 << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("-p1234"), QStringLiteral("-m3") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.rar") << QStringLiteral("1234") << true << 3 << QStringList { QStringLiteral("a"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("-hp1234"), QStringLiteral("-m3") }; } void CliRarTest::testAddArgs() { QFETCH(QString, archiveName); CliPlugin *rarPlugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(rarPlugin); const QStringList addArgs = { QStringLiteral("a"), QStringLiteral("$Archive"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$CompressionLevelSwitch"), QStringLiteral("$Files") }; QFETCH(QString, password); QFETCH(bool, encryptHeader); QFETCH(int, compressionLevel); QStringList replacedArgs = rarPlugin->substituteAddVariables(addArgs, {}, password, encryptHeader, compressionLevel); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); rarPlugin->deleteLater(); } void CliRarTest::testExtractArgs_data() { QTest::addColumn("archiveName"); - QTest::addColumn("files"); + QTest::addColumn>("files"); QTest::addColumn("preservePaths"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("preserve paths, encrypted") << QStringLiteral("/tmp/foo.rar") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << true << QStringLiteral("1234") << QStringList { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("x"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("preserve paths, unencrypted") << QStringLiteral("/tmp/foo.rar") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << true << QString() << QStringList { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("x"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, encrypted") << QStringLiteral("/tmp/foo.rar") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << false << QStringLiteral("1234") << QStringList { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("e"), QStringLiteral("-p1234"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, unencrypted") << QStringLiteral("/tmp/foo.rar") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << false << QString() << QStringList { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("e"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; } void CliRarTest::testExtractArgs() { QFETCH(QString, archiveName); CliPlugin *rarPlugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(rarPlugin); const QStringList extractArgs = { QStringLiteral("-kb"), QStringLiteral("-p-"), QStringLiteral("$PreservePathSwitch"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$Archive"), QStringLiteral("$Files") }; - QFETCH(QVariantList, files); + QFETCH(QList, files); QFETCH(bool, preservePaths); QFETCH(QString, password); QStringList replacedArgs = rarPlugin->substituteCopyVariables(extractArgs, files, preservePaths, password); QVERIFY(replacedArgs.size() >= extractArgs.size()); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); rarPlugin->deleteLater(); } diff --git a/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp b/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp index a913b1ad..1b6ee6ba 100644 --- a/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp +++ b/autotests/plugins/cliunarchiverplugin/cliunarchivertest.cpp @@ -1,383 +1,383 @@ /* * Copyright (C) 2016 Elvis Angelaccio * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliunarchivertest.h" #include "jobs.h" #include "kerfuffle/archiveentry.h" #include #include #include #include #include #include QTEST_GUILESS_MAIN(CliUnarchiverTest) using namespace Kerfuffle; void CliUnarchiverTest::initTestCase() { m_plugin = new Plugin(this); foreach (Plugin *plugin, m_pluginManger.availablePlugins()) { if (plugin->metaData().pluginId() == QStringLiteral("kerfuffle_cliunarchiver")) { m_plugin = plugin; return; } } } void CliUnarchiverTest::testArchive_data() { QTest::addColumn("archivePath"); QTest::addColumn("expectedFileName"); QTest::addColumn("isReadOnly"); QTest::addColumn("isSingleFolder"); QTest::addColumn("expectedEncryptionType"); QTest::addColumn("expectedSubfolderName"); QString archivePath = QFINDTESTDATA("data/one_toplevel_folder.rar"); QTest::newRow("archive with one top-level folder") << archivePath << QFileInfo(archivePath).fileName() << true << true << Archive::Unencrypted << QStringLiteral("A"); archivePath = QFINDTESTDATA("data/multiple_toplevel_entries.rar"); QTest::newRow("archive with multiple top-level entries") << archivePath << QFileInfo(archivePath).fileName() << true << false << Archive::Unencrypted << QStringLiteral("multiple_toplevel_entries"); archivePath = QFINDTESTDATA("data/encrypted_entries.rar"); QTest::newRow("archive with encrypted entries") << archivePath << QFileInfo(archivePath).fileName() << true << true << Archive::Encrypted << QStringLiteral("A"); } void CliUnarchiverTest::testArchive() { if (!m_plugin->isValid()) { QSKIP("cliunarchiver plugin not available. Skipping test.", SkipSingle); } QFETCH(QString, archivePath); Archive *archive = Archive::create(archivePath, m_plugin, this); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not load the cliunarchiver plugin. Skipping test.", SkipSingle); } QFETCH(QString, expectedFileName); QCOMPARE(QFileInfo(archive->fileName()).fileName(), expectedFileName); QFETCH(bool, isReadOnly); QCOMPARE(archive->isReadOnly(), isReadOnly); QFETCH(bool, isSingleFolder); QCOMPARE(archive->isSingleFolderArchive(), isSingleFolder); QFETCH(Archive::EncryptionType, expectedEncryptionType); QCOMPARE(archive->encryptionType(), expectedEncryptionType); QFETCH(QString, expectedSubfolderName); QCOMPARE(archive->subfolderName(), expectedSubfolderName); } void CliUnarchiverTest::testList_data() { QTest::addColumn("jsonFilePath"); QTest::addColumn("expectedEntriesCount"); // Index of some entry to be tested. QTest::addColumn("someEntryIndex"); // Entry metadata. QTest::addColumn("expectedName"); QTest::addColumn("isDirectory"); QTest::addColumn("isPasswordProtected"); QTest::addColumn("expectedSize"); QTest::addColumn("expectedCompressedSize"); QTest::addColumn("expectedTimestamp"); QTest::newRow("archive with one top-level folder") << QFINDTESTDATA("data/one_toplevel_folder.json") << 9 << 6 << QStringLiteral("A/B/C/") << true << false << (qulonglong) 0 << (qulonglong) 0 << QStringLiteral("2015-12-21 16:57:20 +0100"); QTest::newRow("archive with multiple top-level entries") << QFINDTESTDATA("data/multiple_toplevel_entries.json") << 12 << 4 << QStringLiteral("data/A/B/test1.txt") << false << false << (qulonglong) 7 << (qulonglong) 7 << QStringLiteral("2015-12-21 16:56:28 +0100"); QTest::newRow("archive with encrypted entries") << QFINDTESTDATA("data/encrypted_entries.json") << 9 << 5 << QStringLiteral("A/test1.txt") << false << true << (qulonglong) 7 << (qulonglong) 32 << QStringLiteral("2015-12-21 16:56:28 +0100"); QTest::newRow("huge archive") << QFINDTESTDATA("data/huge_archive.json") << 250 << 8 << QStringLiteral("PsycOPacK/Base Dictionnaries/att800") << false << false << (qulonglong) 593687 << (qulonglong) 225219 << QStringLiteral("2011-08-14 03:10:10 +0200"); } void CliUnarchiverTest::testList() { CliPlugin *unarPlugin = new CliPlugin(this, {QStringLiteral("dummy.rar")}); QSignalSpy signalSpy(unarPlugin, SIGNAL(entry(ArchiveEntry))); QFETCH(QString, jsonFilePath); QFETCH(int, expectedEntriesCount); QFile jsonFile(jsonFilePath); QVERIFY(jsonFile.open(QIODevice::ReadOnly)); QTextStream stream(&jsonFile); unarPlugin->setJsonOutput(stream.readAll()); QCOMPARE(signalSpy.count(), expectedEntriesCount); QFETCH(int, someEntryIndex); QVERIFY(someEntryIndex < signalSpy.count()); Archive::Entry *entry = signalSpy.at(someEntryIndex).at(0).value(); QFETCH(QString, expectedName); - QCOMPARE(entry->property("fileName").toString(), expectedName); + QCOMPARE(entry->property("fullPath").toString(), expectedName); QFETCH(bool, isDirectory); QCOMPARE(entry->isDir(), isDirectory); QFETCH(bool, isPasswordProtected); QCOMPARE(entry->property("isPasswordProtected").toBool(), isPasswordProtected); QFETCH(qulonglong, expectedSize); QCOMPARE(entry->property("size").toULongLong(), expectedSize); QFETCH(qulonglong, expectedCompressedSize); QCOMPARE(entry->property("compressedSize").toULongLong(), expectedCompressedSize); QFETCH(QString, expectedTimestamp); QCOMPARE(entry->property("timestamp").toString(), expectedTimestamp); unarPlugin->deleteLater(); } void CliUnarchiverTest::testListArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.rar") << QString() << QStringList { QStringLiteral("-json"), QStringLiteral("/tmp/foo.rar") }; QTest::newRow("header-encrypted") << QStringLiteral("/tmp/foo.rar") << QStringLiteral("1234") << QStringList { QStringLiteral("-json"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("-password"), QStringLiteral("1234") }; } void CliUnarchiverTest::testListArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList listArgs = { QStringLiteral("-json"), QStringLiteral("$Archive"), QStringLiteral("$PasswordSwitch") }; QFETCH(QString, password); const auto replacedArgs = plugin->substituteListVariables(listArgs, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void CliUnarchiverTest::testExtraction_data() { QTest::addColumn("archivePath"); - QTest::addColumn("entriesToExtract"); + QTest::addColumn>("entriesToExtract"); QTest::addColumn("extractionOptions"); QTest::addColumn("expectedExtractedEntriesCount"); ExtractionOptions options; options[QStringLiteral("AlwaysUseTmpDir")] = true; ExtractionOptions optionsPreservePaths = options; optionsPreservePaths[QStringLiteral("PreservePaths")] = true; ExtractionOptions dragAndDropOptions = optionsPreservePaths; dragAndDropOptions[QStringLiteral("DragAndDrop")] = true; QTest::newRow("extract the whole multiple_toplevel_entries.rar") << QFINDTESTDATA("data/multiple_toplevel_entries.rar") - << QVariantList() + << QList() << optionsPreservePaths << 12; QTest::newRow("extract selected entries from a rar, without paths") << QFINDTESTDATA("data/one_toplevel_folder.rar") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/test2.txt"), QStringLiteral("A"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B"))) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/test2.txt"), QStringLiteral("A")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B")) } << options << 2; QTest::newRow("extract selected entries from a rar, preserve paths") << QFINDTESTDATA("data/one_toplevel_folder.rar") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/test2.txt"), QStringLiteral("A"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B"))) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/test2.txt"), QStringLiteral("A")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/B/test1.txt"), QStringLiteral("A/B")) } << optionsPreservePaths << 4; QTest::newRow("extract selected entries from a rar, drag-and-drop") << QFINDTESTDATA("data/one_toplevel_folder.rar") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/C/"), QStringLiteral("A/B/"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/test2.txt"), QStringLiteral("A/"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/C/test1.txt"), QStringLiteral("A/B/"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("A/B/C/test2.txt"), QStringLiteral("A/B/"))) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/B/C/"), QStringLiteral("A/B/")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/test2.txt"), QStringLiteral("A/")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/B/C/test1.txt"), QStringLiteral("A/B/")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("A/B/C/test2.txt"), QStringLiteral("A/B/")) } << dragAndDropOptions << 4; QTest::newRow("rar with empty folders") << QFINDTESTDATA("data/empty_folders.rar") - << QVariantList() + << QList() << optionsPreservePaths << 5; } // TODO: we can remove this test (which is duplicated from kerfuffle/archivetest) // if we ever ends up using a temp dir for any cliplugin, instead of only for cliunarchiver. void CliUnarchiverTest::testExtraction() { if (!m_plugin->isValid()) { QSKIP("cliunarchiver plugin not available. Skipping test.", SkipSingle); } QFETCH(QString, archivePath); Archive *archive = Archive::create(archivePath, m_plugin, this); QVERIFY(archive); if (!archive->isValid()) { QSKIP("Could not load the cliunarchiver plugin. Skipping test.", SkipSingle); } QTemporaryDir destDir; if (!destDir.isValid()) { QSKIP("Could not create a temporary directory for extraction. Skipping test.", SkipSingle); } - QFETCH(QVariantList, entriesToExtract); + QFETCH(QList, entriesToExtract); QFETCH(ExtractionOptions, extractionOptions); auto extractionJob = archive->copyFiles(entriesToExtract, destDir.path(), extractionOptions); QEventLoop eventLoop(this); connect(extractionJob, &KJob::result, &eventLoop, &QEventLoop::quit); extractionJob->start(); eventLoop.exec(); // krazy:exclude=crashy QFETCH(int, expectedExtractedEntriesCount); int extractedEntriesCount = 0; QDirIterator dirIt(destDir.path(), QDir::AllEntries | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (dirIt.hasNext()) { extractedEntriesCount++; dirIt.next(); } QCOMPARE(extractedEntriesCount, expectedExtractedEntriesCount); archive->deleteLater(); } void CliUnarchiverTest::testExtractArgs_data() { QTest::addColumn("archiveName"); - QTest::addColumn("files"); + QTest::addColumn>("files"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("encrypted, multiple files") << QStringLiteral("/tmp/foo.rar") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << QStringLiteral("1234") << QStringList { QStringLiteral("-D"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), QStringLiteral("-password"), QStringLiteral("1234") }; QTest::newRow("unencrypted, multiple files") << QStringLiteral("/tmp/foo.rar") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << QString() << QStringList { QStringLiteral("-D"), QStringLiteral("/tmp/foo.rar"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; } void CliUnarchiverTest::testExtractArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList extractArgs = { QStringLiteral("-D"), QStringLiteral("$Archive"), QStringLiteral("$Files"), QStringLiteral("$PasswordSwitch") }; - QFETCH(QVariantList, files); + QFETCH(QList, files); QFETCH(QString, password); QStringList replacedArgs = plugin->substituteCopyVariables(extractArgs, files, false, password); QVERIFY(replacedArgs.size() >= extractArgs.size()); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } diff --git a/autotests/plugins/clizipplugin/cliziptest.cpp b/autotests/plugins/clizipplugin/cliziptest.cpp index 5f066d8f..99a11e70 100644 --- a/autotests/plugins/clizipplugin/cliziptest.cpp +++ b/autotests/plugins/clizipplugin/cliziptest.cpp @@ -1,203 +1,203 @@ /* * Copyright (c) 2016 Elvis Angelaccio * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "cliziptest.h" #include QTEST_GUILESS_MAIN(CliZipTest) using namespace Kerfuffle; void CliZipTest::testListArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("expectedArgs"); QTest::newRow("fake zip") << QStringLiteral("/tmp/foo.zip") << QStringList { QStringLiteral("-l"), QStringLiteral("-T"), QStringLiteral("-z"), QStringLiteral("/tmp/foo.zip") }; } void CliZipTest::testListArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList listArgs = { QStringLiteral("-l"), QStringLiteral("-T"), QStringLiteral("-z"), QStringLiteral("$Archive") }; const auto replacedArgs = plugin->substituteListVariables(listArgs, QString()); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void CliZipTest::testAddArgs_data() { QTest::addColumn("archiveName"); QTest::addColumn("password"); QTest::addColumn("compressionLevel"); QTest::addColumn("expectedArgs"); QTest::newRow("unencrypted") << QStringLiteral("/tmp/foo.zip") << QString() << 3 << QStringList { QStringLiteral("-r"), QStringLiteral("/tmp/foo.zip"), QStringLiteral("-3") }; QTest::newRow("encrypted") << QStringLiteral("/tmp/foo.zip") << QStringLiteral("1234") << 3 << QStringList { QStringLiteral("-r"), QStringLiteral("/tmp/foo.zip"), QStringLiteral("-P1234"), QStringLiteral("-3") }; } void CliZipTest::testAddArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList addArgs = { QStringLiteral("-r"), QStringLiteral("$Archive"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$CompressionLevelSwitch"), QStringLiteral("$Files") }; QFETCH(QString, password); QFETCH(int, compressionLevel); QStringList replacedArgs = plugin->substituteAddVariables(addArgs, {}, password, false, compressionLevel); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } void CliZipTest::testExtractArgs_data() { QTest::addColumn("archiveName"); - QTest::addColumn("files"); + QTest::addColumn>("files"); QTest::addColumn("preservePaths"); QTest::addColumn("password"); QTest::addColumn("expectedArgs"); QTest::newRow("preserve paths, encrypted") << QStringLiteral("/tmp/foo.zip") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << true << QStringLiteral("1234") << QStringList { QStringLiteral("-P1234"), QStringLiteral("/tmp/foo.zip"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("preserve paths, unencrypted") << QStringLiteral("/tmp/foo.zip") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << true << QString() << QStringList { QStringLiteral("/tmp/foo.zip"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, encrypted") << QStringLiteral("/tmp/foo.zip") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << false << QStringLiteral("1234") << QStringList { QStringLiteral("-j"), QStringLiteral("-P1234"), QStringLiteral("/tmp/foo.zip"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; QTest::newRow("without paths, unencrypted") << QStringLiteral("/tmp/foo.zip") - << QVariantList { - QVariant::fromValue(fileRootNodePair(QStringLiteral("aDir/b.txt"), QStringLiteral("aDir"))), - QVariant::fromValue(fileRootNodePair(QStringLiteral("c.txt"), QString())) + << QList { + new Archive::Entry(Q_NULLPTR, QStringLiteral("aDir/b.txt"), QStringLiteral("aDir")), + new Archive::Entry(Q_NULLPTR, QStringLiteral("c.txt"), QString()) } << false << QString() << QStringList { QStringLiteral("-j"), QStringLiteral("/tmp/foo.zip"), QStringLiteral("aDir/b.txt"), QStringLiteral("c.txt"), }; } void CliZipTest::testExtractArgs() { QFETCH(QString, archiveName); CliPlugin *plugin = new CliPlugin(this, {QVariant(archiveName)}); QVERIFY(plugin); const QStringList extractArgs = { QStringLiteral("$PreservePathSwitch"), QStringLiteral("$PasswordSwitch"), QStringLiteral("$Archive"), QStringLiteral("$Files") }; - QFETCH(QVariantList, files); + QFETCH(QList, files); QFETCH(bool, preservePaths); QFETCH(QString, password); QStringList replacedArgs = plugin->substituteCopyVariables(extractArgs, files, preservePaths, password); QFETCH(QStringList, expectedArgs); QCOMPARE(replacedArgs, expectedArgs); plugin->deleteLater(); } diff --git a/kerfuffle/addtoarchive.cpp b/kerfuffle/addtoarchive.cpp index 0f0fa1c4..dd0ed572 100644 --- a/kerfuffle/addtoarchive.cpp +++ b/kerfuffle/addtoarchive.cpp @@ -1,270 +1,271 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2008 Harald Hvaal * Copyright (C) 2009 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "addtoarchive.h" #include "ark_debug.h" #include "archive_kerfuffle.h" #include "createdialog.h" #include "jobs.h" #include #include #include #include #include #include #include #include #include #include #include namespace Kerfuffle { AddToArchive::AddToArchive(QObject *parent) : KJob(parent), m_changeToFirstPath(false) { } AddToArchive::~AddToArchive() { } void AddToArchive::setAutoFilenameSuffix(const QString& suffix) { m_autoFilenameSuffix = suffix; } void AddToArchive::setChangeToFirstPath(bool value) { m_changeToFirstPath = value; } void AddToArchive::setFilename(const QUrl &path) { m_filename = path.toDisplayString(QUrl::PreferLocalFile); } void AddToArchive::setMimeType(const QString & mimeType) { m_mimeType = mimeType; } void AddToArchive::setPassword(const QString &password) { m_password = password; } void AddToArchive::setHeaderEncryptionEnabled(bool enabled) { m_enableHeaderEncryption = enabled; } bool AddToArchive::showAddDialog() { qCDebug(ARK) << "Opening add dialog"; QPointer dialog = new Kerfuffle::CreateDialog( Q_NULLPTR, // parent i18n("Compress to Archive"), // caption QUrl::fromLocalFile(m_firstPath)); // startDir bool ret = dialog.data()->exec(); if (ret) { qCDebug(ARK) << "CreateDialog returned URL:" << dialog.data()->selectedUrl().toString(); qCDebug(ARK) << "CreateDialog returned mime:" << dialog.data()->currentMimeType().name(); setFilename(dialog.data()->selectedUrl()); setMimeType(dialog.data()->currentMimeType().name()); setPassword(dialog.data()->password()); setHeaderEncryptionEnabled(dialog.data()->isHeaderEncryptionEnabled()); } delete dialog.data(); return ret; } bool AddToArchive::addInput(const QUrl &url) { - m_inputs << url.toDisplayString(QUrl::PreferLocalFile); + Archive::Entry *entry = new Archive::Entry(Q_NULLPTR); + entry->setFullPath(url.toDisplayString(QUrl::PreferLocalFile)); + m_entries << entry; if (m_firstPath.isEmpty()) { QString firstEntry = url.toDisplayString(QUrl::PreferLocalFile); m_firstPath = QFileInfo(firstEntry).dir().absolutePath(); } return true; } void AddToArchive::start() { qCDebug(ARK) << "Starting job"; QTimer::singleShot(0, this, &AddToArchive::slotStartJob); } void AddToArchive::slotStartJob() { Kerfuffle::CompressionOptions options; - if (m_inputs.isEmpty()) { + if (m_entries.isEmpty()) { KMessageBox::error(NULL, i18n("No input files were given.")); emitResult(); return; } Kerfuffle::Archive *archive; if (!m_filename.isEmpty()) { archive = Kerfuffle::Archive::create(m_filename, m_mimeType, this); qCDebug(ARK) << "Set filename to " << m_filename; } else { if (m_autoFilenameSuffix.isEmpty()) { KMessageBox::error(Q_NULLPTR, xi18n("You need to either supply a filename for the archive or a suffix (such as rar, tar.gz) with the --autofilename argument.")); emitResult(); return; } if (m_firstPath.isEmpty()) { qCWarning(ARK) << "Weird, this should not happen. no firstpath defined. aborting"; emitResult(); return; } - const QString base = detectBaseName(m_inputs); + const QString base = detectBaseName(m_entries); QString finalName = base + QLatin1Char( '.' ) + m_autoFilenameSuffix; //if file already exists, append a number to the base until it doesn't //exist int appendNumber = 0; while (QFileInfo::exists(finalName)) { ++appendNumber; finalName = base + QLatin1Char( '_' ) + QString::number(appendNumber) + QLatin1Char( '.' ) + m_autoFilenameSuffix; } qCDebug(ARK) << "Autoset filename to "<< finalName; archive = Kerfuffle::Archive::create(finalName, m_mimeType, this); } Q_ASSERT(archive); if (!archive->isValid()) { if (archive->error() == NoPlugin) { KMessageBox::error(Q_NULLPTR, i18n("Failed to create the new archive. No suitable plugin found.")); emitResult(); return; } if (archive->error() == FailedPlugin) { KMessageBox::error(Q_NULLPTR, i18n("Failed to create the new archive. Could not load a suitable plugin.")); emitResult(); return; } } else if (archive->isReadOnly()) { KMessageBox::error(Q_NULLPTR, i18n("It is not possible to create archives of this type.")); emitResult(); return; } if (!m_password.isEmpty()) { archive->encrypt(m_password, m_enableHeaderEncryption); } if (m_changeToFirstPath) { if (m_firstPath.isEmpty()) { qCWarning(ARK) << "Weird, this should not happen. no firstpath defined. aborting"; emitResult(); return; } const QDir stripDir(m_firstPath); - for (int i = 0; i < m_inputs.size(); ++i) { - m_inputs[i] = stripDir.absoluteFilePath(m_inputs.at(i)); + foreach (Archive::Entry *entry, m_entries) { + entry->setFullPath(stripDir.absoluteFilePath(entry->property("fullPath").toString())); } - options[QStringLiteral( "GlobalWorkDir" )] = stripDir.path(); qCDebug(ARK) << "Setting GlobalWorkDir to " << stripDir.path(); } Kerfuffle::AddJob *job = - archive->addFiles(m_inputs, options); + archive->addFiles(m_entries, options); KIO::getJobTracker()->registerJob(job); connect(job, &Kerfuffle::AddJob::result, this, &AddToArchive::slotFinished); job->start(); } void AddToArchive::slotFinished(KJob *job) { qCDebug(ARK) << "AddToArchive job finished"; if (job->error() && !job->errorText().isEmpty()) { KMessageBox::error(Q_NULLPTR, job->errorText()); } emitResult(); } -QString AddToArchive::detectBaseName(const QStringList &paths) const +QString AddToArchive::detectBaseName(const QList &entries) const { - QFileInfo fileInfo = QFileInfo(paths.first()); + QFileInfo fileInfo = QFileInfo(entries.first()->property("fullPath").toString()); QDir parentDir = fileInfo.dir(); QString base = parentDir.absolutePath() + QLatin1Char('/'); - if (paths.size() > 1) { + 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 b6766dce..460e9f8e 100644 --- a/kerfuffle/addtoarchive.h +++ b/kerfuffle/addtoarchive.h @@ -1,92 +1,93 @@ /* * 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 = 0); ~AddToArchive(); bool showAddDialog(); void setPreservePaths(bool value); void setChangeToFirstPath(bool value); - QString detectBaseName(const QStringList &paths) const; + QString detectBaseName(const QList &entries) const; public 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() Q_DECL_OVERRIDE; private slots: void slotFinished(KJob*); void slotStartJob(); private: QString m_filename; QString m_strippedPath; QString m_autoFilenameSuffix; QString m_firstPath; QString m_mimeType; QString m_password; - QStringList m_inputs; + QList m_entries; bool m_changeToFirstPath; bool m_enableHeaderEncryption; }; } #endif // ADDTOARCHIVE_H diff --git a/kerfuffle/archive_kerfuffle.cpp b/kerfuffle/archive_kerfuffle.cpp index 47cdb16c..14cca8bb 100644 --- a/kerfuffle/archive_kerfuffle.cpp +++ b/kerfuffle/archive_kerfuffle.cpp @@ -1,463 +1,448 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008 Harald Hvaal * Copyright (c) 2009-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "archive_kerfuffle.h" #include "archiveentry.h" -#include "ark_debug.h" #include "archiveinterface.h" #include "jobs.h" #include "mimetypes.h" #include "pluginmanager.h" -#include -#include #include -#include -#include -#include #include #include namespace Kerfuffle { -QDebug operator<<(QDebug d, const fileRootNodePair &pair) -{ - d.nospace() << "fileRootNodePair(" << pair.file << "," << pair.rootNode << ")"; - return d.space(); -} - Archive *Archive::create(const QString &fileName, QObject *parent) { return create(fileName, QString(), parent); } Archive *Archive::create(const QString &fileName, const QString &fixedMimeType, QObject *parent) { qCDebug(ARK) << "Going to create archive" << fileName; PluginManager pluginManager; const QMimeType mimeType = fixedMimeType.isEmpty() ? determineMimeType(fileName) : QMimeDatabase().mimeTypeForName(fixedMimeType); const QVector offers = pluginManager.preferredPluginsFor(mimeType); if (offers.isEmpty()) { qCCritical(ARK) << "Could not find a plugin to handle" << fileName; return new Archive(NoPlugin, parent); } Archive *archive; foreach (Plugin *plugin, offers) { archive = create(fileName, plugin, parent); // Use the first valid plugin, according to the priority sorting. if (archive->isValid()) { return archive; } } qCCritical(ARK) << "Failed to find a usable plugin for" << fileName; return archive; } Archive *Archive::create(const QString &fileName, Plugin *plugin, QObject *parent) { Q_ASSERT(plugin); qCDebug(ARK) << "Checking plugin" << plugin->metaData().pluginId(); KPluginFactory *factory = KPluginLoader(plugin->metaData().fileName()).factory(); if (!factory) { qCWarning(ARK) << "Invalid plugin factory for" << plugin->metaData().pluginId(); return new Archive(FailedPlugin, parent); } const QVariantList args = {QVariant(QFileInfo(fileName).absoluteFilePath())}; ReadOnlyArchiveInterface *iface = factory->create(Q_NULLPTR, args); if (!iface) { qCWarning(ARK) << "Could not create plugin instance" << plugin->metaData().pluginId(); return new Archive(FailedPlugin, parent); } if (!plugin->isValid()) { qCDebug(ARK) << "Cannot use plugin" << plugin->metaData().pluginId() << "- check whether" << plugin->readOnlyExecutables() << "are installed."; return new Archive(FailedPlugin, parent); } qCDebug(ARK) << "Successfully loaded plugin" << plugin->metaData().pluginId(); return new Archive(iface, !plugin->isReadWrite(), parent); } Archive::Archive(ArchiveError errorCode, QObject *parent) : QObject(parent) , m_iface(Q_NULLPTR) , m_error(errorCode) { qCDebug(ARK) << "Created archive instance with error"; } Archive::Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent) : QObject(parent) , m_iface(archiveInterface) , m_hasBeenListed(false) , m_isReadOnly(isReadOnly) , m_isSingleFolderArchive(false) , m_extractedFilesSize(0) , m_error(NoError) , m_encryptionType(Unencrypted) , m_numberOfFiles(0) { qCDebug(ARK) << "Created archive instance"; Q_ASSERT(archiveInterface); archiveInterface->setParent(this); - QMetaType::registerComparators(); - QMetaType::registerDebugStreamOperator(); - connect(m_iface, &ReadOnlyArchiveInterface::entry, this, &Archive::onNewEntry); } Archive::~Archive() { } QString Archive::completeBaseName() const { QString base = QFileInfo(fileName()).completeBaseName(); // Special case for compressed tar archives. if (base.right(4).toUpper() == QLatin1String(".TAR")) { base.chop(4); } return base; } QString Archive::fileName() const { return isValid() ? m_iface->filename() : QString(); } QString Archive::comment() const { return isValid() ? m_iface->comment() : QString(); } CommentJob* Archive::addComment(const QString &comment) { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to add comment:" << comment; Q_ASSERT(!isReadOnly()); CommentJob *job = new CommentJob(comment, static_cast(m_iface)); return job; } TestJob* Archive::testArchive() { if (!isValid()) { return Q_NULLPTR; } qCDebug(ARK) << "Going to test archive"; TestJob *job = new TestJob(m_iface); return job; } QMimeType Archive::mimeType() { if (!isValid()) { return QMimeType(); } if (!m_mimeType.isValid()) { m_mimeType = determineMimeType(fileName()); } return m_mimeType; } bool Archive::isReadOnly() const { return isValid() ? (m_iface->isReadOnly() || m_isReadOnly) : false; } bool Archive::isSingleFolderArchive() { if (!isValid()) { return false; } listIfNotListed(); return m_isSingleFolderArchive; } bool Archive::hasComment() const { return isValid() ? !comment().isEmpty() : false; } Archive::EncryptionType Archive::encryptionType() { if (!isValid()) { return Unencrypted; } listIfNotListed(); return m_encryptionType; } qulonglong Archive::numberOfFiles() { if (!isValid()) { return 0; } listIfNotListed(); return m_numberOfFiles; } qulonglong Archive::unpackedSize() { if (!isValid()) { return 0; } listIfNotListed(); return m_extractedFilesSize; } qulonglong Archive::packedSize() const { return isValid() ? QFileInfo(fileName()).size() : 0; } QString Archive::subfolderName() { if (!isValid()) { return QString(); } listIfNotListed(); return m_subfolderName; } void Archive::onNewEntry(const Archive::Entry *entry) { if (!entry->isDir()) { m_numberOfFiles++; } } bool Archive::isValid() const { return m_iface && (m_error == NoError); } ArchiveError Archive::error() const { return m_error; } KJob* Archive::open() { return 0; } KJob* Archive::create() { return 0; } ListJob* Archive::list() { if (!isValid() || !QFileInfo::exists(fileName())) { return Q_NULLPTR; } qCDebug(ARK) << "Going to list files"; ListJob *job = new ListJob(m_iface); //if this job has not been listed before, we grab the opportunity to //collect some information about the archive if (!m_hasBeenListed) { connect(job, &ListJob::result, this, &Archive::onListFinished); } return job; } -DeleteJob* Archive::deleteFiles(const QList & files) +DeleteJob* Archive::deleteFiles(QList &entries) { if (!isValid()) { return Q_NULLPTR; } - qCDebug(ARK) << "Going to delete files" << files; + qCDebug(ARK) << "Going to delete entries" << entries; if (m_iface->isReadOnly()) { return 0; } - DeleteJob *newJob = new DeleteJob(files, static_cast(m_iface)); + DeleteJob *newJob = new DeleteJob(entries, static_cast(m_iface)); return newJob; } -AddJob* Archive::addFiles(const QStringList & files, const CompressionOptions& options) +AddJob* Archive::addFiles(QList &files, const CompressionOptions& options) { if (!isValid()) { return Q_NULLPTR; } CompressionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral("PasswordProtectedHint")] = true; } qCDebug(ARK) << "Going to add files" << files << "with options" << newOptions; Q_ASSERT(!m_iface->isReadOnly()); AddJob *newJob = new AddJob(files, newOptions, static_cast(m_iface)); connect(newJob, &AddJob::result, this, &Archive::onAddFinished); return newJob; } -ExtractJob* Archive::copyFiles(const QList& files, const QString& destinationDir, const ExtractionOptions& options) +ExtractJob* Archive::copyFiles(const QList &files, const QString& destinationDir, const ExtractionOptions& options) { if (!isValid()) { return Q_NULLPTR; } ExtractionOptions newOptions = options; if (encryptionType() != Unencrypted) { newOptions[QStringLiteral( "PasswordProtectedHint" )] = true; } ExtractJob *newJob = new ExtractJob(files, destinationDir, newOptions, m_iface); return newJob; } -PreviewJob *Archive::preview(const QString &file) +PreviewJob *Archive::preview(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } - PreviewJob *job = new PreviewJob(file, (encryptionType() != Unencrypted), m_iface); + PreviewJob *job = new PreviewJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } -OpenJob *Archive::open(const QString &file) +OpenJob *Archive::open(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } - OpenJob *job = new OpenJob(file, (encryptionType() != Unencrypted), m_iface); + OpenJob *job = new OpenJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } -OpenWithJob *Archive::openWith(const QString &file) +OpenWithJob *Archive::openWith(Archive::Entry *entry) { if (!isValid()) { return Q_NULLPTR; } - OpenWithJob *job = new OpenWithJob(file, (encryptionType() != Unencrypted), m_iface); + OpenWithJob *job = new OpenWithJob(entry, (encryptionType() != Unencrypted), m_iface); return job; } void Archive::encrypt(const QString &password, bool encryptHeader) { if (!isValid()) { return; } m_iface->setPassword(password); m_iface->setHeaderEncryptionEnabled(encryptHeader); m_encryptionType = encryptHeader ? HeaderEncrypted : Encrypted; } void Archive::onAddFinished(KJob* job) { //if the archive was previously a single folder archive and an add job //has successfully finished, then it is no longer a single folder //archive (for the current implementation, which does not allow adding //folders/files other places than the root. //TODO: handle the case of creating a new file and singlefolderarchive //then. if (m_isSingleFolderArchive && !job->error()) { m_isSingleFolderArchive = false; } } void Archive::onListFinished(KJob* job) { ListJob *ljob = qobject_cast(job); m_extractedFilesSize = ljob->extractedFilesSize(); m_isSingleFolderArchive = ljob->isSingleFolderArchive(); m_subfolderName = ljob->subfolderName(); if (m_subfolderName.isEmpty()) { m_subfolderName = completeBaseName(); } if (ljob->isPasswordProtected()) { // If we already know the password, it means that the archive is header-encrypted. m_encryptionType = m_iface->password().isEmpty() ? Encrypted : HeaderEncrypted; } m_hasBeenListed = true; } void Archive::listIfNotListed() { if (!m_hasBeenListed) { ListJob *job = list(); if (!job) { return; } connect(job, &ListJob::userQuery, this, &Archive::onUserQuery); QEventLoop loop(this); connect(job, &KJob::result, &loop, &QEventLoop::quit); job->start(); loop.exec(); // krazy:exclude=crashy } } void Archive::onUserQuery(Query* query) { query->execute(); } } // namespace Kerfuffle diff --git a/kerfuffle/archive_kerfuffle.h b/kerfuffle/archive_kerfuffle.h index 112d6384..cf01deb3 100644 --- a/kerfuffle/archive_kerfuffle.h +++ b/kerfuffle/archive_kerfuffle.h @@ -1,234 +1,232 @@ /* * 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 #include #include #include #include class KJob; namespace Kerfuffle { class ListJob; class ExtractJob; class DeleteJob; class AddJob; class CommentJob; class TestJob; class OpenJob; class OpenWithJob; class Plugin; class PreviewJob; class Query; class ReadOnlyArchiveInterface; enum ArchiveError { NoError = 0, NoPlugin, FailedPlugin }; /** These are the extra options for doing the compression. Naming convention is CamelCase with either Global, or the compression type (such as Zip, Rar, etc), followed by the property name used */ typedef QHash CompressionOptions; typedef QHash ExtractionOptions; /** * Stores a filename and rootnode pair. This is used to cut an individual * rootnode from the path of each file, e.g. when drag'n'drop extracting a * selection of files. */ -struct fileRootNodePair +struct fileRootNodePair_ { QString file; QString rootNode; - fileRootNodePair() + fileRootNodePair_() {} - fileRootNodePair(const QString &f) + fileRootNodePair_(const QString &f) : file(f) {} - fileRootNodePair(const QString &f, const QString &n) + fileRootNodePair_(const QString &f, const QString &n) : file(f), rootNode(n) {} // Required to compare QVariants with this type. - bool operator==(const fileRootNodePair &right) const + bool operator==(const fileRootNodePair_ &right) const { if (file == right.file) return true; else return false; } - bool operator<(const fileRootNodePair &) const + bool operator<(const fileRootNodePair_ &) const { return false; } }; -QDebug operator<<(QDebug d, const fileRootNodePair &pair); class KERFUFFLE_EXPORT Archive : public QObject { Q_OBJECT Q_ENUMS(EncryptionType) /** * Complete base name, without the "tar" extension (if any). */ Q_PROPERTY(QString completeBaseName READ completeBaseName CONSTANT) Q_PROPERTY(QString fileName READ fileName CONSTANT) Q_PROPERTY(QString comment READ comment CONSTANT) Q_PROPERTY(QMimeType mimeType READ mimeType CONSTANT) Q_PROPERTY(bool isReadOnly READ isReadOnly CONSTANT) Q_PROPERTY(bool isSingleFolderArchive READ isSingleFolderArchive) Q_PROPERTY(EncryptionType encryptionType READ encryptionType) Q_PROPERTY(qulonglong numberOfFiles READ numberOfFiles) Q_PROPERTY(qulonglong unpackedSize READ unpackedSize) Q_PROPERTY(qulonglong packedSize READ packedSize) Q_PROPERTY(QString subfolderName READ subfolderName) public: enum EncryptionType { Unencrypted, Encrypted, HeaderEncrypted }; class Entry; QString completeBaseName() const; QString fileName() const; QString comment() const; QMimeType mimeType(); bool isReadOnly() const; bool isSingleFolderArchive(); bool hasComment() const; EncryptionType encryptionType(); qulonglong numberOfFiles(); qulonglong unpackedSize(); qulonglong packedSize() const; QString subfolderName(); static Archive *create(const QString &fileName, QObject *parent = 0); static Archive *create(const QString &fileName, const QString &fixedMimeType, QObject *parent = 0); /** * Create an archive instance from a given @p plugin. * @param fileName The name of the archive. * @return A valid archive if the plugin could be loaded, an invalid one otherwise (with the FailedPlugin error set). */ static Archive *create(const QString &fileName, Plugin *plugin, QObject *parent = Q_NULLPTR); ~Archive(); ArchiveError error() const; bool isValid() const; KJob* open(); KJob* create(); /** * @return A ListJob if the archive already exists. A null pointer otherwise. */ ListJob* list(); - DeleteJob* deleteFiles(const QList & files); + DeleteJob* deleteFiles(QList &entries); CommentJob* addComment(const QString &comment); TestJob* testArchive(); /** * Compression options that should be handled by all interfaces: * * GlobalWorkDir - Change to this dir before adding the new files. * The path names should then be added relative to this directory. * * TODO: find a way to actually add files to specific locations in * the archive * (not supported yet) GlobalPathInArchive - a path relative to the * archive root where the files will be added under * */ - AddJob* addFiles(const QStringList & files, const CompressionOptions& options = CompressionOptions()); + AddJob* addFiles(QList &files, const CompressionOptions& options = CompressionOptions()); - ExtractJob* copyFiles(const QList &files, const QString &destinationDir, const ExtractionOptions &options = ExtractionOptions()); + ExtractJob* copyFiles(const QList &files, const QString &destinationDir, const ExtractionOptions &options = ExtractionOptions()); - PreviewJob* preview(const QString &file); - OpenJob* open(const QString &file); - OpenWithJob* openWith(const QString &file); + PreviewJob* preview(Archive::Entry *entry); + OpenJob* open(Archive::Entry *entry); + OpenWithJob* openWith(Archive::Entry *entry); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ void encrypt(const QString &password, bool encryptHeader); private slots: void onListFinished(KJob*); void onAddFinished(KJob*); void onUserQuery(Kerfuffle::Query*); void onNewEntry(const Archive::Entry *entry); private: Archive(ReadOnlyArchiveInterface *archiveInterface, bool isReadOnly, QObject *parent = 0); Archive(ArchiveError errorCode, QObject *parent = 0); void listIfNotListed(); ReadOnlyArchiveInterface *m_iface; bool m_hasBeenListed; bool m_isReadOnly; bool m_isSingleFolderArchive; QString m_subfolderName; qulonglong m_extractedFilesSize; ArchiveError m_error; EncryptionType m_encryptionType; qulonglong m_numberOfFiles; QMimeType m_mimeType; }; } // namespace Kerfuffle Q_DECLARE_METATYPE(Kerfuffle::Archive::EncryptionType) -Q_DECLARE_METATYPE(Kerfuffle::fileRootNodePair) #endif // ARCHIVE_H diff --git a/kerfuffle/archiveentry.cpp b/kerfuffle/archiveentry.cpp index e94cf94b..07a30bcd 100644 --- a/kerfuffle/archiveentry.cpp +++ b/kerfuffle/archiveentry.cpp @@ -1,162 +1,182 @@ // // Created by mvlabat on 5/27/16. // #include "archiveentry.h" namespace Kerfuffle { -Archive::Entry::Entry(Entry *parent) +Archive::Entry::Entry(Entry *parent, QString fullPath, QString rootNode) : QObject(parent) + , rootNode(rootNode) , compressedSizeIsSet(true) , m_parent(parent) { clearMetaData(); + if (!fullPath.isEmpty()) + setFullPath(fullPath); } Archive::Entry::~Entry() { clear(); } QList Archive::Entry::entries() { Q_ASSERT(isDir()); return m_entries; } const QList Archive::Entry::entries() const { Q_ASSERT(isDir()); return m_entries; } void Archive::Entry::setEntryAt(int index, Entry *value) { Q_ASSERT(isDir()); m_entries[index] = value; } void Archive::Entry::appendEntry(Entry *entry) { Q_ASSERT(isDir()); m_entries.append(entry); } void Archive::Entry::removeEntryAt(int index) { Q_ASSERT(isDir()); delete m_entries.takeAt(index); } Archive::Entry *Archive::Entry::getParent() const { return m_parent; } void Archive::Entry::setParent(Archive::Entry *parent) { m_parent = parent; } +void Archive::Entry::setFullPath(const QString &fullPath) +{ + m_fullPath = fullPath; + processNameAndIcon(); +} + +void Archive::Entry::setIsDirectory(const bool isDirectory) +{ + m_isDirectory = isDirectory; + processNameAndIcon(); +} + int Archive::Entry::row() const { if (getParent()) { return getParent()->entries().indexOf(const_cast(this)); } return 0; } bool Archive::Entry::isDir() const { return m_isDirectory; } void Archive::Entry::processNameAndIcon() { - const QStringList pieces = m_fileName.split(QLatin1Char( '/' ), QString::SkipEmptyParts); + const QStringList pieces = m_fullPath.split(QLatin1Char( '/' ), QString::SkipEmptyParts); m_name = pieces.isEmpty() ? QString() : pieces.last(); QMimeDatabase db; if (isDir()) { m_icon = QIcon::fromTheme(db.mimeTypeForName(QStringLiteral("inode/directory")).iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small)); } else { - m_icon = QIcon::fromTheme(db.mimeTypeForFile(m_fileName).iconName()).pixmap(IconSize(KIconLoader::Small), + m_icon = QIcon::fromTheme(db.mimeTypeForFile(m_fullPath).iconName()).pixmap(IconSize(KIconLoader::Small), IconSize(KIconLoader::Small)); } } QPixmap Archive::Entry::icon() const { return m_icon; } QString Archive::Entry::name() const { return m_name; } Archive::Entry *Archive::Entry::find(const QString & name) { foreach(Entry *entry, m_entries) { if (entry && (entry->name() == name)) { return entry; } } return 0; } Archive::Entry *Archive::Entry::findByPath(const QStringList & pieces, int index) { if (index == pieces.count()) { return 0; } Entry *next = find(pieces.at(index)); if (index == pieces.count() - 1) { return next; } if (next && next->isDir()) { return next->findByPath(pieces, index + 1); } return 0; } void Archive::Entry::clearMetaData() { - m_fileName.clear(); + m_fullPath.clear(); m_permissions.clear(); m_owner.clear(); m_group.clear(); m_size = 0; m_compressedSize = 0; m_link.clear(); m_ratio.clear(); m_CRC.clear(); m_method.clear(); m_version.clear(); m_timestamp = QDateTime(); m_isDirectory = false; m_comment.clear(); m_isPasswordProtected = false; } void Archive::Entry::returnDirEntries(QList *store) { foreach(Entry *entry, m_entries) { if (entry->isDir()) { store->prepend(entry); entry->returnDirEntries(store); } } } void Archive::Entry::clear() { if (isDir()) { qDeleteAll(m_entries); m_entries.clear(); } } +bool Archive::Entry::operator==(const Archive::Entry *right) const +{ + return m_fullPath == right->m_fullPath; +} + } diff --git a/kerfuffle/archiveentry.h b/kerfuffle/archiveentry.h index 2d9dc900..c9501316 100644 --- a/kerfuffle/archiveentry.h +++ b/kerfuffle/archiveentry.h @@ -1,101 +1,109 @@ // // Created by mvlabat on 5/27/16. // #ifndef ARK_ENTRY_H #define ARK_ENTRY_H #include "archive_kerfuffle.h" #include "app/ark_debug.h" #include #include #include #include #include namespace Kerfuffle { class Archive::Entry : public QObject { Q_OBJECT /** * Meta data related to one entry in a compressed archive. * * When creating a plugin, information about every single entry in * an archive is contained in an ArchiveEntry, and metadata * is set with the entries in this enum. * * Please notice that not all archive formats support all the properties * below, so set those that are available. */ - Q_PROPERTY(QString fileName MEMBER m_fileName) + 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) + Q_PROPERTY(bool isDirectory MEMBER m_isDirectory WRITE setIsDirectory) Q_PROPERTY(QString comment MEMBER m_comment) Q_PROPERTY(bool isPasswordProtected MEMBER m_isPasswordProtected) public: - Entry(Entry *parent); + Entry(Entry *parent, QString fullPath = QString(), QString rootNode = QString()); ~Entry(); QList entries(); const QList 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); + void setIsDirectory(const bool isDirectory); int row() const; bool isDir() const; - void processNameAndIcon(); QPixmap icon() const; QString name() const; Entry *find(const QString & name); Entry *findByPath(const QStringList & pieces, int index = 0); void clearMetaData(); void returnDirEntries(QList *store); void clear(); + bool operator==(const Archive::Entry *right) const; + +private: + void processNameAndIcon(); + public: + QString rootNode; bool compressedSizeIsSet; private: QList m_entries; QPixmap m_icon; QString m_name; Entry *m_parent; - QString m_fileName; + 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; QString m_comment; bool m_isPasswordProtected; }; } #endif //ARK_ENTRY_H diff --git a/kerfuffle/archiveinterface.h b/kerfuffle/archiveinterface.h index 0afffa60..84b8c4a9 100644 --- a/kerfuffle/archiveinterface.h +++ b/kerfuffle/archiveinterface.h @@ -1,157 +1,157 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef ARCHIVEINTERFACE_H #define ARCHIVEINTERFACE_H #include "archive_kerfuffle.h" #include "archive_entry.h" #include "kerfuffle_export.h" #include #include #include #include namespace Kerfuffle { class Query; class KERFUFFLE_EXPORT ReadOnlyArchiveInterface: public QObject { Q_OBJECT public: explicit ReadOnlyArchiveInterface(QObject *parent, const QVariantList & args); virtual ~ReadOnlyArchiveInterface(); /** * Returns the filename of the archive currently being handled. */ QString filename() const; /** * Returns the comment of the archive. */ QString comment() const; /** * @return The password of the archive, if any. */ QString password() const; /** * Returns whether the file can only be read. * * @return @c true The file cannot be written. * @return @c false The file can be read and written. */ virtual bool isReadOnly() const; virtual bool open(); /** * List archive contents. * This runs the process of reading archive contents. * When subclassing, you can block as long as you need (unless you called setWaitForFinishedSignal(true)). * @returns whether the listing succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ virtual bool list() = 0; virtual bool testArchive() = 0; void setPassword(const QString &password); void setHeaderEncryptionEnabled(bool enabled); /** * Extract files from archive. * Globally recognized extraction options: * @li PreservePaths - preserve file paths (extract flat if false) * @li RootNode - node in the archive which will correspond to the @arg destinationDirectory * When subclassing, you can block as long as you need (unless you called setWaitForFinishedSignal(true)). * @returns whether the listing succeeded. * @note If returning false, make sure to emit the error() signal beforewards to notify * the user of the error condition. */ - virtual bool copyFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) = 0; + virtual bool copyFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) = 0; bool waitForFinishedSignal(); virtual bool doKill(); virtual bool doSuspend(); virtual bool doResume(); bool isHeaderEncryptionEnabled() const; signals: void cancelled(); void error(const QString &message, const QString &details = QString()); void entry(Archive::Entry *archiveEntry); void entryRemoved(const QString &path); void progress(double progress); void info(const QString &info); void finished(bool result); void userQuery(Query *query); void testSuccess(); protected: /** * Setting this option to true will not run the functions in their own thread. * Instead it will be necessary to call finished(bool) when the operation is actually finished. */ void setWaitForFinishedSignal(bool value); void setCorrupt(bool isCorrupt); bool isCorrupt() const; QString m_comment; private: QString m_filename; QString m_password; bool m_waitForFinishedSignal; bool m_isHeaderEncryptionEnabled; bool m_isCorrupt; }; class KERFUFFLE_EXPORT ReadWriteArchiveInterface: public ReadOnlyArchiveInterface { Q_OBJECT public: explicit ReadWriteArchiveInterface(QObject *parent, const QVariantList & args); virtual ~ReadWriteArchiveInterface(); bool isReadOnly() const Q_DECL_OVERRIDE; //see archive.h for a list of what the compressionoptions might //contain - virtual bool addFiles(const QStringList & files, const CompressionOptions& options) = 0; - virtual bool deleteFiles(const QList & files) = 0; + virtual bool addFiles(const QList &files, const CompressionOptions& options) = 0; + virtual bool deleteFiles(const QList &files) = 0; virtual bool addComment(const QString &comment) = 0; }; } // namespace Kerfuffle #endif // ARCHIVEINTERFACE_H diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp index f72b3ea9..ba8712d6 100644 --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -1,1259 +1,1258 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "cliinterface.h" #include "ark_debug.h" #include "queries.h" #ifdef Q_OS_WIN # include #else # include # include #endif #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include namespace Kerfuffle { CliInterface::CliInterface(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args), m_process(0), m_listEmptyLines(false), m_abortingOperation(false), m_extractTempDir(Q_NULLPTR), m_commentTempFile(Q_NULLPTR) { //because this interface uses the event loop setWaitForFinishedSignal(true); if (QMetaType::type("QProcess::ExitStatus") == 0) { qRegisterMetaType("QProcess::ExitStatus"); } } void CliInterface::cacheParameterList() { m_param = parameterList(); Q_ASSERT(m_param.contains(ExtractProgram)); Q_ASSERT(m_param.contains(ListProgram)); Q_ASSERT(m_param.contains(PreservePathSwitch)); Q_ASSERT(m_param.contains(FileExistsExpression)); Q_ASSERT(m_param.contains(FileExistsInput)); } CliInterface::~CliInterface() { Q_ASSERT(!m_process); delete m_commentTempFile; } void CliInterface::setListEmptyLines(bool emptyLines) { m_listEmptyLines = emptyLines; } bool CliInterface::list() { resetParsing(); cacheParameterList(); m_operationMode = List; const auto args = substituteListVariables(m_param.value(ListArgs).toStringList(), password()); if (!runProcess(m_param.value(ListProgram).toStringList(), args)) { return false; } return true; } -bool CliInterface::copyFiles(const QVariantList &files, const QString &destinationDirectory, const ExtractionOptions &options) +bool CliInterface::copyFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) { qCDebug(ARK) << Q_FUNC_INFO << "to" << destinationDirectory; cacheParameterList(); m_operationMode = Copy; m_compressionOptions = options; m_copiedFiles = files; m_extractDestDir = destinationDirectory; const QStringList extractArgs = m_param.value(ExtractArgs).toStringList(); if (extractArgs.contains(QStringLiteral("$PasswordSwitch")) && options.value(QStringLiteral("PasswordProtectedHint")).toBool() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } // Populate the argument list. const QStringList args = substituteCopyVariables(extractArgs, files, options.value(QStringLiteral("PreservePaths")).toBool(), password()); QUrl destDir = QUrl(destinationDirectory); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); bool useTmpExtractDir = options.value(QStringLiteral("DragAndDrop")).toBool() || options.value(QStringLiteral("AlwaysUseTmpDir")).toBool(); if (useTmpExtractDir) { Q_ASSERT(!m_extractTempDir); m_extractTempDir = new QTemporaryDir(QApplication::applicationName() + QLatin1Char('-')); qCDebug(ARK) << "Using temporary extraction dir:" << m_extractTempDir->path(); if (!m_extractTempDir->isValid()) { qCDebug(ARK) << "Creation of temporary directory failed."; emit finished(false); return false; } m_oldWorkingDir = QDir::currentPath(); destDir = QUrl(m_extractTempDir->path()); QDir::setCurrent(destDir.adjusted(QUrl::RemoveScheme).url()); } if (!runProcess(m_param.value(ExtractProgram).toStringList(), args)) { return false; } return true; } -bool CliInterface::addFiles(const QStringList & files, const CompressionOptions& options) +bool CliInterface::addFiles(const QList &files, const CompressionOptions& options) { cacheParameterList(); m_operationMode = Add; const QStringList addArgs = m_param.value(AddArgs).toStringList(); if (addArgs.contains(QStringLiteral("$PasswordSwitch")) && options.value(QStringLiteral("PasswordProtectedHint")).toBool() && password().isEmpty()) { qCDebug(ARK) << "Password hint enabled, querying user"; if (!passwordQuery()) { return false; } } int compLevel = options.value(QStringLiteral("CompressionLevel"), -1).toInt(); const auto args = substituteAddVariables(m_param.value(AddArgs).toStringList(), files, password(), isHeaderEncryptionEnabled(), compLevel); - if (!runProcess(m_param.value(AddProgram).toStringList(), args)) { - return false; - } - - return true; + return runProcess(m_param.value(AddProgram).toStringList(), args); } -bool CliInterface::deleteFiles(const QList & files) +bool CliInterface::deleteFiles(const QList &files) { cacheParameterList(); m_operationMode = Delete; m_removedFiles = files; const auto deleteArgs = m_param.value(DeleteArgs).toStringList(); const auto args = substituteDeleteVariables(deleteArgs, files, password()); - if (!runProcess(m_param.value(DeleteProgram).toStringList(), args)) { - return false; - } - - return true; + return runProcess(m_param.value(DeleteProgram).toStringList(), args); } bool CliInterface::testArchive() { resetParsing(); cacheParameterList(); m_operationMode = Test; const auto args = substituteTestVariables(m_param.value(TestArgs).toStringList()); - if (!runProcess(m_param.value(TestProgram).toStringList(), args)) { - return false; - } - - return true; + return runProcess(m_param.value(TestProgram).toStringList(), args); } bool CliInterface::runProcess(const QStringList& programNames, const QStringList& arguments) { Q_ASSERT(!m_process); QString programPath; for (int i = 0; i < programNames.count(); i++) { programPath = QStandardPaths::findExecutable(programNames.at(i)); if (!programPath.isEmpty()) break; } if (programPath.isEmpty()) { const QString names = programNames.join(QStringLiteral(", ")); emit error(xi18ncp("@info", "Failed to locate program %2 on disk.", "Failed to locate programs %2 on disk.", programNames.count(), names)); emit finished(false); return false; } qCDebug(ARK) << "Executing" << programPath << arguments << "within directory" << QDir::currentPath(); #ifdef Q_OS_WIN m_process = new KProcess; #else m_process = new KPtyProcess; m_process->setPtyChannels(KPtyProcess::StdinChannel); #endif m_process->setOutputChannelMode(KProcess::MergedChannels); m_process->setNextOpenMode(QIODevice::ReadWrite | QIODevice::Unbuffered | QIODevice::Text); m_process->setProgram(programPath, arguments); connect(m_process, SIGNAL(readyReadStandardOutput()), SLOT(readStdout()), Qt::DirectConnection); if (m_operationMode == Copy) { // Extraction jobs need a dedicated post-processing function. connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::copyProcessFinished, Qt::DirectConnection); } else { connect(m_process, static_cast(&KPtyProcess::finished), this, &CliInterface::processFinished, Qt::DirectConnection); } m_stdOutData.clear(); m_process->start(); return true; } void CliInterface::processFinished(int exitCode, QProcess::ExitStatus exitStatus) { m_exitCode = exitCode; qCDebug(ARK) << "Process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { //handle all the remaining data in the process readStdout(true); delete m_process; m_process = Q_NULLPTR; } // #193908 - #222392 // Don't emit finished() if the job was killed quietly. if (m_abortingOperation) { return; } if (m_operationMode == Delete) { - foreach(const QVariant& v, m_removedFiles) { - emit entryRemoved(v.toString()); + foreach(const Archive::Entry *e, m_removedFiles) { + emit entryRemoved(e->property("fullPath").toString()); } } if (m_operationMode == Add) { list(); } else if (m_operationMode == List && isCorrupt()) { Kerfuffle::LoadCorruptQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (!query.responseYes()) { emit cancelled(); emit finished(false); } else { emit progress(1.0); emit finished(true); } } else { emit progress(1.0); emit finished(true); } } void CliInterface::copyProcessFinished(int exitCode, QProcess::ExitStatus exitStatus) { Q_ASSERT(m_operationMode == Copy); m_exitCode = exitCode; qCDebug(ARK) << "Extraction process finished, exitcode:" << exitCode << "exitstatus:" << exitStatus; if (m_process) { // Handle all the remaining data in the process. readStdout(true); delete m_process; m_process = Q_NULLPTR; } if (m_compressionOptions.value(QStringLiteral("AlwaysUseTmpDir")).toBool()) { // unar exits with code 1 if extraction fails. // This happens at least with wrong passwords or not enough space in the destination folder. if (m_exitCode == 1) { if (password().isEmpty()) { qCWarning(ARK) << "Extraction aborted, destination folder might not have enough space."; emit error(i18n("Extraction failed. Make sure that enough space is available.")); } else { qCWarning(ARK) << "Extraction aborted, either the password is wrong or the destination folder doesn't have enough space."; emit error(i18n("Extraction failed. Make sure you provided the correct password and that enough space is available.")); setPassword(QString()); } copyProcessCleanup(); emit finished(false); return; } if (!m_compressionOptions.value(QStringLiteral("DragAndDrop")).toBool()) { if (!moveToDestination(QDir::current(), QDir(m_extractDestDir), m_compressionOptions[QStringLiteral("PreservePaths")].toBool())) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_copiedFiles.size())); copyProcessCleanup(); emit finished(false); return; } copyProcessCleanup(); } } if (m_compressionOptions.value(QStringLiteral("DragAndDrop")).toBool()) { if (!moveDroppedFilesToDest(m_copiedFiles, m_extractDestDir)) { emit error(i18ncp("@info", "Could not move the extracted file to the destination directory.", "Could not move the extracted files to the destination directory.", m_copiedFiles.size())); copyProcessCleanup(); emit finished(false); return; } copyProcessCleanup(); } emit progress(1.0); emit finished(true); } -bool CliInterface::moveDroppedFilesToDest(const QVariantList &files, const QString &finalDest) +bool CliInterface::moveDroppedFilesToDest(const QList &files, const QString &finalDest) { // Move extracted files from a QTemporaryDir to the final destination. QDir finalDestDir(finalDest); qCDebug(ARK) << "Setting final dir to" << finalDest; bool overwriteAll = false; bool skipAll = false; - foreach (const QVariant& file, files) { + foreach (const Archive::Entry *file, files) { - QFileInfo relEntry(file.value().file.remove(file.value().rootNode)); - QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + file.value().file); + QString fullPath = file->property("fullPath").toString(); + QFileInfo relEntry(fullPath.remove(file->rootNode)); + QFileInfo absSourceEntry(QDir::current().absolutePath() + QLatin1Char('/') + fullPath); QFileInfo absDestEntry(finalDestDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absSourceEntry.isDir()) { // For directories, just create the path. if (!finalDestDir.mkpath(relEntry.filePath())) { qCWarning(ARK) << "Failed to create directory" << relEntry.filePath() << "in final destination."; } } else { // If destination file exists, prompt the user. if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; if (!skipAll && !overwriteAll) { Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); emit userQuery(&query); query.waitForResponse(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } } // Create any parent directories. if (!finalDestDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } // Move files to the final destination. if (!QFile(absSourceEntry.absoluteFilePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << absSourceEntry.filePath() << "to final destination."; return false; } } } return true; } bool CliInterface::isEmptyDir(const QDir &dir) { QDir d = dir; d.setFilter(QDir::AllEntries | QDir::NoDotAndDotDot); return d.count() == 0; } void CliInterface::copyProcessCleanup() { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } if (m_extractTempDir) { delete m_extractTempDir; m_extractTempDir = Q_NULLPTR; } } bool CliInterface::moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths) { qCDebug(ARK) << "Moving extracted files from temp dir" << tempDir.path() << "to final destination" << destDir.path(); bool overwriteAll = false; bool skipAll = false; QDirIterator dirIt(tempDir.path(), QDir::AllEntries | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (dirIt.hasNext()) { dirIt.next(); // We skip directories if: // 1. We are not preserving paths // 2. The dir is not empty. Only empty directories need to be explicitly moved. // The non-empty ones are created by QDir::mkpath() below. if (dirIt.fileInfo().isDir()) { if (!preservePaths || !isEmptyDir(QDir(dirIt.filePath()))) { continue; } } QFileInfo relEntry; if (preservePaths) { relEntry = QFileInfo(dirIt.filePath().remove(tempDir.path() + QLatin1Char('/'))); } else { relEntry = QFileInfo(dirIt.fileName()); } QFileInfo absDestEntry(destDir.path() + QLatin1Char('/') + relEntry.filePath()); if (absDestEntry.exists()) { qCWarning(ARK) << "File" << absDestEntry.absoluteFilePath() << "exists."; Kerfuffle::OverwriteQuery query(absDestEntry.absoluteFilePath()); query.setNoRenameMode(true); emit userQuery(&query); query.waitForResponse(); if (query.responseOverwrite() || query.responseOverwriteAll()) { if (query.responseOverwriteAll()) { overwriteAll = true; } if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } else if (query.responseSkip() || query.responseAutoSkip()) { if (query.responseAutoSkip()) { skipAll = true; } continue; } else if (query.responseCancelled()) { qCDebug(ARK) << "Copy action cancelled."; return false; } } else if (skipAll) { continue; } else if (overwriteAll) { if (!QFile::remove(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to remove" << absDestEntry.absoluteFilePath(); } } if (preservePaths) { // Create any parent directories. if (!destDir.mkpath(relEntry.path())) { qCWarning(ARK) << "Failed to create parent directory for file:" << absDestEntry.filePath(); } } // Move file to the final destination. if (!QFile(dirIt.filePath()).rename(absDestEntry.absoluteFilePath())) { qCWarning(ARK) << "Failed to move file" << dirIt.filePath() << "to final destination."; return false; } } return true; } QStringList CliInterface::substituteListVariables(const QStringList &listArgs, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, listArgs) { if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } // Simple argument (e.g. -slt in 7z), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } -QStringList CliInterface::substituteCopyVariables(const QStringList &extractArgs, const QVariantList &files, bool preservePaths, const QString &password) +QStringList CliInterface::substituteCopyVariables(const QStringList &extractArgs, const QList &entries, bool preservePaths, const QString &password) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, extractArgs) { qCDebug(ARK) << "Processing argument" << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PreservePathSwitch")) { args << preservePathSwitch(preservePaths); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } if (arg == QLatin1String("$Files")) { - args << copyFilesList(files); + args << copyFilesList(entries); continue; } // Simple argument (e.g. -kb in unrar), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } -QStringList CliInterface::substituteAddVariables(const QStringList &addArgs, const QStringList &files, const QString &password, bool encryptHeader, int compLevel) +QStringList CliInterface::substituteAddVariables(const QStringList &addArgs, const QList &entries, const QString &password, bool encryptHeader, int compLevel) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, addArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << (encryptHeader ? passwordHeaderSwitch(password) : passwordSwitch(password)); continue; } if (arg == QLatin1String("$CompressionLevelSwitch")) { args << compressionLevelSwitch(compLevel); continue; } if (arg == QLatin1String("$Files")) { - args << files; + args << entryFileNames(entries); continue; } // Simple argument (e.g. a in 7z), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } -QStringList CliInterface::substituteDeleteVariables(const QStringList &deleteArgs, const QVariantList &files, const QString &password) +QStringList CliInterface::substituteDeleteVariables(const QStringList &deleteArgs, const QList &entries, const QString &password) { cacheParameterList(); QStringList args; foreach (const QString& arg, deleteArgs) { qCDebug(ARK) << "Processing argument" << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$PasswordSwitch")) { args << passwordSwitch(password); continue; } if (arg == QLatin1String("$Files")) { - foreach (const QVariant& file, files) { - args << escapeFileName(file.toString()); + foreach (const Archive::Entry *e, entries) { + args << escapeFileName(e->property("fileName").toString()); } continue; } // Simple argument (e.g. d in rar), nothing to substitute, just add it to the list. args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteCommentVariables(const QStringList &commentArgs, const QString &commentFile) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, commentArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } if (arg == QLatin1String("$CommentSwitch")) { QString commentSwitch = m_param.value(CommentSwitch).toString(); commentSwitch.replace(QStringLiteral("$CommentFile"), commentFile); args << commentSwitch; continue; } args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QStringList CliInterface::substituteTestVariables(const QStringList &testArgs) { // Required if we call this function from unit tests. cacheParameterList(); QStringList args; foreach (const QString& arg, testArgs) { qCDebug(ARK) << "Processing argument " << arg; if (arg == QLatin1String("$Archive")) { args << filename(); continue; } args << arg; } // Remove empty strings, if any. args.removeAll(QString()); return args; } QString CliInterface::preservePathSwitch(bool preservePaths) const { Q_ASSERT(m_param.contains(PreservePathSwitch)); const QStringList theSwitch = m_param.value(PreservePathSwitch).toStringList(); Q_ASSERT(theSwitch.size() == 2); return (preservePaths ? theSwitch.at(0) : theSwitch.at(1)); } QStringList CliInterface::passwordHeaderSwitch(const QString& password) const { if (password.isEmpty()) { return QStringList(); } Q_ASSERT(m_param.contains(PasswordHeaderSwitch)); QStringList passwordHeaderSwitch = m_param.value(PasswordHeaderSwitch).toStringList(); Q_ASSERT(!passwordHeaderSwitch.isEmpty() && passwordHeaderSwitch.size() <= 2); if (passwordHeaderSwitch.size() == 1) { passwordHeaderSwitch[0].replace(QLatin1String("$Password"), password); } else { passwordHeaderSwitch[1] = password; } return passwordHeaderSwitch; } QStringList CliInterface::passwordSwitch(const QString& password) const { if (password.isEmpty()) { return QStringList(); } Q_ASSERT(m_param.contains(PasswordSwitch)); QStringList passwordSwitch = m_param.value(PasswordSwitch).toStringList(); Q_ASSERT(!passwordSwitch.isEmpty() && passwordSwitch.size() <= 2); if (passwordSwitch.size() == 1) { passwordSwitch[0].replace(QLatin1String("$Password"), password); } else { passwordSwitch[1] = password; } return passwordSwitch; } QString CliInterface::compressionLevelSwitch(int level) const { if (level < 0 || level > 9) { return QString(); } Q_ASSERT(m_param.contains(CompressionLevelSwitch)); QString compLevelSwitch = m_param.value(CompressionLevelSwitch).toString(); Q_ASSERT(!compLevelSwitch.isEmpty()); compLevelSwitch.replace(QLatin1String("$CompressionLevel"), QString::number(level)); return compLevelSwitch; } -QStringList CliInterface::copyFilesList(const QVariantList& files) const +QStringList CliInterface::copyFilesList(const QList &entries) const { QStringList filesList; - foreach (const QVariant& f, files) { - filesList << escapeFileName(f.value().file); + foreach (const Archive::Entry *e, entries) { + filesList << escapeFileName(e->property("fullName").toString()); } return filesList; } +QStringList CliInterface::entryFileNames(const QList &entries) const +{ + QStringList filesList; + foreach (const Archive::Entry *file, entries) { + filesList << file->property("fullPath").toString(); + } + + return filesList; +} + void CliInterface::killProcess(bool emitFinished) { // TODO: Would be good to unit test #304764/#304178. if (!m_process) { return; } m_abortingOperation = !emitFinished; // Give some time for the application to finish gracefully if (!m_process->waitForFinished(5)) { m_process->kill(); // It takes a few hundred ms for the process to be killed. m_process->waitForFinished(1000); } m_abortingOperation = false; } bool CliInterface::passwordQuery() { Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); // There is no process running, so finished() must be emitted manually. emit finished(false); return false; } setPassword(query.password()); return true; } void CliInterface::readStdout(bool handleAll) { //when hacking this function, please remember the following: //- standard output comes in unpredictable chunks, this is why //you can never know if the last part of the output is a complete line or not //- console applications are not really consistent about what //characters they send out (newline, backspace, carriage return, //etc), so keep in mind that this function is supposed to handle //all those special cases and be the lowest common denominator if (m_abortingOperation) return; Q_ASSERT(m_process); if (!m_process->bytesAvailable()) { //if process has no more data, we can just bail out return; } QByteArray dd = m_process->readAllStandardOutput(); m_stdOutData += dd; QList lines = m_stdOutData.split('\n'); //The reason for this check is that archivers often do not end //queries (such as file exists, wrong password) on a new line, but //freeze waiting for input. So we check for errors on the last line in //all cases. // TODO: QLatin1String() might not be the best choice here. // The call to handleLine() at the end of the method uses // QString::fromLocal8Bit(), for example. // TODO: The same check methods are called in handleLine(), this // is suboptimal. bool wrongPasswordMessage = checkForErrorMessage(QLatin1String( lines.last() ), WrongPasswordPatterns); bool foundErrorMessage = (wrongPasswordMessage || checkForErrorMessage(QLatin1String(lines.last()), DiskFullPatterns) || checkForErrorMessage(QLatin1String(lines.last()), ExtractionFailedPatterns) || checkForPasswordPromptMessage(QLatin1String(lines.last())) || checkForErrorMessage(QLatin1String(lines.last()), FileExistsExpression)); if (foundErrorMessage) { handleAll = true; } if (wrongPasswordMessage) { setPassword(QString()); } //this is complex, here's an explanation: //if there is no newline, then there is no guaranteed full line to //handle in the output. The exception is that it is supposed to handle //all the data, OR if there's been an error message found in the //partial data. if (lines.size() == 1 && !handleAll) { return; } if (handleAll) { m_stdOutData.clear(); } else { //because the last line might be incomplete we leave it for now //note, this last line may be an empty string if the stdoutdata ends //with a newline m_stdOutData = lines.takeLast(); } foreach(const QByteArray& line, lines) { if (!line.isEmpty() || (m_listEmptyLines && m_operationMode == List)) { handleLine(QString::fromLocal8Bit(line)); } } } void CliInterface::handleLine(const QString& line) { // TODO: This should be implemented by each plugin; the way progress is // shown by each CLI application is subject to a lot of variation. if ((m_operationMode == Copy || m_operationMode == Add) && m_param.contains(CaptureProgress) && m_param.value(CaptureProgress).toBool()) { //read the percentage int pos = line.indexOf(QLatin1Char( '%' )); if (pos > 1) { int percentage = line.midRef(pos - 2, 2).toInt(); emit progress(float(percentage) / 100); return; } } if (m_operationMode == Copy) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); killProcess(); return; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return; } if (checkForErrorMessage(line, DiskFullPatterns)) { qCWarning(ARK) << "Found disk full message:" << line; emit error(i18nc("@info", "Extraction failed because the disk is full.")); killProcess(); return; } if (checkForErrorMessage(line, WrongPasswordPatterns)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18nc("@info", "Extraction failed: Incorrect password")); killProcess(); return; } if (checkForErrorMessage(line, ExtractionFailedPatterns)) { qCWarning(ARK) << "Error in extraction:" << line; emit error(i18n("Extraction failed because of an unexpected error.")); killProcess(); return; } if (handleFileExistsMessage(line)) { return; } } if (m_operationMode == List) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; Kerfuffle::PasswordNeededQuery query(filename()); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); killProcess(); return; } setPassword(query.password()); const QString response(password() + QLatin1Char('\n')); writeToProcess(response.toLocal8Bit()); return; } if (checkForErrorMessage(line, WrongPasswordPatterns)) { qCWarning(ARK) << "Wrong password!"; setPassword(QString()); emit error(i18n("Incorrect password.")); killProcess(); return; } if (checkForErrorMessage(line, ExtractionFailedPatterns)) { qCWarning(ARK) << "Error in extraction!!"; emit error(i18n("Extraction failed because of an unexpected error.")); killProcess(); return; } if (checkForErrorMessage(line, CorruptArchivePatterns)) { qCWarning(ARK) << "Archive corrupt"; setCorrupt(true); return; } if (handleFileExistsMessage(line)) { return; } readListLine(line); return; } if (m_operationMode == Test) { if (checkForPasswordPromptMessage(line)) { qCDebug(ARK) << "Found a password prompt"; emit error(i18n("Ark does not currently support testing password-protected archives.")); killProcess(); return; } if (checkForTestSuccessMessage(line)) { qCDebug(ARK) << "Test successful"; emit testSuccess(); return; } } } bool CliInterface::checkForPasswordPromptMessage(const QString& line) { const QString passwordPromptPattern(m_param.value(PasswordPromptPattern).toString()); if (passwordPromptPattern.isEmpty()) return false; if (m_passwordPromptPattern.pattern().isEmpty()) { m_passwordPromptPattern.setPattern(m_param.value(PasswordPromptPattern).toString()); } if (m_passwordPromptPattern.match(line).hasMatch()) { return true; } return false; } bool CliInterface::handleFileExistsMessage(const QString& line) { // Check for a filename and store it. foreach (const QString &pattern, m_param.value(FileExistsFileName).toStringList()) { const QRegularExpression rxFileNamePattern(pattern); const QRegularExpressionMatch rxMatch = rxFileNamePattern.match(line); if (rxMatch.hasMatch()) { m_storedFileName = rxMatch.captured(1); qCWarning(ARK) << "Detected existing file:" << m_storedFileName; } } if (!checkForErrorMessage(line, FileExistsExpression)) { return false; } Kerfuffle::OverwriteQuery query(QDir::current().path() + QLatin1Char( '/' ) + m_storedFileName); query.setNoRenameMode(true); emit userQuery(&query); qCDebug(ARK) << "Waiting response"; query.waitForResponse(); qCDebug(ARK) << "Finished response"; QString responseToProcess; const QStringList choices = m_param.value(FileExistsInput).toStringList(); if (query.responseOverwrite()) { responseToProcess = choices.at(0); } else if (query.responseSkip()) { responseToProcess = choices.at(1); } else if (query.responseOverwriteAll()) { responseToProcess = choices.at(2); } else if (query.responseAutoSkip()) { responseToProcess = choices.at(3); } else if (query.responseCancelled()) { if (choices.count() < 5) { // If the program has no way to cancel the extraction, we resort to killing it return doKill(); } responseToProcess = choices.at(4); } Q_ASSERT(!responseToProcess.isEmpty()); responseToProcess += QLatin1Char( '\n' ); writeToProcess(responseToProcess.toLocal8Bit()); return true; } bool CliInterface::checkForErrorMessage(const QString& line, int parameterIndex) { QList patterns; if (m_patternCache.contains(parameterIndex)) { patterns = m_patternCache.value(parameterIndex); } else { if (!m_param.contains(parameterIndex)) { return false; } foreach(const QString& rawPattern, m_param.value(parameterIndex).toStringList()) { patterns << QRegularExpression(rawPattern); } m_patternCache[parameterIndex] = patterns; } foreach(const QRegularExpression& pattern, patterns) { if (pattern.match(line).hasMatch()) { return true; } } return false; } bool CliInterface::checkForTestSuccessMessage(const QString& line) { const QRegularExpression rx(m_param.value(TestPassedPattern).toString()); const QRegularExpressionMatch rxMatch = rx.match(line); if (rxMatch.hasMatch()) { return true; } return false; } bool CliInterface::doKill() { if (m_process) { killProcess(false); return true; } return false; } bool CliInterface::doSuspend() { return false; } bool CliInterface::doResume() { return false; } QString CliInterface::escapeFileName(const QString& fileName) const { return fileName; } void CliInterface::writeToProcess(const QByteArray& data) { Q_ASSERT(m_process); Q_ASSERT(!data.isNull()); qCDebug(ARK) << "Writing" << data << "to the process"; #ifdef Q_OS_WIN m_process->write(data); #else m_process->pty()->write(data); #endif } bool CliInterface::addComment(const QString &comment) { cacheParameterList(); m_operationMode = Comment; m_commentTempFile = new QTemporaryFile; if (!m_commentTempFile->open()) { qCWarning(ARK) << "Failed to create temporary file for comment"; emit finished(false); return false; } QTextStream stream(m_commentTempFile); stream << comment << endl; m_commentTempFile->close(); const auto args = substituteCommentVariables(m_param.value(CommentArgs).toStringList(), m_commentTempFile->fileName()); if (!runProcess(m_param.value(AddProgram).toStringList(), args)) { return false; } m_comment = comment; return true; } } diff --git a/kerfuffle/cliinterface.h b/kerfuffle/cliinterface.h index 2aae7c61..f91c1b69 100644 --- a/kerfuffle/cliinterface.h +++ b/kerfuffle/cliinterface.h @@ -1,450 +1,456 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef CLIINTERFACE_H #define CLIINTERFACE_H #include "archiveinterface.h" +#include "archiveentry.h" #include "kerfuffle_export.h" #include #include class KProcess; class KPtyProcess; class QDir; class QTemporaryDir; class QTemporaryFile; namespace Kerfuffle { enum CliInterfaceParameters { ///////////////[ COMMON ]///////////// /** * Bool (default false) * Will look for the %-sign in the stdout while working, in the form of * (2%, 14%, 35%, etc etc), and report progress based upon this */ CaptureProgress = 0, /** * QString * Default: empty * A regexp pattern that matches the program's password prompt. */ PasswordPromptPattern, ///////////////[ LIST ]///////////// /** * QStringList * The names to the program that will handle listing of this * archive (eg "rar"). Will be searched for in PATH */ ListProgram, /** * QStringList * The arguments that are passed to the program above for * listing the archive. Special strings that will be * substituted: * $Archive - the path of the archive */ ListArgs, /** * QStringList (default empty) * List of regexp patterns that indicate a corrupt archive. */ CorruptArchivePatterns, ///////////////[ EXTRACT ]///////////// /** * QStringList * The names to the program that will handle extracting of this * archive (eg "rar"). Will be searched for in PATH */ ExtractProgram, /** * QStringList * The arguments that are passed to the program above for * extracting the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be extracted, if any * $PreservePathSwitch - the flag for extracting with full paths * $PasswordSwitch - the switch setting the password. Note that this * will not be inserted unless the listing function has emitted an * entry with the IsPasswordProtected property set to true. */ ExtractArgs, /** * Bool (default false) * When passing directories to the extract program, do not * include trailing slashes * e.g. if the user selected "foo/" and "foo/bar" in the gui, the * paths "foo" and "foo/bar" will be sent to the program. */ NoTrailingSlashes, /** * QStringList * This should be a qstringlist with either two elements. The first * string is what PreservePathSwitch in the ExtractArgs will be replaced * with if PreservePath is True/enabled. The second is for the disabled * case. An empty string means that the argument will not be used in * that case. * Example: for rar, "x" means extract with full paths, and "e" means * extract without full paths. in this case we will use the stringlist * ("x", "e"). Or, for another format that might use the switch * "--extractFull" for preservePaths, and nothing otherwise: we use the * stringlist ("--extractFull", "") */ PreservePathSwitch, /** * QStringList (default empty) * The format of the root node switch. The variable $Password will be * substituted for the password string. NOTE: supplying passwords * through a virtual terminal is not supported (yet?), because this * is not cross platform compatible. As of KDE 4.3 there are no plans to * change this. * Example: ("-p$Password) * or ("--password", "$Password") */ PasswordSwitch, /** * QString * The format of the compression level switch. The variable $CompressionLevel * will be substituted for the level. * Example: ("-mx=$CompressionLevel) */ CompressionLevelSwitch, /** * QStringList * This is a stringlist with regexps, defining how to recognize the last * line in a "File already exists" prompt when extracting. */ FileExistsExpression, /** * QStringList * This is a stringlist with regexps defining how to recognize the line * containing the filename in a "File already exists" prompt when * extracting. It should have one captured string, which is the filename * of the file/folder that already exists. */ FileExistsFileName, /** * int * This sets on what output channel the FileExistsExpression regex * should be applied on, in other words, on what stream the "file * exists" output will appear in. Values accepted: * 0 - Standard error, stderr (default) * 1 - Standard output, stdout */ FileExistsMode, /** * QStringList * The various responses that can be supplied as a response to the * "file exists" prompt. The various items are to be supplied in the * following order: * index 0 - Yes (overwrite) * index 1 - No (skip/do not overwrite) * index 2 - All (overwrite all) * index 3 - Do not overwrite any files (autoskip) * index 4 - Cancel operation */ FileExistsInput, /** * QStringList * Regexp patterns capturing disk is full error messages. */ DiskFullPatterns, ///////////////[ DELETE ]///////////// /** * QStringList * The names to the program that will handle deleting of elements in this * archive format (eg "rar"). Will be searched for in PATH */ DeleteProgram, /** * QStringList * The arguments that are passed to the program above for * deleting from the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be deleted */ DeleteArgs, /** * QStringList * Default: empty * A list of regexp patterns that will cause the extraction to exit * with a general fail message */ ExtractionFailedPatterns, /** * QStringList * Default: empty * A list of regexp patterns that will alert the user that the password * was wrong. */ WrongPasswordPatterns, ///////////////[ ADD ]///////////// /** * QStringList * The names to the program that will handle adding in this * archive format (eg "rar"). Will be searched for in PATH */ AddProgram, /** * QStringList * The arguments that are passed to the program above for * adding to the archive. Special strings that will be * substituted: * $Archive - the path of the archive * $Files - the files selected to be added */ AddArgs, ///////////////[ ENCRYPT ]///////////// /** * QStringList (default empty) * The variable $Password will be * substituted for the password string used to encrypt the header. * Example (rar plugin): ("-hp$Password") */ PasswordHeaderSwitch, ///////////////[ COMMENT ]///////////// /** * QStringList * The arguments that are passed to AddProgram when adding * a comment. */ CommentArgs, /** * QString * The variable $CommentFile will be substituted for the file * containing the comment. * Example (rar plugin): -z$CommentFile */ CommentSwitch, TestProgram, TestArgs, TestPassedPattern }; typedef QHash ParameterList; class KERFUFFLE_EXPORT CliInterface : public ReadWriteArchiveInterface { Q_OBJECT public: enum OperationMode { List, Copy, Add, Delete, Comment, Test }; OperationMode m_operationMode; explicit CliInterface(QObject *parent, const QVariantList & args); virtual ~CliInterface(); virtual bool list() Q_DECL_OVERRIDE; - virtual bool copyFiles(const QList& files, const QString& destinationDirectory, const ExtractionOptions& options) Q_DECL_OVERRIDE; - virtual bool addFiles(const QStringList & files, const CompressionOptions& options) Q_DECL_OVERRIDE; - virtual bool deleteFiles(const QList & files) Q_DECL_OVERRIDE; + virtual bool copyFiles(const QList &files, const QString& destinationDirectory, const ExtractionOptions& options) Q_DECL_OVERRIDE; + virtual bool addFiles(const QList &files, const CompressionOptions& options) Q_DECL_OVERRIDE; + virtual bool deleteFiles(const QList &files) Q_DECL_OVERRIDE; virtual bool addComment(const QString &comment) Q_DECL_OVERRIDE; virtual bool testArchive() Q_DECL_OVERRIDE; virtual void resetParsing() = 0; virtual ParameterList parameterList() const = 0; virtual bool readListLine(const QString &line) = 0; bool doKill() Q_DECL_OVERRIDE; bool doSuspend() Q_DECL_OVERRIDE; bool doResume() Q_DECL_OVERRIDE; /** * Sets if the listing should include empty lines. * * The default value is false. */ void setListEmptyLines(bool emptyLines); /** * Move all files from @p tmpDir to @p destDir, preserving paths if @p preservePaths is true. * @return Whether the operation has been successful. */ bool moveToDestination(const QDir &tempDir, const QDir &destDir, bool preservePaths); QStringList substituteListVariables(const QStringList &listArgs, const QString &password); - QStringList substituteCopyVariables(const QStringList &extractArgs, const QVariantList &files, bool preservePaths, const QString &password); - QStringList substituteAddVariables(const QStringList &addArgs, const QStringList &files, const QString &password, bool encryptHeader, int compLevel); - QStringList substituteDeleteVariables(const QStringList &deleteArgs, const QVariantList &files, const QString &password); + QStringList substituteCopyVariables(const QStringList &extractArgs, const QList &entries, bool preservePaths, const QString &password); + QStringList substituteAddVariables(const QStringList &addArgs, const QList &entries, const QString &password, bool encryptHeader, int compLevel); + QStringList substituteDeleteVariables(const QStringList &deleteArgs, const QList &entries, const QString &password); QStringList substituteCommentVariables(const QStringList &commentArgs, const QString &commentFile); QStringList substituteTestVariables(const QStringList &testArgs); /** * @return The preserve path switch, according to the @p preservePaths extraction option. */ QString preservePathSwitch(bool preservePaths) const; /** * @return The password header-switch with the given @p password. */ virtual QStringList passwordHeaderSwitch(const QString& password) const; /** * @return The password switch with the given @p password. */ QStringList passwordSwitch(const QString& password) const; /** * @return The compression level switch with the given @p level. */ QString compressionLevelSwitch(int level) const; /** * @return The list of selected files to extract. */ - QStringList copyFilesList(const QVariantList& files) const; + QStringList copyFilesList(const QList &files) const; + + /** + * @return The list of filenames retrieved from the list of entries. + */ + QStringList entryFileNames(const QList &entries) const; protected: virtual void handleLine(const QString& line); virtual void cacheParameterList(); /** * Run @p programName with the given @p arguments. * * @param programName The program that will be run (not the whole path). * @param arguments A list of arguments that will be passed to the program. * * @return @c true if the program was found and the process was started correctly, * @c false otherwise (in which case finished(false) is emitted). */ bool runProcess(const QStringList& programNames, const QStringList& arguments); /** * Kill the running process. The finished signal is emitted according to @p emitFinished. */ void killProcess(bool emitFinished = true); /** * Ask the password *before* running any process. * @return True if the user supplies a password, false otherwise (in which case finished() is emitted). */ bool passwordQuery(); ParameterList m_param; int m_exitCode; protected slots: virtual void readStdout(bool handleAll = false); private: /** * Checks whether a line of the program's output is a password prompt. * * It uses the regular expression in the @c PasswordPromptPattern parameter * for the check. * * @param line A line of the program's output. * * @return @c true if the given @p line is a password prompt, @c false * otherwise. */ bool checkForPasswordPromptMessage(const QString& line); bool handleFileExistsMessage(const QString& filename); bool checkForErrorMessage(const QString& line, int parameterIndex); bool checkForTestSuccessMessage(const QString& line); /** * Performs any additional escaping and processing on @p fileName * before passing it to the underlying process. * * The default implementation returns @p fileName unchanged. * * @param fileName String to escape. */ virtual QString escapeFileName(const QString &fileName) const; /** * Wrapper around KProcess::write() or KPtyDevice::write(), depending on * the platform. */ void writeToProcess(const QByteArray& data); - bool moveDroppedFilesToDest(const QVariantList &files, const QString &finalDest); + bool moveDroppedFilesToDest(const QList &files, const QString &finalDest); /** * @return Whether @p dir is an empty directory. */ bool isEmptyDir(const QDir &dir); void copyProcessCleanup(); QByteArray m_stdOutData; QRegularExpression m_passwordPromptPattern; QHash > m_patternCache; #ifdef Q_OS_WIN KProcess *m_process; #else KPtyProcess *m_process; #endif - QVariantList m_removedFiles; + QList m_removedFiles; bool m_listEmptyLines; bool m_abortingOperation; QString m_storedFileName; CompressionOptions m_compressionOptions; QString m_oldWorkingDir; QString m_extractDestDir; QTemporaryDir *m_extractTempDir; QTemporaryFile *m_commentTempFile; - QVariantList m_copiedFiles; + QList m_copiedFiles; private slots: void processFinished(int exitCode, QProcess::ExitStatus exitStatus); void copyProcessFinished(int exitCode, QProcess::ExitStatus exitStatus); }; } #endif /* CLIINTERFACE_H */ diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp index 33427f18..54a355f4 100644 --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -1,559 +1,563 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "jobs.h" #include "archiveentry.h" #include "ark_debug.h" #include #include #include #include #include #include //#define DEBUG_RACECONDITION namespace Kerfuffle { class Job::Private : public QThread { public: Private(Job *job, QObject *parent = 0) : QThread(parent) , q(job) { connect(q, &KJob::result, this, &QThread::quit); } virtual void run() Q_DECL_OVERRIDE; private: Job *q; }; void Job::Private::run() { q->doWork(); if (q->isRunning()) { exec(); } #ifdef DEBUG_RACECONDITION QThread::sleep(2); #endif } Job::Job(ReadOnlyArchiveInterface *interface) : KJob() , m_archiveInterface(interface) , m_isRunning(false) , d(new Private(this)) { static bool onlyOnce = false; if (!onlyOnce) { qRegisterMetaType >("QPair"); onlyOnce = true; } setCapabilities(KJob::Killable); } Job::~Job() { + qDeleteAll(m_archiveEntries); + m_archiveEntries.clear(); + if (d->isRunning()) { d->wait(); } delete d; } ReadOnlyArchiveInterface *Job::archiveInterface() { return m_archiveInterface; } bool Job::isRunning() const { return m_isRunning; } void Job::start() { jobTimer.start(); m_isRunning = true; if (archiveInterface()->waitForFinishedSignal()) { // CLI-based interfaces run a QProcess, no need to use threads. QTimer::singleShot(0, this, &Job::doWork); } else { // Run the job in another thread. d->start(); } } void Job::emitResult() { m_isRunning = false; KJob::emitResult(); } void Job::connectToArchiveInterfaceSignals() { connect(archiveInterface(), &ReadOnlyArchiveInterface::cancelled, this, &Job::onCancelled); connect(archiveInterface(), &ReadOnlyArchiveInterface::error, this, &Job::onError); connect(archiveInterface(), &ReadOnlyArchiveInterface::entry, this, &Job::onEntry); connect(archiveInterface(), &ReadOnlyArchiveInterface::entryRemoved, this, &Job::onEntryRemoved); connect(archiveInterface(), &ReadOnlyArchiveInterface::progress, this, &Job::onProgress); connect(archiveInterface(), &ReadOnlyArchiveInterface::info, this, &Job::onInfo); connect(archiveInterface(), &ReadOnlyArchiveInterface::finished, this, &Job::onFinished, Qt::DirectConnection); connect(archiveInterface(), &ReadOnlyArchiveInterface::userQuery, this, &Job::onUserQuery); } void Job::onCancelled() { qCDebug(ARK) << "Cancelled emitted"; setError(KJob::KilledJobError); } void Job::onError(const QString & message, const QString & details) { Q_UNUSED(details) qCDebug(ARK) << "Error emitted:" << message; setError(KJob::UserDefinedError); setErrorText(message); } void Job::onEntry(Archive::Entry *entry) { emit newEntry(entry); } void Job::onProgress(double value) { setPercent(static_cast(100.0*value)); } void Job::onInfo(const QString& info) { emit infoMessage(this, info); } void Job::onEntryRemoved(const QString & path) { emit entryRemoved(path); } void Job::onFinished(bool result) { qCDebug(ARK) << "Job finished, result:" << result << ", time:" << jobTimer.elapsed() << "ms"; emitResult(); } void Job::onUserQuery(Query *query) { emit userQuery(query); } bool Job::doKill() { bool ret = archiveInterface()->doKill(); if (!ret) { qCWarning(ARK) << "Killing does not seem to be supported here."; } return ret; } ListJob::ListJob(ReadOnlyArchiveInterface *interface) : Job(interface) , m_isSingleFolderArchive(true) , m_isPasswordProtected(false) , m_extractedFilesSize(0) , m_dirCount(0) , m_filesCount(0) { qCDebug(ARK) << "ListJob started"; connect(this, &ListJob::newEntry, this, &ListJob::onNewEntry); } void ListJob::doWork() { emit description(this, i18n("Loading archive...")); connectToArchiveInterfaceSignals(); bool ret = archiveInterface()->list(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } qlonglong ListJob::extractedFilesSize() const { return m_extractedFilesSize; } bool ListJob::isPasswordProtected() const { return m_isPasswordProtected; } bool ListJob::isSingleFolderArchive() const { if (m_filesCount == 1 && m_dirCount == 0) { return false; } return m_isSingleFolderArchive; } void ListJob::onNewEntry(const Archive::Entry *entry) { m_extractedFilesSize += entry->property("size").toLongLong(); m_isPasswordProtected |= entry->property("isPasswordProtected").toBool(); if (entry->isDir()) { m_dirCount++; } else { m_filesCount++; } if (m_isSingleFolderArchive) { // RPM filenames have the ./ prefix, and "." would be detected as the subfolder name, so we remove it. - const QString fileName = entry->property("fileName").toString().replace(QRegularExpression(QStringLiteral("^\\./")), QString()); - const QString basePath = fileName.split(QLatin1Char('/')).at(0); + const QString fullPath = entry->property("fullPath").toString().replace(QRegularExpression(QStringLiteral("^\\./")), QString()); + const QString basePath = fullPath.split(QLatin1Char('/')).at(0); if (m_basePath.isEmpty()) { m_basePath = basePath; m_subfolderName = basePath; } else { if (m_basePath != basePath) { m_isSingleFolderArchive = false; m_subfolderName.clear(); } } } } QString ListJob::subfolderName() const { if (!isSingleFolderArchive()) { return QString(); } return m_subfolderName; } -ExtractJob::ExtractJob(const QVariantList& files, const QString& destinationDir, const ExtractionOptions& options, ReadOnlyArchiveInterface *interface) +ExtractJob::ExtractJob(const QList &entries, const QString &destinationDir, const ExtractionOptions &options, ReadOnlyArchiveInterface *interface) : Job(interface) - , m_files(files) + , m_entries(entries) , m_destinationDir(destinationDir) , m_options(options) { qCDebug(ARK) << "ExtractJob created"; setDefaultOptions(); } void ExtractJob::doWork() { QString desc; - if (m_files.count() == 0) { + if (m_entries.count() == 0) { desc = i18n("Extracting all files"); } else { - desc = i18np("Extracting one file", "Extracting %1 files", m_files.count()); + desc = i18np("Extracting one file", "Extracting %1 files", m_entries.count()); } emit description(this, desc); QFileInfo destDirInfo(m_destinationDir); if (destDirInfo.isDir() && (!destDirInfo.isWritable() || !destDirInfo.isExecutable())) { onError(xi18n("Could not write to destination %1.Check whether you have sufficient permissions.", m_destinationDir), QString()); onFinished(false); return; } connectToArchiveInterfaceSignals(); qCDebug(ARK) << "Starting extraction with selected files:" - << m_files + << m_entries << "Destination dir:" << m_destinationDir << "Options:" << m_options; - bool ret = archiveInterface()->copyFiles(m_files, m_destinationDir, m_options); + bool ret = archiveInterface()->copyFiles(m_entries, m_destinationDir, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void ExtractJob::setDefaultOptions() { ExtractionOptions defaultOptions; defaultOptions[QStringLiteral("PreservePaths")] = false; ExtractionOptions::const_iterator it = defaultOptions.constBegin(); for (; it != defaultOptions.constEnd(); ++it) { if (!m_options.contains(it.key())) { m_options[it.key()] = it.value(); } } } QString ExtractJob::destinationDirectory() const { return m_destinationDir; } ExtractionOptions ExtractJob::extractionOptions() const { return m_options; } -TempExtractJob::TempExtractJob(const QString &file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) +TempExtractJob::TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) : Job(interface) - , m_file(file) + , m_entry(entry) , m_passwordProtectedHint(passwordProtectedHint) { } QString TempExtractJob::validatedFilePath() const { - QString path = extractionDir() + QLatin1Char('/') + m_file; + QString path = extractionDir() + QLatin1Char('/') + m_entry->property("fullPath").toString(); // Make sure a maliciously crafted archive with parent folders named ".." do // not cause the previewed file path to be located outside the temporary // directory, resulting in a directory traversal issue. path.remove(QStringLiteral("../")); return path; } ExtractionOptions TempExtractJob::extractionOptions() const { ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; if (m_passwordProtectedHint) { options[QStringLiteral("PasswordProtectedHint")] = true; } return options; } void TempExtractJob::doWork() { emit description(this, i18n("Extracting one file")); connectToArchiveInterfaceSignals(); - qCDebug(ARK) << "Extracting:" << m_file; + qCDebug(ARK) << "Extracting:" << m_entry; - bool ret = archiveInterface()->copyFiles({ QVariant::fromValue(fileRootNodePair(m_file)) }, extractionDir(), extractionOptions()); + bool ret = archiveInterface()->copyFiles({ m_entry }, extractionDir(), extractionOptions()); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } -PreviewJob::PreviewJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) - : TempExtractJob(file, passwordProtectedHint, interface) +PreviewJob::PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) + : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "PreviewJob started"; } QString PreviewJob::extractionDir() const { return m_tmpExtractDir.path(); } -OpenJob::OpenJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) - : TempExtractJob(file, passwordProtectedHint, interface) +OpenJob::OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) + : TempExtractJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "OpenJob started"; m_tmpExtractDir = new QTemporaryDir(); } QTemporaryDir *OpenJob::tempDir() const { return m_tmpExtractDir; } QString OpenJob::extractionDir() const { return m_tmpExtractDir->path(); } -OpenWithJob::OpenWithJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) - : OpenJob(file, passwordProtectedHint, interface) +OpenWithJob::OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface) + : OpenJob(entry, passwordProtectedHint, interface) { qCDebug(ARK) << "OpenWithJob started"; } -AddJob::AddJob(const QStringList& files, const CompressionOptions& options , ReadWriteArchiveInterface *interface) +AddJob::AddJob(QList &entries, const CompressionOptions& options , ReadWriteArchiveInterface *interface) : Job(interface) - , m_files(files) + , m_entries(entries) , m_options(options) { qCDebug(ARK) << "AddJob started"; } void AddJob::doWork() { - qCDebug(ARK) << "AddJob: going to add" << m_files.count() << "file(s)"; + qCDebug(ARK) << "AddJob: going to add" << m_entries.count() << "file(s)"; - emit description(this, i18np("Adding a file", "Adding %1 files", m_files.count())); + emit description(this, i18np("Adding a file", "Adding %1 files", m_entries.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); const QString globalWorkDir = m_options.value(QStringLiteral("GlobalWorkDir")).toString(); const QDir workDir = globalWorkDir.isEmpty() ? QDir::current() : QDir(globalWorkDir); if (!globalWorkDir.isEmpty()) { qCDebug(ARK) << "GlobalWorkDir is set, changing dir to " << globalWorkDir; m_oldWorkingDir = QDir::currentPath(); QDir::setCurrent(globalWorkDir); } // The file paths must be relative to GlobalWorkDir. QStringList relativeFiles; - foreach (const QString& file, m_files) { + foreach (const Archive::Entry *entry, m_entries) { // #191821: workDir must be used instead of QDir::current() // so that symlinks aren't resolved automatically - QString relativePath = workDir.relativeFilePath(file); + const QString &fullPath = entry->property("fullPath").toString(); + QString relativePath = workDir.relativeFilePath(fullPath); - if (file.endsWith(QLatin1Char('/'))) { + if (fullPath.endsWith(QLatin1Char('/'))) { relativePath += QLatin1Char('/'); } relativeFiles << relativePath; } connectToArchiveInterfaceSignals(); - bool ret = m_writeInterface->addFiles(relativeFiles, m_options); + bool ret = m_writeInterface->addFiles(m_entries, m_options); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void AddJob::onFinished(bool result) { if (!m_oldWorkingDir.isEmpty()) { QDir::setCurrent(m_oldWorkingDir); } Job::onFinished(result); } -DeleteJob::DeleteJob(const QVariantList& files, ReadWriteArchiveInterface *interface) +DeleteJob::DeleteJob(QList &entries, ReadWriteArchiveInterface *interface) : Job(interface) - , m_files(files) + , m_entries(entries) { } void DeleteJob::doWork() { - emit description(this, i18np("Deleting a file from the archive", "Deleting %1 files", m_files.count())); + emit description(this, i18np("Deleting a file from the archive", "Deleting %1 files", m_entries.count())); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); - bool ret = m_writeInterface->deleteFiles(m_files); + bool ret = m_writeInterface->deleteFiles(m_entries); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } CommentJob::CommentJob(const QString& comment, ReadWriteArchiveInterface *interface) : Job(interface) , m_comment(comment) { } void CommentJob::doWork() { emit description(this, i18n("Adding comment")); ReadWriteArchiveInterface *m_writeInterface = qobject_cast(archiveInterface()); Q_ASSERT(m_writeInterface); connectToArchiveInterfaceSignals(); bool ret = m_writeInterface->addComment(m_comment); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } TestJob::TestJob(ReadOnlyArchiveInterface *interface) : Job(interface) { m_testSuccess = false; } void TestJob::doWork() { qCDebug(ARK) << "TestJob started"; emit description(this, i18n("Testing archive")); connectToArchiveInterfaceSignals(); connect(archiveInterface(), &ReadOnlyArchiveInterface::testSuccess, this, &TestJob::onTestSuccess); bool ret = archiveInterface()->testArchive(); if (!archiveInterface()->waitForFinishedSignal()) { onFinished(ret); } } void TestJob::onTestSuccess() { m_testSuccess = true; } bool TestJob::testSucceeded() { return m_testSuccess; } } // namespace Kerfuffle diff --git a/kerfuffle/jobs.h b/kerfuffle/jobs.h index 67a75d13..3279bffb 100644 --- a/kerfuffle/jobs.h +++ b/kerfuffle/jobs.h @@ -1,288 +1,290 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2009-2012 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef JOBS_H #define JOBS_H #include "kerfuffle_export.h" #include "archiveinterface.h" #include "archive_kerfuffle.h" +#include "archiveentry.h" #include "queries.h" #include #include #include namespace Kerfuffle { class KERFUFFLE_EXPORT Job : public KJob { Q_OBJECT public: void start(); bool isRunning() const; protected: Job(ReadOnlyArchiveInterface *interface); virtual ~Job(); virtual bool doKill(); virtual void emitResult(); ReadOnlyArchiveInterface *archiveInterface(); + QList m_archiveEntries; void connectToArchiveInterfaceSignals(); public slots: virtual void doWork() = 0; protected slots: virtual void onCancelled(); virtual void onError(const QString &message, const QString &details); virtual void onInfo(const QString &info); virtual void onEntry(Archive::Entry *entry); virtual void onProgress(double progress); virtual void onEntryRemoved(const QString &path); virtual void onFinished(bool result); virtual void onUserQuery(Query *query); signals: void entryRemoved(const QString & entry); void error(const QString& errorMessage, const QString& details); void newEntry(Archive::Entry*); void userQuery(Kerfuffle::Query*); private: ReadOnlyArchiveInterface *m_archiveInterface; bool m_isRunning; QElapsedTimer jobTimer; class Private; Private * const d; }; class KERFUFFLE_EXPORT ListJob : public Job { Q_OBJECT public: explicit ListJob(ReadOnlyArchiveInterface *interface); qlonglong extractedFilesSize() const; bool isPasswordProtected() const; bool isSingleFolderArchive() const; QString subfolderName() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: bool m_isSingleFolderArchive; bool m_isPasswordProtected; QString m_subfolderName; QString m_basePath; qlonglong m_extractedFilesSize; qlonglong m_dirCount; qlonglong m_filesCount; private slots: void onNewEntry(const Archive::Entry*); }; class KERFUFFLE_EXPORT ExtractJob : public Job { Q_OBJECT public: - ExtractJob(const QVariantList& files, const QString& destinationDir, const ExtractionOptions& options, ReadOnlyArchiveInterface *interface); + ExtractJob(const QList &entries, const QString& destinationDir, const ExtractionOptions& options, ReadOnlyArchiveInterface *interface); QString destinationDirectory() const; ExtractionOptions extractionOptions() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: // TODO: Maybe this should be a method if ExtractionOptions were a class? void setDefaultOptions(); - QVariantList m_files; + QList m_entries; QString m_destinationDir; ExtractionOptions m_options; }; /** * Abstract base class for jobs that extract a single file to a temporary dir. * It's not possible to pass extraction options and paths will be always preserved. * The only option that the job needs to know is whether the file is password protected. */ class KERFUFFLE_EXPORT TempExtractJob : public Job { Q_OBJECT public: - TempExtractJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); + TempExtractJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); /** * @return The absolute path of the extracted file. * The path is validated in order to prevent directory traversal attacks. */ QString validatedFilePath() const; ExtractionOptions extractionOptions() const; public slots: virtual void doWork() Q_DECL_OVERRIDE; private: virtual QString extractionDir() const = 0; - QString m_file; + Archive::Entry *m_entry; bool m_passwordProtectedHint; }; /** * This TempExtractJob can be used to preview a file. * The temporary extraction directory will be deleted upon job's completion. */ class KERFUFFLE_EXPORT PreviewJob : public TempExtractJob { Q_OBJECT public: - PreviewJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); + PreviewJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); private: QString extractionDir() const Q_DECL_OVERRIDE; QTemporaryDir m_tmpExtractDir; }; /** * This TempExtractJob can be used to open a file in its dedicated application. * For this reason, the temporary extraction directory will NOT be deleted upon job's completion. */ class KERFUFFLE_EXPORT OpenJob : public TempExtractJob { Q_OBJECT public: - OpenJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); + OpenJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); /** * @return The temporary dir used for the extraction. * It is safe to delete this pointer in order to remove the directory. */ QTemporaryDir *tempDir() const; private: QString extractionDir() const Q_DECL_OVERRIDE; QTemporaryDir *m_tmpExtractDir; }; class KERFUFFLE_EXPORT OpenWithJob : public OpenJob { Q_OBJECT public: - OpenWithJob(const QString& file, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); + OpenWithJob(Archive::Entry *entry, bool passwordProtectedHint, ReadOnlyArchiveInterface *interface); }; class KERFUFFLE_EXPORT AddJob : public Job { Q_OBJECT public: - AddJob(const QStringList& files, const CompressionOptions& options, ReadWriteArchiveInterface *interface); + AddJob(QList &files, const CompressionOptions& options, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; protected slots: virtual void onFinished(bool result) Q_DECL_OVERRIDE; private: QString m_oldWorkingDir; - QStringList m_files; + QList m_entries; CompressionOptions m_options; }; class KERFUFFLE_EXPORT DeleteJob : public Job { Q_OBJECT public: - DeleteJob(const QVariantList& files, ReadWriteArchiveInterface *interface); + DeleteJob(QList &files, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; private: - QVariantList m_files; + QList m_entries; }; class KERFUFFLE_EXPORT CommentJob : public Job { Q_OBJECT public: CommentJob(const QString& comment, ReadWriteArchiveInterface *interface); public slots: virtual void doWork() Q_DECL_OVERRIDE; private: QString m_comment; }; class KERFUFFLE_EXPORT TestJob : public Job { Q_OBJECT public: TestJob(ReadOnlyArchiveInterface *interface); bool testSucceeded(); public slots: virtual void doWork() Q_DECL_OVERRIDE; private slots: virtual void onTestSuccess(); private: bool m_testSuccess; }; } // namespace Kerfuffle #endif // JOBS_H diff --git a/part/archivemodel.cpp b/part/archivemodel.cpp index ee4e63ff..56ad3bf6 100644 --- a/part/archivemodel.cpp +++ b/part/archivemodel.cpp @@ -1,887 +1,882 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2010-2012 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "archivemodel.h" #include "kerfuffle/jobs.h" #include #include #include #include #include #include using namespace Kerfuffle; //used to speed up the loading of large archives static Archive::Entry *s_previousMatch = Q_NULLPTR; Q_GLOBAL_STATIC(QStringList, s_previousPieces) /** * Meta data related to one entry in a compressed archive. * * This is used for indexing entry properties as numbers * and for determining data displaying order in part's view. */ enum EntryMetaDataType { - FileName, /**< The entry's file name */ + FullPath, /**< The entry's file name */ Size, /**< The entry's original size */ CompressedSize, /**< The compressed size for the entry */ Permissions, /**< The entry's permissions */ Owner, /**< The user the entry belongs to */ Group, /**< The user group the entry belongs to */ Ratio, /**< The compression ratio for the entry */ CRC, /**< The entry's CRC */ Method, /**< The compression method used on the entry */ Version, /**< The archiver version needed to extract the entry */ Timestamp, /**< The timestamp for the current entry */ Comment, }; /** * Mappings between column indexes and entry properties. */ static QMap initializePropertiesList() { QMap propertiesList = QMap(); - propertiesList.insert(FileName, QStringLiteral("fileName")); + propertiesList.insert(FullPath, QStringLiteral("fullPath")); propertiesList.insert(Size, QStringLiteral("size")); propertiesList.insert(CompressedSize, QStringLiteral("compressedSize")); propertiesList.insert(Permissions, QStringLiteral("permissions")); propertiesList.insert(Owner, QStringLiteral("owner")); propertiesList.insert(Group, QStringLiteral("group")); propertiesList.insert(Ratio, QStringLiteral("ratio")); propertiesList.insert(CRC, QStringLiteral("CRC")); propertiesList.insert(Method, QStringLiteral("method")); propertiesList.insert(Version, QStringLiteral("version")); propertiesList.insert(Timestamp, QStringLiteral("timestamp")); propertiesList.insert(Comment, QStringLiteral("comment")); return propertiesList; } static const QMap propertiesList = initializePropertiesList(); /** * Helper functor used by qStableSort. * * It always sorts folders before files. * * @internal */ class ArchiveModelSorter { public: ArchiveModelSorter(int column, Qt::SortOrder order) : m_sortColumn(column) , m_sortOrder(order) { } virtual ~ArchiveModelSorter() { } inline bool operator()(const QPair &left, const QPair &right) const { if (m_sortOrder == Qt::AscendingOrder) { return lessThan(left, right); } else { return !lessThan(left, right); } } protected: bool lessThan(const QPair &left, const QPair &right) const { const Archive::Entry * const leftEntry = left.first; const Archive::Entry * const rightEntry = right.first; // #234373: sort folders before files if ((leftEntry->isDir()) && (!rightEntry->isDir())) { return (m_sortOrder == Qt::AscendingOrder); } else if ((!leftEntry->isDir()) && (rightEntry->isDir())) { return !(m_sortOrder == Qt::AscendingOrder); } EntryMetaDataType column = static_cast(m_sortColumn); const QVariant &leftEntryMetaData = leftEntry->property(propertiesList[column].toStdString().c_str()); const QVariant &rightEntryMetaData = rightEntry->property(propertiesList[column].toStdString().c_str()); switch (m_sortColumn) { - case FileName: + case FullPath: return leftEntry->name() < rightEntry->name(); case Size: case CompressedSize: return leftEntryMetaData.toInt() < rightEntryMetaData.toInt(); default: return leftEntryMetaData.toString() < rightEntryMetaData.toString(); } // We should not get here. Q_ASSERT(false); return false; } private: int m_sortColumn; Qt::SortOrder m_sortOrder; }; ArchiveModel::ArchiveModel(const QString &dbusPathName, QObject *parent) : QAbstractItemModel(parent) , m_rootEntry(Q_NULLPTR) , m_dbusPathName(dbusPathName) { m_rootEntry.setProperty("isDirectory", true); } ArchiveModel::~ArchiveModel() { } QVariant ArchiveModel::data(const QModelIndex &index, int role) const { if (index.isValid()) { Archive::Entry *entry = static_cast(index.internalPointer()); switch (role) { case Qt::DisplayRole: { //TODO: complete the columns int column = m_showColumns.at(index.column()); switch (column) { - case FileName: + case FullPath: return entry->name(); case Size: if (entry->isDir()) { int dirs; int files; const int children = childCount(index, dirs, files); return KIO::itemsSummaryString(children, files, dirs, 0, false); } else if (!entry->property("link").toString().isEmpty()) { return QVariant(); } else { return KIO::convertSize(entry->property("size").toULongLong()); } case CompressedSize: if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { qulonglong compressedSize = entry->property("compressedSize").toULongLong(); if (compressedSize != 0) { return KIO::convertSize(compressedSize); } else { return QVariant(); } } case Ratio: // TODO: Use entry->metaData()[Ratio] when available if (entry->isDir() || !entry->property("link").toString().isEmpty()) { return QVariant(); } else { qulonglong compressedSize = entry->property("compressedSize").toULongLong(); qulonglong size = entry->property("size").toULongLong(); if (compressedSize == 0 || size == 0) { return QVariant(); } else { int ratio = int(100 * ((double)size - compressedSize) / size); return QString(QString::number(ratio) + QStringLiteral(" %")); } } case Timestamp: { const QDateTime timeStamp = entry->property("timestamp").toDateTime(); return QLocale().toString(timeStamp, QLocale::ShortFormat); } default: return entry->property(propertiesList[column].toStdString().c_str()); } } case Qt::DecorationRole: if (index.column() == 0) { return entry->icon(); } return QVariant(); case Qt::FontRole: { QFont f; f.setItalic(entry->property("isPasswordProtected").toBool()); return f; } default: return QVariant(); } } return QVariant(); } Qt::ItemFlags ArchiveModel::flags(const QModelIndex &index) const { Qt::ItemFlags defaultFlags = QAbstractItemModel::flags(index); if (index.isValid()) { return Qt::ItemIsEnabled | Qt::ItemIsSelectable | Qt::ItemIsDragEnabled | defaultFlags; } return 0; } QVariant ArchiveModel::headerData(int section, Qt::Orientation, int role) const { if (role == Qt::DisplayRole) { if (section >= m_showColumns.size()) { qCDebug(ARK) << "WEIRD: showColumns.size = " << m_showColumns.size() << " and section = " << section; return QVariant(); } int columnId = m_showColumns.at(section); switch (columnId) { - case FileName: + case FullPath: return i18nc("Name of a file inside an archive", "Name"); case Size: return i18nc("Uncompressed size of a file inside an archive", "Size"); case CompressedSize: return i18nc("Compressed size of a file inside an archive", "Compressed"); case Ratio: return i18nc("Compression rate of file", "Rate"); case Owner: return i18nc("File's owner username", "Owner"); case Group: return i18nc("File's group", "Group"); case Permissions: return i18nc("File permissions", "Mode"); case CRC: return i18nc("CRC hash code", "CRC"); case Method: return i18nc("Compression method", "Method"); case Version: //TODO: what exactly is a file version? return i18nc("File version", "Version"); case Timestamp: return i18nc("Timestamp", "Date"); case Comment: return i18nc("File comment", "Comment"); default: return i18nc("Unnamed column", "??"); } } return QVariant(); } QModelIndex ArchiveModel::index(int row, int column, const QModelIndex &parent) const { if (hasIndex(row, column, parent)) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : &m_rootEntry; Q_ASSERT(parentEntry->isDir()); const Archive::Entry *item = parentEntry->entries().value(row, Q_NULLPTR); if (item != Q_NULLPTR) { return createIndex(row, column, const_cast(item)); } } return QModelIndex(); } QModelIndex ArchiveModel::parent(const QModelIndex &index) const { if (index.isValid()) { Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); if (item->getParent() && (item->getParent() != &m_rootEntry)) { return createIndex(item->getParent()->row(), 0, item->getParent()); } } return QModelIndex(); } Archive::Entry *ArchiveModel::entryForIndex(const QModelIndex &index) { if (index.isValid()) { Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); return item; } return Q_NULLPTR; } int ArchiveModel::childCount(const QModelIndex &index, int &dirs, int &files) const { if (index.isValid()) { dirs = files = 0; Archive::Entry *item = static_cast(index.internalPointer()); Q_ASSERT(item); if (item->isDir()) { const QList entries = static_cast(item)->entries(); foreach(const Archive::Entry *entry, entries) { if (entry->isDir()) { dirs++; } else { files++; } } return entries.count(); } return 0; } return -1; } int ArchiveModel::rowCount(const QModelIndex &parent) const { if (parent.column() <= 0) { const Archive::Entry *parentEntry = parent.isValid() ? static_cast(parent.internalPointer()) : &m_rootEntry; if (parentEntry && parentEntry->isDir()) { return parentEntry->entries().count(); } } return 0; } int ArchiveModel::columnCount(const QModelIndex &parent) const { return m_showColumns.size(); } void ArchiveModel::sort(int column, Qt::SortOrder order) { if (m_showColumns.size() <= column) { return; } emit layoutAboutToBeChanged(); QList dirEntries; m_rootEntry.returnDirEntries(&dirEntries); dirEntries.append(&m_rootEntry); const ArchiveModelSorter modelSorter(m_showColumns.at(column), order); foreach(Archive::Entry *dir, dirEntries) { QVector < QPair > sorting(dir->entries().count()); for (int i = 0; i < dir->entries().count(); ++i) { Archive::Entry *item = dir->entries().at(i); sorting[i].first = item; sorting[i].second = i; } qStableSort(sorting.begin(), sorting.end(), modelSorter); QModelIndexList fromIndexes; QModelIndexList toIndexes; for (int r = 0; r < sorting.count(); ++r) { Archive::Entry *item = sorting.at(r).first; toIndexes.append(createIndex(r, 0, item)); fromIndexes.append(createIndex(sorting.at(r).second, 0, sorting.at(r).first)); dir->setEntryAt(r, sorting.at(r).first); } changePersistentIndexList(fromIndexes, toIndexes); emit dataChanged( index(0, 0, indexForEntry(dir)), index(dir->entries().size() - 1, 0, indexForEntry(dir))); } emit layoutChanged(); } Qt::DropActions ArchiveModel::supportedDropActions() const { return Qt::CopyAction | Qt::MoveAction; } QStringList ArchiveModel::mimeTypes() const { QStringList types; // MIME types we accept for dragging (eg. Dolphin -> Ark). types << QStringLiteral("text/uri-list") << QStringLiteral("text/plain") << QStringLiteral("text/x-moz-url"); // MIME types we accept for dropping (eg. Ark -> Dolphin). types << QStringLiteral("application/x-kde-ark-dndextract-service") << QStringLiteral("application/x-kde-ark-dndextract-path"); return types; } QMimeData *ArchiveModel::mimeData(const QModelIndexList &indexes) const { Q_UNUSED(indexes) QMimeData *mimeData = new QMimeData; mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-service"), QDBusConnection::sessionBus().baseService().toUtf8()); mimeData->setData(QStringLiteral("application/x-kde-ark-dndextract-path"), m_dbusPathName.toUtf8()); return mimeData; } bool ArchiveModel::dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) { Q_UNUSED(action) Q_UNUSED(row) Q_UNUSED(column) Q_UNUSED(parent) if (!data->hasUrls()) { return false; } QStringList paths; foreach(const QUrl &url, data->urls()) { paths << url.toLocalFile(); } //for now, this code is not used because adding files to paths inside the //archive is not supported yet. need a solution for this later. QString path; #if 0 if (parent.isValid()) { QModelIndex droppedOnto = index(row, column, parent); Archive::Entry *entry = entryForIndex(droppedOnto); if (entry->isDir()) { qCDebug(ARK) << "Using entry"; path = entry->fileName.toString(); } else { path = entryForIndex(parent)->fileName.toString(); } } qCDebug(ARK) << "Dropped onto " << path; #endif emit droppedFiles(paths, path); return true; } // For a rationale, see bugs #194241, #241967 and #355839 QString ArchiveModel::cleanFileName(const QString& fileName) { // Skip entries with filename "/" or "//" or "." // "." is present in ISO files QRegularExpression pattern(QStringLiteral("/+|\\.")); QRegularExpressionMatch match; if (fileName.contains(pattern, &match) && match.captured() == fileName) { qCDebug(ARK) << "Skipping entry with filename" << fileName; return QString(); } else if (fileName.startsWith(QLatin1String("./"))) { return fileName.mid(2); } return fileName; } Archive::Entry *ArchiveModel::parentFor(const Archive::Entry *entry) { - QStringList pieces = entry->property("fileName").toString().split(QLatin1Char( '/' ), QString::SkipEmptyParts); + QStringList pieces = entry->property("fullPath").toString().split(QLatin1Char( '/' ), QString::SkipEmptyParts); if (pieces.isEmpty()) { return Q_NULLPTR; } pieces.removeLast(); if (s_previousMatch) { //the number of path elements must be the same for the shortcut //to work if (s_previousPieces->count() == pieces.count()) { bool equal = true; //make sure all the pieces match up for (int i = 0; i < s_previousPieces->count(); ++i) { if (s_previousPieces->at(i) != pieces.at(i)) { equal = false; break; } } //if match return it if (equal) { return s_previousMatch; } } } Archive::Entry *parent = &m_rootEntry; foreach(const QString &piece, pieces) { Archive::Entry *entry = parent->find(piece); if (!entry) { // Directory entry will be traversed later (that happens for some archive formats, 7z for instance). // We have to create one before, in order to construct tree from its children, // and then delete the existing one (see ArchiveModel::newEntry). entry = new Archive::Entry(parent); - entry->setProperty("fileName", (parent == &m_rootEntry) + entry->setProperty("fullPath", (parent == &m_rootEntry) ? piece - : parent->property("fileName").toString() + QLatin1Char( '/' ) + piece); + : parent->property("fullPath").toString() + QLatin1Char( '/' ) + piece); entry->setProperty("isDirectory", true); - entry->processNameAndIcon(); insertEntry(entry); } if (!entry->isDir()) { Archive::Entry *e = new Archive::Entry(parent); copyEntryMetaData(e, entry); - e->processNameAndIcon(); // Maybe we have both a file and a directory of the same name. // We avoid removing previous entries unless necessary. insertEntry(e); } parent = entry; } s_previousMatch = parent; *s_previousPieces = pieces; return parent; } QModelIndex ArchiveModel::indexForEntry(Archive::Entry *entry) { Q_ASSERT(entry); if (entry != &m_rootEntry) { Q_ASSERT(entry->getParent()); Q_ASSERT(entry->getParent()->isDir()); return createIndex(entry->row(), 0, entry); } return QModelIndex(); } void ArchiveModel::slotEntryRemoved(const QString & path) { const QString entryFileName(cleanFileName(path)); if (entryFileName.isEmpty()) { return; } Archive::Entry *entry = m_rootEntry.findByPath(entryFileName.split(QLatin1Char( '/' ), QString::SkipEmptyParts)); if (entry) { Archive::Entry *parent = entry->getParent(); QModelIndex index = indexForEntry(entry); Q_UNUSED(index); beginRemoveRows(indexForEntry(parent), entry->row(), entry->row()); //delete parent->entries()[ metaData->row() ]; //parent->entries()[ metaData->row() ] = 0; parent->removeEntryAt(entry->row()); endRemoveRows(); } } void ArchiveModel::slotUserQuery(Kerfuffle::Query *query) { query->execute(); } void ArchiveModel::slotNewEntryFromSetArchive(Archive::Entry *entry) { // we cache all entries that appear when opening a new archive // so we can all them together once it's done, this is a huge // performance improvement because we save from doing lots of // begin/endInsertRows m_newArchiveEntries.push_back(entry); } void ArchiveModel::slotNewEntry(Archive::Entry *entry) { newEntry(entry, NotifyViews); } void ArchiveModel::newEntry(Archive::Entry *receivedEntry, InsertBehaviour behaviour) { - if (receivedEntry->property("fileName").toString().isEmpty()) { + if (receivedEntry->property("fullPath").toString().isEmpty()) { qCDebug(ARK) << "Weird, received empty entry (no filename) - skipping"; return; } //if there are no addidional columns registered, then have a look at the //entry and populate some if (m_showColumns.isEmpty()) { QList toInsert; QMap::const_iterator i = propertiesList.begin(); while (i != propertiesList.end()) { if (!receivedEntry->property(i.value().toStdString().c_str()).toString().isEmpty()) { if (i.key() != CompressedSize || receivedEntry->compressedSizeIsSet) { toInsert << i.key(); } } ++i; } beginInsertColumns(QModelIndex(), 0, toInsert.size() - 1); m_showColumns << toInsert; endInsertColumns(); qCDebug(ARK) << "Showing columns: " << m_showColumns; } //#194241: Filenames such as "./file" should be displayed as "file" //#241967: Entries called "/" should be ignored //#355839: Entries called "//" should be ignored - QString entryFileName = cleanFileName(receivedEntry->property("fileName").toString()); + QString entryFileName = cleanFileName(receivedEntry->property("fullPath").toString()); if (entryFileName.isEmpty()) { // The entry contains only "." or "./" return; } - receivedEntry->setProperty("fileName", entryFileName); + receivedEntry->setProperty("fullPath", entryFileName); /// 1. Skip already created entries Archive::Entry *existing = m_rootEntry.findByPath(entryFileName.split(QLatin1Char( '/' ))); if (existing) { qCDebug(ARK) << "Refreshing entry for" << entryFileName; - existing->setProperty("fileName", entryFileName); + existing->setProperty("fullPath", entryFileName); // Multi-volume files are repeated at least in RAR archives. // In that case, we need to sum the compressed size for each volume qulonglong currentCompressedSize = existing->property("compressedSize").toULongLong(); existing->setProperty("compressedSize", currentCompressedSize + receivedEntry->property("compressedSize").toULongLong()); - existing->processNameAndIcon(); return; } /// 2. Find Parent Entry, creating missing direcotry ArchiveEntries in the process Archive::Entry *parent = parentFor(receivedEntry); /// 3. Create an Archive::Entry const QStringList path = entryFileName.split(QLatin1Char('/'), QString::SkipEmptyParts); const QString name = path.last(); Archive::Entry *entry = parent->find(name); if (entry) { copyEntryMetaData(entry, receivedEntry); - entry->setProperty("fileName", entryFileName); - entry->processNameAndIcon(); + entry->setProperty("fullPath", entryFileName); delete receivedEntry; } else { receivedEntry->setParent(parent); - receivedEntry->processNameAndIcon(); insertEntry(receivedEntry, behaviour); } } void ArchiveModel::slotLoadingFinished(KJob *job) { int i = 0; foreach(Archive::Entry *entry, m_newArchiveEntries) { newEntry(entry, DoNotNotifyViews); i++; } beginResetModel(); endResetModel(); m_newArchiveEntries.clear(); qCDebug(ARK) << "Added" << i << "entries to model"; emit loadingFinished(job); } void ArchiveModel::copyEntryMetaData(Archive::Entry *destinationEntry, const Archive::Entry *sourceEntry) { - destinationEntry->setProperty("fileName", sourceEntry->property("fileName")); + destinationEntry->setProperty("fullPath", sourceEntry->property("fullPath")); destinationEntry->setProperty("permissions", sourceEntry->property("permissions")); destinationEntry->setProperty("owner", sourceEntry->property("owner")); destinationEntry->setProperty("group", sourceEntry->property("group")); destinationEntry->setProperty("size", sourceEntry->property("size")); destinationEntry->setProperty("compressedSize", sourceEntry->property("compressedSize")); destinationEntry->setProperty("link", sourceEntry->property("link")); destinationEntry->setProperty("ratio", sourceEntry->property("ratio")); destinationEntry->setProperty("CRC", sourceEntry->property("CRC")); destinationEntry->setProperty("method", sourceEntry->property("method")); destinationEntry->setProperty("version", sourceEntry->property("version")); destinationEntry->setProperty("timestamp", sourceEntry->property("timestamp").toDateTime()); destinationEntry->setProperty("isDirectory", sourceEntry->property("isDirectory")); destinationEntry->setProperty("comment", sourceEntry->property("comment")); destinationEntry->setProperty("isPasswordProtected", sourceEntry->property("isPasswordProtected")); } void ArchiveModel::insertEntry(Archive::Entry *entry, InsertBehaviour behaviour) { Q_ASSERT(entry); Archive::Entry *parent = entry->getParent(); Q_ASSERT(parent); if (behaviour == NotifyViews) { beginInsertRows(indexForEntry(parent), parent->entries().count(), parent->entries().count()); } parent->appendEntry(entry); if (behaviour == NotifyViews) { endInsertRows(); } } Kerfuffle::Archive* ArchiveModel::archive() const { return m_archive.data(); } KJob* ArchiveModel::setArchive(Kerfuffle::Archive *archive) { m_archive.reset(archive); m_rootEntry.clear(); s_previousMatch = Q_NULLPTR; s_previousPieces->clear(); Kerfuffle::ListJob *job = Q_NULLPTR; m_newArchiveEntries.clear(); if (m_archive) { job = m_archive->list(); // TODO: call "open" or "create"? if (job) { connect(job, &Kerfuffle::ListJob::newEntry, this, &ArchiveModel::slotNewEntryFromSetArchive); connect(job, &Kerfuffle::ListJob::result, this, &ArchiveModel::slotLoadingFinished); connect(job, &Kerfuffle::ListJob::userQuery, this, &ArchiveModel::slotUserQuery); emit loadingStarted(); // TODO: make sure if it's ok to not have calls to beginRemoveColumns here m_showColumns.clear(); } } beginResetModel(); endResetModel(); return job; } -ExtractJob* ArchiveModel::extractFile(const QVariant& fileName, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const +ExtractJob* ArchiveModel::extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { - QList files; - files << QVariant::fromValue(fileRootNodePair(fileName.toString())); + QList files; + files << file; return extractFiles(files, destinationDir, options); } -ExtractJob* ArchiveModel::extractFiles(const QList& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const +ExtractJob* ArchiveModel::extractFiles(const QList& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options) const { Q_ASSERT(m_archive); ExtractJob *newJob = m_archive->copyFiles(files, destinationDir, options); connect(newJob, &ExtractJob::userQuery, this, &ArchiveModel::slotUserQuery); return newJob; } -Kerfuffle::PreviewJob *ArchiveModel::preview(const QString& file) const +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(const QString& file) const +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(const QString& file) const +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(const QStringList & filenames, const CompressionOptions& options) +AddJob* ArchiveModel::addFiles(QList &entries, const CompressionOptions& options) { if (!m_archive) { return Q_NULLPTR; } if (!m_archive->isReadOnly()) { - AddJob *job = m_archive->addFiles(filenames, options); + AddJob *job = m_archive->addFiles(entries, options); connect(job, &AddJob::newEntry, this, &ArchiveModel::slotNewEntry); connect(job, &AddJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return Q_NULLPTR; } -DeleteJob* ArchiveModel::deleteFiles(const QList & files) +DeleteJob* ArchiveModel::deleteFiles(QList entries) { Q_ASSERT(m_archive); if (!m_archive->isReadOnly()) { - DeleteJob *job = m_archive->deleteFiles(files); + DeleteJob *job = m_archive->deleteFiles(entries); connect(job, &DeleteJob::entryRemoved, this, &ArchiveModel::slotEntryRemoved); connect(job, &DeleteJob::finished, this, &ArchiveModel::slotCleanupEmptyDirs); connect(job, &DeleteJob::userQuery, this, &ArchiveModel::slotUserQuery); return job; } return Q_NULLPTR; } void ArchiveModel::encryptArchive(const QString &password, bool encryptHeader) { if (!m_archive) { return; } m_archive->encrypt(password, encryptHeader); } void ArchiveModel::slotCleanupEmptyDirs() { QList queue; QList nodesToDelete; //add root nodes for (int i = 0; i < rowCount(); ++i) { queue.append(QPersistentModelIndex(index(i, 0))); } //breadth-first traverse while (!queue.isEmpty()) { QPersistentModelIndex node = queue.takeFirst(); Archive::Entry *entry = entryForIndex(node); if (!hasChildren(node)) { - if (!entry->property("fileName").toString().isEmpty()) { + if (!entry->property("fullPath").toString().isEmpty()) { nodesToDelete << node; } } else { for (int i = 0; i < rowCount(node); ++i) { queue.append(QPersistentModelIndex(index(i, 0, node))); } } } foreach(const QPersistentModelIndex& node, nodesToDelete) { Archive::Entry *rawEntry = static_cast(node.internalPointer()); qCDebug(ARK) << "Delete with parent entries " << rawEntry->getParent()->entries() << " and row " << rawEntry->row(); beginRemoveRows(parent(node), rawEntry->row(), rawEntry->row()); rawEntry->getParent()->removeEntryAt(rawEntry->row()); endRemoveRows(); } } diff --git a/part/archivemodel.h b/part/archivemodel.h index 55273ac1..b8935374 100644 --- a/part/archivemodel.h +++ b/part/archivemodel.h @@ -1,133 +1,133 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 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. * */ #ifndef ARCHIVEMODEL_H #define ARCHIVEMODEL_H #include #include #include #include "kerfuffle/archiveentry.h" using Kerfuffle::Archive; namespace Kerfuffle { class Query; } class ArchiveModel: public QAbstractItemModel { Q_OBJECT public: explicit ArchiveModel(const QString &dbusPathName, QObject *parent = 0); ~ArchiveModel(); QVariant data(const QModelIndex &index, int role) const Q_DECL_OVERRIDE; Qt::ItemFlags flags(const QModelIndex &index) const Q_DECL_OVERRIDE; QVariant headerData(int section, Qt::Orientation orientation, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE; QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; QModelIndex parent(const QModelIndex &index) const Q_DECL_OVERRIDE; int rowCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; int columnCount(const QModelIndex &parent = QModelIndex()) const Q_DECL_OVERRIDE; virtual void sort(int column, Qt::SortOrder order = Qt::AscendingOrder) Q_DECL_OVERRIDE; //drag and drop related Qt::DropActions supportedDropActions() const Q_DECL_OVERRIDE; QStringList mimeTypes() const Q_DECL_OVERRIDE; QMimeData *mimeData(const QModelIndexList & indexes) const Q_DECL_OVERRIDE; bool dropMimeData(const QMimeData * data, Qt::DropAction action, int row, int column, const QModelIndex & parent) Q_DECL_OVERRIDE; KJob* setArchive(Kerfuffle::Archive *archive); Kerfuffle::Archive *archive() const; Archive::Entry *entryForIndex(const QModelIndex &index); int childCount(const QModelIndex &index, int &dirs, int &files) const; - Kerfuffle::ExtractJob* extractFile(const QVariant& fileName, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; - Kerfuffle::ExtractJob* extractFiles(const QList& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; + Kerfuffle::ExtractJob* extractFile(Archive::Entry *file, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; + Kerfuffle::ExtractJob* extractFiles(const QList& files, const QString& destinationDir, const Kerfuffle::ExtractionOptions& options = Kerfuffle::ExtractionOptions()) const; - Kerfuffle::PreviewJob* preview(const QString& file) const; - Kerfuffle::OpenJob* open(const QString& file) const; - Kerfuffle::OpenWithJob* openWith(const QString& file) const; + Kerfuffle::PreviewJob* preview(Archive::Entry *file) const; + Kerfuffle::OpenJob* open(Archive::Entry *file) const; + Kerfuffle::OpenWithJob* openWith(Archive::Entry *file) const; - Kerfuffle::AddJob* addFiles(const QStringList & paths, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); - Kerfuffle::DeleteJob* deleteFiles(const QList & files); + Kerfuffle::AddJob* addFiles(QList &entries, const Kerfuffle::CompressionOptions& options = Kerfuffle::CompressionOptions()); + Kerfuffle::DeleteJob* deleteFiles(QList entries); /** * @param password The password to encrypt the archive with. * @param encryptHeader Whether to encrypt also the list of files. */ void encryptArchive(const QString &password, bool encryptHeader); signals: void loadingStarted(); void loadingFinished(KJob *); void extractionFinished(bool success); void error(const QString& error, const QString& details); void droppedFiles(const QStringList& files, const QString& path = QString()); private slots: void slotNewEntryFromSetArchive(Archive::Entry *entry); void slotNewEntry(Archive::Entry *entry); void slotLoadingFinished(KJob *job); void slotEntryRemoved(const QString & path); void slotUserQuery(Kerfuffle::Query *query); void slotCleanupEmptyDirs(); private: /** * Strips file names that start with './'. * * For more information, see bug 194241. * * @param fileName The file name that will be stripped. * * @return @p fileName without the leading './' */ QString cleanFileName(const QString& fileName); Archive::Entry *parentFor(const Kerfuffle::Archive::Entry *entry); QModelIndex indexForEntry(Archive::Entry *entry); static bool compareAscending(const QModelIndex& a, const QModelIndex& b); static bool compareDescending(const QModelIndex& a, const QModelIndex& b); /** * Insert the node @p node into the model, ensuring all views are notified * of the change. */ enum InsertBehaviour { NotifyViews, DoNotNotifyViews }; void copyEntryMetaData(Archive::Entry *destinationEntry, const Archive::Entry *sourceEntry); void insertEntry(Archive::Entry *entry, InsertBehaviour behaviour = NotifyViews); void newEntry(Kerfuffle::Archive::Entry *receivedEntry, InsertBehaviour behaviour); QList m_newArchiveEntries; // holds entries from opening a new archive until it's totally open QList m_showColumns; QScopedPointer m_archive; Archive::Entry m_rootEntry; QString m_dbusPathName; }; #endif // ARCHIVEMODEL_H diff --git a/part/infopanel.cpp b/part/infopanel.cpp index 55a8a560..d46f0e1b 100644 --- a/part/infopanel.cpp +++ b/part/infopanel.cpp @@ -1,206 +1,206 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "infopanel.h" #include "kerfuffle/archiveentry.h" #include #include #include using namespace Kerfuffle; static QPixmap getDesktopIconForName(const QString& name) { return QIcon::fromTheme(name).pixmap(IconSize(KIconLoader::Desktop), IconSize(KIconLoader::Desktop)); } InfoPanel::InfoPanel(ArchiveModel *model, QWidget *parent) : QFrame(parent), m_model(model) { setupUi(this); // Make the file name font bigger than the rest QFont fnt = fileName->font(); if (fnt.pointSize() > -1) { fnt.setPointSize(fnt.pointSize() + 1); } else { fnt.setPixelSize(fnt.pixelSize() + 3); } fileName->setFont(fnt); updateWithDefaults(); } InfoPanel::~InfoPanel() { } void InfoPanel::updateWithDefaults() { iconLabel->setPixmap(getDesktopIconForName(QStringLiteral("utilities-file-archiver"))); const QString currentFileName = prettyFileName(); if (currentFileName.isEmpty()) { fileName->setText(i18n("No archive loaded")); } else { fileName->setText(currentFileName); } additionalInfo->setText(QString()); hideMetaData(); } QString InfoPanel::prettyFileName() const { if (m_prettyFileName.isEmpty()) { if (m_model->archive()) { QFileInfo fileInfo(m_model->archive()->fileName()); return fileInfo.fileName(); } } return m_prettyFileName; } void InfoPanel::setPrettyFileName(const QString& fileName) { m_prettyFileName = fileName; } void InfoPanel::setIndex(const QModelIndex& index) { if (!index.isValid()) { updateWithDefaults(); } else { const Archive::Entry *entry = m_model->entryForIndex(index); QMimeDatabase db; QMimeType mimeType; if (entry->isDir()) { mimeType = db.mimeTypeForName(QStringLiteral("inode/directory")); } else { - mimeType = db.mimeTypeForFile(entry->property("fileName").toString(), QMimeDatabase::MatchExtension); + mimeType = db.mimeTypeForFile(entry->property("fullPath").toString(), QMimeDatabase::MatchExtension); } iconLabel->setPixmap(getDesktopIconForName(mimeType.iconName())); if (entry->isDir()) { int dirs; int files; const int children = m_model->childCount(index, dirs, files); additionalInfo->setText(KIO::itemsSummaryString(children, files, dirs, 0, false)); } else if (!entry->property("link").toString().isEmpty()) { additionalInfo->setText(i18n("Symbolic Link")); } else { if (entry->property("size") != 0) { additionalInfo->setText(KIO::convertSize(entry->property("size").toULongLong())); } else { additionalInfo->setText(i18n("Unknown size")); } } - const QStringList nameParts = entry->property("fileName").toString().split(QLatin1Char( '/' ), QString::SkipEmptyParts); - const QString name = (nameParts.count() > 0) ? nameParts.last() : entry->property("fileName").toString(); + const QStringList nameParts = entry->property("fullPath").toString().split(QLatin1Char( '/' ), QString::SkipEmptyParts); + const QString name = (nameParts.count() > 0) ? nameParts.last() : entry->property("fullPath").toString(); fileName->setText(name); showMetaDataFor(index); } } void InfoPanel::setIndexes(const QModelIndexList &list) { if (list.size() == 0) { setIndex(QModelIndex()); } else if (list.size() == 1) { setIndex(list[ 0 ]); } else { iconLabel->setPixmap(getDesktopIconForName(QStringLiteral("utilities-file-archiver"))); fileName->setText(i18np("One file selected", "%1 files selected", list.size())); quint64 totalSize = 0; foreach(const QModelIndex& index, list) { const Archive::Entry *entry = m_model->entryForIndex(index); totalSize += entry->property("size").toULongLong(); } additionalInfo->setText(KIO::convertSize(totalSize)); hideMetaData(); } } void InfoPanel::showMetaData() { m_separator->show(); m_metaDataWidget->show(); } void InfoPanel::hideMetaData() { m_separator->hide(); m_metaDataWidget->hide(); } void InfoPanel::showMetaDataFor(const QModelIndex &index) { showMetaData(); const Archive::Entry *entry = m_model->entryForIndex(index); QMimeDatabase db; QMimeType mimeType; if (entry->isDir()) { mimeType = db.mimeTypeForName(QStringLiteral("inode/directory")); } else { - mimeType = db.mimeTypeForFile(entry->property("fileName").toString(), QMimeDatabase::MatchExtension); + mimeType = db.mimeTypeForFile(entry->property("fullPath").toString(), QMimeDatabase::MatchExtension); } m_typeLabel->setText(i18n("Type: %1", mimeType.comment())); if (!entry->property("owner").toString().isEmpty()) { m_ownerLabel->show(); m_ownerLabel->setText(i18n("Owner: %1", entry->property("owner").toString())); } else { m_ownerLabel->hide(); } if (!entry->property("group").toString().isEmpty()) { m_groupLabel->show(); m_groupLabel->setText(i18n("Group: %1", entry->property("group").toString())); } else { m_groupLabel->hide(); } if (!entry->property("link").toString().isEmpty()) { m_targetLabel->show(); m_targetLabel->setText(i18n("Target: %1", entry->property("link").toString())); } else { m_targetLabel->hide(); } if (entry->property("isPasswordProtected").toBool()) { m_passwordLabel->show(); m_passwordLabel->setText(i18n("Password protected: Yes")); } else { m_passwordLabel->hide(); } } diff --git a/part/part.cpp b/part/part.cpp index fb486f0b..e9e0e8db 100644 --- a/part/part.cpp +++ b/part/part.cpp @@ -1,1370 +1,1372 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2009-2012 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "part.h" #include "ark_debug.h" #include "archiveformat.h" #include "archivemodel.h" #include "archiveview.h" #include "arkviewer.h" #include "dnddbusinterfaceadaptor.h" #include "infopanel.h" #include "jobtracker.h" -#include "kerfuffle/archiveentry.h" #include "kerfuffle/extractiondialog.h" #include "kerfuffle/extractionsettingspage.h" #include "kerfuffle/jobs.h" #include "kerfuffle/settings.h" #include "kerfuffle/previewsettingspage.h" #include "kerfuffle/propertiesdialog.h" #include "pluginmanager.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY(Factory, registerPlugin();) namespace Ark { static quint32 s_instanceCounter = 1; Part::Part(QWidget *parentWidget, QObject *parent, const QVariantList& args) : KParts::ReadWritePart(parent), m_splitter(Q_NULLPTR), m_busy(false), m_jobTracker(Q_NULLPTR) { Q_UNUSED(args) setComponentData(*createAboutData(), false); new DndExtractAdaptor(this); const QString pathName = QStringLiteral("/DndExtract/%1").arg(s_instanceCounter++); if (!QDBusConnection::sessionBus().registerObject(pathName, this)) { qCCritical(ARK) << "Could not register a D-Bus object for drag'n'drop"; } // m_vlayout is needed for later insertion of QMessageWidget QWidget *mainWidget = new QWidget; m_vlayout = new QVBoxLayout; m_model = new ArchiveModel(pathName, this); m_splitter = new QSplitter(Qt::Horizontal, parentWidget); m_view = new ArchiveView; m_infoPanel = new InfoPanel(m_model); // Add widgets for the comment field. m_commentView = new QPlainTextEdit(); m_commentView->setReadOnly(true); m_commentView->setFont(QFontDatabase::systemFont(QFontDatabase::FixedFont)); m_commentBox = new QGroupBox(i18n("Comment")); m_commentBox->hide(); QVBoxLayout *vbox = new QVBoxLayout; vbox->addWidget(m_commentView); m_commentBox->setLayout(vbox); m_commentMsgWidget = new KMessageWidget(); m_commentMsgWidget->setText(i18n("Comment has been modified.")); m_commentMsgWidget->setMessageType(KMessageWidget::Information); m_commentMsgWidget->setCloseButtonVisible(false); m_commentMsgWidget->hide(); QAction *saveAction = new QAction(i18n("Save"), m_commentMsgWidget); m_commentMsgWidget->addAction(saveAction); connect(saveAction, &QAction::triggered, this, &Part::slotAddComment); m_commentBox->layout()->addWidget(m_commentMsgWidget); connect(m_commentView, &QPlainTextEdit::textChanged, this, &Part::slotCommentChanged); setWidget(mainWidget); mainWidget->setLayout(m_vlayout); // Configure the QVBoxLayout and add widgets m_vlayout->setContentsMargins(0,0,0,0); m_vlayout->addWidget(m_splitter); // Vertical QSplitter for the file view and comment field. m_commentSplitter = new QSplitter(Qt::Vertical, parentWidget); m_commentSplitter->setOpaqueResize(false); m_commentSplitter->addWidget(m_view); m_commentSplitter->addWidget(m_commentBox); m_commentSplitter->setCollapsible(0, false); // Horizontal QSplitter for the file view and infopanel. m_splitter->addWidget(m_commentSplitter); m_splitter->addWidget(m_infoPanel); // Read settings from config file and show/hide infoPanel. if (!ArkSettings::showInfoPanel()) { m_infoPanel->hide(); } else { m_splitter->setSizes(ArkSettings::splitterSizes()); } setupView(); setupActions(); connect(m_model, &ArchiveModel::loadingStarted, this, &Part::slotLoadingStarted); connect(m_model, &ArchiveModel::loadingFinished, this, &Part::slotLoadingFinished); connect(m_model, &ArchiveModel::droppedFiles, this, static_cast(&Part::slotAddFiles)); connect(m_model, &ArchiveModel::error, this, &Part::slotError); connect(this, &Part::busy, this, &Part::setBusyGui); connect(this, &Part::ready, this, &Part::setReadyGui); connect(this, static_cast(&KParts::ReadOnlyPart::completed), this, &Part::setFileNameFromArchive); m_statusBarExtension = new KParts::StatusBarExtension(this); setXMLFile(QStringLiteral("ark_part.rc")); } Part::~Part() { qDeleteAll(m_tmpOpenDirList); // Only save splitterSizes if infopanel is visible, // because we don't want to store zero size for infopanel. if (m_showInfoPanelAction->isChecked()) { ArkSettings::setSplitterSizes(m_splitter->sizes()); } ArkSettings::setShowInfoPanel(m_showInfoPanelAction->isChecked()); ArkSettings::self()->save(); m_extractArchiveAction->menu()->deleteLater(); m_extractAction->menu()->deleteLater(); } void Part::slotCommentChanged() { if (m_commentMsgWidget->isHidden() && m_commentView->toPlainText() != m_model->archive()->comment()) { m_commentMsgWidget->animatedShow(); } else if (m_commentMsgWidget->isVisible() && m_commentView->toPlainText() == m_model->archive()->comment()) { m_commentMsgWidget->hide(); } } KAboutData *Part::createAboutData() { return new KAboutData(QStringLiteral("ark"), i18n("ArkPart"), QStringLiteral("3.0")); } void Part::registerJob(KJob* job) { if (!m_jobTracker) { m_jobTracker = new JobTracker(widget()); m_statusBarExtension->addStatusBarItem(m_jobTracker->widget(0), 0, true); m_jobTracker->widget(job)->show(); } m_jobTracker->registerJob(job); emit busy(); connect(job, &KJob::result, this, &Part::ready); } // TODO: KIO::mostLocalHere is used here to resolve some KIO URLs to local // paths (e.g. desktop:/), but more work is needed to support extraction // to non-local destinations. See bugs #189322 and #204323. void Part::extractSelectedFilesTo(const QString& localPath) { if (!m_model) { return; } const QUrl url = QUrl::fromUserInput(localPath, QString()); KIO::StatJob* statJob = nullptr; // Try to resolve the URL to a local path. if (!url.isLocalFile() && !url.scheme().isEmpty()) { statJob = KIO::mostLocalUrl(url); if (!statJob->exec() || statJob->error() != 0) { return; } } const QString destination = statJob ? statJob->statResult().stringValue(KIO::UDSEntry::UDS_LOCAL_PATH) : localPath; delete statJob; // The URL could not be resolved to a local path. if (!url.isLocalFile() && destination.isEmpty()) { qCWarning(ARK) << "Ark cannot extract to non-local destination:" << localPath; KMessageBox::sorry(widget(), xi18nc("@info", "Ark can only extract to local destinations.")); return; } qCDebug(ARK) << "Extract to" << destination; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; options[QStringLiteral("RemoveRootNode")] = true; options[QStringLiteral("DragAndDrop")] = true; // Create and start the ExtractJob. ExtractJob *job = m_model->extractFiles(filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())), destination, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } void Part::setupView() { m_view->setContextMenuPolicy(Qt::CustomContextMenu); m_view->setModel(m_model); m_view->setSortingEnabled(true); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::updateActions); connect(m_view->selectionModel(), &QItemSelectionModel::selectionChanged, this, &Part::selectionChanged); connect(m_view, &QTreeView::activated, this, &Part::slotActivated); connect(m_view, &QWidget::customContextMenuRequested, this, &Part::slotShowContextMenu); connect(m_model, &QAbstractItemModel::columnsInserted, this, &Part::adjustColumns); } void Part::slotActivated(QModelIndex) { // The activated signal is emitted when items are selected with the mouse, // so do nothing if CTRL or SHIFT key is pressed. if (QGuiApplication::keyboardModifiers() != Qt::ShiftModifier && QGuiApplication::keyboardModifiers() != Qt::ControlModifier) { ArkSettings::defaultOpenAction() == ArkSettings::EnumDefaultOpenAction::Preview ? slotOpenEntry(Preview) : slotOpenEntry(OpenFile); } } void Part::setupActions() { // We use a QSignalMapper for the preview, open and openwith actions. This // way we can connect all three actions to the same slot slotOpenEntry and // pass the OpenFileMode as argument to the slot. m_signalMapper = new QSignalMapper; m_showInfoPanelAction = new KToggleAction(i18nc("@action:inmenu", "Show information panel"), this); actionCollection()->addAction(QStringLiteral( "show-infopanel" ), m_showInfoPanelAction); m_showInfoPanelAction->setChecked(ArkSettings::showInfoPanel()); connect(m_showInfoPanelAction, &QAction::triggered, this, &Part::slotToggleInfoPanel); m_saveAsAction = actionCollection()->addAction(KStandardAction::SaveAs, QStringLiteral("ark_file_save_as"), this, SLOT(slotSaveAs())); m_openFileAction = actionCollection()->addAction(QStringLiteral("openfile")); m_openFileAction->setText(i18nc("open a file with external program", "&Open")); m_openFileAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with the associated application")); connect(m_openFileAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_openFileAction, OpenFile); m_openFileWithAction = actionCollection()->addAction(QStringLiteral("openfilewith")); m_openFileWithAction->setText(i18nc("open a file with external program", "Open &With...")); m_openFileWithAction->setIcon(QIcon::fromTheme(QStringLiteral("document-open"))); m_openFileWithAction->setToolTip(i18nc("@info:tooltip", "Click to open the selected file with an external program")); connect(m_openFileWithAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_openFileWithAction, OpenFileWith); m_previewAction = actionCollection()->addAction(QStringLiteral("preview")); m_previewAction->setText(i18nc("to preview a file inside an archive", "Pre&view")); m_previewAction->setIcon(QIcon::fromTheme(QStringLiteral("document-preview-archive"))); m_previewAction->setToolTip(i18nc("@info:tooltip", "Click to preview the selected file")); actionCollection()->setDefaultShortcut(m_previewAction, Qt::CTRL + Qt::Key_P); connect(m_previewAction, SIGNAL(triggered(bool)), m_signalMapper, SLOT(map())); m_signalMapper->setMapping(m_previewAction, Preview); m_extractArchiveAction = actionCollection()->addAction(QStringLiteral("extract_all")); m_extractArchiveAction->setText(i18nc("@action:inmenu", "E&xtract All")); m_extractArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); m_extractArchiveAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose how to extract all the files in the archive")); actionCollection()->setDefaultShortcut(m_extractArchiveAction, Qt::CTRL + Qt::SHIFT + Qt::Key_E); connect(m_extractArchiveAction, &QAction::triggered, this, &Part::slotExtractArchive); m_extractAction = actionCollection()->addAction(QStringLiteral("extract")); m_extractAction->setText(i18nc("@action:inmenu", "&Extract")); m_extractAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); actionCollection()->setDefaultShortcut(m_extractAction, Qt::CTRL + Qt::Key_E); m_extractAction->setToolTip(i18n("Click to open an extraction dialog, where you can choose to extract either all files or just the selected ones")); connect(m_extractAction, &QAction::triggered, this, &Part::slotShowExtractionDialog); m_addFilesAction = actionCollection()->addAction(QStringLiteral("add")); m_addFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert"))); m_addFilesAction->setText(i18n("Add &File...")); m_addFilesAction->setToolTip(i18nc("@info:tooltip", "Click to add files to the archive")); connect(m_addFilesAction, SIGNAL(triggered(bool)), this, SLOT(slotAddFiles())); m_addDirAction = actionCollection()->addAction(QStringLiteral("add-dir")); m_addDirAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-insert-directory"))); m_addDirAction->setText(i18n("Add Fo&lder...")); m_addDirAction->setToolTip(i18nc("@info:tooltip", "Click to add a folder to the archive")); connect(m_addDirAction, &QAction::triggered, this, &Part::slotAddDir); m_deleteFilesAction = actionCollection()->addAction(QStringLiteral("delete")); m_deleteFilesAction->setIcon(QIcon::fromTheme(QStringLiteral("archive-remove"))); m_deleteFilesAction->setText(i18n("De&lete")); actionCollection()->setDefaultShortcut(m_deleteFilesAction, Qt::Key_Delete); m_deleteFilesAction->setToolTip(i18nc("@info:tooltip", "Click to delete the selected files")); connect(m_deleteFilesAction, &QAction::triggered, this, &Part::slotDeleteFiles); m_propertiesAction = actionCollection()->addAction(QStringLiteral("properties")); m_propertiesAction->setIcon(QIcon::fromTheme(QStringLiteral("document-properties"))); m_propertiesAction->setText(i18nc("@action:inmenu", "&Properties")); actionCollection()->setDefaultShortcut(m_propertiesAction, Qt::ALT + Qt::Key_Return); m_propertiesAction->setToolTip(i18nc("@info:tooltip", "Click to see properties for archive")); connect(m_propertiesAction, &QAction::triggered, this, &Part::slotShowProperties); m_editCommentAction = actionCollection()->addAction(QStringLiteral("edit_comment")); m_editCommentAction->setIcon(QIcon::fromTheme(QStringLiteral("document-edit"))); actionCollection()->setDefaultShortcut(m_editCommentAction, Qt::ALT + Qt::Key_C); m_editCommentAction->setToolTip(i18nc("@info:tooltip", "Click to add or edit comment")); connect(m_editCommentAction, &QAction::triggered, this, &Part::slotShowComment); m_testArchiveAction = actionCollection()->addAction(QStringLiteral("test_archive")); m_testArchiveAction->setIcon(QIcon::fromTheme(QStringLiteral("checkmark"))); m_testArchiveAction->setText(i18nc("@action:inmenu", "&Test Integrity")); actionCollection()->setDefaultShortcut(m_testArchiveAction, Qt::ALT + Qt::Key_T); m_testArchiveAction->setToolTip(i18nc("@info:tooltip", "Click to test the archive for integrity")); connect(m_testArchiveAction, &QAction::triggered, this, &Part::slotTestArchive); connect(m_signalMapper, static_cast(&QSignalMapper::mapped), this, &Part::slotOpenEntry); updateActions(); updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); } void Part::updateActions() { bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); const Archive::Entry *entry = m_model->entryForIndex(m_view->selectionModel()->currentIndex()); int selectedEntriesCount = m_view->selectionModel()->selectedRows().count(); // Figure out if entry size is larger than preview size limit. const int maxPreviewSize = ArkSettings::previewFileSizeLimit() * 1024 * 1024; const bool limit = ArkSettings::limitPreviewFileSize(); bool isPreviewable = (!limit || (limit && entry != Q_NULLPTR && entry->property("size").toLongLong() < maxPreviewSize)); m_previewAction->setEnabled(!isBusy() && isPreviewable && !entry->isDir() && (selectedEntriesCount == 1)); m_extractArchiveAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_extractAction->setEnabled(!isBusy() && (m_model->rowCount() > 0)); m_saveAsAction->setEnabled(!isBusy() && m_model->rowCount() > 0); m_addFilesAction->setEnabled(!isBusy() && isWritable); m_addDirAction->setEnabled(!isBusy() && isWritable); m_deleteFilesAction->setEnabled(!isBusy() && isWritable && (selectedEntriesCount > 0)); m_openFileAction->setEnabled(!isBusy() && isPreviewable && !entry->isDir() && (selectedEntriesCount == 1)); m_openFileWithAction->setEnabled(!isBusy() && isPreviewable && !entry->isDir() && (selectedEntriesCount == 1)); m_propertiesAction->setEnabled(!isBusy() && m_model->archive()); m_commentView->setEnabled(!isBusy()); m_commentMsgWidget->setEnabled(!isBusy()); m_editCommentAction->setEnabled(false); m_testArchiveAction->setEnabled(false); if (m_model->archive()) { const KPluginMetaData metadata = PluginManager().preferredPluginFor(m_model->archive()->mimeType())->metaData(); bool supportsWriteComment = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsWriteComment(); m_editCommentAction->setEnabled(!isBusy() && supportsWriteComment); m_commentView->setReadOnly(!supportsWriteComment); m_editCommentAction->setText(m_model->archive()->hasComment() ? i18nc("@action:inmenu mutually exclusive with Add &Comment", "Edit &Comment") : i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment")); bool supportsTesting = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsTesting(); m_testArchiveAction->setEnabled(!isBusy() && supportsTesting); } else { m_commentView->setReadOnly(true); m_editCommentAction->setText(i18nc("@action:inmenu mutually exclusive with Edit &Comment", "Add &Comment")); } } void Part::slotShowComment() { if (!m_commentBox->isVisible()) { m_commentBox->show(); m_commentSplitter->setSizes(QList() << m_view->height() * 0.6 << 1); } m_commentView->setFocus(); } void Part::slotAddComment() { CommentJob *job = m_model->archive()->addComment(m_commentView->toPlainText()); if (!job) { return; } registerJob(job); job->start(); m_commentMsgWidget->hide(); if (m_commentView->toPlainText().isEmpty()) { m_commentBox->hide(); } } void Part::slotTestArchive() { TestJob *job = m_model->archive()->testArchive(); if (!job) { return; } registerJob(job); connect(job, &KJob::result, this, &Part::slotTestingDone); job->start(); } void Part::slotTestingDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else if (static_cast(job)->testSucceeded()) { KMessageBox::information(widget(), i18n("The archive passed the integrity test."), i18n("Test Results")); } else { KMessageBox::error(widget(), i18n("The archive failed the integrity test."), i18n("Test Results")); } } void Part::updateQuickExtractMenu(QAction *extractAction) { if (!extractAction) { return; } QMenu *menu = extractAction->menu(); if (!menu) { menu = new QMenu(); extractAction->setMenu(menu); connect(menu, &QMenu::triggered, this, &Part::slotQuickExtractFiles); // Remember to keep this action's properties as similar to // extractAction's as possible (except where it does not make // sense, such as the text or the shortcut). QAction *extractTo = menu->addAction(i18n("Extract To...")); extractTo->setIcon(extractAction->icon()); extractTo->setToolTip(extractAction->toolTip()); if (extractAction == m_extractArchiveAction) { connect(extractTo, &QAction::triggered, this, &Part::slotExtractArchive); } else { connect(extractTo, &QAction::triggered, this, &Part::slotShowExtractionDialog); } menu->addSeparator(); QAction *header = menu->addAction(i18n("Quick Extract To...")); header->setEnabled(false); header->setIcon(QIcon::fromTheme(QStringLiteral("archive-extract"))); } while (menu->actions().size() > 3) { menu->removeAction(menu->actions().last()); } const KConfigGroup conf(KSharedConfig::openConfig(), "ExtractDialog"); const QStringList dirHistory = conf.readPathEntry("DirHistory", QStringList()); for (int i = 0; i < qMin(10, dirHistory.size()); ++i) { const QString dir = QUrl(dirHistory.value(i)).toString(QUrl::RemoveScheme | QUrl::NormalizePathSegments | QUrl::PreferLocalFile); if (QDir(dir).exists()) { QAction *newAction = menu->addAction(dir); newAction->setData(dir); } } } void Part::slotQuickExtractFiles(QAction *triggeredAction) { // #190507: triggeredAction->data.isNull() means it's the "Extract to..." // action, and we do not want it to run here if (!triggeredAction->data().isNull()) { const QString userDestination = triggeredAction->data().toString(); qCDebug(ARK) << "Extract to user dest" << userDestination; QString finalDestinationDirectory; const QString detectedSubfolder = detectSubfolder(); qCDebug(ARK) << "Detected subfolder" << detectedSubfolder; if (!isSingleFolderArchive()) { finalDestinationDirectory = userDestination + QDir::separator() + detectedSubfolder; QDir(userDestination).mkdir(detectedSubfolder); } else { finalDestinationDirectory = userDestination; } qCDebug(ARK) << "Extract to final dest" << finalDestinationDirectory; Kerfuffle::ExtractionOptions options; options[QStringLiteral("PreservePaths")] = true; - QList files = filesAndRootNodesForIndexes(m_view->selectionModel()->selectedRows()); + QList files = filesAndRootNodesForIndexes(m_view->selectionModel()->selectedRows()); ExtractJob *job = m_model->extractFiles(files, finalDestinationDirectory, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } } void Part::selectionChanged() { m_infoPanel->setIndexes(m_view->selectionModel()->selectedRows()); } bool Part::openFile() { qCDebug(ARK) << "Attempting to open archive" << localFilePath(); if (!isLocalFileValid()) { return false; } const QString fixedMimeType = arguments().metaData()[QStringLiteral("fixedMimeType")]; QScopedPointer archive(Kerfuffle::Archive::create(localFilePath(), fixedMimeType, m_model)); Q_ASSERT(archive); if (archive->error() == NoPlugin) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open %1. No suitable plugin found." "Ark does not seem to support this file type.", QFileInfo(localFilePath()).fileName())); return false; } if (archive->error() == FailedPlugin) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Ark was not able to open %1. Failed to load a suitable plugin." "Make sure any executables needed to handle the archive type are installed.", QFileInfo(localFilePath()).fileName())); return false; } Q_ASSERT(archive->isValid()); // Plugin loaded successfully. KJob *job = m_model->setArchive(archive.take()); if (job) { registerJob(job); job->start(); } else { updateActions(); } m_infoPanel->setIndex(QModelIndex()); if (arguments().metaData()[QStringLiteral("showExtractDialog")] == QLatin1String("true")) { QTimer::singleShot(0, this, &Part::slotShowExtractionDialog); } const QString password = arguments().metaData()[QStringLiteral("encryptionPassword")]; if (!password.isEmpty()) { m_model->encryptArchive(password, arguments().metaData()[QStringLiteral("encryptHeader")] == QLatin1String("true")); } return true; } bool Part::saveFile() { return true; } bool Part::isBusy() const { return m_busy; } KConfigSkeleton *Part::config() const { return ArkSettings::self(); } QList Part::settingsPages(QWidget *parent) const { QList pages; pages.append(new ExtractionSettingsPage(parent, i18nc("@title:tab", "Extraction Settings"), QStringLiteral("archive-extract"))); pages.append(new PreviewSettingsPage(parent, i18nc("@title:tab", "Preview Settings"), QStringLiteral("document-preview-archive"))); return pages; } bool Part::isLocalFileValid() { const QString localFile = localFilePath(); const QFileInfo localFileInfo(localFile); const bool creatingNewArchive = arguments().metaData()[QStringLiteral("createNewArchive")] == QLatin1String("true"); if (localFileInfo.isDir()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "%1 is a directory.", localFile)); return false; } if (creatingNewArchive) { if (localFileInfo.exists()) { if (!confirmAndDelete(localFile)) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Could not overwrite %1. Check whether you have write permission.", localFile)); return false; } } displayMsgWidget(KMessageWidget::Information, xi18nc("@info", "The archive %1 will be created as soon as you add a file.", localFile)); } else { if (!localFileInfo.exists()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 was not found.", localFile)); return false; } if (!localFileInfo.isReadable()) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "The archive %1 could not be loaded, as it was not possible to read from it.", localFile)); return false; } } return true; } bool Part::confirmAndDelete(const QString &targetFile) { QFileInfo targetInfo(targetFile); const auto buttonCode = KMessageBox::warningYesNo(widget(), xi18nc("@info", "The archive %1 already exists. Do you wish to overwrite it?", targetInfo.fileName()), i18nc("@title:window", "File Exists"), KGuiItem(i18nc("@action:button", "Overwrite")), KStandardGuiItem::cancel()); if (buttonCode != KMessageBox::Yes || !targetInfo.isWritable()) { return false; } qCDebug(ARK) << "Removing file" << targetFile; return QFile(targetFile).remove(); } void Part::slotLoadingStarted() { } void Part::slotLoadingFinished(KJob *job) { if (job->error()) { if (arguments().metaData()[QStringLiteral("createNewArchive")] != QLatin1String("true")) { if (job->error() != KJob::KilledJobError) { displayMsgWidget(KMessageWidget::Error, xi18nc("@info", "Loading the archive %1 failed with the following error:%2", localFilePath(), job->errorText())); } // The file failed to open, so reset the open archive, info panel and caption. m_model->setArchive(Q_NULLPTR); m_infoPanel->setPrettyFileName(QString()); m_infoPanel->updateWithDefaults(); emit setWindowCaption(QString()); } } m_view->sortByColumn(0, Qt::AscendingOrder); // #303708: expand the first level only when there is just one root folder. // Typical use case: an archive with source files. if (m_view->model()->rowCount() == 1) { m_view->expandToDepth(0); } // After loading all files, resize the columns to fit all fields m_view->header()->resizeSections(QHeaderView::ResizeToContents); updateActions(); if (!m_model->archive()) { return; } if (!m_model->archive()->comment().isEmpty()) { m_commentView->setPlainText(m_model->archive()->comment()); slotShowComment(); } else { m_commentView->clear(); m_commentBox->hide(); } if (m_model->rowCount() == 0) { qCWarning(ARK) << "No entry listed by the plugin"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "The archive is empty or Ark could not open its content.")); } else if (m_model->rowCount() == 1) { if (m_model->archive()->mimeType().inherits(QStringLiteral("application/x-cd-image")) && - m_model->entryForIndex(m_model->index(0, 0))->property("fileName").toString() == QLatin1String("README.TXT")) { + m_model->entryForIndex(m_model->index(0, 0))->property("fullPath").toString() == QLatin1String("README.TXT")) { qCWarning(ARK) << "Detected ISO image with UDF filesystem"; displayMsgWidget(KMessageWidget::Warning, xi18nc("@info", "Ark does not currently support ISO files with UDF filesystem.")); } } } void Part::setReadyGui() { QApplication::restoreOverrideCursor(); m_busy = false; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->hide(); } m_view->setEnabled(true); updateActions(); } void Part::setBusyGui() { QApplication::setOverrideCursor(QCursor(Qt::WaitCursor)); m_busy = true; if (m_statusBarExtension->statusBar()) { m_statusBarExtension->statusBar()->show(); } m_view->setEnabled(false); updateActions(); } void Part::setFileNameFromArchive() { const QString prettyName = url().fileName(); m_infoPanel->setPrettyFileName(prettyName); m_infoPanel->updateWithDefaults(); emit setWindowCaption(prettyName); } void Part::slotOpenEntry(int mode) { qCDebug(ARK) << "Opening with mode" << mode; QModelIndex index = m_view->selectionModel()->currentIndex(); - const Archive::Entry *entry = m_model->entryForIndex(index); + Archive::Entry *entry = m_model->entryForIndex(index); // Don't open directories. if (entry->isDir()) { return; } // We don't support opening symlinks. if (!entry->property("link").toString().isEmpty()) { displayMsgWidget(KMessageWidget::Information, i18n("Ark cannot open symlinks.")); return; } // Extract the entry. if (!entry->property("fileName").toString().isEmpty()) { m_openFileMode = static_cast(mode); KJob *job = Q_NULLPTR; if (m_openFileMode == Preview) { - job = m_model->preview(entry->property("fileName").toString()); + job = m_model->preview(entry); connect(job, &KJob::result, this, &Part::slotPreviewExtractedEntry); } else { - const QString file = entry->property("fileName").toString(); - job = (m_openFileMode == OpenFile) ? m_model->open(file) : m_model->openWith(file); + job = (m_openFileMode == OpenFile) ? m_model->open(entry) : m_model->openWith(entry); connect(job, &KJob::result, this, &Part::slotOpenExtractedEntry); } registerJob(job); job->start(); } } void Part::slotOpenExtractedEntry(KJob *job) { if (!job->error()) { OpenJob *openJob = qobject_cast(job); Q_ASSERT(openJob); // Since the user could modify the file (unlike the Preview case), // we'll need to manually delete the temp dir in the Part destructor. m_tmpOpenDirList << openJob->tempDir(); const QString fullName = openJob->validatedFilePath(); bool isWritable = m_model->archive() && !m_model->archive()->isReadOnly(); // If archive is readonly set temporarily extracted file to readonly as // well so user will be notified if trying to modify and save the file. if (!isWritable) { QFile::setPermissions(fullName, QFileDevice::ReadOwner | QFileDevice::ReadGroup | QFileDevice::ReadOther); } if (isWritable) { m_fileWatcher = new QFileSystemWatcher; connect(m_fileWatcher, &QFileSystemWatcher::fileChanged, this, &Part::slotWatchedFileModified); m_fileWatcher->addPath(fullName); } if (qobject_cast(job)) { const QList urls = {QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile)}; KRun::displayOpenWithDialog(urls, widget()); } else { KRun::runUrl(QUrl::fromUserInput(fullName, QString(), QUrl::AssumeLocalFile), QMimeDatabase().mimeTypeForFile(fullName).name(), widget()); } } else if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotPreviewExtractedEntry(KJob *job) { if (!job->error()) { PreviewJob *previewJob = qobject_cast(job); Q_ASSERT(previewJob); ArkViewer::view(previewJob->validatedFilePath()); } else if (job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } setReadyGui(); } void Part::slotWatchedFileModified(const QString& file) { qCDebug(ARK) << "Watched file modified:" << file; // Find the relative path of the file within the archive. QString relPath = file; foreach (QTemporaryDir *tmpDir, m_tmpOpenDirList) { relPath.remove(tmpDir->path()); //Remove tmpDir. } relPath = relPath.mid(1); //Remove leading slash. if (relPath.contains(QLatin1Char('/'))) { relPath = relPath.section(QLatin1Char('/'), 0, -2); //Remove filename. } else { // File is in the root of the archive, no path. relPath = QString(); } // Set up a string for display in KMessageBox. QString prettyFilename; if (relPath.isEmpty()) { prettyFilename = file.section(QLatin1Char('/'), -1); } else { prettyFilename = relPath + QLatin1Char('/') + file.section(QLatin1Char('/'), -1); } if (KMessageBox::questionYesNo(widget(), xi18n("The file %1 was modified. Do you want to update the archive?", prettyFilename), i18nc("@title:window", "File Modified")) == KMessageBox::Yes) { QStringList list = QStringList() << file; qCDebug(ARK) << "Updating file" << file << "with path" << relPath; slotAddFiles(list, relPath); } // This is needed because some apps, such as Kate, delete and recreate // files when saving. m_fileWatcher->addPath(file); } void Part::slotError(const QString& errorMessage, const QString& details) { if (details.isEmpty()) { KMessageBox::error(widget(), errorMessage); } else { KMessageBox::detailedError(widget(), errorMessage, details); } } bool Part::isSingleFolderArchive() const { return m_model->archive()->isSingleFolderArchive(); } QString Part::detectSubfolder() const { if (!m_model) { return QString(); } return m_model->archive()->subfolderName(); } void Part::slotExtractArchive() { if (m_view->selectionModel()->selectedRows().count() > 0) { m_view->selectionModel()->clear(); } slotShowExtractionDialog(); } void Part::slotShowExtractionDialog() { if (!m_model) { return; } QPointer dialog(new Kerfuffle::ExtractionDialog); dialog.data()->setModal(true); if (m_view->selectionModel()->selectedRows().count() > 0) { dialog.data()->setShowSelectedFiles(true); } dialog.data()->setSingleFolderArchive(isSingleFolderArchive()); dialog.data()->setSubfolder(detectSubfolder()); dialog.data()->setCurrentUrl(QUrl::fromLocalFile(QFileInfo(m_model->archive()->fileName()).absolutePath())); dialog.data()->show(); dialog.data()->restoreWindowSize(); if (dialog.data()->exec()) { updateQuickExtractMenu(m_extractArchiveAction); updateQuickExtractMenu(m_extractAction); - QVariantList files; + QList files; // If the user has chosen to extract only selected entries, fetch these // from the QTreeView. if (!dialog.data()->extractAllFiles()) { files = filesAndRootNodesForIndexes(addChildren(m_view->selectionModel()->selectedRows())); } qCDebug(ARK) << "Selected " << files; Kerfuffle::ExtractionOptions options; if (dialog.data()->preservePaths()) { options[QStringLiteral("PreservePaths")] = true; } options[QStringLiteral("FollowExtractionDialogSettings")] = true; const QString destinationDirectory = dialog.data()->destinationDirectory().toDisplayString(QUrl::PreferLocalFile); ExtractJob *job = m_model->extractFiles(files, destinationDirectory, options); registerJob(job); connect(job, &KJob::result, this, &Part::slotExtractionDone); job->start(); } delete dialog.data(); } QModelIndexList Part::addChildren(const QModelIndexList &list) const { Q_ASSERT(m_model); QModelIndexList ret = list; // Iterate over indexes in list and add all children. for (int i = 0; i < ret.size(); ++i) { QModelIndex index = ret.at(i); for (int j = 0; j < m_model->rowCount(index); ++j) { QModelIndex child = m_model->index(j, 0, index); if (!ret.contains(child)) { ret << child; } } } return ret; } -QList Part::filesForIndexes(const QModelIndexList& list) const +QList Part::filesForIndexes(const QModelIndexList& list) const { - QVariantList ret; + QList ret; foreach(const QModelIndex& index, list) { - const Archive::Entry *entry = m_model->entryForIndex(index); - ret << entry->property("fileName").toString(); + ret << m_model->entryForIndex(index); } return ret; } -QList Part::filesAndRootNodesForIndexes(const QModelIndexList& list) const +QList Part::filesAndRootNodesForIndexes(const QModelIndexList& list) const { - QVariantList fileList; + QList fileList; foreach (const QModelIndex& index, list) { // Find the topmost unselected parent. This is done by iterating up // through the directory hierarchy and see if each parent is included // in the selection OR if the parent is already part of list. // The latter is needed for unselected folders which are subfolders of // a selected parent folder. QModelIndex selectionRoot = index.parent(); while (m_view->selectionModel()->isSelected(selectionRoot) || list.contains(selectionRoot)) { selectionRoot = selectionRoot.parent(); } // Fetch the root node for the unselected parent. const QString rootFileName = - m_model->entryForIndex(selectionRoot)->property("fileName").toString(); + m_model->entryForIndex(selectionRoot)->property("fullPath").toString(); // Append index with root node to fileList. QModelIndexList alist = QModelIndexList() << index; - foreach (const QVariant &file, filesForIndexes(alist)) { - QVariant v = QVariant::fromValue(fileRootNodePair(file.toString(), rootFileName)); - if (!fileList.contains(v)) { - fileList.append(v); + foreach (Archive::Entry *entry, filesForIndexes(alist)) { + if (!fileList.contains(entry)) { + entry->rootNode = rootFileName; + fileList.append(entry); } } } return fileList; } void Part::slotExtractionDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } else { ExtractJob *extractJob = qobject_cast(job); Q_ASSERT(extractJob); const bool followExtractionDialogSettings = extractJob->extractionOptions().value(QStringLiteral("FollowExtractionDialogSettings"), false).toBool(); if (!followExtractionDialogSettings) { return; } if (ArkSettings::openDestinationFolderAfterExtraction()) { qCDebug(ARK) << "Shall open" << extractJob->destinationDirectory(); QUrl destinationDirectory = QUrl::fromLocalFile(extractJob->destinationDirectory()).adjusted(QUrl::NormalizePathSegments); qCDebug(ARK) << "Shall open URL" << destinationDirectory; KRun::runUrl(destinationDirectory, QStringLiteral("inode/directory"), widget()); } if (ArkSettings::closeAfterExtraction()) { emit quit(); } } } void Part::adjustColumns() { m_view->header()->setSectionResizeMode(0, QHeaderView::ResizeToContents); } void Part::slotAddFiles(const QStringList& filesToAdd, const QString& path) { if (filesToAdd.isEmpty()) { return; } qCDebug(ARK) << "Adding " << filesToAdd << " to " << path; // Add a trailing slash to directories. QStringList cleanFilesToAdd(filesToAdd); for (int i = 0; i < cleanFilesToAdd.size(); ++i) { QString& file = cleanFilesToAdd[i]; if (QFileInfo(file).isDir()) { if (!file.endsWith(QLatin1Char( '/' ))) { file += QLatin1Char( '/' ); } } } // GlobalWorkDir is used by AddJob and should contain the part of the // absolute path of files to be added that should NOT be included in the // directory structure within the archive. // Example: We add file "/home/user/somedir/somefile.txt" and want the file // to have the relative path within the archive "somedir/somefile.txt". // GlobalWorkDir is then: "/home/user" QString globalWorkDir = cleanFilesToAdd.first(); // path represents the path of the file within the archive. This needs to // be removed from globalWorkDir, otherwise the files will be added to the // root of the archive. In the example above, path would be "somedir/". if (!path.isEmpty()) { globalWorkDir.remove(path); } // Remove trailing slash (needed when adding dirs). if (globalWorkDir.right(1) == QLatin1String("/")) { globalWorkDir.chop(1); } // Now take the absolute path of the parent directory. globalWorkDir = QFileInfo(globalWorkDir).dir().absolutePath(); qCDebug(ARK) << "Detected GlobalWorkDir to be " << globalWorkDir; CompressionOptions options; options[QStringLiteral("GlobalWorkDir")] = globalWorkDir; if (arguments().metaData().contains(QStringLiteral("compressionLevel"))) { options[QStringLiteral("CompressionLevel")] = arguments().metaData()[QStringLiteral("compressionLevel")]; } - AddJob *job = m_model->addFiles(cleanFilesToAdd, options); + foreach (const QString& file, cleanFilesToAdd) { + m_jobTempEntries.push_back(new Archive::Entry(Q_NULLPTR, file)); + } + AddJob *job = m_model->addFiles(m_jobTempEntries, options); if (!job) { return; } connect(job, &KJob::result, this, &Part::slotAddFilesDone); registerJob(job); job->start(); } void Part::slotAddFiles() { // #264819: passing widget() as the parent will not work as expected. // KFileDialog will create a KFileWidget, which runs an internal // event loop to stat the given directory. This, in turn, leads to // events being delivered to widget(), which is a QSplitter, which // in turn reimplements childEvent() and will end up calling // QWidget::show() on the KFileDialog (thus showing it in a // non-modal state). // When KFileDialog::exec() is called, the widget is already shown // and nothing happens. const QStringList filesToAdd = QFileDialog::getOpenFileNames(widget(), i18nc("@title:window", "Add Files")); slotAddFiles(filesToAdd); } void Part::slotAddDir() { const QString dirToAdd = QFileDialog::getExistingDirectory(widget(), i18nc("@title:window", "Add Folder")); if (!dirToAdd.isEmpty()) { slotAddFiles(QStringList() << dirToAdd); } } void Part::slotAddFilesDone(KJob* job) { + qDeleteAll(m_jobTempEntries); + m_jobTempEntries.clear(); if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } } void Part::slotDeleteFilesDone(KJob* job) { if (job->error() && job->error() != KJob::KilledJobError) { KMessageBox::error(widget(), job->errorString()); } } void Part::slotDeleteFiles() { const int selectionsCount = m_view->selectionModel()->selectedRows().count(); const auto reallyDelete = KMessageBox::questionYesNo(widget(), i18ncp("@info", "Deleting this file is not undoable. Are you sure you want to do this?", "Deleting these files is not undoable. Are you sure you want to do this?", selectionsCount), i18ncp("@title:window", "Delete File", "Delete Files", selectionsCount), KStandardGuiItem::del(), KStandardGuiItem::no(), QString(), KMessageBox::Dangerous | KMessageBox::Notify); if (reallyDelete == KMessageBox::No) { return; } DeleteJob *job = m_model->deleteFiles(filesForIndexes(addChildren(m_view->selectionModel()->selectedRows()))); connect(job, &KJob::result, this, &Part::slotDeleteFilesDone); registerJob(job); job->start(); } void Part::slotShowProperties() { QPointer dialog(new Kerfuffle::PropertiesDialog(0, m_model->archive())); dialog.data()->show(); } void Part::slotToggleInfoPanel(bool visible) { if (visible) { m_splitter->setSizes(ArkSettings::splitterSizes()); m_infoPanel->show(); } else { // We need to save the splitterSizes before hiding, otherwise // Ark won't remember resizing done by the user. ArkSettings::setSplitterSizes(m_splitter->sizes()); m_infoPanel->hide(); } } void Part::slotSaveAs() { QUrl saveUrl = QFileDialog::getSaveFileUrl(widget(), i18nc("@title:window", "Save Archive As"), url()); if ((saveUrl.isValid()) && (!saveUrl.isEmpty())) { auto statJob = KIO::stat(saveUrl, KIO::StatJob::DestinationSide, 0); KJobWidgets::setWindow(statJob, widget()); if (statJob->exec()) { int overwrite = KMessageBox::warningContinueCancel(widget(), xi18nc("@info", "An archive named %1 already exists. Are you sure you want to overwrite it?", saveUrl.fileName()), QString(), KStandardGuiItem::overwrite()); if (overwrite != KMessageBox::Continue) { return; } } QUrl srcUrl = QUrl::fromLocalFile(localFilePath()); if (!QFile::exists(localFilePath())) { if (url().isLocalFile()) { KMessageBox::error(widget(), xi18nc("@info", "The archive %1 cannot be copied to the specified location. The archive does not exist anymore.", localFilePath())); return; } else { srcUrl = url(); } } KIO::Job *copyJob = KIO::file_copy(srcUrl, saveUrl, -1, KIO::Overwrite); KJobWidgets::setWindow(copyJob, widget()); copyJob->exec(); if (copyJob->error()) { KMessageBox::error(widget(), xi18nc("@info", "The archive could not be saved as %1. Try saving it to another location.", saveUrl.path())); } } } void Part::slotShowContextMenu() { if (!factory()) { return; } QMenu *popup = static_cast(factory()->container(QStringLiteral("context_menu"), this)); popup->popup(QCursor::pos()); } void Part::displayMsgWidget(KMessageWidget::MessageType type, const QString& msg) { KMessageWidget *msgWidget = new KMessageWidget(); msgWidget->setText(msg); msgWidget->setMessageType(type); m_vlayout->insertWidget(0, msgWidget); msgWidget->animatedShow(); } } // namespace Ark #include "part.moc" diff --git a/part/part.h b/part/part.h index aa520744..a11b92df 100644 --- a/part/part.h +++ b/part/part.h @@ -1,181 +1,183 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2007 Henrique Pinto * Copyright (C) 2008-2009 Harald Hvaal * Copyright (C) 2009 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef PART_H #define PART_H #include "interface.h" +#include "kerfuffle/archiveentry.h" #include #include #include #include #include class ArchiveModel; class InfoPanel; class KAboutData; class KAbstractWidgetJobTracker; class KJob; class KToggleAction; class QAction; class QSplitter; class QTreeView; class QTemporaryDir; class QVBoxLayout; class QSignalMapper; class QFileSystemWatcher; class QGroupBox; class QPlainTextEdit; namespace Ark { class Part: public KParts::ReadWritePart, public Interface { Q_OBJECT Q_INTERFACES(Interface) public: enum OpenFileMode { Preview, OpenFile, OpenFileWith }; Part(QWidget *parentWidget, QObject *parent, const QVariantList &); ~Part(); static KAboutData *createAboutData(); bool openFile() Q_DECL_OVERRIDE; bool saveFile() Q_DECL_OVERRIDE; bool isBusy() const Q_DECL_OVERRIDE; KConfigSkeleton *config() const Q_DECL_OVERRIDE; QList settingsPages(QWidget *parent) const Q_DECL_OVERRIDE; /** * Validate the localFilePath() associated to this part. * If the file is not valid, an error message is displayed to the user. * @return Whether the localFilePath() can be loaded by the part. */ bool isLocalFileValid(); /** * Ask the user whether to overwrite @p targetFile, when creating a new archive with the same path. * @return True if the file has been successfully removed upon user's will. False otherwise. */ bool confirmAndDelete(const QString& targetFile); public slots: void extractSelectedFilesTo(const QString& localPath); private slots: void slotLoadingStarted(); void slotLoadingFinished(KJob *job); void slotOpenExtractedEntry(KJob*); void slotPreviewExtractedEntry(KJob* job); void slotOpenEntry(int mode); void slotError(const QString& errorMessage, const QString& details); void slotExtractArchive(); void slotShowExtractionDialog(); void slotExtractionDone(KJob*); void slotQuickExtractFiles(QAction*); void slotAddFiles(); void slotAddFiles(const QStringList& files, const QString& path = QString()); void slotAddDir(); void slotAddFilesDone(KJob*); void slotTestingDone(KJob*); void slotDeleteFiles(); void slotDeleteFilesDone(KJob*); void slotShowProperties(); void slotShowContextMenu(); void slotActivated(QModelIndex); void slotToggleInfoPanel(bool); void slotSaveAs(); void updateActions(); void updateQuickExtractMenu(QAction *extractAction); void selectionChanged(); void adjustColumns(); void setBusyGui(); void setReadyGui(); void setFileNameFromArchive(); void slotWatchedFileModified(const QString& file); void slotShowComment(); void slotAddComment(); void slotCommentChanged(); void slotTestArchive(); signals: void busy(); void ready(); void quit(); private: void setupView(); void setupActions(); bool isSingleFolderArchive() const; QString detectSubfolder() const; - QList filesForIndexes(const QModelIndexList& list) const; - QList filesAndRootNodesForIndexes(const QModelIndexList& list) const; + QList filesForIndexes(const QModelIndexList& list) const; + QList filesAndRootNodesForIndexes(const QModelIndexList& list) const; QModelIndexList addChildren(const QModelIndexList &list) const; void registerJob(KJob *job); void displayMsgWidget(KMessageWidget::MessageType type, const QString& msg); ArchiveModel *m_model; QTreeView *m_view; QAction *m_previewAction; QAction *m_openFileAction; QAction *m_openFileWithAction; QAction *m_extractArchiveAction; QAction *m_extractAction; QAction *m_addFilesAction; QAction *m_addDirAction; QAction *m_deleteFilesAction; QAction *m_saveAsAction; QAction *m_propertiesAction; QAction *m_editCommentAction; QAction *m_testArchiveAction; KToggleAction *m_showInfoPanelAction; InfoPanel *m_infoPanel; QSplitter *m_splitter; QList m_tmpOpenDirList; bool m_busy; OpenFileMode m_openFileMode; + QList m_jobTempEntries; KAbstractWidgetJobTracker *m_jobTracker; KParts::StatusBarExtension *m_statusBarExtension; QVBoxLayout *m_vlayout; QSignalMapper *m_signalMapper; QFileSystemWatcher *m_fileWatcher; QSplitter *m_commentSplitter; QGroupBox *m_commentBox; QPlainTextEdit *m_commentView; KMessageWidget *m_commentMsgWidget; }; } // namespace Ark #endif // PART_H diff --git a/plugins/cli7zplugin/cliplugin.cpp b/plugins/cli7zplugin/cliplugin.cpp index cbb794a9..6e97dac7 100644 --- a/plugins/cli7zplugin/cliplugin.cpp +++ b/plugins/cli7zplugin/cliplugin.cpp @@ -1,265 +1,265 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/cliinterface.h" #include "kerfuffle/kerfuffle_export.h" #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_cli7z.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList & args) : CliInterface(parent, args) , m_archiveType(ArchiveType7z) , m_parseState(ParseStateTitle) , m_linesComment(0) , m_isFirstInformationEntry(true) { qCDebug(ARK) << "Loaded cli_7z plugin"; } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateTitle; m_comment.clear(); } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { //p[CaptureProgress] = true; p[ListProgram] = p[ExtractProgram] = p[DeleteProgram] = p[AddProgram] = p[TestProgram] = QStringList() << QStringLiteral("7z"); p[ListArgs] = QStringList() << QStringLiteral("l") << QStringLiteral("-slt") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive"); p[ExtractArgs] = QStringList() << QStringLiteral("$PreservePathSwitch") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive") << QStringLiteral("$Files"); p[PreservePathSwitch] = QStringList() << QStringLiteral("x") << QStringLiteral("e"); p[PasswordSwitch] = QStringList() << QStringLiteral("-p$Password"); p[PasswordHeaderSwitch] = QStringList { QStringLiteral("-p$Password"), QStringLiteral("-mhe=on") }; p[WrongPasswordPatterns] = QStringList() << QStringLiteral("Wrong password"); p[CompressionLevelSwitch] = QStringLiteral("-mx=$CompressionLevel"); p[AddArgs] = QStringList() << QStringLiteral("a") << QStringLiteral("$Archive") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$CompressionLevelSwitch") << QStringLiteral("$Files"); p[DeleteArgs] = QStringList() << QStringLiteral("d") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive") << QStringLiteral("$Files"); p[TestArgs] = QStringList() << QStringLiteral("t") << QStringLiteral("$Archive"); p[TestPassedPattern] = QStringLiteral("^Everything is Ok$"); p[FileExistsExpression] = QStringList() << QStringLiteral("^\\(Y\\)es / \\(N\\)o / \\(A\\)lways / \\(S\\)kip all / A\\(u\\)to rename all / \\(Q\\)uit\\? $") << QStringLiteral("^\\? \\(Y\\)es / \\(N\\)o / \\(A\\)lways / \\(S\\)kip all / A\\(u\\)to rename all / \\(Q\\)uit\\? $"); p[FileExistsFileName] = QStringList() << QStringLiteral("^file \\./(.*)$") << QStringLiteral("^ Path: \\./(.*)$"); p[FileExistsInput] = QStringList() << QStringLiteral("Y") //overwrite << QStringLiteral("N") //skip << QStringLiteral("A") //overwrite all << QStringLiteral("S") //autoskip << QStringLiteral("Q"); //cancel p[PasswordPromptPattern] = QStringLiteral("Enter password \\(will not be echoed\\)"); p[ExtractionFailedPatterns] = QStringList() << QStringLiteral("ERROR: E_FAIL"); p[CorruptArchivePatterns] = QStringList() << QStringLiteral("Unexpected end of archive") << QStringLiteral("Headers Error"); p[DiskFullPatterns] = QStringList() << QStringLiteral("No space left on device"); } return p; } bool CliPlugin::readListLine(const QString& line) { static const QLatin1String archiveInfoDelimiter1("--"); // 7z 9.13+ static const QLatin1String archiveInfoDelimiter2("----"); // 7z 9.04 static const QLatin1String entryInfoDelimiter("----------"); const QRegularExpression rxComment(QStringLiteral("Comment = .+$")); if (m_parseState == ParseStateTitle) { const QRegularExpression rxVersionLine(QStringLiteral("^p7zip Version ([\\d\\.]+) .*$")); QRegularExpressionMatch matchVersion = rxVersionLine.match(line); if (matchVersion.hasMatch()) { m_parseState = ParseStateHeader; const QString p7zipVersion = matchVersion.captured(1); qCDebug(ARK) << "p7zip version" << p7zipVersion << "detected"; } } else if (m_parseState == ParseStateHeader) { if (line.startsWith(QStringLiteral("Listing archive:"))) { qCDebug(ARK) << "Archive name: " << line.right(line.size() - 16).trimmed(); } else if ((line == archiveInfoDelimiter1) || (line == archiveInfoDelimiter2)) { m_parseState = ParseStateArchiveInformation; } else if (line.contains(QStringLiteral("Error: "))) { qCWarning(ARK) << line.mid(7); } } else if (m_parseState == ParseStateArchiveInformation) { if (line == entryInfoDelimiter) { m_parseState = ParseStateEntryInformation; } else if (line.startsWith(QStringLiteral("Type = "))) { const QString type = line.mid(7).trimmed(); qCDebug(ARK) << "Archive type: " << type; if (type == QLatin1String("7z")) { m_archiveType = ArchiveType7z; } else if (type == QLatin1String("bzip2")) { m_archiveType = ArchiveTypeBZip2; } else if (type == QLatin1String("gzip")) { m_archiveType = ArchiveTypeGZip; } else if (type == QLatin1String("xz")) { m_archiveType = ArchiveTypeXz; } else if (type == QLatin1String("tar")) { m_archiveType = ArchiveTypeTar; } else if (type == QLatin1String("zip")) { m_archiveType = ArchiveTypeZip; } else if (type == QLatin1String("Rar")) { m_archiveType = ArchiveTypeRar; } else { // Should not happen qCWarning(ARK) << "Unsupported archive type"; return false; } } else if (rxComment.match(line).hasMatch()) { m_parseState = ParseStateComment; m_comment.append(line.section(QLatin1Char('='), 1) + QLatin1Char('\n')); } } else if (m_parseState == ParseStateComment) { if (line == entryInfoDelimiter) { m_parseState = ParseStateEntryInformation; if (!m_comment.trimmed().isEmpty()) { m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } } else if (m_parseState == ParseStateEntryInformation) { if (m_isFirstInformationEntry) { m_isFirstInformationEntry = false; m_currentArchiveEntry = new Archive::Entry(Q_NULLPTR); m_currentArchiveEntry->compressedSizeIsSet = false; } if (line.startsWith(QStringLiteral("Path = "))) { const QString entryFilename = QDir::fromNativeSeparators(line.mid(7).trimmed()); - m_currentArchiveEntry->setProperty("fileName", entryFilename); + m_currentArchiveEntry->setProperty("fullPath", entryFilename); } else if (line.startsWith(QStringLiteral("Size = "))) { m_currentArchiveEntry->setProperty("size", line.mid(7).trimmed()); } else if (line.startsWith(QStringLiteral("Packed Size = "))) { // #236696: 7z files only show a single Packed Size value // corresponding to the whole archive. if (m_archiveType != ArchiveType7z) { m_currentArchiveEntry->compressedSizeIsSet = true; m_currentArchiveEntry->setProperty("compressedSize", line.mid(14).trimmed()); } } else if (line.startsWith(QStringLiteral("Modified = "))) { m_currentArchiveEntry->setProperty("timestamp", QDateTime::fromString(line.mid(11).trimmed(), QStringLiteral("yyyy-MM-dd hh:mm:ss"))); } else if (line.startsWith(QStringLiteral("Attributes = "))) { const QString attributes = line.mid(13).trimmed(); const bool isDirectory = attributes.startsWith(QLatin1Char('D')); m_currentArchiveEntry->setProperty("isDirectory", isDirectory); if (isDirectory) { const QString directoryName = - m_currentArchiveEntry->property("fileName").toString(); + m_currentArchiveEntry->property("fullPath").toString(); if (!directoryName.endsWith(QLatin1Char('/'))) { const bool isPasswordProtected = (line.at(12) == QLatin1Char('+')); - m_currentArchiveEntry->setProperty("fileName", QString(directoryName + QLatin1Char('/'))); + m_currentArchiveEntry->setProperty("fullPath", QString(directoryName + QLatin1Char('/'))); m_currentArchiveEntry->setProperty("isPasswordProtected", isPasswordProtected); } } m_currentArchiveEntry->setProperty("permissions", attributes.mid(1)); } else if (line.startsWith(QStringLiteral("CRC = "))) { m_currentArchiveEntry->setProperty("CRC", line.mid(6).trimmed()); } else if (line.startsWith(QStringLiteral("Method = "))) { m_currentArchiveEntry->setProperty("method", line.mid(9).trimmed()); } else if (line.startsWith(QStringLiteral("Encrypted = ")) && line.size() >= 13) { m_currentArchiveEntry->setProperty("isPasswordProtected", line.at(12) == QLatin1Char('+')); } else if (line.startsWith(QStringLiteral("Block = ")) || line.startsWith(QStringLiteral("Version = "))) { m_isFirstInformationEntry = true; - if (!m_currentArchiveEntry->property("fileName").toString().isEmpty()) { + if (!m_currentArchiveEntry->property("fullPath").toString().isEmpty()) { emit entry(m_currentArchiveEntry); } else { delete m_currentArchiveEntry; } m_currentArchiveEntry = Q_NULLPTR; } } return true; } QStringList CliPlugin::passwordHeaderSwitch(const QString& password) const { if (password.isEmpty()) { return QStringList(); } Q_ASSERT(m_param.contains(PasswordHeaderSwitch)); QStringList passwordHeaderSwitch = m_param.value(PasswordHeaderSwitch).toStringList(); Q_ASSERT(!passwordHeaderSwitch.isEmpty() && passwordHeaderSwitch.size() == 2); passwordHeaderSwitch[0].replace(QLatin1String("$Password"), password); return passwordHeaderSwitch; } #include "cliplugin.moc" diff --git a/plugins/cliplugin-example/cliplugin.cpp b/plugins/cliplugin-example/cliplugin.cpp index fa21fbb6..2626879c 100644 --- a/plugins/cliplugin-example/cliplugin.cpp +++ b/plugins/cliplugin-example/cliplugin.cpp @@ -1,149 +1,149 @@ /* * 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_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_cli.json", registerPlugin();) 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(Q_NULLPTR); - e->setProperty("fileName", m_entryFilename); + 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/clirarplugin/cliplugin.cpp b/plugins/clirarplugin/cliplugin.cpp index 545bed0f..4a3bd6cd 100644 --- a/plugins/clirarplugin/cliplugin.cpp +++ b/plugins/clirarplugin/cliplugin.cpp @@ -1,514 +1,514 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2010-2011,2014 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/archiveentry.h" #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_clirar.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList& args) : CliInterface(parent, args) , m_parseState(ParseStateTitle) , m_isUnrar5(false) , m_isPasswordProtected(false) , m_isMultiVolume(false) , m_isSolid(false) , m_remainingIgnoreLines(1) //The first line of UNRAR output is empty. , m_linesComment(0) { qCDebug(ARK) << "Loaded cli_rar plugin"; // Empty lines are needed for parsing output of unrar. setListEmptyLines(true); } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateTitle; m_remainingIgnoreLines = 1; m_comment.clear(); } // #272281: the proprietary unrar program does not like trailing '/'s // in directories passed to it when extracting only part of // the files in an archive. QString CliPlugin::escapeFileName(const QString &fileName) const { if (fileName.endsWith(QLatin1Char('/'))) { return fileName.left(fileName.length() - 1); } return fileName; } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { p[CaptureProgress] = true; p[ListProgram] = p[ExtractProgram] = p[TestProgram] = QStringList() << QStringLiteral( "unrar" ); p[DeleteProgram] = p[AddProgram] = QStringList() << QStringLiteral( "rar" ); p[ListArgs] = QStringList() << QStringLiteral("vt") << QStringLiteral("-v") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive"); p[ExtractArgs] = QStringList() << QStringLiteral( "-kb" ) << QStringLiteral( "-p-" ) << QStringLiteral( "$PreservePathSwitch" ) << QStringLiteral( "$PasswordSwitch" ) << QStringLiteral( "$Archive" ) << QStringLiteral( "$Files" ); p[PreservePathSwitch] = QStringList() << QStringLiteral( "x" ) << QStringLiteral( "e" ); p[PasswordSwitch] = QStringList() << QStringLiteral( "-p$Password" ); p[PasswordHeaderSwitch] = QStringList() << QStringLiteral("-hp$Password"); p[CompressionLevelSwitch] = QStringLiteral("-m$CompressionLevel"); p[DeleteArgs] = QStringList() << QStringLiteral( "d" ) << QStringLiteral( "$PasswordSwitch" ) << QStringLiteral( "$Archive" ) << QStringLiteral( "$Files" ); p[FileExistsExpression] = QStringList() << QStringLiteral("^\\[Y\\]es, \\[N\\]o, \\[A\\]ll, n\\[E\\]ver, \\[R\\]ename, \\[Q\\]uit $"); p[FileExistsFileName] = QStringList() << QStringLiteral("^(.+) already exists. Overwrite it") // unrar 3 & 4 << QStringLiteral("^Would you like to replace the existing file (.+)$"); // unrar 5 p[FileExistsInput] = QStringList() << QStringLiteral( "Y" ) //overwrite << QStringLiteral( "N" ) //skip << QStringLiteral( "A" ) //overwrite all << QStringLiteral( "E" ) //autoskip << QStringLiteral( "Q" ); //cancel p[AddArgs] = QStringList() << QStringLiteral( "a" ) << QStringLiteral( "$Archive" ) << QStringLiteral("$PasswordSwitch") << QStringLiteral("$CompressionLevelSwitch") << QStringLiteral( "$Files" ); p[PasswordPromptPattern] = QLatin1String("Enter password \\(will not be echoed\\) for"); p[WrongPasswordPatterns] = QStringList() << QStringLiteral("password incorrect") << QStringLiteral("wrong password"); p[ExtractionFailedPatterns] = QStringList() << QStringLiteral( "CRC failed" ) << QStringLiteral( "Cannot find volume" ); p[CorruptArchivePatterns] = QStringList() << QStringLiteral("Unexpected end of archive") << QStringLiteral("the file header is corrupt"); p[DiskFullPatterns] = QStringList() << QStringLiteral("No space left on device"); p[CommentArgs] = QStringList() << QStringLiteral("c") << QStringLiteral("$CommentSwitch") << QStringLiteral("$Archive"); p[CommentSwitch] = QStringLiteral("-z$CommentFile"); p[TestArgs] = QStringList() << QStringLiteral("t") << QStringLiteral("$Archive"); p[TestPassedPattern] = QStringLiteral("^All OK$"); } return p; } bool CliPlugin::readListLine(const QString &line) { // Ignore number of lines corresponding to m_remainingIgnoreLines. if (m_remainingIgnoreLines > 0) { --m_remainingIgnoreLines; return true; } // Parse the title line, which contains the version of unrar. if (m_parseState == ParseStateTitle) { QRegularExpression rxVersionLine(QStringLiteral("^UNRAR (\\d+\\.\\d+)( beta \\d)? .*$")); QRegularExpressionMatch matchVersion = rxVersionLine.match(line); if (matchVersion.hasMatch()) { m_parseState = ParseStateComment; QString unrarVersion = matchVersion.captured(1); qCDebug(ARK) << "UNRAR version" << unrarVersion << "detected"; if (unrarVersion.toFloat() >= 5) { m_isUnrar5 = true; qCDebug(ARK) << "Using UNRAR 5 parser"; } else { qCDebug(ARK) << "Using UNRAR 4 parser"; } } else { // If the second line doesn't contain an UNRAR title, something // is wrong. qCCritical(ARK) << "Failed to detect UNRAR output."; return false; } // Or see what version of unrar we are dealing with and call specific // handler functions. } else if (m_isUnrar5) { handleUnrar5Line(line); } else { handleUnrar4Line(line); } return true; } void CliPlugin::handleUnrar5Line(const QString &line) { // Parses the comment field. if (m_parseState == ParseStateComment) { // RegExp matching end of comment field. // FIXME: Comment itself could also contain the Archive path string here. QRegularExpression rxCommentEnd(QStringLiteral("^Archive: .+$")); if (rxCommentEnd.match(line).hasMatch()) { m_parseState = ParseStateHeader; m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; if (!m_comment.isEmpty()) { qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } return; } // Parses the header, which is whatever is between the comment field // and the entries. else if (m_parseState == ParseStateHeader) { // "Details: " indicates end of header. if (line.startsWith(QStringLiteral("Details: "))) { ignoreLines(1, ParseStateEntryDetails); if (line.contains(QLatin1String("volume")) && !m_isMultiVolume) { m_isMultiVolume = true; qCDebug(ARK) << "Multi-volume archive detected"; } if (line.contains(QLatin1String("solid")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } } return; } // Parses the entry details for each entry. else if (m_parseState == ParseStateEntryDetails) { // For multi-volume archives there is a header between the entries in // each volume. if (line.startsWith(QLatin1String("Archive: "))) { m_parseState = ParseStateHeader; return; // Empty line indicates end of entry. } else if (line.trimmed().isEmpty() && !m_unrar5Details.isEmpty()) { handleUnrar5Entry(); } else { // All detail lines should contain a colon. if (!line.contains(QLatin1Char(':'))) { qCWarning(ARK) << "Unrecognized line:" << line; return; } // The details are on separate lines, so we store them in the QHash // m_unrar5Details. m_unrar5Details.insert(line.section(QLatin1Char(':'), 0, 0).trimmed().toLower(), line.section(QLatin1Char(':'), 1).trimmed()); } return; } } void CliPlugin::handleUnrar5Entry() { Archive::Entry *e = new Archive::Entry(Q_NULLPTR); QString compressionRatio = m_unrar5Details.value(QStringLiteral("ratio")); compressionRatio.chop(1); // Remove the '%' e->setProperty("ratio", compressionRatio); QString time = m_unrar5Details.value(QStringLiteral("mtime")); QDateTime ts = QDateTime::fromString(time, QStringLiteral("yyyy-MM-dd HH:mm:ss,zzz")); e->setProperty("timestamp", ts); bool isDirectory = (m_unrar5Details.value(QStringLiteral("type")) == QLatin1String("Directory")); e->setProperty("isDirectory", isDirectory); if (isDirectory && !m_unrar5Details.value(QStringLiteral("name")).endsWith(QLatin1Char('/'))) { m_unrar5Details[QStringLiteral("name")] += QLatin1Char('/'); } QString compression = m_unrar5Details.value(QStringLiteral("compression")); int optionPos = compression.indexOf(QLatin1Char('-')); if (optionPos != -1) { e->setProperty("method", compression.mid(optionPos)); e->setProperty("version", compression.left(optionPos).trimmed()); } else { // No method specified. e->setProperty("method", QStringLiteral("")); e->setProperty("version", compression); } m_isPasswordProtected = m_unrar5Details.value(QStringLiteral("flags")).contains(QStringLiteral("encrypted")); e->setProperty("isPasswordProtected", m_isPasswordProtected); - e->setProperty("fileName", m_unrar5Details.value(QStringLiteral("name"))); + e->setProperty("fullPath", m_unrar5Details.value(QStringLiteral("name"))); e->setProperty("size", m_unrar5Details.value(QStringLiteral("size"))); e->setProperty("compressedSize", m_unrar5Details.value(QStringLiteral("packed size"))); e->setProperty("permissions", m_unrar5Details.value(QStringLiteral("attributes"))); e->setProperty("CRC", m_unrar5Details.value(QStringLiteral("crc32"))); if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { e->setProperty("link", m_unrar5Details.value(QStringLiteral("target"))); } m_unrar5Details.clear(); emit entry(e); } void CliPlugin::handleUnrar4Line(const QString &line) { // Parses the comment field. if (m_parseState == ParseStateComment) { // RegExp matching end of comment field. // FIXME: Comment itself could also contain the Archive path string here. QRegularExpression rxCommentEnd(QStringLiteral("^(Solid archive|Archive|Volume) .+$")); if (rxCommentEnd.match(line).hasMatch()) { if (line.startsWith(QLatin1String("Volume")) && !m_isMultiVolume) { m_isMultiVolume = true; qCDebug(ARK) << "Multi-volume archive detected"; } if (line.startsWith(QLatin1String("Solid archive")) && !m_isSolid) { m_isSolid = true; qCDebug(ARK) << "Solid archive detected"; } m_parseState = ParseStateHeader; m_comment = m_comment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; if (!m_comment.isEmpty()) { qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_comment.append(line + QLatin1Char('\n')); } return; } // Parses the header, which is whatever is between the comment field // and the entries. else if (m_parseState == ParseStateHeader) { // Horizontal line indicates end of header. if (line.startsWith(QStringLiteral("--------------------"))) { m_parseState = ParseStateEntryFileName; } return; } // Parses the entry name, which is on the first line of each entry. else if (m_parseState == ParseStateEntryFileName) { // Ignore empty lines. if (line.trimmed().isEmpty()) { return; } // Three types of subHeaders can be displayed for unrar 3 and 4. // STM has 4 lines, RR has 3, and CMT has lines corresponding to // length of comment field +3. We ignore the subheaders. QRegularExpression rxSubHeader(QStringLiteral("^Data header type: (CMT|STM|RR)$")); QRegularExpressionMatch matchSubHeader = rxSubHeader.match(line); if (matchSubHeader.hasMatch()) { qCDebug(ARK) << "SubHeader of type" << matchSubHeader.captured(1) << "found"; if (matchSubHeader.captured(1) == QLatin1String("STM")) { ignoreLines(4, ParseStateEntryFileName); } else if (matchSubHeader.captured(1) == QLatin1String("CMT")) { ignoreLines(m_linesComment + 3, ParseStateEntryFileName); } else if (matchSubHeader.captured(1) == QLatin1String("RR")) { ignoreLines(3, ParseStateEntryFileName); } return; } // The entries list ends with a horizontal line, followed by a // single summary line or, for multi-volume archives, another header. if (line.startsWith(QStringLiteral("-----------------"))) { m_parseState = ParseStateHeader; return; // Encrypted files are marked with an asterisk. } else if (line.startsWith(QLatin1Char('*'))) { m_isPasswordProtected = true; m_unrar4Details.append(QString(line.trimmed()).remove(0, 1)); //Remove the asterisk // Entry names always start at the second position, so a line not // starting with a space is not an entry name. } else if (!line.startsWith(QLatin1Char(' '))) { qCWarning(ARK) << "Unrecognized line:" << line; return; // If we reach this, then we can assume the line is an entry name, so // save it, and move on to the rest of the entry details. } else { m_unrar4Details.append(line.trimmed()); } m_parseState = ParseStateEntryDetails; return; } // Parses the remainder of the entry details for each entry. else if (m_parseState == ParseStateEntryDetails) { // If the line following an entry name is empty, we did something // wrong. Q_ASSERT(!line.trimmed().isEmpty()); // If we reach a horizontal line, then the previous line was not an // entry name, so go back to header. if (line.startsWith(QStringLiteral("-----------------"))) { m_parseState = ParseStateHeader; return; } // In unrar 3 and 4 the details are on a single line, so we // pass a QStringList containing the details. We need to store // it due to symlinks (see below). m_unrar4Details.append(line.split(QLatin1Char(' '), QString::SkipEmptyParts)); // The details line contains 9 fields, so m_unrar4Details // should now contain 9 + the filename = 10 strings. If not, this is // not an archive entry. if (m_unrar4Details.size() != 10) { m_parseState = ParseStateHeader; return; } // When unrar 3 and 4 list a symlink, they output an extra line // containing the link target. The extra line is output after // the line we ignore, so we first need to ignore one line. if (m_unrar4Details.at(6).startsWith(QLatin1Char('l'))) { ignoreLines(1, ParseStateLinkTarget); return; } else { handleUnrar4Entry(); } // Unrar 3 & 4 show a third line for each entry, which contains // three details: Host OS, Solid, and Old. We can ignore this // line. ignoreLines(1, ParseStateEntryFileName); return; } // Parses a symlink target. else if (m_parseState == ParseStateLinkTarget) { m_unrar4Details.append(QString(line).remove(QStringLiteral("-->")).trimmed()); handleUnrar4Entry(); m_parseState = ParseStateEntryFileName; return; } } void CliPlugin::handleUnrar4Entry() { Archive::Entry *e = new Archive::Entry(NULL); QDateTime ts = QDateTime::fromString(QString(m_unrar4Details.at(4) + QLatin1Char(' ') + m_unrar4Details.at(5)), QStringLiteral("dd-MM-yy hh:mm")); // Unrar 3 & 4 output dates with a 2-digit year but QDateTime takes it as // 19??. Let's take 1950 as cut-off; similar to KDateTime. if (ts.date().year() < 1950) { ts = ts.addYears(100); } e->setProperty("timestamp", ts); bool isDirectory = ((m_unrar4Details.at(6).at(0) == QLatin1Char('d')) || (m_unrar4Details.at(6).at(1) == QLatin1Char('D'))); e->setProperty("isDirectory", isDirectory); if (isDirectory && !m_unrar4Details.at(0).endsWith(QLatin1Char('/'))) { m_unrar4Details[0] += QLatin1Char('/'); } // Unrar reports the ratio as ((compressed size * 100) / size); // we consider ratio as (100 * ((size - compressed size) / size)). // If the archive is a multivolume archive, a string indicating // whether the archive's position in the volume is displayed // instead of the compression ratio. QString compressionRatio = m_unrar4Details.at(3); if ((compressionRatio == QStringLiteral("<--")) || (compressionRatio == QStringLiteral("<->")) || (compressionRatio == QStringLiteral("-->"))) { compressionRatio = QLatin1Char('0'); } else { compressionRatio.chop(1); // Remove the '%' } e->setProperty("ratio", compressionRatio); // TODO: // - Permissions differ depending on the system the entry was added // to the archive. - e->setProperty("fileName", m_unrar4Details.at(0)); + e->setProperty("fullPath", m_unrar4Details.at(0)); e->setProperty("size", m_unrar4Details.at(1)); e->setProperty("compressedSize", m_unrar4Details.at(2)); e->setProperty("permissions", m_unrar4Details.at(6)); e->setProperty("CRC", m_unrar4Details.at(7)); e->setProperty("method", m_unrar4Details.at(8)); e->setProperty("version", m_unrar4Details.at(9)); e->setProperty("isPasswordProtected", m_isPasswordProtected); if (e->property("permissions").toString().startsWith(QLatin1Char('l'))) { e->setProperty("link", m_unrar4Details.at(10)); } m_unrar4Details.clear(); emit entry(e); } void CliPlugin::ignoreLines(int lines, ParseState nextState) { m_remainingIgnoreLines = lines; m_parseState = nextState; } #include "cliplugin.moc" diff --git a/plugins/cliunarchiverplugin/cliplugin.cpp b/plugins/cliunarchiverplugin/cliplugin.cpp index 7e3e4ff9..e542c5ef 100644 --- a/plugins/cliunarchiverplugin/cliplugin.cpp +++ b/plugins/cliunarchiverplugin/cliplugin.cpp @@ -1,215 +1,213 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2011 Luke Shumaker * Copyright (C) 2016 Elvis Angelaccio * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #include "cliplugin.h" -#include "ark_debug.h" -#include "kerfuffle/kerfuffle_export.h" #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_cliunarchiver.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList &args) : CliInterface(parent, args) { qCDebug(ARK) << "Loaded cli_unarchiver plugin"; } CliPlugin::~CliPlugin() { } bool CliPlugin::list() { resetParsing(); cacheParameterList(); m_operationMode = List; const auto args = substituteListVariables(m_param.value(ListArgs).toStringList(), password()); if (!runProcess(m_param.value(ListProgram).toStringList(), args)) { return false; } if (!password().isEmpty()) { // lsar -json exits with error code 1 if the archive is header-encrypted and the password is wrong. if (m_exitCode == 1) { qCWarning(ARK) << "Wrong password, list() aborted"; emit error(i18n("Wrong password.")); emit finished(false); killProcess(); setPassword(QString()); return false; } // lsar -json exits with error code 2 if the archive is header-encrypted and no password is given as argument. // At this point we have already asked a password to the user, so we can just list() again. if (m_exitCode == 2) { return CliPlugin::list(); } } return true; } -bool CliPlugin::copyFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) +bool CliPlugin::copyFiles(const QList &files, const QString &destinationDirectory, const ExtractionOptions &options) { ExtractionOptions newOptions = options; // unar has the following limitations: // 1. creates an empty file upon entering a wrong password. // 2. detects that the stdout has been redirected and blocks the stdin. // This prevents Ark from executing unar's overwrite queries. // To prevent both, we always extract to a temporary directory // and then we move the files to the intended destination. qCDebug(ARK) << "Enabling extraction to temporary directory."; newOptions[QStringLiteral("AlwaysUseTmpDir")] = true; return CliInterface::copyFiles(files, destinationDirectory, newOptions); } void CliPlugin::resetParsing() { m_jsonOutput.clear(); } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { ///////////////[ COMMON ]///////////// p[CaptureProgress] = false; // Displayed when running lsar -json with header-encrypted archives. p[PasswordPromptPattern] = QStringLiteral("This archive requires a password to unpack. Use the -p option to provide one."); ///////////////[ LIST ]///////////// p[ListProgram] = QStringLiteral("lsar"); p[ListArgs] = QStringList() << QStringLiteral("-json") << QStringLiteral("$Archive") << QStringLiteral("$PasswordSwitch"); ///////////////[ EXTRACT ]///////////// p[ExtractProgram] = QStringLiteral("unar"); p[ExtractArgs] = QStringList() << QStringLiteral("-D") << QStringLiteral("$Archive") << QStringLiteral("$Files") << QStringLiteral("$PasswordSwitch"); p[NoTrailingSlashes] = true; p[PasswordSwitch] = QStringList() << QStringLiteral("-password") << QStringLiteral("$Password"); ///////////////[ ERRORS ]///////////// p[ExtractionFailedPatterns] = QStringList() << QStringLiteral("Failed! \\((.+)\\)$") << QStringLiteral("Segmentation fault$"); } return p; } bool CliPlugin::readListLine(const QString &line) { Q_UNUSED(line) return true; } void CliPlugin::setJsonOutput(const QString &jsonOutput) { m_jsonOutput = jsonOutput; readJsonOutput(); } void CliPlugin::readStdout(bool handleAll) { if (!handleAll) { CliInterface::readStdout(false); return; } // We are ready to read the json output. readJsonOutput(); } void CliPlugin::cacheParameterList() { m_param = parameterList(); Q_ASSERT(m_param.contains(ExtractProgram)); Q_ASSERT(m_param.contains(ListProgram)); } void CliPlugin::handleLine(const QString& line) { // Collect the json output line by line. if (m_operationMode == List) { m_jsonOutput += line + QLatin1Char('\n'); } CliInterface::handleLine(line); } void CliPlugin::readJsonOutput() { QJsonParseError error; QJsonDocument jsonDoc = QJsonDocument::fromJson(m_jsonOutput.toUtf8(), &error); if (error.error != QJsonParseError::NoError) { qCDebug(ARK) << "Could not parse json output:" << error.errorString(); return; } const QJsonObject json = jsonDoc.object(); const QJsonArray entries = json.value(QStringLiteral("lsarContents")).toArray(); foreach (const QJsonValue& value, entries) { const QJsonObject currentEntry = value.toObject(); m_currentEntry->clearMetaData(); QString filename = currentEntry.value(QStringLiteral("XADFileName")).toString(); m_currentEntry->setProperty("isDirectory", !currentEntry.value(QStringLiteral("XADIsDirectory")).isUndefined()); if (m_currentEntry->isDir()) { filename += QLatin1Char('/'); } - m_currentEntry->setProperty("fileName", filename); + m_currentEntry->setProperty("fullPath", filename); // FIXME: archives created from OSX (i.e. with the __MACOSX folder) list each entry twice, the 2nd time with size 0 m_currentEntry->setProperty("size", currentEntry.value(QStringLiteral("XADFileSize"))); m_currentEntry->setProperty("compressedSize", currentEntry.value(QStringLiteral("XADCompressedSize"))); m_currentEntry->setProperty("timestamp", currentEntry.value(QStringLiteral("XADLastModificationDate")).toVariant()); m_currentEntry->setProperty("size", currentEntry.value(QStringLiteral("XADFileSize"))); m_currentEntry->setProperty("isPasswordProtected", (currentEntry.value(QStringLiteral("XADIsEncrypted")).toInt() == 1)); // TODO: missing fields emit entry(m_currentEntry); } } #include "cliplugin.moc" diff --git a/plugins/cliunarchiverplugin/cliplugin.h b/plugins/cliunarchiverplugin/cliplugin.h index 0923c487..2a9c49ce 100644 --- a/plugins/cliunarchiverplugin/cliplugin.h +++ b/plugins/cliunarchiverplugin/cliplugin.h @@ -1,64 +1,64 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2011 Luke Shumaker * Copyright (C) 2016 Elvis Angelaccio * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. * */ #ifndef CLIPLUGIN_H #define CLIPLUGIN_H #include "kerfuffle/archiveentry.h" #include "kerfuffle/cliinterface.h" class CliPlugin : public Kerfuffle::CliInterface { Q_OBJECT public: explicit CliPlugin(QObject *parent, const QVariantList &args); virtual ~CliPlugin(); virtual bool list() Q_DECL_OVERRIDE; - virtual bool copyFiles(const QList &files, const QString &destinationDirectory, const Kerfuffle::ExtractionOptions &options) Q_DECL_OVERRIDE; + virtual bool copyFiles(const QList &files, const QString &destinationDirectory, const Kerfuffle::ExtractionOptions &options) Q_DECL_OVERRIDE; virtual void resetParsing() Q_DECL_OVERRIDE; virtual Kerfuffle::ParameterList parameterList() const Q_DECL_OVERRIDE; virtual bool readListLine(const QString &line) Q_DECL_OVERRIDE; /** * Fill the lsar's json output all in once (useful for unit testing). */ void setJsonOutput(const QString &jsonOutput); protected slots: void readStdout(bool handleAll = false) Q_DECL_OVERRIDE; protected: void cacheParameterList() Q_DECL_OVERRIDE; void handleLine(const QString& line) Q_DECL_OVERRIDE; private: void readJsonOutput(); Kerfuffle::Archive::Entry *m_currentEntry; QString m_jsonOutput; }; #endif // CLIPLUGIN_H diff --git a/plugins/clizipplugin/cliplugin.cpp b/plugins/clizipplugin/cliplugin.cpp index bf61f94e..3f35ca25 100644 --- a/plugins/clizipplugin/cliplugin.cpp +++ b/plugins/clizipplugin/cliplugin.cpp @@ -1,193 +1,193 @@ /* * ark -- archiver for the KDE project * * Copyright (C) 2009 Harald Hvaal * Copyright (C) 2009-2011 Raphael Kubo da Costa * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU General Public License * as published by the Free Software Foundation; either version 2 * of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program; if not, write to the Free Software * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ #include "cliplugin.h" #include "ark_debug.h" #include "kerfuffle/cliinterface.h" #include "kerfuffle/kerfuffle_export.h" #include "kerfuffle/archiveentry.h" #include #include #include #include using namespace Kerfuffle; K_PLUGIN_FACTORY_WITH_JSON(CliPluginFactory, "kerfuffle_clizip.json", registerPlugin();) CliPlugin::CliPlugin(QObject *parent, const QVariantList & args) : CliInterface(parent, args) , m_parseState(ParseStateHeader) , m_linesComment(0) { qCDebug(ARK) << "Loaded cli_zip plugin"; } CliPlugin::~CliPlugin() { } void CliPlugin::resetParsing() { m_parseState = ParseStateHeader; m_tempComment.clear(); m_comment.clear(); } // #208091: infozip applies special meanings to some characters, so we // need to escape them with backslashes.see match.c in // infozip's source code QString CliPlugin::escapeFileName(const QString &fileName) const { const QString escapedCharacters(QStringLiteral("[]*?^-\\!")); QString quoted; const int len = fileName.length(); const QLatin1Char backslash('\\'); quoted.reserve(len * 2); for (int i = 0; i < len; ++i) { if (escapedCharacters.contains(fileName.at(i))) { quoted.append(backslash); } quoted.append(fileName.at(i)); } return quoted; } ParameterList CliPlugin::parameterList() const { static ParameterList p; if (p.isEmpty()) { p[CaptureProgress] = false; p[ListProgram] = QStringList() << QStringLiteral("zipinfo"); p[ExtractProgram] = p[TestProgram] = QStringList() << QStringLiteral("unzip"); p[DeleteProgram] = p[AddProgram] = QStringList() << QStringLiteral("zip"); p[ListArgs] = QStringList() << QStringLiteral("-l") << QStringLiteral("-T") << QStringLiteral("-z") << QStringLiteral("$Archive"); p[ExtractArgs] = QStringList() << QStringLiteral("$PreservePathSwitch") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$Archive") << QStringLiteral("$Files"); p[PreservePathSwitch] = QStringList() << QStringLiteral("") << QStringLiteral("-j"); p[PasswordSwitch] = QStringList() << QStringLiteral("-P$Password"); p[CompressionLevelSwitch] = QStringLiteral("-$CompressionLevel"); p[DeleteArgs] = QStringList() << QStringLiteral("-d") << QStringLiteral("$Archive") << QStringLiteral("$Files"); p[FileExistsExpression] = QStringList() << QStringLiteral("^replace (.+)\\? \\[y\\]es, \\[n\\]o, \\[A\\]ll, \\[N\\]one, \\[r\\]ename: $"); p[FileExistsFileName] = QStringList() << p[FileExistsExpression].toString(); p[FileExistsInput] = QStringList() << QStringLiteral("y") //overwrite << QStringLiteral("n") //skip << QStringLiteral("A") //overwrite all << QStringLiteral("N"); //autoskip p[AddArgs] = QStringList() << QStringLiteral("-r") << QStringLiteral("$Archive") << QStringLiteral("$PasswordSwitch") << QStringLiteral("$CompressionLevelSwitch") << QStringLiteral("$Files"); p[PasswordPromptPattern] = QStringLiteral(" password: "); p[WrongPasswordPatterns] = QStringList() << QStringLiteral("incorrect password"); //p[ExtractionFailedPatterns] = QStringList() << "CRC failed"; p[CorruptArchivePatterns] = QStringList() << QStringLiteral("End-of-central-directory signature not found"); p[DiskFullPatterns] = QStringList() << QStringLiteral("write error \\(disk full\\?\\)") << QStringLiteral("No space left on device"); p[TestArgs] = QStringList() << QStringLiteral("-t") << QStringLiteral("$Archive"); p[TestPassedPattern] = QStringLiteral("^No errors detected in compressed data of "); } return p; } bool CliPlugin::readListLine(const QString &line) { static const QRegularExpression entryPattern(QStringLiteral( "^(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\S+)\\s+(\\d{8}).(\\d{6})\\s+(.+)$") ); // RegExp to identify the line preceding comments. const QRegularExpression commentPattern(QStringLiteral("^Archive: .*$")); // RegExp to identify the line following comments. const QRegularExpression commentEndPattern(QStringLiteral("^Zip file size: .*$")); switch (m_parseState) { case ParseStateHeader: if (commentPattern.match(line).hasMatch()) { m_parseState = ParseStateComment; } else if (commentEndPattern.match(line).hasMatch()){ m_parseState = ParseStateEntry; } break; case ParseStateComment: if (commentEndPattern.match(line).hasMatch()) { m_parseState = ParseStateEntry; if (!m_tempComment.trimmed().isEmpty()) { m_comment = m_tempComment.trimmed(); m_linesComment = m_comment.count(QLatin1Char('\n')) + 1; qCDebug(ARK) << "Found a comment with" << m_linesComment << "lines"; } } else { m_tempComment.append(line + QLatin1Char('\n')); } case ParseStateEntry: QRegularExpressionMatch rxMatch = entryPattern.match(line); if (rxMatch.hasMatch()) { Archive::Entry *e = new Archive::Entry(Q_NULLPTR); e->setProperty("permissions", rxMatch.captured(1)); // #280354: infozip may not show the right attributes for a given directory, so an entry // ending with '/' is actually more reliable than 'd' bein in the attributes. e->setProperty("isDirectory", rxMatch.captured(10).endsWith(QLatin1Char('/'))); e->setProperty("size", rxMatch.captured(4)); QString status = rxMatch.captured(5); if (status[0].isUpper()) { e->setProperty("isPasswordProtected", true); } e->setProperty("compressedSize", rxMatch.captured(6).toInt()); const QDateTime ts(QDate::fromString(rxMatch.captured(8), QStringLiteral("yyyyMMdd")), QTime::fromString(rxMatch.captured(9), QStringLiteral("hhmmss"))); e->setProperty("timestamp", ts); - e->setProperty("fileName", rxMatch.captured(10)); + e->setProperty("fullPath", rxMatch.captured(10)); emit entry(e); } break; } return true; } #include "cliplugin.moc" diff --git a/plugins/libarchive/libarchiveplugin.cpp b/plugins/libarchive/libarchiveplugin.cpp index ad7685cc..345003a9 100644 --- a/plugins/libarchive/libarchiveplugin.cpp +++ b/plugins/libarchive/libarchiveplugin.cpp @@ -1,522 +1,519 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2010 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "libarchiveplugin.h" -#include "ark_debug.h" -#include "kerfuffle/kerfuffle_export.h" -#include "kerfuffle/archiveentry.h" #include "kerfuffle/queries.h" -#include - #include -#include #include -#include -#include -#include LibarchivePlugin::LibarchivePlugin(QObject *parent, const QVariantList & args) : ReadWriteArchiveInterface(parent, args) , m_archiveReadDisk(archive_read_disk_new()) , m_abortOperation(false) , m_cachedArchiveEntryCount(0) , m_emitNoEntries(false) , m_extractedFilesSize(0) { qCDebug(ARK) << "Initializing libarchive plugin"; archive_read_disk_set_standard_lookup(m_archiveReadDisk.data()); } LibarchivePlugin::~LibarchivePlugin() { } bool LibarchivePlugin::list() { qCDebug(ARK) << "Listing archive contents"; ArchiveRead arch_reader(archive_read_new()); if (!(arch_reader.data())) { return false; } if (archive_read_support_filter_all(arch_reader.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(arch_reader.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(arch_reader.data(), QFile::encodeName(filename()), 10240) != ARCHIVE_OK) { emit error(i18nc("@info", "Could not open the archive.")); return false; } qDebug(ARK) << "Detected compression filter:" << archive_filter_name(arch_reader.data(), 0); m_cachedArchiveEntryCount = 0; m_extractedFilesSize = 0; struct archive_entry *aentry; int result = ARCHIVE_RETRY; bool firstEntry = true; while (!m_abortOperation && (result = archive_read_next_header(arch_reader.data(), &aentry)) == ARCHIVE_OK) { if (firstEntry) { qDebug(ARK) << "Detected format for first entry:" << archive_format_name(arch_reader.data()); firstEntry = false; } if (!m_emitNoEntries) { emitEntryFromArchiveEntry(aentry); } m_extractedFilesSize += (qlonglong)archive_entry_size(aentry); m_cachedArchiveEntryCount++; archive_read_data_skip(arch_reader.data()); } m_abortOperation = false; if (result != ARCHIVE_EOF) { const QString errorString = QLatin1String(archive_error_string(arch_reader.data())); // FIXME: what about the other archive_error_string() calls? Do they also happen to return empty strings? emit error(errorString.isEmpty() ? i18nc("@info", "Could not read until the end of the archive") : errorString); return false; } return archive_read_close(arch_reader.data()) == ARCHIVE_OK; } -bool LibarchivePlugin::addFiles(const QStringList &files, const CompressionOptions &options) +bool LibarchivePlugin::addFiles(const QList &files, const CompressionOptions &options) { Q_UNUSED(files) Q_UNUSED(options) return false; } -bool LibarchivePlugin::deleteFiles(const QList &files) +bool LibarchivePlugin::deleteFiles(const QList &files) { Q_UNUSED(files) return false; } bool LibarchivePlugin::addComment(const QString& comment) { Q_UNUSED(comment) return false; } bool LibarchivePlugin::testArchive() { return false; } bool LibarchivePlugin::doKill() { m_abortOperation = true; return true; } -bool LibarchivePlugin::copyFiles(const QVariantList& files, const QString& destinationDirectory, const ExtractionOptions& options) +bool LibarchivePlugin::copyFiles(const QList& files, const QString& destinationDirectory, const ExtractionOptions& options) { qCDebug(ARK) << "Changing current directory to " << destinationDirectory; QDir::setCurrent(destinationDirectory); const bool extractAll = files.isEmpty(); const bool preservePaths = options.value(QStringLiteral( "PreservePaths" )).toBool(); bool removeRootNode = options.value(QStringLiteral("RemoveRootNode"), QVariant()).toBool(); // To avoid traversing the entire archive when extracting a limited set of // entries, we maintain a list of remaining entries and stop when it's // empty. - QVariantList remainingFiles = files; + QList remainingFiles = files; ArchiveRead arch(archive_read_new()); if (!(arch.data())) { return false; } if (archive_read_support_filter_all(arch.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(arch.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(arch.data(), QFile::encodeName(filename()), 10240) != ARCHIVE_OK) { // This error might be shown outside of a running Ark part (e.g. from a batch job). emit error(xi18nc("@info", "Could not open the archive %1." "Check whether you have sufficient permissions.", filename())); return false; } ArchiveWrite writer(archive_write_disk_new()); if (!writer.data()) { return false; } archive_write_disk_set_options(writer.data(), extractionFlags()); int entryNr = 0; int totalCount = 0; if (extractAll) { if (!m_cachedArchiveEntryCount) { emit progress(0); //TODO: once information progress has been implemented, send //feedback here that the archive is being read qCDebug(ARK) << "For getting progress information, the archive will be listed once"; m_emitNoEntries = true; list(); m_emitNoEntries = false; } totalCount = m_cachedArchiveEntryCount; } else { totalCount = files.size(); } qCDebug(ARK) << "Going to extract" << totalCount << "entries"; // Initialize variables. bool overwriteAll = false; // Whether to overwrite all files bool skipAll = false; // Whether to skip all files bool dontPromptErrors = false; // Whether to prompt for errors m_currentExtractedFilesSize = 0; int no_entries = 0; struct archive_entry *entry; QString fileBeingRenamed; // Iterate through all entries in archive. while (!m_abortOperation && (archive_read_next_header(arch.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(arch.data()); continue; } // entryName is the name inside the archive, full path QString entryName = QDir::fromNativeSeparators(QFile::decodeName(archive_entry_pathname(entry))); // For now we just can't handle absolute filenames in a tar archive. // TODO: find out what to do here!! if (entryName.startsWith(QLatin1Char( '/' ))) { emit error(i18n("This archive contains archive entries with absolute paths, " "which are not supported by Ark.")); return false; } + Archive::Entry e(Q_NULLPTR); + e.setFullPath(entryName); + // Should the entry be extracted? if (extractAll || - remainingFiles.contains(QVariant::fromValue(fileRootNodePair(entryName))) || + remainingFiles.contains(&e) || entryName == fileBeingRenamed) { // Find the index of entry. if (entryName != fileBeingRenamed) { - index = files.indexOf(QVariant::fromValue(fileRootNodePair(entryName))); + index = files.indexOf(&e); } 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 && - !files.at(index).value().rootNode.isEmpty()) { - - //qCDebug(ARK) << "Removing" << files.at(index).value().rootNode << "from" << entryName; - - const QString truncatedFilename(entryName.remove(0, files.at(index).value().rootNode.size())); - archive_entry_copy_pathname(entry, QFile::encodeName(truncatedFilename).constData()); - entryFI = QFileInfo(truncatedFilename); + } else if (!extractAll && removeRootNode && entryName != fileBeingRenamed) { + const QString &rootNode = files.at(index)->rootNode; + if (rootNode.isEmpty()) { + //qCDebug(ARK) << "Removing" << files.at(index).value().rootNode << "from" << entryName; + + const QString truncatedFilename(entryName.remove(0, rootNode.size())); + archive_entry_copy_pathname(entry, QFile::encodeName(truncatedFilename).constData()); + entryFI = QFileInfo(truncatedFilename); + } } // Check if the file about to be written already exists. if (!entryIsDir && entryFI.exists()) { if (skipAll) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); continue; } else if (!overwriteAll && !skipAll) { Kerfuffle::OverwriteQuery query(entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); break; } else if (query.responseSkip()) { archive_read_data_skip(arch.data()); archive_entry_clear(entry); continue; } else if (query.responseAutoSkip()) { archive_read_data_skip(arch.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(arch.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, arch.data(), writer.data(), (extractAll && m_extractedFilesSize)); break; case ARCHIVE_FAILED: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); // If they user previously decided to ignore future errors, // don't bother prompting again. if (!dontPromptErrors) { // Ask the user if he wants to continue extraction despite an error for this entry. Kerfuffle::ContinueExtractionQuery query(QLatin1String(archive_error_string(writer.data())), entryName); emit userQuery(&query); query.waitForResponse(); if (query.responseCancelled()) { emit cancelled(); return false; } dontPromptErrors = query.dontAskAgain(); } break; case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(writer.data()); emit error(xi18nc("@info", "Extraction failed at:%1", entryName)); return false; default: qCDebug(ARK) << "archive_write_header() returned" << returnCode << "which will be ignored."; break; } // If we only partially extract the archive and the number of // archive entries is available we use a simple progress based on // number of items extracted. if (!extractAll && m_cachedArchiveEntryCount) { ++entryNr; emit progress(float(entryNr) / totalCount); } archive_entry_clear(entry); no_entries++; - remainingFiles.removeOne(QVariant::fromValue(fileRootNodePair(entryName))); + Archive::Entry e(Q_NULLPTR); + e.setFullPath(entryName); + remainingFiles.removeOne(&e); } else { // Archive entry not among selected files, skip it. archive_read_data_skip(arch.data()); } } // While entries left to read in archive. m_abortOperation = false; qCDebug(ARK) << "Extracted" << no_entries << "entries"; return archive_read_close(arch.data()) == ARCHIVE_OK; } void LibarchivePlugin::emitEntryFromArchiveEntry(struct archive_entry *aentry) { Archive::Entry *e = new Archive::Entry(NULL); #ifdef _MSC_VER - e->fileName = QDir::fromNativeSeparators(QString::fromUtf16((ushort*)archive_entry_pathname_w(aentry))); + e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromUtf16((ushort*)archive_entry_pathname_w(aentry)))); #else - e->setProperty("fileName", QDir::fromNativeSeparators(QString::fromWCharArray(archive_entry_pathname_w(aentry)))); + e->setProperty("fullPath", QDir::fromNativeSeparators(QString::fromWCharArray(archive_entry_pathname_w(aentry)))); #endif const QString owner = QString::fromLatin1(archive_entry_uname(aentry)); if (!owner.isEmpty()) { e->setProperty("owner", owner); } const QString group = QString::fromLatin1(archive_entry_gname(aentry)); if (!group.isEmpty()) { e->setProperty("group", group); } e->compressedSizeIsSet = false; e->setProperty("size", (qlonglong)archive_entry_size(aentry)); e->setProperty("isDirectory", S_ISDIR(archive_entry_mode(aentry))); if (archive_entry_symlink(aentry)) { e->setProperty("link", QLatin1String( archive_entry_symlink(aentry) )); } e->setProperty("timestamp", QDateTime::fromTime_t(archive_entry_mtime(aentry))); emit entry(e); } int LibarchivePlugin::extractionFlags() const { int result = ARCHIVE_EXTRACT_TIME; result |= ARCHIVE_EXTRACT_SECURE_NODOTDOT; // TODO: Don't use arksettings here /*if ( ArkSettings::preservePerms() ) { result &= ARCHIVE_EXTRACT_PERM; } if ( !ArkSettings::extractOverwrite() ) { result &= ARCHIVE_EXTRACT_NO_OVERWRITE; }*/ return result; } void LibarchivePlugin::copyData(const QString& filename, struct archive *dest, bool partialprogress) { char buff[10240]; ssize_t readBytes; QFile file(filename); if (!file.open(QIODevice::ReadOnly)) { return; } readBytes = file.read(buff, sizeof(buff)); while (readBytes > 0) { archive_write_data(dest, buff, readBytes); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while writing" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = file.read(buff, sizeof(buff)); } file.close(); } void LibarchivePlugin::copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress) { char buff[10240]; ssize_t readBytes; readBytes = archive_read_data(source, buff, sizeof(buff)); while (readBytes > 0) { archive_write_data(dest, buff, readBytes); if (archive_errno(dest) != ARCHIVE_OK) { qCCritical(ARK) << "Error while extracting" << filename << ":" << archive_error_string(dest) << "(error no =" << archive_errno(dest) << ')'; return; } if (partialprogress) { m_currentExtractedFilesSize += readBytes; emit progress(float(m_currentExtractedFilesSize) / m_extractedFilesSize); } readBytes = archive_read_data(source, buff, sizeof(buff)); } } #include "libarchiveplugin.moc" diff --git a/plugins/libarchive/libarchiveplugin.h b/plugins/libarchive/libarchiveplugin.h index 77a319a1..9661efa2 100644 --- a/plugins/libarchive/libarchiveplugin.h +++ b/plugins/libarchive/libarchiveplugin.h @@ -1,95 +1,96 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #ifndef LIBARCHIVEPLUGIN_H #define LIBARCHIVEPLUGIN_H #include "kerfuffle/archiveinterface.h" +#include "kerfuffle/archiveentry.h" #include #include using namespace Kerfuffle; class LibarchivePlugin : public ReadWriteArchiveInterface { Q_OBJECT public: explicit LibarchivePlugin(QObject *parent, const QVariantList& args); virtual ~LibarchivePlugin(); virtual bool list() Q_DECL_OVERRIDE; virtual bool doKill() Q_DECL_OVERRIDE; - virtual bool copyFiles(const QVariantList& files, const QString& destinationDirectory, const ExtractionOptions& options) Q_DECL_OVERRIDE; + virtual bool copyFiles(const QList& files, const QString& destinationDirectory, const ExtractionOptions& options) Q_DECL_OVERRIDE; - virtual bool addFiles(const QStringList& files, const CompressionOptions& options) Q_DECL_OVERRIDE; - virtual bool deleteFiles(const QList& files) Q_DECL_OVERRIDE; + virtual bool addFiles(const QList& files, const CompressionOptions& options) Q_DECL_OVERRIDE; + virtual bool deleteFiles(const QList& files) Q_DECL_OVERRIDE; virtual bool addComment(const QString& comment) Q_DECL_OVERRIDE; virtual bool testArchive() Q_DECL_OVERRIDE; protected: void emitEntryFromArchiveEntry(struct archive_entry *entry); void copyData(const QString& filename, struct archive *dest, bool partialprogress = true); void copyData(const QString& filename, struct archive *source, struct archive *dest, bool partialprogress = true); struct ArchiveReadCustomDeleter { static inline void cleanup(struct archive *a) { if (a) { archive_read_free(a); } } }; struct ArchiveWriteCustomDeleter { static inline void cleanup(struct archive *a) { if (a) { archive_write_free(a); } } }; typedef QScopedPointer ArchiveRead; typedef QScopedPointer ArchiveWrite; ArchiveRead m_archiveReadDisk; bool m_abortOperation; private: int extractionFlags() const; int m_cachedArchiveEntryCount; qlonglong m_currentExtractedFilesSize; bool m_emitNoEntries; qlonglong m_extractedFilesSize; }; #endif // LIBARCHIVEPLUGIN_H diff --git a/plugins/libarchive/readwritelibarchiveplugin.cpp b/plugins/libarchive/readwritelibarchiveplugin.cpp index 565bea9a..9822c690 100644 --- a/plugins/libarchive/readwritelibarchiveplugin.cpp +++ b/plugins/libarchive/readwritelibarchiveplugin.cpp @@ -1,500 +1,498 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * Copyright (c) 2010 Raphael Kubo da Costa * * Redistribution and use in source and binary forms, with or without * modification, are permitted provided that the following conditions * are met: * * 1. Redistributions of source code must retain the above copyright * notice, this list of conditions and the following disclaimer. * 2. Redistributions in binary form must reproduce the above copyright * notice, this list of conditions and the following disclaimer in the * documentation and/or other materials provided with the distribution. * * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR * IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. * IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, * INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES ( INCLUDING, BUT * NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, * DATA, OR PROFITS; OR BUSINESS INTERRUPTION ) HOWEVER CAUSED AND ON ANY * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT * ( INCLUDING NEGLIGENCE OR OTHERWISE ) ARISING IN ANY WAY OUT OF THE USE OF * THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. */ #include "readwritelibarchiveplugin.h" -#include "ark_debug.h" - -#include #include #include #include #include K_PLUGIN_FACTORY_WITH_JSON(ReadWriteLibarchivePluginFactory, "kerfuffle_libarchive.json", registerPlugin();) ReadWriteLibarchivePlugin::ReadWriteLibarchivePlugin(QObject *parent, const QVariantList & args) : LibarchivePlugin(parent, args) { qCDebug(ARK) << "Loaded libarchive read-write plugin"; } ReadWriteLibarchivePlugin::~ReadWriteLibarchivePlugin() { } -bool ReadWriteLibarchivePlugin::addFiles(const QStringList& files, const CompressionOptions& options) +bool ReadWriteLibarchivePlugin::addFiles(const QList& files, const CompressionOptions& options) { qCDebug(ARK) << "Adding" << files.size() << "entries with CompressionOptions" << options; const bool creatingNewFile = !QFileInfo::exists(filename()); m_writtenFiles.clear(); ArchiveRead arch_reader; if (!creatingNewFile) { arch_reader.reset(archive_read_new()); if (!arch_reader.data()) { emit error(i18n("The archive reader could not be initialized.")); return false; } if (archive_read_support_filter_all(arch_reader.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(arch_reader.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(arch_reader.data(), QFile::encodeName(filename()), 10240) != ARCHIVE_OK) { emit error(i18n("The source file could not be read.")); return false; } } // |tempFile| needs to be created before |arch_writer| so that when we go // out of scope in a `return false' case ArchiveWriteCustomDeleter is // called before destructor of QSaveFile (ie. we call archive_write_close() // before close()'ing the file descriptor). QSaveFile tempFile(filename()); if (!tempFile.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) { emit error(xi18nc("@info", "Failed to create a temporary file to compress %1.", filename())); return false; } ArchiveWrite arch_writer(archive_write_new()); if (!(arch_writer.data())) { emit error(i18n("The archive writer could not be initialized.")); return false; } // pax_restricted is the libarchive default, let's go with that. archive_write_set_format_pax_restricted(arch_writer.data()); int ret; bool requiresExecutable = false; if (creatingNewFile) { if (filename().right(2).toUpper() == QLatin1String("GZ")) { qCDebug(ARK) << "Detected gzip compression for new file"; ret = archive_write_add_filter_gzip(arch_writer.data()); } else if (filename().right(3).toUpper() == QLatin1String("BZ2")) { qCDebug(ARK) << "Detected bzip2 compression for new file"; ret = archive_write_add_filter_bzip2(arch_writer.data()); } else if (filename().right(2).toUpper() == QLatin1String("XZ")) { qCDebug(ARK) << "Detected xz compression for new file"; ret = archive_write_add_filter_xz(arch_writer.data()); } else if (filename().right(4).toUpper() == QLatin1String("LZMA")) { qCDebug(ARK) << "Detected lzma compression for new file"; ret = archive_write_add_filter_lzma(arch_writer.data()); } else if (filename().right(2).toUpper() == QLatin1String(".Z")) { qCDebug(ARK) << "Detected compress (.Z) compression for new file"; ret = archive_write_add_filter_compress(arch_writer.data()); } else if (filename().right(2).toUpper() == QLatin1String("LZ")) { qCDebug(ARK) << "Detected lzip compression for new file"; ret = archive_write_add_filter_lzip(arch_writer.data()); } else if (filename().right(3).toUpper() == QLatin1String("LZO")) { qCDebug(ARK) << "Detected lzop compression for new file"; ret = archive_write_add_filter_lzop(arch_writer.data()); } else if (filename().right(3).toUpper() == QLatin1String("LRZ")) { qCDebug(ARK) << "Detected lrzip compression for new file"; ret = archive_write_add_filter_lrzip(arch_writer.data()); requiresExecutable = true; #ifdef HAVE_LIBARCHIVE_3_2_0 } else if (filename().right(3).toUpper() == QLatin1String("LZ4")) { qCDebug(ARK) << "Detected lz4 compression for new file"; ret = archive_write_add_filter_lz4(arch_writer.data()); requiresExecutable = true; #endif } else if (filename().right(3).toUpper() == QLatin1String("TAR")) { qCDebug(ARK) << "Detected no compression for new file (pure tar)"; ret = archive_write_add_filter_none(arch_writer.data()); } else { qCDebug(ARK) << "Falling back to gzip"; ret = archive_write_add_filter_gzip(arch_writer.data()); } // Libarchive emits a warning for lrzip due to using external executable. if ((requiresExecutable && ret != ARCHIVE_WARN) || (!requiresExecutable && ret != ARCHIVE_OK)) { emit error(xi18nc("@info", "Setting the compression method failed with the following error:%1", QLatin1String(archive_error_string(arch_writer.data())))); return false; } // Set compression level if passed in CompressionOptions. if (options.contains(QStringLiteral("CompressionLevel"))) { qCDebug(ARK) << "Using compression level:" << options.value(QStringLiteral("CompressionLevel")).toString(); ret = archive_write_set_filter_option(arch_writer.data(), NULL, "compression-level", options.value(QStringLiteral("CompressionLevel")).toString().toUtf8()); if (ret != ARCHIVE_OK) { qCWarning(ARK) << "Failed to set compression level"; emit error(xi18nc("@info", "Setting the compression level failed with the following error:%1", QLatin1String(archive_error_string(arch_writer.data())))); return false; } } } else { switch (archive_filter_code(arch_reader.data(), 0)) { case ARCHIVE_FILTER_GZIP: ret = archive_write_add_filter_gzip(arch_writer.data()); break; case ARCHIVE_FILTER_BZIP2: ret = archive_write_add_filter_bzip2(arch_writer.data()); break; case ARCHIVE_FILTER_XZ: ret = archive_write_add_filter_xz(arch_writer.data()); break; case ARCHIVE_FILTER_LZMA: ret = archive_write_add_filter_lzma(arch_writer.data()); break; case ARCHIVE_FILTER_COMPRESS: ret = archive_write_add_filter_compress(arch_writer.data()); break; case ARCHIVE_FILTER_LZIP: ret = archive_write_add_filter_lzip(arch_writer.data()); break; case ARCHIVE_FILTER_LZOP: ret = archive_write_add_filter_lzop(arch_writer.data()); break; case ARCHIVE_FILTER_LRZIP: ret = archive_write_add_filter_lrzip(arch_writer.data()); requiresExecutable = true; break; #ifdef HAVE_LIBARCHIVE_3_2_0 case ARCHIVE_FILTER_LZ4: ret = archive_write_add_filter_lz4(arch_writer.data()); requiresExecutable = true; break; #endif case ARCHIVE_FILTER_NONE: ret = archive_write_add_filter_none(arch_writer.data()); break; default: emit error(i18n("The compression type '%1' is not supported by Ark.", QLatin1String(archive_filter_name(arch_reader.data(), 0)))); return false; } // Libarchive emits a warning for lrzip due to using external executable. if ((requiresExecutable && ret != ARCHIVE_WARN) || (!requiresExecutable && ret != ARCHIVE_OK)) { emit error(xi18nc("@info", "Setting the compression method failed with the following error:%1", QLatin1String(archive_error_string(arch_writer.data())))); return false; } } ret = archive_write_open_fd(arch_writer.data(), tempFile.handle()); if (ret != ARCHIVE_OK) { emit error(xi18nc("@info", "Opening the archive for writing failed with the following error:%1", QLatin1String(archive_error_string(arch_writer.data())))); return false; } // First write the new files. qCDebug(ARK) << "Writing new entries"; int no_entries = 0; - foreach(const QString& selectedFile, files) { + foreach(Archive::Entry *selectedFile, files) { if (m_abortOperation) { break; } - if (!writeFile(selectedFile, arch_writer.data())) { + if (!writeFile(selectedFile->property("fullPath").toString(), arch_writer.data())) { return false; } no_entries++; // For directories, write all subfiles/folders. - if (QFileInfo(selectedFile).isDir()) { - QDirIterator it(selectedFile, + const QString &fullPath = selectedFile->property("fullPath").toString(); + if (QFileInfo(fullPath).isDir()) { + QDirIterator it(fullPath, QDir::AllEntries | QDir::Readable | QDir::Hidden | QDir::NoDotAndDotDot, QDirIterator::Subdirectories); while (!m_abortOperation && it.hasNext()) { QString path = it.next(); if ((it.fileName() == QLatin1String("..")) || (it.fileName() == QLatin1String("."))) { continue; } const bool isRealDir = it.fileInfo().isDir() && !it.fileInfo().isSymLink(); if (isRealDir) { path.append(QLatin1Char('/')); } if (!writeFile(path, arch_writer.data())) { return false; } no_entries++; } } } qCDebug(ARK) << "Added" << no_entries << "new entries to archive"; struct archive_entry *entry; // If we have old archive entries. if (!creatingNewFile) { qCDebug(ARK) << "Copying any old entries"; no_entries = 0; // Copy old entries from previous archive to new archive. while (!m_abortOperation && (archive_read_next_header(arch_reader.data(), &entry) == ARCHIVE_OK)) { const QString entryName = QFile::decodeName(archive_entry_pathname(entry)); if (m_writtenFiles.contains(entryName)) { archive_read_data_skip(arch_reader.data()); qCDebug(ARK) << entryName << "is already present in the new archive, skipping."; continue; } const int returnCode = archive_write_header(arch_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(QLatin1String(archive_entry_pathname(entry)), arch_reader.data(), arch_writer.data(), false); no_entries++; break; case ARCHIVE_FAILED: case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(arch_writer.data()); emit error(xi18nc("@info", "Compression failed while processing:" "%1Operation aborted.", entryName)); QFile::remove(tempFile.fileName()); return false; default: qCWarning(ARK) << "archive_writer_header() has returned" << returnCode << "which will be ignored."; break; } archive_entry_clear(entry); } qCDebug(ARK) << "Added" << no_entries << "old entries to archive"; } m_abortOperation = false; // In the success case, we need to manually close the archive_writer before // calling QSaveFile::commit(), otherwise the latter will close() the // file descriptor archive_writer is still working on. // TODO: We need to abstract this code better so that we only deal with one // object that manages both QSaveFile and ArchiveWriter. archive_write_close(arch_writer.data()); tempFile.commit(); return true; } -bool ReadWriteLibarchivePlugin::deleteFiles(const QVariantList& files) +bool ReadWriteLibarchivePlugin::deleteFiles(const QList& files) { qCDebug(ARK) << "Deleting" << files.size() << "entries"; ArchiveRead arch_reader(archive_read_new()); if (!(arch_reader.data())) { emit error(i18n("The archive reader could not be initialized.")); return false; } if (archive_read_support_filter_all(arch_reader.data()) != ARCHIVE_OK) { return false; } if (archive_read_support_format_all(arch_reader.data()) != ARCHIVE_OK) { return false; } if (archive_read_open_filename(arch_reader.data(), QFile::encodeName(filename()), 10240) != ARCHIVE_OK) { emit error(i18n("The source file could not be read.")); return false; } // |tempFile| needs to be created before |arch_writer| so that when we go // out of scope in a `return false' case ArchiveWriteCustomDeleter is // called before destructor of QSaveFile (ie. we call archive_write_close() // before close()'ing the file descriptor). QSaveFile tempFile(filename()); if (!tempFile.open(QIODevice::WriteOnly | QIODevice::Unbuffered)) { emit error(i18nc("@info", "Failed to create a temporary file.")); return false; } ArchiveWrite arch_writer(archive_write_new()); if (!(arch_writer.data())) { emit error(i18n("The archive writer could not be initialized.")); return false; } // pax_restricted is the libarchive default, let's go with that. archive_write_set_format_pax_restricted(arch_writer.data()); int ret; switch (archive_filter_code(arch_reader.data(), 0)) { case ARCHIVE_FILTER_GZIP: ret = archive_write_add_filter_gzip(arch_writer.data()); break; case ARCHIVE_FILTER_BZIP2: ret = archive_write_add_filter_bzip2(arch_writer.data()); break; case ARCHIVE_FILTER_XZ: ret = archive_write_add_filter_xz(arch_writer.data()); break; case ARCHIVE_FILTER_LZMA: ret = archive_write_add_filter_lzma(arch_writer.data()); break; case ARCHIVE_FILTER_NONE: ret = archive_write_add_filter_none(arch_writer.data()); break; default: emit error(i18n("The compression type '%1' is not supported by Ark.", QLatin1String(archive_filter_name(arch_reader.data(), 0)))); return false; } if (ret != ARCHIVE_OK) { emit error(xi18nc("@info", "Setting the compression method failed with the following error:" "%1", QLatin1String(archive_error_string(arch_writer.data())))); return false; } ret = archive_write_open_fd(arch_writer.data(), tempFile.handle()); if (ret != ARCHIVE_OK) { emit error(xi18nc("@info", "Opening the archive for writing failed with the following error:" "%1", QLatin1String(archive_error_string(arch_writer.data())))); return false; } struct archive_entry *entry; // Copy old elements from previous archive to new archive. int no_entries = 0; while (archive_read_next_header(arch_reader.data(), &entry) == ARCHIVE_OK) { - const QString entryName = QFile::decodeName(archive_entry_pathname(entry)); + Archive::Entry *e = new Archive::Entry(Q_NULLPTR, QFile::decodeName(archive_entry_pathname(entry))); - if (files.contains(entryName)) { + if (files.contains(e)) { archive_read_data_skip(arch_reader.data()); - emit entryRemoved(entryName); + emit entryRemoved(e->property("fullPath").toString()); no_entries++; continue; } const int returnCode = archive_write_header(arch_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(QLatin1String(archive_entry_pathname(entry)), arch_reader.data(), arch_writer.data(), false); break; case ARCHIVE_FAILED: case ARCHIVE_FATAL: qCCritical(ARK) << "archive_write_header() has returned" << returnCode << "with errno" << archive_errno(arch_writer.data()); emit error(xi18nc("@info", "Compression failed while processing:" - "%1Operation aborted.", entryName)); + "%1Operation aborted.", e->property("fullPath").toString())); return false; default: qCDebug(ARK) << "archive_writer_header() has returned" << returnCode << "which will be ignored."; break; } } qCDebug(ARK) << "Removed" << no_entries << "entries from archive"; m_abortOperation = false; // In the success case, we need to manually close the archive_writer before // calling QSaveFile::commit(), otherwise the latter will close() the // file descriptor archive_writer is still working on. // TODO: We need to abstract this code better so that we only deal with one // object that manages both QSaveFile and ArchiveWriter. archive_write_close(arch_writer.data()); tempFile.commit(); return true; } // TODO: if we merge this with copyData(), we can pass more data // such as an fd to archive_read_disk_entry_from_file() bool ReadWriteLibarchivePlugin::writeFile(const QString& relativeName, struct archive* arch_writer) { int header_response; const QString absoluteFilename = QFileInfo(relativeName).absoluteFilePath(); // #253059: Even if we use archive_read_disk_entry_from_file, // libarchive may have been compiled without HAVE_LSTAT, // or something may have caused it to follow symlinks, in // which case stat() will be called. To avoid this, we // call lstat() ourselves. struct stat st; lstat(QFile::encodeName(absoluteFilename).constData(), &st); struct archive_entry *entry = archive_entry_new(); archive_entry_set_pathname(entry, QFile::encodeName(relativeName).constData()); archive_entry_copy_sourcepath(entry, QFile::encodeName(absoluteFilename).constData()); archive_read_disk_entry_from_file(m_archiveReadDisk.data(), entry, -1, &st); if ((header_response = archive_write_header(arch_writer, entry)) == ARCHIVE_OK) { // If the whole archive is extracted and the total filesize is // available, we use partial progress. copyData(absoluteFilename, arch_writer, false); } else { qCCritical(ARK) << "Writing header failed with error code " << header_response; qCCritical(ARK) << "Error while writing..." << archive_error_string(arch_writer) << "(error no =" << archive_errno(arch_writer) << ')'; emit error(xi18nc("@info Error in a message box", "Ark could not compress %1:%2", absoluteFilename, QString::fromUtf8(archive_error_string(arch_writer)))); archive_entry_free(entry); return false; } m_writtenFiles.push_back(relativeName); emitEntryFromArchiveEntry(entry); archive_entry_free(entry); return true; } #include "readwritelibarchiveplugin.moc" diff --git a/plugins/libarchive/readwritelibarchiveplugin.h b/plugins/libarchive/readwritelibarchiveplugin.h index c46482ac..1fb99380 100644 --- a/plugins/libarchive/readwritelibarchiveplugin.h +++ b/plugins/libarchive/readwritelibarchiveplugin.h @@ -1,54 +1,54 @@ /* * Copyright (c) 2007 Henrique Pinto * Copyright (c) 2008-2009 Harald Hvaal * * 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 using namespace Kerfuffle; class ReadWriteLibarchivePlugin : public LibarchivePlugin { Q_OBJECT public: explicit ReadWriteLibarchivePlugin(QObject *parent, const QVariantList& args); ~ReadWriteLibarchivePlugin(); - bool addFiles(const QStringList& files, const CompressionOptions& options) Q_DECL_OVERRIDE; - bool deleteFiles(const QVariantList& files) Q_DECL_OVERRIDE; + bool addFiles(const QList& files, const CompressionOptions& options) Q_DECL_OVERRIDE; + bool deleteFiles(const QList& files) Q_DECL_OVERRIDE; private: bool writeFile(const QString& relativeName, struct archive* arch); QStringList m_writtenFiles; }; #endif // READWRITELIBARCHIVEPLUGIN_H diff --git a/plugins/libsinglefileplugin/singlefileplugin.cpp b/plugins/libsinglefileplugin/singlefileplugin.cpp index 2c1eb045..3529cd05 100644 --- a/plugins/libsinglefileplugin/singlefileplugin.cpp +++ b/plugins/libsinglefileplugin/singlefileplugin.cpp @@ -1,164 +1,164 @@ /* * 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 "singlefileplugin.h" #include "ark_debug.h" #include "kerfuffle/archiveentry.h" #include "kerfuffle/queries.h" #include #include #include #include LibSingleFileInterface::LibSingleFileInterface(QObject *parent, const QVariantList & args) : Kerfuffle::ReadOnlyArchiveInterface(parent, args) { qCDebug(ARK) << "Loaded singlefile plugin"; } LibSingleFileInterface::~LibSingleFileInterface() { } -bool LibSingleFileInterface::copyFiles(const QList& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) +bool LibSingleFileInterface::copyFiles(const QList &files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) { Q_UNUSED(files) Q_UNUSED(options) QString outputFileName = destinationDirectory; if (!destinationDirectory.endsWith(QLatin1Char('/'))) { outputFileName += QLatin1Char('/'); } outputFileName += uncompressedFileName(); outputFileName = overwriteFileName(outputFileName); if (outputFileName.isEmpty()) { return true; } qCDebug(ARK) << "Extracting to" << outputFileName; QFile outputFile(outputFileName); if (!outputFile.open(QIODevice::WriteOnly)) { qCCritical(ARK) << "Failed to open output file" << outputFile.errorString(); emit error(xi18nc("@info", "Ark could not extract %1.", outputFile.fileName())); return false; } KCompressionDevice *device = new KCompressionDevice(filename(), KFilterDev::compressionTypeForMimeType(m_mimeType)); if (!device) { qCCritical(ARK) << "Could not create KCompressionDevice"; emit error(xi18nc("@info", "Ark could not open %1 for extraction.", filename())); return false; } device->open(QIODevice::ReadOnly); qint64 bytesRead; QByteArray dataChunk(1024*16, '\0'); // 16Kb while (true) { bytesRead = device->read(dataChunk.data(), dataChunk.size()); if (bytesRead == -1) { emit error(xi18nc("@info", "There was an error while reading %1 during extraction.", filename())); break; } else if (bytesRead == 0) { break; } outputFile.write(dataChunk.data(), bytesRead); } delete device; return true; } bool LibSingleFileInterface::list() { qCDebug(ARK) << "Listing archive contents"; Kerfuffle::Archive::Entry *e = new Kerfuffle::Archive::Entry(Q_NULLPTR); - e->setProperty("fileName", uncompressedFileName()); + e->setProperty("fullPath", uncompressedFileName()); emit entry(e); return true; } QString LibSingleFileInterface::overwriteFileName(QString& filename) { QString newFileName(filename); while (QFile::exists(newFileName)) { Kerfuffle::OverwriteQuery query(newFileName); query.setMultiMode(false); emit userQuery(&query); query.waitForResponse(); if ((query.responseCancelled()) || (query.responseSkip())) { return QString(); } else if (query.responseOverwrite()) { break; } else if (query.responseRename()) { newFileName = query.newFilename(); } } return newFileName; } const QString LibSingleFileInterface::uncompressedFileName() const { QString uncompressedName(QFileInfo(filename()).fileName()); // Bug 252701: For .svgz just remove the terminal "z". if (uncompressedName.endsWith(QLatin1String(".svgz"), Qt::CaseInsensitive)) { uncompressedName.chop(1); return uncompressedName; } foreach(const QString & extension, m_possibleExtensions) { qCDebug(ARK) << extension; if (uncompressedName.endsWith(extension, Qt::CaseInsensitive)) { uncompressedName.chop(extension.size()); return uncompressedName; } } return uncompressedName + QStringLiteral( ".uncompressed" ); } bool LibSingleFileInterface::testArchive() { return false; } diff --git a/plugins/libsinglefileplugin/singlefileplugin.h b/plugins/libsinglefileplugin/singlefileplugin.h index bbc32ca5..29d2a927 100644 --- a/plugins/libsinglefileplugin/singlefileplugin.h +++ b/plugins/libsinglefileplugin/singlefileplugin.h @@ -1,51 +1,52 @@ /* * 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 SINGLEFILEPLUGIN_H #define SINGLEFILEPLUGIN_H #include "kerfuffle/archiveinterface.h" +#include "kerfuffle/archiveentry.h" class LibSingleFileInterface : public Kerfuffle::ReadOnlyArchiveInterface { Q_OBJECT public: LibSingleFileInterface(QObject *parent, const QVariantList & args); virtual ~LibSingleFileInterface(); virtual bool list() Q_DECL_OVERRIDE; virtual bool testArchive() Q_DECL_OVERRIDE; - virtual bool copyFiles(const QList& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) Q_DECL_OVERRIDE; + virtual bool copyFiles(const QList &files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) Q_DECL_OVERRIDE; protected: const QString uncompressedFileName() const; QString overwriteFileName(QString& filename); QString m_mimeType; QStringList m_possibleExtensions; }; #endif // SINGLEFILEPLUGIN_H