From 7d37f65ad00c49f2f3542ae9bcfda00b73a13c49 Mon Sep 17 00:00:00 2001 From: Andre Blanke Date: Wed, 3 Nov 2021 23:29:44 -0400 Subject: [PATCH] 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 --- docs/man/keepassxc-cli.1.adoc | 14 ++ share/translations/keepassxc_en.ts | 88 ++++++++++++ src/cli/AttachmentExport.cpp | 88 ++++++++++++ src/cli/AttachmentExport.h | 33 +++++ src/cli/AttachmentImport.cpp | 87 ++++++++++++ src/cli/AttachmentImport.h | 33 +++++ src/cli/AttachmentRemove.cpp | 68 +++++++++ src/cli/AttachmentRemove.h | 31 ++++ src/cli/CMakeLists.txt | 3 + src/cli/Command.cpp | 8 +- src/cli/Show.cpp | 24 ++++ src/cli/Show.h | 1 + tests/TestCli.cpp | 218 ++++++++++++++++++++++++++++- tests/TestCli.h | 3 + tests/data/Attachment.txt | 1 + tests/data/NewDatabase.kdbx | Bin 20590 -> 24654 bytes 16 files changed, 697 insertions(+), 3 deletions(-) create mode 100644 src/cli/AttachmentExport.cpp create mode 100644 src/cli/AttachmentExport.h create mode 100644 src/cli/AttachmentImport.cpp create mode 100644 src/cli/AttachmentImport.h create mode 100644 src/cli/AttachmentRemove.cpp create mode 100644 src/cli/AttachmentRemove.h create mode 100644 tests/data/Attachment.txt diff --git a/docs/man/keepassxc-cli.1.adoc b/docs/man/keepassxc-cli.1.adoc index 1cee5ad68..b5eabd550 100644 --- a/docs/man/keepassxc-cli.1.adoc +++ b/docs/man/keepassxc-cli.1.adoc @@ -40,6 +40,17 @@ It provides the ability to query and modify the entries of a KeePass database, d *analyze* [_options_] <__database__>:: 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_]:: 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. @@ -244,6 +255,9 @@ The same password generation options as documented for the generate command can *-s*, *--show-protected*:: Shows the protected attributes in clear text. +*--show-attachments*:: + Shows the attachment names along with the size of the attachments. + *-t*, *--totp*:: Also shows the current TOTP, reporting an error if no TOTP is configured for the entry. diff --git a/share/translations/keepassxc_en.ts b/share/translations/keepassxc_en.ts index c3ffb2d9c..dd87e1920 100644 --- a/share/translations/keepassxc_en.ts +++ b/share/translations/keepassxc_en.ts @@ -7386,6 +7386,94 @@ Please consider generating a new key file. %1 (invalid executable path) + + Export an attachment of an entry. + + + + Path of the entry with the target attachment. + + + + Name of the attachment to be exported. + + + + Path to which the attachment should be exported. + + + + Could not find attachment with name %1. + + + + No export target given. Please use '--stdout' or specify an 'export-file'. + + + + Could not open output file %1. + + + + Successfully exported attachment %1 of entry %2 to %3. + + + + Overwrite existing attachments. + + + + Imports an attachment to an entry. + + + + Path of the entry. + + + + Name of the attachment to be added. + + + + Path of the attachment to be imported. + + + + Attachment %1 already exists for entry %2. + + + + Could not open attachment file %1. + + + + Successfully imported attachment %1 as %2 to entry %3. + + + + Remove an attachment of an entry. + + + + Name of the attachment to be removed. + + + + Successfully removed attachment %1 from entry %2. + + + + Show the attachments of the entry. + + + + No attachments present. + + + + Attachments: + + QtIOCompressor diff --git a/src/cli/AttachmentExport.cpp b/src/cli/AttachmentExport.cpp new file mode 100644 index 000000000..46dc5f4b6 --- /dev/null +++ b/src/cli/AttachmentExport.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * 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 . + */ + +#include "AttachmentExport.h" + +#include "Utils.h" +#include "core/Group.h" + +#include +#include + +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, QSharedPointer 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; +} diff --git a/src/cli/AttachmentExport.h b/src/cli/AttachmentExport.h new file mode 100644 index 000000000..9fd8c15ec --- /dev/null +++ b/src/cli/AttachmentExport.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_ATTACHMENTEXPORT_H +#define KEEPASSXC_ATTACHMENTEXPORT_H + +#include "DatabaseCommand.h" + +class AttachmentExport : public DatabaseCommand +{ +public: + AttachmentExport(); + + int executeWithDatabase(QSharedPointer db, QSharedPointer parser) override; + + static const QCommandLineOption StdoutOption; +}; + +#endif // KEEPASSXC_ATTACHMENTEXPORT_H diff --git a/src/cli/AttachmentImport.cpp b/src/cli/AttachmentImport.cpp new file mode 100644 index 000000000..080f81af0 --- /dev/null +++ b/src/cli/AttachmentImport.cpp @@ -0,0 +1,87 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * 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 . + */ + +#include "AttachmentImport.h" + +#include "Utils.h" +#include "core/Group.h" + +#include +#include + +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, QSharedPointer 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; +} diff --git a/src/cli/AttachmentImport.h b/src/cli/AttachmentImport.h new file mode 100644 index 000000000..ff6686af9 --- /dev/null +++ b/src/cli/AttachmentImport.h @@ -0,0 +1,33 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_ATTACHMENTIMPORT_H +#define KEEPASSXC_ATTACHMENTIMPORT_H + +#include "DatabaseCommand.h" + +class AttachmentImport : public DatabaseCommand +{ +public: + AttachmentImport(); + + int executeWithDatabase(QSharedPointer db, QSharedPointer parser) override; + + static const QCommandLineOption ForceOption; +}; + +#endif // KEEPASSXC_ATTACHMENTIMPORT_H diff --git a/src/cli/AttachmentRemove.cpp b/src/cli/AttachmentRemove.cpp new file mode 100644 index 000000000..6561948b0 --- /dev/null +++ b/src/cli/AttachmentRemove.cpp @@ -0,0 +1,68 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * 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 . + */ + +#include "AttachmentRemove.h" + +#include "Utils.h" +#include "core/Group.h" + +#include + +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, QSharedPointer 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; +} diff --git a/src/cli/AttachmentRemove.h b/src/cli/AttachmentRemove.h new file mode 100644 index 000000000..c6a494a3b --- /dev/null +++ b/src/cli/AttachmentRemove.h @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2019 KeePassXC Team + * + * 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 . + */ + +#ifndef KEEPASSXC_ATTACHMENTREMOVE_H +#define KEEPASSXC_ATTACHMENTREMOVE_H + +#include "DatabaseCommand.h" + +class AttachmentRemove : public DatabaseCommand +{ +public: + AttachmentRemove(); + + int executeWithDatabase(QSharedPointer db, QSharedPointer parser) override; +}; + +#endif // KEEPASSXC_ATTACHMENTMOVE_H diff --git a/src/cli/CMakeLists.txt b/src/cli/CMakeLists.txt index 0b348d3c9..470af6283 100644 --- a/src/cli/CMakeLists.txt +++ b/src/cli/CMakeLists.txt @@ -17,6 +17,9 @@ set(cli_SOURCES Add.cpp AddGroup.cpp Analyze.cpp + AttachmentExport.cpp + AttachmentImport.cpp + AttachmentRemove.cpp Clip.cpp Close.cpp Create.cpp diff --git a/src/cli/Command.cpp b/src/cli/Command.cpp index 0699921ac..2eab320a3 100644 --- a/src/cli/Command.cpp +++ b/src/cli/Command.cpp @@ -18,6 +18,9 @@ #include "Add.h" #include "AddGroup.h" #include "Analyze.h" +#include "AttachmentExport.h" +#include "AttachmentImport.h" +#include "AttachmentRemove.h" #include "Clip.h" #include "Close.h" #include "Create.h" @@ -107,7 +110,7 @@ QString Command::getDescriptionLine() { QString response = name; QString space(" "); - QString spaces = space.repeated(15 - name.length()); + QString spaces = space.repeated(20 - name.length()); response = response.append(spaces); response = response.append(description); response = response.append("\n"); @@ -164,6 +167,9 @@ namespace Commands s_commands.insert(QStringLiteral("add"), QSharedPointer(new Add())); s_commands.insert(QStringLiteral("analyze"), QSharedPointer(new Analyze())); + s_commands.insert(QStringLiteral("attachment-export"), QSharedPointer(new AttachmentExport())); + s_commands.insert(QStringLiteral("attachment-import"), QSharedPointer(new AttachmentImport())); + s_commands.insert(QStringLiteral("attachment-rm"), QSharedPointer(new AttachmentRemove())); s_commands.insert(QStringLiteral("clip"), QSharedPointer(new Clip())); s_commands.insert(QStringLiteral("close"), QSharedPointer(new Close())); s_commands.insert(QStringLiteral("db-create"), QSharedPointer(new Create())); diff --git a/src/cli/Show.cpp b/src/cli/Show.cpp index e378c8e4a..f4d8097f6 100644 --- a/src/cli/Show.cpp +++ b/src/cli/Show.cpp @@ -19,6 +19,7 @@ #include "Utils.h" #include "core/Group.h" +#include "core/Tools.h" #include @@ -31,6 +32,9 @@ const QCommandLineOption Show::ProtectedAttributesOption = << "show-protected", 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( QStringList() << "a" << "attributes", @@ -47,6 +51,7 @@ Show::Show() options.append(Show::TotpOption); options.append(Show::AttributesOption); options.append(Show::ProtectedAttributesOption); + options.append(Show::AttachmentsOption); positionalArguments.append({QString("entry"), QObject::tr("Name of the entry to show."), QString("")}); } @@ -104,6 +109,25 @@ int Show::executeWithDatabase(QSharedPointer 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) { out << entry->totp() << endl; } diff --git a/src/cli/Show.h b/src/cli/Show.h index bf76c6973..93713dbd8 100644 --- a/src/cli/Show.h +++ b/src/cli/Show.h @@ -30,6 +30,7 @@ public: static const QCommandLineOption TotpOption; static const QCommandLineOption AttributesOption; static const QCommandLineOption ProtectedAttributesOption; + static const QCommandLineOption AttachmentsOption; }; #endif // KEEPASSXC_SHOW_H diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index 03e6f2439..a6f1a0d54 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -30,6 +30,9 @@ #include "cli/Add.h" #include "cli/AddGroup.h" #include "cli/Analyze.h" +#include "cli/AttachmentExport.h" +#include "cli/AttachmentImport.h" +#include "cli/AttachmentRemove.h" #include "cli/Clip.h" #include "cli/Create.h" #include "cli/Diceware.h" @@ -215,6 +218,9 @@ void TestCli::testBatchCommands() Commands::setupCommands(false); QVERIFY(Commands::getCommand("add")); 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("close")); QVERIFY(Commands::getCommand("db-create")); @@ -236,7 +242,7 @@ void TestCli::testBatchCommands() QVERIFY(Commands::getCommand("show")); QVERIFY(Commands::getCommand("search")); QVERIFY(!Commands::getCommand("doesnotexist")); - QCOMPARE(Commands::getCommands().size(), 22); + QCOMPARE(Commands::getCommands().size(), 25); } void TestCli::testInteractiveCommands() @@ -244,6 +250,9 @@ void TestCli::testInteractiveCommands() Commands::setupCommands(true); QVERIFY(Commands::getCommand("add")); 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("close")); QVERIFY(Commands::getCommand("db-create")); @@ -265,7 +274,7 @@ void TestCli::testInteractiveCommands() QVERIFY(Commands::getCommand("show")); QVERIFY(Commands::getCommand("search")); QVERIFY(!Commands::getCommand("doesnotexist")); - QCOMPARE(Commands::getCommands().size(), 22); + QCOMPARE(Commands::getCommands().size(), 25); } void TestCli::testAdd() @@ -435,6 +444,184 @@ void TestCli::testAnalyze() 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() { if (QProcessEnvironment::systemEnvironment().contains("WAYLAND_DISPLAY")) { @@ -1751,6 +1938,33 @@ void TestCli::testShow() "URL: http://www.somesite.com/\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"); execCmd(showCmd, {"show", "-a", "Title", m_dbFile->fileName(), "/Sample Entry"}); QCOMPARE(m_stdout->readAll(), QByteArray("Sample Entry\n")); diff --git a/tests/TestCli.h b/tests/TestCli.h index 299264dd7..bbe80a4b3 100644 --- a/tests/TestCli.h +++ b/tests/TestCli.h @@ -47,6 +47,9 @@ private slots: void testAdd(); void testAddGroup(); void testAnalyze(); + void testAttachmentExport(); + void testAttachmentImport(); + void testAttachmentRemove(); void testClip(); void testCommandParsing_data(); void testCommandParsing(); diff --git a/tests/data/Attachment.txt b/tests/data/Attachment.txt new file mode 100644 index 000000000..39c9f3681 --- /dev/null +++ b/tests/data/Attachment.txt @@ -0,0 +1 @@ +Content diff --git a/tests/data/NewDatabase.kdbx b/tests/data/NewDatabase.kdbx index dfffcc1e4e037bc90059f4b7fb1f81e5d2ed1d61..cf7b2ffaa5c3e264d6eeb0852bbdb571aec9c4ab 100644 GIT binary patch literal 24654 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 literal 20590 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 NB)%UEn7S1h@7fwt7oY$D