From 744b4abce801440ff4e143fb2bb416b3d7a91e07 Mon Sep 17 00:00:00 2001 From: Jonathan White Date: Mon, 14 Oct 2019 09:31:23 -0400 Subject: [PATCH] Move FileWatcher into Database class * Fix #3506 * Fix #2389 * Fix #2536 * Fix #2230 Every database that has been opened now watch's it's own file. This allows the database class to manage file changes and detect fail conditions during saving. Additionally, all stakeholders of the database can listen for the database file changed notification and respond accordingly. Performed significant cleanup of the autoreload code within DatabaseWidget. Fixed several issues with handling changes due to merging, not merging, and other scenarios while reloading. Prevent database saves to the same file if there are changes on disk that have not been merged with the open database. --- src/core/Database.cpp | 74 +++++++++++++++-------- src/core/Database.h | 5 ++ src/core/FileWatcher.cpp | 107 +++++++++++++++++++++------------- src/core/FileWatcher.h | 23 +++++--- src/gui/DatabaseTabWidget.cpp | 5 +- src/gui/DatabaseWidget.cpp | 41 ++----------- src/gui/DatabaseWidget.h | 6 +- tests/CMakeLists.txt | 2 +- tests/TestDatabase.cpp | 57 +++++++++++++++++- tests/TestDatabase.h | 2 + 10 files changed, 204 insertions(+), 118 deletions(-) diff --git a/src/core/Database.cpp b/src/core/Database.cpp index 03d2e96eef..b818a90342 100644 --- a/src/core/Database.cpp +++ b/src/core/Database.cpp @@ -19,6 +19,7 @@ #include "Database.h" #include "core/Clock.h" +#include "core/FileWatcher.h" #include "core/Group.h" #include "core/Merger.h" #include "core/Metadata.h" @@ -42,6 +43,7 @@ Database::Database() , m_data() , m_rootGroup(nullptr) , m_timer(new QTimer(this)) + , m_fileWatcher(new FileWatcher(this)) , m_emitModified(false) , m_uuid(QUuid::createUuid()) { @@ -54,7 +56,9 @@ Database::Database() connect(m_metadata, SIGNAL(metadataModified()), this, SLOT(markAsModified())); connect(m_timer, SIGNAL(timeout()), SIGNAL(databaseModified())); + connect(this, SIGNAL(databaseOpened()), SLOT(updateCommonUsernames())); connect(this, SIGNAL(databaseSaved()), SLOT(updateCommonUsernames())); + connect(m_fileWatcher, SIGNAL(fileChanged()), SIGNAL(databaseFileChanged())); m_modified = false; m_emitModified = true; @@ -116,6 +120,7 @@ bool Database::open(const QString& filePath, QSharedPointer emit databaseDiscarded(); } + m_initialized = false; setEmitModified(false); QFile dbFile(filePath); @@ -138,8 +143,7 @@ bool Database::open(const QString& filePath, QSharedPointer } KeePass2Reader reader; - bool ok = reader.readDatabase(&dbFile, std::move(key), this); - if (reader.hasError()) { + if (!reader.readDatabase(&dbFile, std::move(key), this)) { if (error) { *error = tr("Error while reading the database: %1").arg(reader.errorString()); } @@ -150,22 +154,23 @@ bool Database::open(const QString& filePath, QSharedPointer setFilePath(filePath); dbFile.close(); - updateCommonUsernames(); - - setInitialized(ok); markAsClean(); + m_initialized = true; + emit databaseOpened(); + m_fileWatcher->start(canonicalFilePath()); setEmitModified(true); - return ok; + + return true; } /** * Save the database to the current file path. It is an error to call this function * if no file path has been defined. * + * @param error error message in case of failure * @param atomic Use atomic file transactions * @param backup Backup the existing database file, if exists - * @param error error message in case of failure * @return true on success */ bool Database::save(QString* error, bool atomic, bool backup) @@ -194,27 +199,52 @@ bool Database::save(QString* error, bool atomic, bool backup) * wrong moment. * * @param filePath Absolute path of the file to save + * @param error error message in case of failure * @param atomic Use atomic file transactions * @param backup Backup the existing database file, if exists - * @param error error message in case of failure * @return true on success */ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool backup) { - // Disallow saving to the same file if read-only - if (m_data.isReadOnly && filePath == m_data.filePath) { - Q_ASSERT_X(false, "Database::saveAs", "Could not save, database file is read-only."); - if (error) { - *error = tr("Could not save, database file is read-only."); + if (filePath == m_data.filePath) { + // Disallow saving to the same file if read-only + if (m_data.isReadOnly) { + Q_ASSERT_X(false, "Database::saveAs", "Could not save, database file is read-only."); + if (error) { + *error = tr("Could not save, database file is read-only."); + } + return false; + } + + // Fail-safe check to make sure we don't overwrite underlying file changes + // that have not yet triggered a file reload/merge operation. + if (!m_fileWatcher->hasSameFileChecksum()) { + if (error) { + *error = tr("Database file has unmerged changes."); + } + return false; } - return false; } // Clear read-only flag setReadOnly(false); + m_fileWatcher->stop(); auto& canonicalFilePath = QFileInfo::exists(filePath) ? QFileInfo(filePath).canonicalFilePath() : filePath; + bool ok = performSave(canonicalFilePath, error, atomic, backup); + if (ok) { + setFilePath(filePath); + m_fileWatcher->start(canonicalFilePath); + } else { + // Saving failed, don't rewatch file since it does not represent our database + markAsModified(); + } + + return ok; +} +bool Database::performSave(const QString& filePath, QString* error, bool atomic, bool backup) +{ if (atomic) { QSaveFile saveFile(filePath); if (saveFile.open(QIODevice::WriteOnly)) { @@ -224,12 +254,11 @@ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool } if (backup) { - backupDatabase(canonicalFilePath); + backupDatabase(filePath); } if (saveFile.commit()) { // successfully saved database file - setFilePath(filePath); return true; } } @@ -248,28 +277,26 @@ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool tempFile.close(); // flush to disk if (backup) { - backupDatabase(canonicalFilePath); + backupDatabase(filePath); } // Delete the original db and move the temp file in place - QFile::remove(canonicalFilePath); + QFile::remove(filePath); // Note: call into the QFile rename instead of QTemporaryFile // due to an undocumented difference in how the function handles // errors. This prevents errors when saving across file systems. - if (tempFile.QFile::rename(canonicalFilePath)) { + if (tempFile.QFile::rename(filePath)) { // successfully saved the database tempFile.setAutoRemove(false); - setFilePath(filePath); return true; - } else if (!backup || !restoreDatabase(canonicalFilePath)) { + } else if (!backup || !restoreDatabase(filePath)) { // Failed to copy new database in place, and // failed to restore from backup or backups disabled tempFile.setAutoRemove(false); if (error) { *error = tr("%1\nBackup database located at %2").arg(tempFile.errorString(), tempFile.fileName()); } - markAsModified(); return false; } } @@ -280,7 +307,6 @@ bool Database::saveAs(const QString& filePath, QString* error, bool atomic, bool } // Saving failed - markAsModified(); return false; } @@ -490,6 +516,8 @@ void Database::setFilePath(const QString& filePath) if (filePath != m_data.filePath) { QString oldPath = m_data.filePath; m_data.filePath = filePath; + // Don't watch for changes until the next open or save operation + m_fileWatcher->stop(); emit filePathChanged(oldPath, filePath); } } diff --git a/src/core/Database.h b/src/core/Database.h index ea3453daa0..7f504cc555 100644 --- a/src/core/Database.h +++ b/src/core/Database.h @@ -32,6 +32,7 @@ class Entry; enum class EntryReferenceType; +class FileWatcher; class Group; class Metadata; class QTimer; @@ -144,9 +145,11 @@ public slots: void groupRemoved(); void groupAboutToMove(Group* group, Group* toGroup, int index); void groupMoved(); + void databaseOpened(); void databaseModified(); void databaseSaved(); void databaseDiscarded(); + void databaseFileChanged(); private slots: void startModifiedTimer(); @@ -177,12 +180,14 @@ private slots: bool writeDatabase(QIODevice* device, QString* error = nullptr); bool backupDatabase(const QString& filePath); bool restoreDatabase(const QString& filePath); + bool performSave(const QString& filePath, QString* error, bool atomic, bool backup); Metadata* const m_metadata; DatabaseData m_data; Group* m_rootGroup; QList m_deletedObjects; QPointer m_timer; + QPointer m_fileWatcher; bool m_initialized = false; bool m_modified = false; bool m_emitModified; diff --git a/src/core/FileWatcher.cpp b/src/core/FileWatcher.cpp index ae7878191d..1b39e597d6 100644 --- a/src/core/FileWatcher.cpp +++ b/src/core/FileWatcher.cpp @@ -19,6 +19,7 @@ #include "FileWatcher.h" #include "core/Clock.h" +#include #include #ifdef Q_OS_LINUX @@ -27,36 +28,23 @@ namespace { - const int FileChangeDelay = 500; - const int TimerResolution = 100; + const int FileChangeDelay = 200; } // namespace -DelayingFileWatcher::DelayingFileWatcher(QObject* parent) +FileWatcher::FileWatcher(QObject* parent) : QObject(parent) , m_ignoreFileChange(false) { - connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), this, SLOT(onWatchedFileChanged())); - connect(&m_fileUnblockTimer, SIGNAL(timeout()), this, SLOT(observeFileChanges())); - connect(&m_fileChangeDelayTimer, SIGNAL(timeout()), this, SIGNAL(fileChanged())); + connect(&m_fileWatcher, SIGNAL(fileChanged(QString)), SLOT(onWatchedFileChanged())); + connect(&m_fileChangeDelayTimer, SIGNAL(timeout()), SIGNAL(fileChanged())); + connect(&m_fileChecksumTimer, SIGNAL(timeout()), SLOT(checkFileChecksum())); m_fileChangeDelayTimer.setSingleShot(true); - m_fileUnblockTimer.setSingleShot(true); + m_fileIgnoreDelayTimer.setSingleShot(true); } -void DelayingFileWatcher::restart() +void FileWatcher::start(const QString& filePath, int checksumInterval) { - m_fileWatcher.addPath(m_filePath); -} - -void DelayingFileWatcher::stop() -{ - m_fileWatcher.removePath(m_filePath); -} - -void DelayingFileWatcher::start(const QString& filePath) -{ - if (!m_filePath.isEmpty()) { - m_fileWatcher.removePath(m_filePath); - } + stop(); #if defined(Q_OS_LINUX) struct statfs statfsBuf; @@ -74,45 +62,80 @@ void DelayingFileWatcher::start(const QString& filePath) #endif m_fileWatcher.addPath(filePath); + m_filePath = filePath; + m_fileChecksum = calculateChecksum(); + m_fileChecksumTimer.start(checksumInterval); + m_ignoreFileChange = false; +} - if (!filePath.isEmpty()) { - m_filePath = filePath; +void FileWatcher::stop() +{ + if (!m_filePath.isEmpty()) { + m_fileWatcher.removePath(m_filePath); } + m_filePath.clear(); + m_fileChecksum.clear(); + m_fileChangeDelayTimer.stop(); } -void DelayingFileWatcher::ignoreFileChanges() +void FileWatcher::pause() { m_ignoreFileChange = true; m_fileChangeDelayTimer.stop(); } -void DelayingFileWatcher::observeFileChanges(bool delayed) +void FileWatcher::resume() { - int timeout = 0; - if (delayed) { - timeout = FileChangeDelay; - } else { - m_ignoreFileChange = false; - start(m_filePath); - } - if (timeout > 0 && !m_fileUnblockTimer.isActive()) { - m_fileUnblockTimer.start(timeout); + m_ignoreFileChange = false; + // Add a short delay to start in the next event loop + if (!m_fileIgnoreDelayTimer.isActive()) { + m_fileIgnoreDelayTimer.start(0); } } -void DelayingFileWatcher::onWatchedFileChanged() +void FileWatcher::onWatchedFileChanged() { - if (m_ignoreFileChange) { - // the client forcefully silenced us + // Don't notify if we are ignoring events or already started a notification chain + if (shouldIgnoreChanges()) { return; } - if (m_fileChangeDelayTimer.isActive()) { - // we are waiting to fire the delayed fileChanged event, so nothing - // to do here + + m_fileChecksum = calculateChecksum(); + m_fileChangeDelayTimer.start(0); +} + +bool FileWatcher::shouldIgnoreChanges() +{ + return m_filePath.isEmpty() || m_ignoreFileChange || m_fileIgnoreDelayTimer.isActive() + || m_fileChangeDelayTimer.isActive(); +} + +bool FileWatcher::hasSameFileChecksum() +{ + return calculateChecksum() == m_fileChecksum; +} + +void FileWatcher::checkFileChecksum() +{ + if (shouldIgnoreChanges()) { return; } - m_fileChangeDelayTimer.start(FileChangeDelay); + if (!hasSameFileChecksum()) { + onWatchedFileChanged(); + } +} + +QByteArray FileWatcher::calculateChecksum() +{ + QFile file(m_filePath); + if (file.open(QFile::ReadOnly)) { + QCryptographicHash hash(QCryptographicHash::Sha256); + if (hash.addData(&file)) { + return hash.result(); + } + } + return {}; } BulkFileWatcher::BulkFileWatcher(QObject* parent) @@ -281,7 +304,7 @@ void BulkFileWatcher::observeFileChanges(bool delayed) { int timeout = 0; if (delayed) { - timeout = TimerResolution; + timeout = FileChangeDelay; } else { const QDateTime current = Clock::currentDateTimeUtc(); for (const QString& key : m_watchedFilesIgnored.keys()) { diff --git a/src/core/FileWatcher.h b/src/core/FileWatcher.h index f6953cf809..3793ae8602 100644 --- a/src/core/FileWatcher.h +++ b/src/core/FileWatcher.h @@ -23,34 +23,39 @@ #include #include -class DelayingFileWatcher : public QObject +class FileWatcher : public QObject { Q_OBJECT public: - explicit DelayingFileWatcher(QObject* parent = nullptr); + explicit FileWatcher(QObject* parent = nullptr); - void blockAutoReload(bool block); - void start(const QString& path); - - void restart(); + void start(const QString& path, int checksumInterval = 1000); void stop(); - void ignoreFileChanges(); + + bool hasSameFileChecksum(); signals: void fileChanged(); public slots: - void observeFileChanges(bool delayed = false); + void pause(); + void resume(); private slots: void onWatchedFileChanged(); + void checkFileChecksum(); private: + QByteArray calculateChecksum(); + bool shouldIgnoreChanges(); + QString m_filePath; QFileSystemWatcher m_fileWatcher; + QByteArray m_fileChecksum; QTimer m_fileChangeDelayTimer; - QTimer m_fileUnblockTimer; + QTimer m_fileIgnoreDelayTimer; + QTimer m_fileChecksumTimer; bool m_ignoreFileChange; }; diff --git a/src/gui/DatabaseTabWidget.cpp b/src/gui/DatabaseTabWidget.cpp index 8e7096a1cb..4c8458e52a 100644 --- a/src/gui/DatabaseTabWidget.cpp +++ b/src/gui/DatabaseTabWidget.cpp @@ -200,11 +200,14 @@ void DatabaseTabWidget::addDatabaseTab(DatabaseWidget* dbWidget, bool inBackgrou setCurrentIndex(index); } - connect(dbWidget, SIGNAL(databaseFilePathChanged(QString, QString)), SLOT(updateTabName())); connect(dbWidget, SIGNAL(requestOpenDatabase(QString, bool, QString, QString)), SLOT(addDatabaseTab(QString, bool, QString, QString))); + connect(dbWidget, SIGNAL(databaseFilePathChanged(QString, QString)), SLOT(updateTabName())); connect(dbWidget, SIGNAL(closeRequest()), SLOT(closeDatabaseTabFromSender())); + connect(dbWidget, + SIGNAL(databaseReplaced(const QSharedPointer&, const QSharedPointer&)), + SLOT(updateTabName())); connect(dbWidget, SIGNAL(databaseModified()), SLOT(updateTabName())); connect(dbWidget, SIGNAL(databaseSaved()), SLOT(updateTabName())); connect(dbWidget, SIGNAL(databaseUnlocked()), SLOT(updateTabName())); diff --git a/src/gui/DatabaseWidget.cpp b/src/gui/DatabaseWidget.cpp index bc6d2b507d..ef76bd82d4 100644 --- a/src/gui/DatabaseWidget.cpp +++ b/src/gui/DatabaseWidget.cpp @@ -94,7 +94,6 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) , m_opVaultOpenWidget(new OpVaultOpenWidget(this)) , m_groupView(new GroupView(m_db.data(), m_mainSplitter)) , m_saveAttempts(0) - , m_fileWatcher(new DelayingFileWatcher(this)) { m_messageWidget->setHidden(true); @@ -199,7 +198,6 @@ DatabaseWidget::DatabaseWidget(QSharedPointer db, QWidget* parent) connect(m_keepass1OpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool))); connect(m_opVaultOpenWidget, SIGNAL(dialogFinished(bool)), SLOT(loadDatabase(bool))); connect(m_csvImportWizard, SIGNAL(importFinished(bool)), SLOT(csvImportFinished(bool))); - connect(m_fileWatcher.data(), SIGNAL(fileChanged()), this, SLOT(reloadDatabaseFile())); connect(this, SIGNAL(currentChanged(int)), SLOT(emitCurrentModeChanged())); // clang-format on @@ -895,6 +893,7 @@ void DatabaseWidget::connectDatabaseSignals() connect(m_db.data(), SIGNAL(databaseModified()), SIGNAL(databaseModified())); connect(m_db.data(), SIGNAL(databaseModified()), SLOT(onDatabaseModified())); connect(m_db.data(), SIGNAL(databaseSaved()), SIGNAL(databaseSaved())); + connect(m_db.data(), SIGNAL(databaseFileChanged()), this, SLOT(reloadDatabaseFile())); } void DatabaseWidget::loadDatabase(bool accepted) @@ -908,14 +907,12 @@ void DatabaseWidget::loadDatabase(bool accepted) if (accepted) { replaceDatabase(openWidget->database()); switchToMainView(); - m_fileWatcher->restart(); m_saveAttempts = 0; emit databaseUnlocked(); if (config()->get("MinimizeAfterUnlock").toBool()) { window()->showMinimized(); } } else { - m_fileWatcher->stop(); if (m_databaseOpenWidget->database()) { m_databaseOpenWidget->database().reset(); } @@ -1063,7 +1060,6 @@ void DatabaseWidget::switchToOpenDatabase() void DatabaseWidget::switchToOpenDatabase(const QString& filePath) { - updateFilePath(filePath); m_databaseOpenWidget->load(filePath); setCurrentWidget(m_databaseOpenWidget); } @@ -1091,14 +1087,12 @@ void DatabaseWidget::csvImportFinished(bool accepted) void DatabaseWidget::switchToImportKeepass1(const QString& filePath) { - updateFilePath(filePath); m_keepass1OpenWidget->load(filePath); setCurrentWidget(m_keepass1OpenWidget); } void DatabaseWidget::switchToImportOpVault(const QString& fileName) { - updateFilePath(fileName); m_opVaultOpenWidget->load(fileName); setCurrentWidget(m_opVaultOpenWidget); } @@ -1384,21 +1378,6 @@ bool DatabaseWidget::lock() return true; } -void DatabaseWidget::updateFilePath(const QString& filePath) -{ - m_fileWatcher->start(filePath); - m_db->setFilePath(filePath); -} - -void DatabaseWidget::blockAutoReload(bool block) -{ - if (block) { - m_fileWatcher->ignoreFileChanges(); - } else { - m_fileWatcher->observeFileChanges(true); - } -} - void DatabaseWidget::reloadDatabaseFile() { if (!m_db || isLocked()) { @@ -1417,22 +1396,20 @@ void DatabaseWidget::reloadDatabaseFile() if (result == MessageBox::No) { // Notify everyone the database does not match the file m_db->markAsModified(); - // Rewatch the database file - m_fileWatcher->restart(); return; } } QString error; auto db = QSharedPointer::create(m_db->filePath()); - if (db->open(database()->key(), &error, true)) { + if (db->open(database()->key(), &error)) { if (m_db->isModified()) { // Ask if we want to merge changes into new database auto result = MessageBox::question( this, tr("Merge Request"), tr("The database file has changed and you have unsaved changes.\nDo you want to merge your changes?"), - MessageBox::Merge | MessageBox::Cancel, + MessageBox::Merge | MessageBox::Discard, MessageBox::Merge); if (result == MessageBox::Merge) { @@ -1442,11 +1419,9 @@ void DatabaseWidget::reloadDatabaseFile() } } - QUuid groupBeforeReload; + QUuid groupBeforeReload = m_db->rootGroup()->uuid(); if (m_groupView && m_groupView->currentGroup()) { groupBeforeReload = m_groupView->currentGroup()->uuid(); - } else { - groupBeforeReload = m_db->rootGroup()->uuid(); } QUuid entryBeforeReload; @@ -1454,19 +1429,15 @@ void DatabaseWidget::reloadDatabaseFile() entryBeforeReload = m_entryView->currentEntry()->uuid(); } - bool isReadOnly = m_db->isReadOnly(); replaceDatabase(db); - m_db->setReadOnly(isReadOnly); restoreGroupEntryFocus(groupBeforeReload, entryBeforeReload); + m_blockAutoSave = false; } else { showMessage(tr("Could not open the new database file while attempting to autoreload.\nError: %1").arg(error), MessageWidget::Error); // Mark db as modified since existing data may differ from file or file was deleted m_db->markAsModified(); } - - // Rewatch the database file - m_fileWatcher->restart(); } int DatabaseWidget::numberOfSelectedEntries() const @@ -1604,7 +1575,6 @@ bool DatabaseWidget::save() } // Prevent recursions and infinite save loops - blockAutoReload(true); m_blockAutoSave = true; ++m_saveAttempts; @@ -1612,7 +1582,6 @@ bool DatabaseWidget::save() bool useAtomicSaves = config()->get("UseAtomicSaves", true).toBool(); QString errorMessage; bool ok = m_db->save(&errorMessage, useAtomicSaves, config()->get("BackupBeforeSave").toBool()); - blockAutoReload(false); if (ok) { m_saveAttempts = 0; diff --git a/src/gui/DatabaseWidget.h b/src/gui/DatabaseWidget.h index 7f5e8099d9..5a163a3124 100644 --- a/src/gui/DatabaseWidget.h +++ b/src/gui/DatabaseWidget.h @@ -35,7 +35,7 @@ class KeePass1OpenWidget; class OpVaultOpenWidget; class DatabaseSettingsDialog; class Database; -class DelayingFileWatcher; +class FileWatcher; class EditEntryWidget; class EditGroupWidget; class Entry; @@ -108,8 +108,6 @@ class DatabaseWidget : public QStackedWidget bool currentEntryHasNotes(); bool currentEntryHasTotp(); - void blockAutoReload(bool block = true); - QByteArray entryViewState() const; bool setEntryViewState(const QByteArray& state) const; QList mainSplitterSizes() const; @@ -210,7 +208,6 @@ public slots: void showEvent(QShowEvent* event) override; private slots: - void updateFilePath(const QString& filePath); void entryActivationSignalReceived(Entry* entry, EntryModel::ModelColumn column); void switchBackToEntryEdit(); void switchToHistoryView(Entry* entry); @@ -273,7 +270,6 @@ private slots: bool m_searchLimitGroup; // Autoreload - QPointer m_fileWatcher; bool m_blockAutoSave; }; diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index c4df1d8e66..288f644703 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -224,7 +224,7 @@ if(WITH_XC_KEESHARE) endif() add_unit_test(NAME testdatabase SOURCES TestDatabase.cpp - LIBS ${TEST_LIBRARIES}) + LIBS testsupport ${TEST_LIBRARIES}) add_unit_test(NAME testtools SOURCES TestTools.cpp LIBS ${TEST_LIBRARIES}) diff --git a/tests/TestDatabase.cpp b/tests/TestDatabase.cpp index 94e3c8ba78..9605788727 100644 --- a/tests/TestDatabase.cpp +++ b/tests/TestDatabase.cpp @@ -20,13 +20,14 @@ #include "TestGlobal.h" #include -#include #include "config-keepassx-tests.h" #include "core/Metadata.h" +#include "core/Tools.h" #include "crypto/Crypto.h" #include "format/KeePass2Writer.h" #include "keys/PasswordKey.h" +#include "util/TemporaryFile.h" QTEST_GUILESS_MAIN(TestDatabase) @@ -35,6 +36,60 @@ void TestDatabase::initTestCase() QVERIFY(Crypto::init()); } +void TestDatabase::testOpen() +{ + auto db = QSharedPointer::create(); + QVERIFY(!db->isInitialized()); + QVERIFY(!db->isModified()); + + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create("a")); + + bool ok = db->open(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx"), key); + QVERIFY(ok); + + QVERIFY(db->isInitialized()); + QVERIFY(!db->isModified()); + + db->metadata()->setName("test"); + QVERIFY(db->isModified()); +} + +void TestDatabase::testSave() +{ + QByteArray data; + QFile sourceDbFile(QString(KEEPASSX_TEST_DATA_DIR).append("/NewDatabase.kdbx")); + QVERIFY(sourceDbFile.open(QIODevice::ReadOnly)); + QVERIFY(Tools::readAllFromDevice(&sourceDbFile, data)); + sourceDbFile.close(); + + TemporaryFile tempFile; + QVERIFY(tempFile.open()); + QCOMPARE(tempFile.write(data), static_cast((data.size()))); + tempFile.close(); + + auto db = QSharedPointer::create(); + auto key = QSharedPointer::create(); + key->addKey(QSharedPointer::create("a")); + + QString error; + bool ok = db->open(tempFile.fileName(), key, &error); + QVERIFY(ok); + + // Test safe saves + db->metadata()->setName("test"); + QVERIFY(db->isModified()); + + // Test unsafe saves + QVERIFY2(db->save(&error, false, false), error.toLatin1()); + + QVERIFY2(db->save(&error), error.toLatin1()); + QVERIFY(!db->isModified()); + + // Test save backups + QVERIFY2(db->save(&error, true, true), error.toLatin1()); +} + void TestDatabase::testEmptyRecycleBinOnDisabled() { QString filename = QString(KEEPASSX_TEST_DATA_DIR).append("/RecycleBinDisabled.kdbx"); diff --git a/tests/TestDatabase.h b/tests/TestDatabase.h index 46deb58aae..b5df8690fe 100644 --- a/tests/TestDatabase.h +++ b/tests/TestDatabase.h @@ -27,6 +27,8 @@ class TestDatabase : public QObject private slots: void initTestCase(); + void testOpen(); + void testSave(); void testEmptyRecycleBinOnDisabled(); void testEmptyRecycleBinOnNotCreated(); void testEmptyRecycleBinOnEmpty();