CLI: Add commands to handle attachments

* Add commands to manipulate entry attachments from the CLI
* Closes #4462

* Add the following commands:
  attachment-export: Exports the content of an attachment to a specified file.

  attachment-import: Imports the attachment into an entry. An existing attachment with the same name may be overwritten if the -f option is specified.

  attachment-rm: Removes the named attachment from an entry.

* Add --show-attachments  to the show command
This commit is contained in:
Andre Blanke 2021-11-03 23:29:44 -04:00 committed by Jonathan White
parent 7811f10dba
commit 7d37f65ad0
16 changed files with 697 additions and 3 deletions

View File

@ -40,6 +40,17 @@ It provides the ability to query and modify the entries of a KeePass database, d
*analyze* [_options_] <__database__>:: *analyze* [_options_] <__database__>::
Analyzes passwords in a database for weaknesses using offline HIBP SHA-1 hash lookup. Analyzes passwords in a database for weaknesses using offline HIBP SHA-1 hash lookup.
*attachment-export* [_options_] <__database__> <__entry__> <__attachment_name__> <__export_file__>::
Exports the content of an attachment to a specified file.
Use *--stdout* option to instead output the contents of the attachment to stdout.
*attachment-import* [_options_] <__database__> <__entry__> <__attachment_name__> <__import_file__>::
Imports the attachment into an entry.
An existing attachment with the same name may be overwritten if the *-f* option is specified.
*attachment-rm* <__database__> <__entry__> <__attachment_name__>::
Removes the named attachment from an entry.
*clip* [_options_] <__database__> <__entry__> [_timeout_]:: *clip* [_options_] <__database__> <__entry__> [_timeout_]::
Copies an attribute or the current TOTP (if the *-t* option is specified) of a database entry to the clipboard. Copies an attribute or the current TOTP (if the *-t* option is specified) of a database entry to the clipboard.
If no attribute name is specified using the *-a* option, the password is copied. If no attribute name is specified using the *-a* option, the password is copied.
@ -244,6 +255,9 @@ The same password generation options as documented for the generate command can
*-s*, *--show-protected*:: *-s*, *--show-protected*::
Shows the protected attributes in clear text. Shows the protected attributes in clear text.
*--show-attachments*::
Shows the attachment names along with the size of the attachments.
*-t*, *--totp*:: *-t*, *--totp*::
Also shows the current TOTP, reporting an error if no TOTP is configured for the entry. Also shows the current TOTP, reporting an error if no TOTP is configured for the entry.

View File

@ -7386,6 +7386,94 @@ Please consider generating a new key file.</source>
<source>%1 (invalid executable path)</source> <source>%1 (invalid executable path)</source>
<translation type="unfinished"></translation> <translation type="unfinished"></translation>
</message> </message>
<message>
<source>Export an attachment of an entry.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Path of the entry with the target attachment.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Name of the attachment to be exported.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Path to which the attachment should be exported.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not find attachment with name %1.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No export target given. Please use &apos;--stdout&apos; or specify an &apos;export-file&apos;.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not open output file %1.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Successfully exported attachment %1 of entry %2 to %3.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Overwrite existing attachments.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Imports an attachment to an entry.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Path of the entry.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Name of the attachment to be added.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Path of the attachment to be imported.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Attachment %1 already exists for entry %2.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Could not open attachment file %1.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Successfully imported attachment %1 as %2 to entry %3.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Remove an attachment of an entry.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Name of the attachment to be removed.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Successfully removed attachment %1 from entry %2.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Show the attachments of the entry.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>No attachments present.</source>
<translation type="unfinished"></translation>
</message>
<message>
<source>Attachments:</source>
<translation type="unfinished"></translation>
</message>
</context> </context>
<context> <context>
<name>QtIOCompressor</name> <name>QtIOCompressor</name>

View File

@ -0,0 +1,88 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* version 3 of the License.
*
* 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, see <http://www.gnu.org/licenses/>.
*/
#include "AttachmentExport.h"
#include "Utils.h"
#include "core/Group.h"
#include <QCommandLineParser>
#include <QFile>
const QCommandLineOption AttachmentExport::StdoutOption =
QCommandLineOption(QStringList() << "stdout", QObject::tr(""));
AttachmentExport::AttachmentExport()
{
name = QString("attachment-export");
description = QObject::tr("Export an attachment of an entry.");
options.append(AttachmentExport::StdoutOption);
positionalArguments.append(
{QString("entry"), QObject::tr("Path of the entry with the target attachment."), QString("")});
positionalArguments.append(
{QString("attachment-name"), QObject::tr("Name of the attachment to be exported."), QString("")});
optionalArguments.append(
{QString("export-file"), QObject::tr("Path to which the attachment should be exported."), QString("")});
}
int AttachmentExport::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser> parser)
{
auto& out = parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT;
auto& err = Utils::STDERR;
auto args = parser->positionalArguments();
auto entryPath = args.at(1);
auto entry = database->rootGroup()->findEntryByPath(entryPath);
if (!entry) {
err << QObject::tr("Could not find entry with path %1.").arg(entryPath) << endl;
return EXIT_FAILURE;
}
auto attachmentName = args.at(2);
auto attachments = entry->attachments();
if (!attachments->hasKey(attachmentName)) {
err << QObject::tr("Could not find attachment with name %1.").arg(attachmentName) << endl;
return EXIT_FAILURE;
}
if (parser->isSet(AttachmentExport::StdoutOption)) {
// Output to STDOUT even in quiet mode
Utils::STDOUT << attachments->value(attachmentName) << flush;
return EXIT_SUCCESS;
}
if (args.size() < 4) {
err << QObject::tr("No export target given. Please use '--stdout' or specify an 'export-file'.") << endl;
return EXIT_FAILURE;
}
auto exportFileName = args.at(3);
QFile exportFile(exportFileName);
if (!exportFile.open(QIODevice::WriteOnly)) {
err << QObject::tr("Could not open output file %1.").arg(exportFileName) << endl;
return EXIT_FAILURE;
}
exportFile.write(attachments->value(attachmentName));
out << QObject::tr("Successfully exported attachment %1 of entry %2 to %3.")
.arg(attachmentName, entryPath, exportFileName)
<< endl;
return EXIT_SUCCESS;
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* version 3 of the License.
*
* 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, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_ATTACHMENTEXPORT_H
#define KEEPASSXC_ATTACHMENTEXPORT_H
#include "DatabaseCommand.h"
class AttachmentExport : public DatabaseCommand
{
public:
AttachmentExport();
int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser) override;
static const QCommandLineOption StdoutOption;
};
#endif // KEEPASSXC_ATTACHMENTEXPORT_H

View File

@ -0,0 +1,87 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* version 3 of the License.
*
* 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, see <http://www.gnu.org/licenses/>.
*/
#include "AttachmentImport.h"
#include "Utils.h"
#include "core/Group.h"
#include <QCommandLineParser>
#include <QFile>
const QCommandLineOption AttachmentImport::ForceOption =
QCommandLineOption(QStringList() << "f"
<< "force",
QObject::tr("Overwrite existing attachments."));
AttachmentImport::AttachmentImport()
{
name = QString("attachment-import");
description = QObject::tr("Imports an attachment to an entry.");
options.append(AttachmentImport::ForceOption);
positionalArguments.append({QString("entry"), QObject::tr("Path of the entry."), QString("")});
positionalArguments.append(
{QString("attachment-name"), QObject::tr("Name of the attachment to be added."), QString("")});
positionalArguments.append(
{QString("import-file"), QObject::tr("Path of the attachment to be imported."), QString("")});
}
int AttachmentImport::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser> parser)
{
auto& out = parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT;
auto& err = Utils::STDERR;
auto args = parser->positionalArguments();
auto entryPath = args.at(1);
auto entry = database->rootGroup()->findEntryByPath(entryPath);
if (!entry) {
err << QObject::tr("Could not find entry with path %1.").arg(entryPath) << endl;
return EXIT_FAILURE;
}
auto attachmentName = args.at(2);
auto attachments = entry->attachments();
if (attachments->hasKey(attachmentName) && !parser->isSet(AttachmentImport::ForceOption)) {
err << QObject::tr("Attachment %1 already exists for entry %2.").arg(attachmentName, entryPath) << endl;
return EXIT_FAILURE;
}
auto importFileName = args.at(3);
QFile importFile(importFileName);
if (!importFile.open(QIODevice::ReadOnly)) {
err << QObject::tr("Could not open attachment file %1.").arg(importFileName) << endl;
return EXIT_FAILURE;
}
entry->beginUpdate();
attachments->set(attachmentName, importFile.readAll());
entry->endUpdate();
QString errorMessage;
if (!database->save(Database::Atomic, false, &errorMessage)) {
err << QObject::tr("Writing the database failed %1.").arg(errorMessage) << endl;
return EXIT_FAILURE;
}
out << QObject::tr("Successfully imported attachment %1 as %2 to entry %3.")
.arg(importFileName, attachmentName, entryPath)
<< endl;
return EXIT_SUCCESS;
}

View File

@ -0,0 +1,33 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* version 3 of the License.
*
* 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, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_ATTACHMENTIMPORT_H
#define KEEPASSXC_ATTACHMENTIMPORT_H
#include "DatabaseCommand.h"
class AttachmentImport : public DatabaseCommand
{
public:
AttachmentImport();
int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser) override;
static const QCommandLineOption ForceOption;
};
#endif // KEEPASSXC_ATTACHMENTIMPORT_H

View File

@ -0,0 +1,68 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* version 3 of the License.
*
* 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, see <http://www.gnu.org/licenses/>.
*/
#include "AttachmentRemove.h"
#include "Utils.h"
#include "core/Group.h"
#include <QCommandLineParser>
AttachmentRemove::AttachmentRemove()
{
name = QString("attachment-rm");
description = QObject::tr("Remove an attachment of an entry.");
positionalArguments.append(
{QString("entry"), QObject::tr("Path of the entry with the target attachment."), QString("")});
positionalArguments.append({QString("name"), QObject::tr("Name of the attachment to be removed."), QString("")});
}
int AttachmentRemove::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser> parser)
{
auto& out = parser->isSet(Command::QuietOption) ? Utils::DEVNULL : Utils::STDOUT;
auto& err = Utils::STDERR;
auto args = parser->positionalArguments();
auto entryPath = args.at(1);
auto entry = database->rootGroup()->findEntryByPath(entryPath);
if (!entry) {
err << QObject::tr("Could not find entry with path %1.").arg(entryPath) << endl;
return EXIT_FAILURE;
}
auto attachmentName = args.at(2);
auto attachments = entry->attachments();
if (!attachments->hasKey(attachmentName)) {
err << QObject::tr("Could not find attachment with name %1.").arg(attachmentName) << endl;
return EXIT_FAILURE;
}
entry->beginUpdate();
attachments->remove(attachmentName);
entry->endUpdate();
QString errorMessage;
if (!database->save(Database::Atomic, false, &errorMessage)) {
err << QObject::tr("Writing the database failed %1.").arg(errorMessage) << endl;
return EXIT_FAILURE;
}
out << QObject::tr("Successfully removed attachment %1 from entry %2.").arg(attachmentName, entryPath) << endl;
return EXIT_SUCCESS;
}

View File

@ -0,0 +1,31 @@
/*
* Copyright (C) 2019 KeePassXC Team <team@keepassxc.org>
*
* 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 or (at your option)
* version 3 of the License.
*
* 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, see <http://www.gnu.org/licenses/>.
*/
#ifndef KEEPASSXC_ATTACHMENTREMOVE_H
#define KEEPASSXC_ATTACHMENTREMOVE_H
#include "DatabaseCommand.h"
class AttachmentRemove : public DatabaseCommand
{
public:
AttachmentRemove();
int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser) override;
};
#endif // KEEPASSXC_ATTACHMENTMOVE_H

View File

@ -17,6 +17,9 @@ set(cli_SOURCES
Add.cpp Add.cpp
AddGroup.cpp AddGroup.cpp
Analyze.cpp Analyze.cpp
AttachmentExport.cpp
AttachmentImport.cpp
AttachmentRemove.cpp
Clip.cpp Clip.cpp
Close.cpp Close.cpp
Create.cpp Create.cpp

View File

@ -18,6 +18,9 @@
#include "Add.h" #include "Add.h"
#include "AddGroup.h" #include "AddGroup.h"
#include "Analyze.h" #include "Analyze.h"
#include "AttachmentExport.h"
#include "AttachmentImport.h"
#include "AttachmentRemove.h"
#include "Clip.h" #include "Clip.h"
#include "Close.h" #include "Close.h"
#include "Create.h" #include "Create.h"
@ -107,7 +110,7 @@ QString Command::getDescriptionLine()
{ {
QString response = name; QString response = name;
QString space(" "); QString space(" ");
QString spaces = space.repeated(15 - name.length()); QString spaces = space.repeated(20 - name.length());
response = response.append(spaces); response = response.append(spaces);
response = response.append(description); response = response.append(description);
response = response.append("\n"); response = response.append("\n");
@ -164,6 +167,9 @@ namespace Commands
s_commands.insert(QStringLiteral("add"), QSharedPointer<Command>(new Add())); s_commands.insert(QStringLiteral("add"), QSharedPointer<Command>(new Add()));
s_commands.insert(QStringLiteral("analyze"), QSharedPointer<Command>(new Analyze())); s_commands.insert(QStringLiteral("analyze"), QSharedPointer<Command>(new Analyze()));
s_commands.insert(QStringLiteral("attachment-export"), QSharedPointer<Command>(new AttachmentExport()));
s_commands.insert(QStringLiteral("attachment-import"), QSharedPointer<Command>(new AttachmentImport()));
s_commands.insert(QStringLiteral("attachment-rm"), QSharedPointer<Command>(new AttachmentRemove()));
s_commands.insert(QStringLiteral("clip"), QSharedPointer<Command>(new Clip())); s_commands.insert(QStringLiteral("clip"), QSharedPointer<Command>(new Clip()));
s_commands.insert(QStringLiteral("close"), QSharedPointer<Command>(new Close())); s_commands.insert(QStringLiteral("close"), QSharedPointer<Command>(new Close()));
s_commands.insert(QStringLiteral("db-create"), QSharedPointer<Command>(new Create())); s_commands.insert(QStringLiteral("db-create"), QSharedPointer<Command>(new Create()));

View File

@ -19,6 +19,7 @@
#include "Utils.h" #include "Utils.h"
#include "core/Group.h" #include "core/Group.h"
#include "core/Tools.h"
#include <QCommandLineParser> #include <QCommandLineParser>
@ -31,6 +32,9 @@ const QCommandLineOption Show::ProtectedAttributesOption =
<< "show-protected", << "show-protected",
QObject::tr("Show the protected attributes in clear text.")); QObject::tr("Show the protected attributes in clear text."));
const QCommandLineOption Show::AttachmentsOption =
QCommandLineOption(QStringList() << "show-attachments", QObject::tr("Show the attachments of the entry."));
const QCommandLineOption Show::AttributesOption = QCommandLineOption( const QCommandLineOption Show::AttributesOption = QCommandLineOption(
QStringList() << "a" QStringList() << "a"
<< "attributes", << "attributes",
@ -47,6 +51,7 @@ Show::Show()
options.append(Show::TotpOption); options.append(Show::TotpOption);
options.append(Show::AttributesOption); options.append(Show::AttributesOption);
options.append(Show::ProtectedAttributesOption); options.append(Show::ProtectedAttributesOption);
options.append(Show::AttachmentsOption);
positionalArguments.append({QString("entry"), QObject::tr("Name of the entry to show."), QString("")}); positionalArguments.append({QString("entry"), QObject::tr("Name of the entry to show."), QString("")});
} }
@ -104,6 +109,25 @@ int Show::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<
} }
} }
if (parser->isSet(Show::AttachmentsOption)) {
// Separate attachment output from attributes output via a newline.
out << endl;
EntryAttachments* attachments = entry->attachments();
if (attachments->isEmpty()) {
out << QObject::tr("No attachments present.") << endl;
} else {
out << QObject::tr("Attachments:") << endl;
// Iterate over the attachments and output their names and size line-by-line, indented.
for (const QString& attachmentName : attachments->keys()) {
// TODO: use QLocale::formattedDataSize when >= Qt 5.10
QString attachmentSize = Tools::humanReadableFileSize(attachments->value(attachmentName).size(), 1);
out << " " << attachmentName << " (" << attachmentSize << ")" << endl;
}
}
}
if (showTotp) { if (showTotp) {
out << entry->totp() << endl; out << entry->totp() << endl;
} }

View File

@ -30,6 +30,7 @@ public:
static const QCommandLineOption TotpOption; static const QCommandLineOption TotpOption;
static const QCommandLineOption AttributesOption; static const QCommandLineOption AttributesOption;
static const QCommandLineOption ProtectedAttributesOption; static const QCommandLineOption ProtectedAttributesOption;
static const QCommandLineOption AttachmentsOption;
}; };
#endif // KEEPASSXC_SHOW_H #endif // KEEPASSXC_SHOW_H

View File

@ -30,6 +30,9 @@
#include "cli/Add.h" #include "cli/Add.h"
#include "cli/AddGroup.h" #include "cli/AddGroup.h"
#include "cli/Analyze.h" #include "cli/Analyze.h"
#include "cli/AttachmentExport.h"
#include "cli/AttachmentImport.h"
#include "cli/AttachmentRemove.h"
#include "cli/Clip.h" #include "cli/Clip.h"
#include "cli/Create.h" #include "cli/Create.h"
#include "cli/Diceware.h" #include "cli/Diceware.h"
@ -215,6 +218,9 @@ void TestCli::testBatchCommands()
Commands::setupCommands(false); Commands::setupCommands(false);
QVERIFY(Commands::getCommand("add")); QVERIFY(Commands::getCommand("add"));
QVERIFY(Commands::getCommand("analyze")); QVERIFY(Commands::getCommand("analyze"));
QVERIFY(Commands::getCommand("attachment-export"));
QVERIFY(Commands::getCommand("attachment-import"));
QVERIFY(Commands::getCommand("attachment-rm"));
QVERIFY(Commands::getCommand("clip")); QVERIFY(Commands::getCommand("clip"));
QVERIFY(Commands::getCommand("close")); QVERIFY(Commands::getCommand("close"));
QVERIFY(Commands::getCommand("db-create")); QVERIFY(Commands::getCommand("db-create"));
@ -236,7 +242,7 @@ void TestCli::testBatchCommands()
QVERIFY(Commands::getCommand("show")); QVERIFY(Commands::getCommand("show"));
QVERIFY(Commands::getCommand("search")); QVERIFY(Commands::getCommand("search"));
QVERIFY(!Commands::getCommand("doesnotexist")); QVERIFY(!Commands::getCommand("doesnotexist"));
QCOMPARE(Commands::getCommands().size(), 22); QCOMPARE(Commands::getCommands().size(), 25);
} }
void TestCli::testInteractiveCommands() void TestCli::testInteractiveCommands()
@ -244,6 +250,9 @@ void TestCli::testInteractiveCommands()
Commands::setupCommands(true); Commands::setupCommands(true);
QVERIFY(Commands::getCommand("add")); QVERIFY(Commands::getCommand("add"));
QVERIFY(Commands::getCommand("analyze")); QVERIFY(Commands::getCommand("analyze"));
QVERIFY(Commands::getCommand("attachment-export"));
QVERIFY(Commands::getCommand("attachment-import"));
QVERIFY(Commands::getCommand("attachment-rm"));
QVERIFY(Commands::getCommand("clip")); QVERIFY(Commands::getCommand("clip"));
QVERIFY(Commands::getCommand("close")); QVERIFY(Commands::getCommand("close"));
QVERIFY(Commands::getCommand("db-create")); QVERIFY(Commands::getCommand("db-create"));
@ -265,7 +274,7 @@ void TestCli::testInteractiveCommands()
QVERIFY(Commands::getCommand("show")); QVERIFY(Commands::getCommand("show"));
QVERIFY(Commands::getCommand("search")); QVERIFY(Commands::getCommand("search"));
QVERIFY(!Commands::getCommand("doesnotexist")); QVERIFY(!Commands::getCommand("doesnotexist"));
QCOMPARE(Commands::getCommands().size(), 22); QCOMPARE(Commands::getCommands().size(), 25);
} }
void TestCli::testAdd() void TestCli::testAdd()
@ -435,6 +444,184 @@ void TestCli::testAnalyze()
QCOMPARE(m_stderr->readAll(), QByteArray()); QCOMPARE(m_stderr->readAll(), QByteArray());
} }
void TestCli::testAttachmentExport()
{
AttachmentExport attachmentExportCmd;
QVERIFY(!attachmentExportCmd.name.isEmpty());
QVERIFY(attachmentExportCmd.getDescriptionLine().contains(attachmentExportCmd.name));
TemporaryFile exportOutput;
exportOutput.open(QIODevice::WriteOnly);
exportOutput.close();
// Try exporting an attachment of a non-existent entry
setInput("a");
execCmd(attachmentExportCmd,
{"attachment-export",
m_dbFile->fileName(),
"invalid_entry_path",
"invalid_attachment_name",
exportOutput.fileName()});
m_stderr->readLine(); // skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray("Could not find entry with path invalid_entry_path.\n"));
QCOMPARE(m_stdout->readAll(), QByteArray());
// Try exporting a non-existent attachment
setInput("a");
execCmd(attachmentExportCmd,
{"attachment-export",
m_dbFile->fileName(),
"/Sample Entry",
"invalid_attachment_name",
exportOutput.fileName()});
m_stderr->readLine(); // skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray("Could not find attachment with name invalid_attachment_name.\n"));
QCOMPARE(m_stdout->readAll(), QByteArray());
// Export an existing attachment to a file
setInput("a");
execCmd(
attachmentExportCmd,
{"attachment-export", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt", exportOutput.fileName()});
m_stderr->readLine(); // skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray());
QCOMPARE(m_stdout->readAll(),
QByteArray(qPrintable(QString("Successfully exported attachment %1 of entry %2 to %3.\n")
.arg("Sample attachment.txt", "/Sample Entry", exportOutput.fileName()))));
exportOutput.open(QIODevice::ReadOnly);
QCOMPARE(exportOutput.readAll(), QByteArray("Sample content\n"));
// Export an existing attachment to stdout
setInput("a");
execCmd(attachmentExportCmd,
{"attachment-export", "--stdout", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt"});
m_stderr->readLine(); // skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray());
QCOMPARE(m_stdout->readAll(), QByteArray("Sample content\n"));
// Ensure --stdout works even in quiet mode
setInput("a");
execCmd(
attachmentExportCmd,
{"attachment-export", "--quiet", "--stdout", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt"});
m_stderr->readLine(); // skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray());
QCOMPARE(m_stdout->readAll(), QByteArray("Sample content\n"));
}
void TestCli::testAttachmentImport()
{
AttachmentImport attachmentImportCmd;
QVERIFY(!attachmentImportCmd.name.isEmpty());
QVERIFY(attachmentImportCmd.getDescriptionLine().contains(attachmentImportCmd.name));
const QString attachmentPath = QString(KEEPASSX_TEST_DATA_DIR).append("/Attachment.txt");
QVERIFY(QFile::exists(attachmentPath));
// Try importing an attachment to a non-existent entry
setInput("a");
execCmd(attachmentImportCmd,
{"attachment-import",
m_dbFile->fileName(),
"invalid_entry_path",
"invalid_attachment_name",
"invalid_attachment_path"});
m_stderr->readLine(); // skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray("Could not find entry with path invalid_entry_path.\n"));
QCOMPARE(m_stdout->readAll(), QByteArray());
// Try importing an attachment with an occupied name without -f option
setInput("a");
execCmd(attachmentImportCmd,
{"attachment-import",
m_dbFile->fileName(),
"/Sample Entry",
"Sample attachment.txt",
"invalid_attachment_path"});
m_stderr->readLine(); // skip password prompt
QCOMPARE(m_stderr->readAll(),
QByteArray("Attachment Sample attachment.txt already exists for entry /Sample Entry.\n"));
QCOMPARE(m_stdout->readAll(), QByteArray());
// Try importing a non-existent attachment
setInput("a");
execCmd(attachmentImportCmd,
{"attachment-import",
m_dbFile->fileName(),
"/Sample Entry",
"Sample attachment 2.txt",
"invalid_attachment_path"});
m_stderr->readLine(); // skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray("Could not open attachment file invalid_attachment_path.\n"));
QCOMPARE(m_stdout->readAll(), QByteArray());
// Try importing an attachment with an occupied name with -f option
setInput("a");
execCmd(
attachmentImportCmd,
{"attachment-import", "-f", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt", attachmentPath});
m_stderr->readLine(); // skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray());
QCOMPARE(m_stdout->readAll(),
QByteArray(qPrintable(
QString("Successfully imported attachment %1 as Sample attachment.txt to entry /Sample Entry.\n")
.arg(attachmentPath))));
// Try importing an attachment with an unoccupied name
setInput("a");
execCmd(attachmentImportCmd,
{"attachment-import", m_dbFile->fileName(), "/Sample Entry", "Attachment.txt", attachmentPath});
m_stderr->readLine(); // skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray());
QCOMPARE(
m_stdout->readAll(),
QByteArray(qPrintable(QString("Successfully imported attachment %1 as Attachment.txt to entry /Sample Entry.\n")
.arg(attachmentPath))));
}
void TestCli::testAttachmentRemove()
{
AttachmentRemove attachmentRemoveCmd;
QVERIFY(!attachmentRemoveCmd.name.isEmpty());
QVERIFY(attachmentRemoveCmd.getDescriptionLine().contains(attachmentRemoveCmd.name));
// Try deleting an attachment belonging to an non-existent entry
setInput("a");
execCmd(attachmentRemoveCmd,
{"attachment-rm", m_dbFile->fileName(), "invalid_entry_path", "invalid_attachment_name"});
m_stderr->readLine(); // skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray("Could not find entry with path invalid_entry_path.\n"));
QCOMPARE(m_stdout->readAll(), QByteArray());
// Try deleting a non-existent attachment from an entry
setInput("a");
execCmd(attachmentRemoveCmd, {"attachment-rm", m_dbFile->fileName(), "/Sample Entry", "invalid_attachment_name"});
m_stderr->readLine(); // skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray("Could not find attachment with name invalid_attachment_name.\n"));
QCOMPARE(m_stdout->readAll(), QByteArray());
// Finally delete an existing attachment from an existing entry
auto db = readDatabase();
QVERIFY(db);
const Entry* entry = db->rootGroup()->findEntryByPath("/Sample Entry");
QVERIFY(entry);
QVERIFY(entry->attachments()->hasKey("Sample attachment.txt"));
setInput("a");
execCmd(attachmentRemoveCmd, {"attachment-rm", m_dbFile->fileName(), "/Sample Entry", "Sample attachment.txt"});
m_stderr->readLine(); // skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray());
QCOMPARE(m_stdout->readAll(),
QByteArray("Successfully removed attachment Sample attachment.txt from entry /Sample Entry.\n"));
db = readDatabase();
QVERIFY(db);
QVERIFY(!db->rootGroup()->findEntryByPath("/Sample Entry")->attachments()->hasKey("Sample attachment.txt"));
}
void TestCli::testClip() void TestCli::testClip()
{ {
if (QProcessEnvironment::systemEnvironment().contains("WAYLAND_DISPLAY")) { if (QProcessEnvironment::systemEnvironment().contains("WAYLAND_DISPLAY")) {
@ -1751,6 +1938,33 @@ void TestCli::testShow()
"URL: http://www.somesite.com/\n" "URL: http://www.somesite.com/\n"
"Notes: Notes\n")); "Notes: Notes\n"));
setInput("a");
execCmd(showCmd, {"show", m_dbFile->fileName(), "--show-attachments", "/Sample Entry"});
m_stderr->readLine(); // Skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray());
QCOMPARE(m_stdout->readAll(),
QByteArray("Title: Sample Entry\n"
"UserName: User Name\n"
"Password: PROTECTED\n"
"URL: http://www.somesite.com/\n"
"Notes: Notes\n"
"\n"
"Attachments:\n"
" Sample attachment.txt (15.0 B)\n"));
setInput("a");
execCmd(showCmd, {"show", m_dbFile->fileName(), "--show-attachments", "/Homebanking/Subgroup/Subgroup Entry"});
m_stderr->readLine(); // Skip password prompt
QCOMPARE(m_stderr->readAll(), QByteArray());
QCOMPARE(m_stdout->readAll(),
QByteArray("Title: Subgroup Entry\n"
"UserName: Bank User Name\n"
"Password: PROTECTED\n"
"URL: https://www.bank.com\n"
"Notes: Important note\n"
"\n"
"No attachments present.\n"));
setInput("a"); setInput("a");
execCmd(showCmd, {"show", "-a", "Title", m_dbFile->fileName(), "/Sample Entry"}); execCmd(showCmd, {"show", "-a", "Title", m_dbFile->fileName(), "/Sample Entry"});
QCOMPARE(m_stdout->readAll(), QByteArray("Sample Entry\n")); QCOMPARE(m_stdout->readAll(), QByteArray("Sample Entry\n"));

View File

@ -47,6 +47,9 @@ private slots:
void testAdd(); void testAdd();
void testAddGroup(); void testAddGroup();
void testAnalyze(); void testAnalyze();
void testAttachmentExport();
void testAttachmentImport();
void testAttachmentRemove();
void testClip(); void testClip();
void testCommandParsing_data(); void testCommandParsing_data();
void testCommandParsing(); void testCommandParsing();

View File

@ -0,0 +1 @@
Content

Binary file not shown.