From 408a1b75ff985f23d66a45dda7e2e495cea5790e Mon Sep 17 00:00:00 2001 From: louib Date: Sun, 16 Jun 2019 17:33:43 -0400 Subject: [PATCH] CLI: Extract database as CSV Allow database extraction as CSV. Added a `--format` option to the `Extract` command for that, which defaults to xml, so the current behavior is unchanged. The `CsvExporter` had to be refactored a bit, but nothing major. It can now print to a file or return a string. --- CHANGELOG | 1 + src/cli/Extract.cpp | 30 +++++++++++++++++++----- src/cli/Extract.h | 2 ++ src/cli/keepassxc-cli.1 | 8 ++++++- src/format/CsvExporter.cpp | 48 ++++++++++++++++++++++---------------- src/format/CsvExporter.h | 4 +++- tests/TestCli.cpp | 31 +++++++++++++++++++++++- 7 files changed, 95 insertions(+), 29 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 91c7604212..95171043b1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,7 @@ - Add 'Monospaced font' option to the Notes field [#3321] - Drop to background when copy feature [#3253] - Fix password generator issues with special characters [#3303] +- CLI: Add '--format' option and CSV support to the 'extract' command [#3277] 2.4.3 (2019-06-12) ========================= diff --git a/src/cli/Extract.cpp b/src/cli/Extract.cpp index b894277fe3..d96069e3db 100644 --- a/src/cli/Extract.cpp +++ b/src/cli/Extract.cpp @@ -23,10 +23,18 @@ #include "cli/TextStream.h" #include "cli/Utils.h" #include "core/Database.h" +#include "format/CsvExporter.h" + +const QCommandLineOption Extract::FormatOption = + QCommandLineOption(QStringList() << "f" + << "format", + QObject::tr("Format to use when extracting. Available choices are xml or csv. Defaults to xml."), + QObject::tr("xml|csv")); Extract::Extract() { name = QString("extract"); + options.append(Extract::FormatOption); description = QObject::tr("Extract and print the content of a database."); } @@ -34,17 +42,27 @@ Extract::~Extract() { } -int Extract::executeWithDatabase(QSharedPointer database, QSharedPointer) +int Extract::executeWithDatabase(QSharedPointer database, QSharedPointer parser) { TextStream outputTextStream(Utils::STDOUT, QIODevice::WriteOnly); TextStream errorTextStream(Utils::STDERR, QIODevice::WriteOnly); - QByteArray xmlData; - QString errorMessage; - if (!database->extract(xmlData, &errorMessage)) { - errorTextStream << QObject::tr("Unable to extract database %1").arg(errorMessage) << endl; + QString format = parser->value(Extract::FormatOption); + if (format.isEmpty() || format == QString("xml")) { + QByteArray xmlData; + QString errorMessage; + if (!database->extract(xmlData, &errorMessage)) { + errorTextStream << QObject::tr("Unable to extract database to XML: %1").arg(errorMessage) << endl; + return EXIT_FAILURE; + } + outputTextStream << xmlData.constData() << endl; + } else if (format == QString("csv")) { + CsvExporter csvExporter; + outputTextStream << csvExporter.exportDatabase(database); + } else { + errorTextStream << QObject::tr("Unsupported format %1").arg(format) << endl; return EXIT_FAILURE; } - outputTextStream << xmlData.constData() << endl; + return EXIT_SUCCESS; } diff --git a/src/cli/Extract.h b/src/cli/Extract.h index 7a6061b988..bcca16396c 100644 --- a/src/cli/Extract.h +++ b/src/cli/Extract.h @@ -27,6 +27,8 @@ class Extract : public DatabaseCommand ~Extract(); int executeWithDatabase(QSharedPointer db, QSharedPointer parser); + + static const QCommandLineOption FormatOption; }; #endif // KEEPASSXC_EXTRACT_H diff --git a/src/cli/keepassxc-cli.1 b/src/cli/keepassxc-cli.1 index 873a973ef5..23c09bfecf 100644 --- a/src/cli/keepassxc-cli.1 +++ b/src/cli/keepassxc-cli.1 @@ -35,7 +35,7 @@ Edits a database entry. A password can be generated (\fI-g\fP option), or a prom Estimates the entropy of a password. The password to estimate can be provided as a positional argument, or using the standard input. .IP "extract [options] " -Extracts and prints the contents of a database to standard output in XML format. +Extracts and prints the contents of a database to standard output in the specified format (defaults to XML). .IP "generate [options]" Generate a random password. @@ -163,6 +163,12 @@ otherwise the program will fail. If the wordlist has < 4000 words a warning will be printed to STDERR. +.SS "Extract options" + +.IP "-f, --format" +Format to use when extracting. Available choices are xml or csv. Defaults to xml. + + .SS "List options" .IP "-R, --recursive" diff --git a/src/format/CsvExporter.cpp b/src/format/CsvExporter.cpp index 03d5a576f0..98fc6fdc83 100644 --- a/src/format/CsvExporter.cpp +++ b/src/format/CsvExporter.cpp @@ -35,21 +35,22 @@ bool CsvExporter::exportDatabase(const QString& filename, const QSharedPointer& db) { - QString header; - addColumn(header, "Group"); - addColumn(header, "Title"); - addColumn(header, "Username"); - addColumn(header, "Password"); - addColumn(header, "URL"); - addColumn(header, "Notes"); - header.append("\n"); + if (device->write(exportHeader().toUtf8()) == -1) { + m_error = device->errorString(); + return false; + } - if (device->write(header.toUtf8()) == -1) { + if (device->write(exportGroup(db->rootGroup()).toUtf8()) == -1) { m_error = device->errorString(); return false; } - return writeGroup(device, db->rootGroup()); + return true; +} + +QString CsvExporter::exportDatabase(const QSharedPointer& db) +{ + return exportHeader() + exportGroup(db->rootGroup()); } QString CsvExporter::errorString() const @@ -57,8 +58,21 @@ QString CsvExporter::errorString() const return m_error; } -bool CsvExporter::writeGroup(QIODevice* device, const Group* group, QString groupPath) +QString CsvExporter::exportHeader() { + QString header; + addColumn(header, "Group"); + addColumn(header, "Title"); + addColumn(header, "Username"); + addColumn(header, "Password"); + addColumn(header, "URL"); + addColumn(header, "Notes"); + return header + QString("\n"); +} + +QString CsvExporter::exportGroup(const Group* group, QString groupPath) +{ + QString response; if (!groupPath.isEmpty()) { groupPath.append("/"); } @@ -76,21 +90,15 @@ bool CsvExporter::writeGroup(QIODevice* device, const Group* group, QString grou addColumn(line, entry->notes()); line.append("\n"); - - if (device->write(line.toUtf8()) == -1) { - m_error = device->errorString(); - return false; - } + response.append(line); } const QList& children = group->children(); for (const Group* child : children) { - if (!writeGroup(device, child, groupPath)) { - return false; - } + response.append(exportGroup(child, groupPath)); } - return true; + return response; } void CsvExporter::addColumn(QString& str, const QString& column) diff --git a/src/format/CsvExporter.h b/src/format/CsvExporter.h index e71cf7fa9a..a982ed1095 100644 --- a/src/format/CsvExporter.h +++ b/src/format/CsvExporter.h @@ -31,10 +31,12 @@ class CsvExporter public: bool exportDatabase(const QString& filename, const QSharedPointer& db); bool exportDatabase(QIODevice* device, const QSharedPointer& db); + QString exportDatabase(const QSharedPointer& db); QString errorString() const; private: - bool writeGroup(QIODevice* device, const Group* group, QString groupPath = QString()); + QString exportGroup(const Group* group, QString groupPath = QString()); + QString exportHeader(); void addColumn(QString& str, const QString& column); QString m_error; diff --git a/tests/TestCli.cpp b/tests/TestCli.cpp index d65a7af6cb..d1bbefd21f 100644 --- a/tests/TestCli.cpp +++ b/tests/TestCli.cpp @@ -681,12 +681,41 @@ void TestCli::testExtract() // Quiet option QScopedPointer dbQuiet(new Database()); qint64 pos = m_stdoutFile->pos(); + qint64 posErr = m_stderrFile->pos(); Utils::Test::setNextPassword("a"); - extractCmd.execute({"extract", "-q", m_dbFile->fileName()}); + extractCmd.execute({"extract", "-f", "xml", "-q", m_dbFile->fileName()}); m_stdoutFile->seek(pos); + m_stderrFile->seek(posErr); reader.readDatabase(m_stdoutFile.data(), dbQuiet.data()); QVERIFY(!reader.hasError()); QVERIFY(db.data()); + QCOMPARE(m_stderrFile->readAll(), QByteArray("")); + + // CSV extraction + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + extractCmd.execute({"extract", "-f", "csv", m_dbFile->fileName()}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip prompt line + m_stderrFile->seek(posErr); + QByteArray csvHeader = m_stdoutFile->readLine(); + QCOMPARE(csvHeader, QByteArray("\"Group\",\"Title\",\"Username\",\"Password\",\"URL\",\"Notes\"\n")); + QByteArray csvData = m_stdoutFile->readAll(); + QVERIFY(csvData.contains(QByteArray( + "\"NewDatabase\",\"Sample Entry\",\"User Name\",\"Password\",\"http://www.somesite.com/\",\"Notes\"\n"))); + QCOMPARE(m_stderrFile->readAll(), QByteArray("")); + + // test invalid format + pos = m_stdoutFile->pos(); + posErr = m_stderrFile->pos(); + Utils::Test::setNextPassword("a"); + extractCmd.execute({"extract", "-f", "yaml", m_dbFile->fileName()}); + m_stdoutFile->seek(pos); + m_stdoutFile->readLine(); // skip prompt line + m_stderrFile->seek(posErr); + QCOMPARE(m_stdoutFile->readLine(), QByteArray("")); + QCOMPARE(m_stderrFile->readLine(), QByteArray("Unsupported format yaml\n")); } void TestCli::testGenerate_data()