Skip to content

Commit

Permalink
CLI: Extract database as CSV
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
louib authored and louib committed Jun 16, 2019
1 parent 04360ed commit f9c366c
Show file tree
Hide file tree
Showing 7 changed files with 98 additions and 29 deletions.
4 changes: 4 additions & 0 deletions CHANGELOG
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
2.5.0-Beta1
=========================
- CLI: Add '--format' option and CSV support to the 'extract' command [#3277]

2.4.3 (2019-06-12)
=========================

Expand Down
30 changes: 24 additions & 6 deletions src/cli/Extract.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,28 +23,46 @@
#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.");
}

Extract::~Extract()
{
}

int Extract::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser>)
int Extract::executeWithDatabase(QSharedPointer<Database> database, QSharedPointer<QCommandLineParser> 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;
}
2 changes: 2 additions & 0 deletions src/cli/Extract.h
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ class Extract : public DatabaseCommand
~Extract();

int executeWithDatabase(QSharedPointer<Database> db, QSharedPointer<QCommandLineParser> parser);

static const QCommandLineOption FormatOption;
};

#endif // KEEPASSXC_EXTRACT_H
8 changes: 7 additions & 1 deletion src/cli/keepassxc-cli.1
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,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] <database>"
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.
Expand Down Expand Up @@ -147,6 +147,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"
Expand Down
48 changes: 28 additions & 20 deletions src/format/CsvExporter.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -35,30 +35,44 @@ bool CsvExporter::exportDatabase(const QString& filename, const QSharedPointer<c

bool CsvExporter::exportDatabase(QIODevice* device, const QSharedPointer<const Database>& 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<const Database>& db)
{
return exportHeader() + exportGroup(db->rootGroup());
}

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("/");
}
Expand All @@ -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<Group*>& 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)
Expand Down
4 changes: 3 additions & 1 deletion src/format/CsvExporter.h
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,12 @@ class CsvExporter
public:
bool exportDatabase(const QString& filename, const QSharedPointer<const Database>& db);
bool exportDatabase(QIODevice* device, const QSharedPointer<const Database>& db);
QString exportDatabase(const QSharedPointer<const Database>& 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;
Expand Down
31 changes: 30 additions & 1 deletion tests/TestCli.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -662,12 +662,41 @@ void TestCli::testExtract()
// Quiet option
QScopedPointer<Database> 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()
Expand Down

0 comments on commit f9c366c

Please sign in to comment.