diff --git a/autotests/kerfuffle/jsonarchiveinterface.h b/autotests/kerfuffle/jsonarchiveinterface.h --- a/autotests/kerfuffle/jsonarchiveinterface.h +++ b/autotests/kerfuffle/jsonarchiveinterface.h @@ -59,6 +59,7 @@ 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; diff --git a/autotests/kerfuffle/jsonarchiveinterface.cpp b/autotests/kerfuffle/jsonarchiveinterface.cpp --- a/autotests/kerfuffle/jsonarchiveinterface.cpp +++ b/autotests/kerfuffle/jsonarchiveinterface.cpp @@ -108,3 +108,8 @@ Q_UNUSED(comment) return true; } + +bool JSONArchiveInterface::testArchive() +{ + return true; +} diff --git a/kerfuffle/archive_kerfuffle.h b/kerfuffle/archive_kerfuffle.h --- a/kerfuffle/archive_kerfuffle.h +++ b/kerfuffle/archive_kerfuffle.h @@ -46,6 +46,7 @@ class DeleteJob; class AddJob; class CommentJob; +class TestJob; class Plugin; class Query; class ReadOnlyArchiveInterface; @@ -200,6 +201,7 @@ DeleteJob* deleteFiles(const QList & files); CommentJob* addComment(const QString &comment); + TestJob* testArchive(); /** * Compression options that should be handled by all interfaces: diff --git a/kerfuffle/archive_kerfuffle.cpp b/kerfuffle/archive_kerfuffle.cpp --- a/kerfuffle/archive_kerfuffle.cpp +++ b/kerfuffle/archive_kerfuffle.cpp @@ -181,6 +181,18 @@ return job; } +TestJob* Archive::testArchive() +{ + if (!isValid()) { + return Q_NULLPTR; + } + + qCDebug(ARK) << "Going to test archive"; + + TestJob *job = new TestJob(m_iface, this); + return job; +} + QMimeType Archive::mimeType() const { return isValid() ? determineMimeType(fileName()) : QMimeType(); diff --git a/kerfuffle/archiveformat.h b/kerfuffle/archiveformat.h --- a/kerfuffle/archiveformat.h +++ b/kerfuffle/archiveformat.h @@ -42,7 +42,8 @@ int minCompLevel, int maxCompLevel, int defaultCompLevel, - bool supportsWriteComment); + bool supportsWriteComment, + bool supportsTesting); /** * @return The archive format of the given @p mimeType, according to the given @p metadata. @@ -63,6 +64,7 @@ int maxCompressionLevel() const; int defaultCompressionLevel() const; bool supportsWriteComment() const; + bool supportsTesting() const; private: QMimeType m_mimeType; @@ -71,6 +73,7 @@ int m_maxCompressionLevel; int m_defaultCompressionLevel; bool m_supportsWriteComment; + bool m_supportsTesting; }; } diff --git a/kerfuffle/archiveformat.cpp b/kerfuffle/archiveformat.cpp --- a/kerfuffle/archiveformat.cpp +++ b/kerfuffle/archiveformat.cpp @@ -38,13 +38,15 @@ int minCompLevel, int maxCompLevel, int defaultCompLevel, - bool supportsWriteComment) : + bool supportsWriteComment, + bool supportsTesting) : m_mimeType(mimeType), m_encryptionType(encryptionType), m_minCompressionLevel(minCompLevel), m_maxCompressionLevel(maxCompLevel), m_defaultCompressionLevel(defaultCompLevel), - m_supportsWriteComment(supportsWriteComment) + m_supportsWriteComment(supportsWriteComment), + m_supportsTesting(supportsTesting) { } @@ -63,6 +65,7 @@ int defaultCompLevel = formatProps[QStringLiteral("CompressionLevelDefault")].toInt(); bool supportsWriteComment = formatProps[QStringLiteral("SupportsWriteComment")].toBool(); + bool supportsTesting = formatProps[QStringLiteral("SupportsTesting")].toBool(); Archive::EncryptionType encType = Archive::Unencrypted; if (formatProps[QStringLiteral("HeaderEncryption")].toBool()) { @@ -71,7 +74,7 @@ encType = Archive::Encrypted; } - return ArchiveFormat(mimeType, encType, minCompLevel, maxCompLevel, defaultCompLevel, supportsWriteComment); + return ArchiveFormat(mimeType, encType, minCompLevel, maxCompLevel, defaultCompLevel, supportsWriteComment, supportsTesting); } return ArchiveFormat(); @@ -107,4 +110,9 @@ return m_supportsWriteComment; } +bool ArchiveFormat::supportsTesting() const +{ + return m_supportsTesting; +} + } diff --git a/kerfuffle/archiveinterface.h b/kerfuffle/archiveinterface.h --- a/kerfuffle/archiveinterface.h +++ b/kerfuffle/archiveinterface.h @@ -81,6 +81,7 @@ * the user of the error condition. */ virtual bool list() = 0; + virtual bool testArchive() = 0; void setPassword(const QString &password); void setHeaderEncryptionEnabled(bool enabled); @@ -114,6 +115,7 @@ void info(const QString &info); void finished(bool result); void userQuery(Query *query); + void testSuccess(); protected: diff --git a/kerfuffle/cliinterface.h b/kerfuffle/cliinterface.h --- a/kerfuffle/cliinterface.h +++ b/kerfuffle/cliinterface.h @@ -271,7 +271,10 @@ * containing the comment. * Example (rar plugin): -z$CommentFile */ - CommentSwitch + CommentSwitch, + TestProgram, + TestArgs, + TestPassedPattern }; typedef QHash ParameterList; @@ -282,7 +285,7 @@ public: enum OperationMode { - List, Copy, Add, Delete, Comment + List, Copy, Add, Delete, Comment, Test }; OperationMode m_operationMode; @@ -294,6 +297,7 @@ virtual bool addFiles(const QStringList & 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; @@ -337,6 +341,7 @@ QStringList substituteCopyVariables(const QStringList &extractArgs, const QVariantList &files, bool preservePaths, const QString &password, const QString &rootNode); QStringList substituteAddVariables(const QStringList &addArgs, const QStringList &files, const QString &password, bool encryptHeader, int compLevel); 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. @@ -416,6 +421,7 @@ 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 diff --git a/kerfuffle/cliinterface.cpp b/kerfuffle/cliinterface.cpp --- a/kerfuffle/cliinterface.cpp +++ b/kerfuffle/cliinterface.cpp @@ -241,6 +241,22 @@ return true; } +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)) { + failOperation(); + return false; + } + + return true; +} + bool CliInterface::runProcess(const QStringList& programNames, const QStringList& arguments) { Q_ASSERT(!m_process); @@ -709,6 +725,29 @@ 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)); @@ -1026,6 +1065,24 @@ 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.")); + emit finished(true); + failOperation(); + return; + } + + if (checkForTestSuccessMessage(line)) { + qCDebug(ARK) << "Test successful"; + emit testSuccess(); + return; + } + } } bool CliInterface::checkForPasswordPromptMessage(const QString& line) @@ -1123,6 +1180,18 @@ return false; } +bool CliInterface::checkForTestSuccessMessage(const QString& line) +{ + // Check for a filename and store it. + const QRegularExpression rx(m_param.value(TestPassedPattern).toString()); + const QRegularExpressionMatch rxMatch = rx.match(line); + if (rxMatch.hasMatch()) { + qCWarning(ARK) << "Test passed:" << rxMatch.captured(0); + return true; + } + return false; +} + bool CliInterface::doKill() { if (m_process) { diff --git a/kerfuffle/jobs.h b/kerfuffle/jobs.h --- a/kerfuffle/jobs.h +++ b/kerfuffle/jobs.h @@ -187,7 +187,24 @@ QString m_comment; }; +class KERFUFFLE_EXPORT TestJob : public Job +{ + Q_OBJECT + +public: + TestJob(ReadOnlyArchiveInterface *interface, QObject *parent = 0); + bool testSucceeded(); + +public slots: + virtual void doWork() Q_DECL_OVERRIDE; + +private slots: + virtual void onTestSuccess(); +private: + bool m_testSuccess; + +}; } // namespace Kerfuffle diff --git a/kerfuffle/jobs.cpp b/kerfuffle/jobs.cpp --- a/kerfuffle/jobs.cpp +++ b/kerfuffle/jobs.cpp @@ -440,6 +440,37 @@ } } +TestJob::TestJob(ReadOnlyArchiveInterface *interface, QObject *parent) + : Job(interface, parent) +{ + 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/part/ark_part.rc b/part/ark_part.rc --- a/part/ark_part.rc +++ b/part/ark_part.rc @@ -1,5 +1,5 @@ - + &Archive @@ -7,8 +7,9 @@ - + + &File diff --git a/part/part.h b/part/part.h --- a/part/part.h +++ b/part/part.h @@ -106,6 +106,7 @@ void slotAddFiles(const QStringList& files, const QString& path = QString()); void slotAddDir(); void slotAddFilesDone(KJob*); + void slotTestingDone(KJob*); void slotDeleteFiles(); void slotDeleteFilesDone(KJob*); void slotShowProperties(); @@ -124,6 +125,7 @@ void slotShowComment(); void slotAddComment(); void slotCommentChanged(); + void slotTestArchive(); signals: void busy(); @@ -154,6 +156,7 @@ QAction *m_saveAsAction; QAction *m_propertiesAction; QAction *m_editCommentAction; + QAction *m_testArchiveAction; KToggleAction *m_showInfoPanelAction; InfoPanel *m_infoPanel; QSplitter *m_splitter; diff --git a/part/part.cpp b/part/part.cpp --- a/part/part.cpp +++ b/part/part.cpp @@ -391,6 +391,13 @@ 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, SIGNAL(mapped(int)), this, SLOT(slotOpenEntry(int))); updateActions(); @@ -441,6 +448,9 @@ 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(); @@ -448,8 +458,11 @@ supportsWriteComment); m_commentView->setReadOnly(!supportsWriteComment); m_model->archive()->comment().isEmpty() ? m_editCommentAction->setText(i18nc("@action:inmenu", "Add &Comment")) : m_editCommentAction->setText(i18nc("@action:inmenu", "Edit &Comment")); + + bool supportsTesting = ArchiveFormat::fromMetadata(m_model->archive()->mimeType(), metadata).supportsTesting(); + m_testArchiveAction->setEnabled(!isBusy() && + supportsTesting); } else { - m_editCommentAction->setEnabled(false); m_commentView->setReadOnly(true); } } @@ -477,6 +490,28 @@ } } +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) { diff --git a/plugins/cli7zplugin/cliplugin.cpp b/plugins/cli7zplugin/cliplugin.cpp --- a/plugins/cli7zplugin/cliplugin.cpp +++ b/plugins/cli7zplugin/cliplugin.cpp @@ -60,7 +60,7 @@ if (p.isEmpty()) { //p[CaptureProgress] = true; - p[ListProgram] = p[ExtractProgram] = p[DeleteProgram] = p[AddProgram] = QStringList() << QStringLiteral("7z"); + p[ListProgram] = p[ExtractProgram] = p[DeleteProgram] = p[AddProgram] = p[TestProgram] = QStringList() << QStringLiteral("7z"); p[ListArgs] = QStringList() << QStringLiteral("l") << QStringLiteral("-slt") << QStringLiteral("$PasswordSwitch") @@ -83,6 +83,9 @@ p[DeleteArgs] = QStringList() << QStringLiteral("d") << 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\\? $") diff --git a/plugins/cli7zplugin/kerfuffle_cli7z.json b/plugins/cli7zplugin/kerfuffle_cli7z.json --- a/plugins/cli7zplugin/kerfuffle_cli7z.json +++ b/plugins/cli7zplugin/kerfuffle_cli7z.json @@ -60,13 +60,15 @@ "application/x-7z-compressed": { "CompressionLevelDefault": 5, "CompressionLevelMax": 9, - "CompressionLevelMin": 0, + "CompressionLevelMin": 0, + "SupportsTesting": true, "HeaderEncryption": true }, "application/zip": { "CompressionLevelDefault": 5, "CompressionLevelMax": 9, "CompressionLevelMin": 0, + "SupportsTesting": true, "Encryption": true } -} \ No newline at end of file +} diff --git a/plugins/clirarplugin/cliplugin.cpp b/plugins/clirarplugin/cliplugin.cpp --- a/plugins/clirarplugin/cliplugin.cpp +++ b/plugins/clirarplugin/cliplugin.cpp @@ -78,7 +78,7 @@ if (p.isEmpty()) { p[CaptureProgress] = true; - p[ListProgram] = p[ExtractProgram] = QStringList() << QStringLiteral( "unrar" ); + p[ListProgram] = p[ExtractProgram] = p[TestProgram] = QStringList() << QStringLiteral( "unrar" ); p[DeleteProgram] = p[AddProgram] = QStringList() << QStringLiteral( "rar" ); p[ListArgs] = QStringList() << QStringLiteral("vt") @@ -126,6 +126,9 @@ << QStringLiteral("$CommentSwitch") << QStringLiteral("$Archive"); p[CommentSwitch] = QStringLiteral("-z$CommentFile"); + p[TestArgs] = QStringList() << QStringLiteral("t") + << QStringLiteral("$Archive"); + p[TestPassedPattern] = QStringLiteral("^All OK$"); } return p; diff --git a/plugins/clirarplugin/kerfuffle_clirar.json b/plugins/clirarplugin/kerfuffle_clirar.json --- a/plugins/clirarplugin/kerfuffle_clirar.json +++ b/plugins/clirarplugin/kerfuffle_clirar.json @@ -64,6 +64,7 @@ "CompressionLevelMax": 5, "CompressionLevelMin": 0, "SupportsWriteComment": true, + "SupportsTesting": true, "HeaderEncryption": true } } diff --git a/plugins/clizipplugin/cliplugin.cpp b/plugins/clizipplugin/cliplugin.cpp --- a/plugins/clizipplugin/cliplugin.cpp +++ b/plugins/clizipplugin/cliplugin.cpp @@ -83,7 +83,7 @@ if (p.isEmpty()) { p[CaptureProgress] = false; p[ListProgram] = QStringList() << QStringLiteral("zipinfo"); - p[ExtractProgram] = QStringList() << QStringLiteral("unzip"); + p[ExtractProgram] = p[TestProgram] = QStringList() << QStringLiteral("unzip"); p[DeleteProgram] = p[AddProgram] = QStringList() << QStringLiteral("zip"); p[ListArgs] = QStringList() << QStringLiteral("-l") @@ -122,6 +122,9 @@ 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; } diff --git a/plugins/clizipplugin/kerfuffle_clizip.json b/plugins/clizipplugin/kerfuffle_clizip.json --- a/plugins/clizipplugin/kerfuffle_clizip.json +++ b/plugins/clizipplugin/kerfuffle_clizip.json @@ -61,13 +61,15 @@ "application/x-java-archive": { "CompressionLevelDefault": 6, "CompressionLevelMax": 9, - "CompressionLevelMin": 0, + "CompressionLevelMin": 0, + "SupportsTesting": true, "Encryption": true }, "application/zip": { "CompressionLevelDefault": 6, "CompressionLevelMax": 9, - "CompressionLevelMin": 0, + "CompressionLevelMin": 0, + "SupportsTesting": true, "Encryption": true } -} \ No newline at end of file +} diff --git a/plugins/libarchive/libarchiveplugin.h b/plugins/libarchive/libarchiveplugin.h --- a/plugins/libarchive/libarchiveplugin.h +++ b/plugins/libarchive/libarchiveplugin.h @@ -50,6 +50,7 @@ virtual bool addFiles(const QStringList& 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); diff --git a/plugins/libarchive/libarchiveplugin.cpp b/plugins/libarchive/libarchiveplugin.cpp --- a/plugins/libarchive/libarchiveplugin.cpp +++ b/plugins/libarchive/libarchiveplugin.cpp @@ -135,6 +135,11 @@ return false; } +bool LibarchivePlugin::testArchive() +{ + return false; +} + bool LibarchivePlugin::doKill() { m_abortOperation = true; diff --git a/plugins/libsinglefileplugin/singlefileplugin.h b/plugins/libsinglefileplugin/singlefileplugin.h --- a/plugins/libsinglefileplugin/singlefileplugin.h +++ b/plugins/libsinglefileplugin/singlefileplugin.h @@ -37,6 +37,7 @@ 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; protected: diff --git a/plugins/libsinglefileplugin/singlefileplugin.cpp b/plugins/libsinglefileplugin/singlefileplugin.cpp --- a/plugins/libsinglefileplugin/singlefileplugin.cpp +++ b/plugins/libsinglefileplugin/singlefileplugin.cpp @@ -164,3 +164,8 @@ return uncompressedName + QStringLiteral( ".uncompressed" ); } +bool LibSingleFileInterface::testArchive() +{ + return false; +} +