Commit 13d70a48 authored by Ragnar Thomsen's avatar Ragnar Thomsen
Browse files

Add basic test functionality

Add a TestJob and associated action in Part. Test functionality is
enabled in plugin json files, currently for clizip, clirar and cli7z.
Test results are displayed simply as success/failure in a KMessageBox.

Testing password-protected archives will currently result in an
errorbox, due to these archives potentially having different passwords
for each file and hence multiple password prompts might pop up.

Libarchive does not support testing archives.

FEATURE: 140492
FIXED-IN: 16.08.0
Differential Revision: D1516
parent 8f652a39
......@@ -108,3 +108,8 @@ bool JSONArchiveInterface::addComment(const QString& comment)
Q_UNUSED(comment)
return true;
}
bool JSONArchiveInterface::testArchive()
{
return true;
}
......@@ -59,6 +59,7 @@ public:
virtual bool copyFiles(const QList<QVariant>& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) Q_DECL_OVERRIDE;
virtual bool deleteFiles(const QList<QVariant>& files) Q_DECL_OVERRIDE;
virtual bool addComment(const QString& comment) Q_DECL_OVERRIDE;
virtual bool testArchive() Q_DECL_OVERRIDE;
private:
JSONParser::JSONArchive m_archive;
......
......@@ -181,6 +181,18 @@ CommentJob* Archive::addComment(const QString &comment)
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();
......
......@@ -46,6 +46,7 @@ class ExtractJob;
class DeleteJob;
class AddJob;
class CommentJob;
class TestJob;
class Plugin;
class Query;
class ReadOnlyArchiveInterface;
......@@ -200,6 +201,7 @@ public:
DeleteJob* deleteFiles(const QList<QVariant> & files);
CommentJob* addComment(const QString &comment);
TestJob* testArchive();
/**
* Compression options that should be handled by all interfaces:
......
......@@ -38,13 +38,15 @@ ArchiveFormat::ArchiveFormat(const QMimeType& mimeType,
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 @@ ArchiveFormat ArchiveFormat::fromMetadata(const QMimeType& mimeType, const KPlug
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 @@ ArchiveFormat ArchiveFormat::fromMetadata(const QMimeType& mimeType, const KPlug
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 @@ bool ArchiveFormat::supportsWriteComment() const
return m_supportsWriteComment;
}
bool ArchiveFormat::supportsTesting() const
{
return m_supportsTesting;
}
}
......@@ -42,7 +42,8 @@ public:
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 @@ public:
int maxCompressionLevel() const;
int defaultCompressionLevel() const;
bool supportsWriteComment() const;
bool supportsTesting() const;
private:
QMimeType m_mimeType;
......@@ -71,6 +73,7 @@ private:
int m_maxCompressionLevel;
int m_defaultCompressionLevel;
bool m_supportsWriteComment;
bool m_supportsTesting;
};
}
......
......@@ -81,6 +81,7 @@ public:
* the user of the error condition.
*/
virtual bool list() = 0;
virtual bool testArchive() = 0;
void setPassword(const QString &password);
void setHeaderEncryptionEnabled(bool enabled);
......@@ -112,6 +113,7 @@ signals:
void info(const QString &info);
void finished(bool result);
void userQuery(Query *query);
void testSuccess();
protected:
......
......@@ -236,6 +236,22 @@ bool CliInterface::deleteFiles(const QList<QVariant> & files)
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);
......@@ -704,6 +720,29 @@ QStringList CliInterface::substituteCommentVariables(const QStringList &commentA
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));
......@@ -1021,6 +1060,24 @@ void CliInterface::handleLine(const QString& line)
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)
......@@ -1118,6 +1175,16 @@ bool CliInterface::checkForErrorMessage(const QString& line, int parameterIndex)
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) {
......
......@@ -271,7 +271,10 @@ enum CliInterfaceParameters {
* containing the comment.
* Example (rar plugin): -z$CommentFile
*/
CommentSwitch
CommentSwitch,
TestProgram,
TestArgs,
TestPassedPattern
};
typedef QHash<int, QVariant> ParameterList;
......@@ -282,7 +285,7 @@ class KERFUFFLE_EXPORT CliInterface : public ReadWriteArchiveInterface
public:
enum OperationMode {
List, Copy, Add, Delete, Comment
List, Copy, Add, Delete, Comment, Test
};
OperationMode m_operationMode;
......@@ -294,6 +297,7 @@ public:
virtual bool addFiles(const QStringList & files, const CompressionOptions& options) Q_DECL_OVERRIDE;
virtual bool deleteFiles(const QList<QVariant> & 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;
......@@ -319,6 +323,7 @@ public:
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.
......@@ -398,6 +403,7 @@ private:
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
......
......@@ -440,6 +440,37 @@ void CommentJob::doWork()
}
}
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
......@@ -187,7 +187,24 @@ private:
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
......
<!DOCTYPE kpartgui>
<kpartgui name="ark_part" version="13">
<kpartgui name="ark_part" version="14">
<MenuBar>
<Menu name="archive">
<text>&amp;Archive</text>
......@@ -7,8 +7,9 @@
<Action name="add" group="archive_edit"/>
<Action name="add-dir" group="archive_edit"/>
<Action name="edit_comment" group="archive_edit"/>
<Action name="extract_all" group="archive_extract"/>
<Action name="extract_all" group="archive_extract"/>
<Action name="properties" group="archive_props"/>
<Action name="test_archive" group="archive_props"/>
</Menu>
<Menu name="ark_file">
<text>&amp;File</text>
......
......@@ -391,6 +391,13 @@ void Part::setupActions()
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 @@ void Part::updateActions()
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 @@ void Part::updateActions()
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::slotAddComment()
}
}
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<TestJob*>(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) {
......
......@@ -106,6 +106,7 @@ private slots:
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 @@ private slots:
void slotShowComment();
void slotAddComment();
void slotCommentChanged();
void slotTestArchive();
signals:
void busy();
......@@ -154,6 +156,7 @@ private:
QAction *m_saveAsAction;
QAction *m_propertiesAction;
QAction *m_editCommentAction;
QAction *m_testArchiveAction;
KToggleAction *m_showInfoPanelAction;
InfoPanel *m_infoPanel;
QSplitter *m_splitter;
......
......@@ -60,7 +60,7 @@ ParameterList CliPlugin::parameterList() const
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 @@ ParameterList CliPlugin::parameterList() const
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\\? $")
......
......@@ -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
}
......@@ -78,7 +78,7 @@ ParameterList CliPlugin::parameterList() const
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 @@ ParameterList CliPlugin::parameterList() const
<< QStringLiteral("$CommentSwitch")
<< QStringLiteral("$Archive");
p[CommentSwitch] = QStringLiteral("-z$CommentFile");
p[TestArgs] = QStringList() << QStringLiteral("t")
<< QStringLiteral("$Archive");
p[TestPassedPattern] = QStringLiteral("^All OK$");
}
return p;
......
......@@ -64,6 +64,7 @@
"CompressionLevelMax": 5,
"CompressionLevelMin": 0,
"SupportsWriteComment": true,
"SupportsTesting": true,
"HeaderEncryption": true
}
}
......@@ -83,7 +83,7 @@ ParameterList CliPlugin::parameterList() const
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 @@ ParameterList CliPlugin::parameterList() const
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;
}
......
......@@ -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
}
......@@ -135,6 +135,11 @@ bool LibarchivePlugin::addComment(const QString& comment)
return false;
}
bool LibarchivePlugin::testArchive()
{
return false;
}
bool LibarchivePlugin::doKill()
{
m_abortOperation = true;
......
......@@ -50,6 +50,7 @@ public:
virtual bool addFiles(const QStringList& files, const CompressionOptions& options) Q_DECL_OVERRIDE;
virtual bool deleteFiles(const QList<QVariant>& 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);
......
......@@ -164,3 +164,8 @@ const QString LibSingleFileInterface::uncompressedFileName() const
return uncompressedName + QStringLiteral( ".uncompressed" );
}
bool LibSingleFileInterface::testArchive()
{
return false;
}
......@@ -37,6 +37,7 @@ public:
virtual ~LibSingleFileInterface();
virtual bool list() Q_DECL_OVERRIDE;
virtual bool testArchive() Q_DECL_OVERRIDE;
virtual bool copyFiles(const QList<QVariant>& files, const QString& destinationDirectory, const Kerfuffle::ExtractionOptions& options) Q_DECL_OVERRIDE;
protected:
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment