isPerDeviceMode takes baseDir context, fix import result path

Change isPerDeviceMode(const QString&) to isPerDeviceMode(const QDir& baseDir)
so the method resolves this->path against the caller-provided base directory.
This fixes silent misclassification when reference paths are relative, since
QFileInfo previously resolved against process CWD instead of the database
directory.

Override Result.path with the actual device file path in importPerDeviceShares
so status messages show which specific container failed, not just the sync
directory.

Add regression tests for relative directory and file paths.
This commit is contained in:
Loose Cannon 2026-03-02 15:52:59 -05:00
parent 870bd4af38
commit 737af8798f
5 changed files with 48 additions and 25 deletions

View file

@ -25,6 +25,7 @@
#include "gui/DatabaseIcons.h"
#include <QDataStream>
#include <QDir>
#include <QFileInfo>
#include <QTextCodec>
#include <QXmlStreamWriter>
@ -287,9 +288,13 @@ namespace KeeShareSettings
return (type & ImportFrom) != 0 && !path.isEmpty();
}
bool Reference::isPerDeviceMode() const
bool Reference::isPerDeviceMode(const QDir& baseDir) const
{
return !path.isEmpty() && QFileInfo(path).isDir();
if (path.isEmpty()) {
return false;
}
const QString resolvedPath = baseDir.absoluteFilePath(path);
return QFileInfo(resolvedPath).isDir();
}
bool Reference::operator<(const Reference& other) const

View file

@ -21,6 +21,8 @@
#include <QSharedPointer>
#include <QUuid>
class QDir;
namespace Botan
{
class Private_Key;
@ -133,7 +135,7 @@ namespace KeeShareSettings
bool isValid() const;
bool isExporting() const;
bool isImporting() const;
bool isPerDeviceMode() const;
bool isPerDeviceMode(const QDir& baseDir) const;
bool operator<(const Reference& other) const;
bool operator==(const Reference& other) const;

View file

@ -102,6 +102,8 @@ void ShareObserver::reinitialize()
QMap<QString, QStringList> imported;
QMap<QString, QStringList> exported;
const QDir baseDir = QFileInfo(m_db->filePath()).absoluteDir();
for (const auto& share : shares) {
auto group = share.first;
auto& reference = share.second;
@ -113,7 +115,7 @@ void ShareObserver::reinitialize()
if (!reference.path.isEmpty() && reference.type != KeeShareSettings::Inactive) {
const auto newResolvedPath = resolvePath(reference.path, m_db);
if (reference.isPerDeviceMode()) {
if (reference.isPerDeviceMode(baseDir)) {
// Per-device mode: watch the directory for changes
auto dirWatcher = QSharedPointer<QFileSystemWatcher>::create();
if (QDir(newResolvedPath).exists()) {
@ -137,10 +139,10 @@ void ShareObserver::reinitialize()
if (reference.isImporting()) {
imported[reference.path] << group->name();
const auto resolvedDir = resolvePath(reference.path, m_db);
if (reference.isPerDeviceMode()) {
if (reference.isPerDeviceMode(baseDir)) {
// Per-device mode: import from all device files in the directory
const auto resolvedDir = resolvePath(reference.path, m_db);
const auto results = importPerDeviceShares(resolvedDir, reference, group);
for (const auto& result : results) {
if (!result.isValid()) {
@ -260,7 +262,8 @@ void ShareObserver::handleDirectoryUpdated(const QString& dirPath)
return;
}
auto reference = KeeShare::referenceOf(group);
if (!reference.isImporting() || !reference.isPerDeviceMode()) {
const QDir handleBaseDir = QFileInfo(m_db->filePath()).absoluteDir();
if (!reference.isImporting() || !reference.isPerDeviceMode(handleBaseDir)) {
return;
}
@ -326,7 +329,9 @@ QList<ShareObserver::Result> ShareObserver::importPerDeviceShares(
continue; // Skip own device's file
}
const auto filePath = dir.absoluteFilePath(fileName);
results << ShareImport::containerInto(filePath, reference, targetGroup);
auto result = ShareImport::containerInto(filePath, reference, targetGroup);
result.path = filePath;
results << result;
}
return results;
}
@ -398,11 +403,13 @@ QList<ShareObserver::Result> ShareObserver::exportShares()
return results;
}
const QDir exportBaseDir = QFileInfo(m_db->filePath()).absoluteDir();
for (auto it = references.cbegin(); it != references.cend(); ++it) {
auto reference = it.value().first();
const QString resolvedPath = resolvePath(reference.config.path, m_db);
if (reference.config.isPerDeviceMode()) {
if (reference.config.isPerDeviceMode(exportBaseDir)) {
// Per-device mode: export to {directory}/{DEVICE_ID}.kdbx
QDir dir(resolvedPath);
if (!dir.exists()) {

View file

@ -24,6 +24,7 @@
#include "keeshare/KeeShare.h"
#include <QDir>
#include <QFileInfo>
#include <QStandardPaths>
EditGroupWidgetKeeShare::EditGroupWidgetKeeShare(QWidget* parent)
@ -110,8 +111,9 @@ void EditGroupWidgetKeeShare::updateSharingState()
// Custom message for active KeeShare reference
const auto reference = KeeShare::referenceOf(m_temporaryGroup);
const QDir uiBaseDir = QFileInfo(m_database->filePath()).absoluteDir();
if (!reference.path.isEmpty()) {
if (reference.isPerDeviceMode()) {
if (reference.isPerDeviceMode(uiBaseDir)) {
// Per-device mode: path is a directory, show info message
m_ui->messageWidget->showMessage(
tr("Per-device sync mode: each device writes its own container in this directory.\n"
@ -151,7 +153,7 @@ void EditGroupWidgetKeeShare::updateSharingState()
conflictExport |= other.isExporting() && reference.isExporting();
// In per-device mode, import+export to the same directory is expected
// (export writes own device file, import reads other devices' files)
if (!reference.isPerDeviceMode()) {
if (!reference.isPerDeviceMode(uiBaseDir)) {
cycleImportExport |=
(other.isImporting() && reference.isExporting()) || (other.isExporting() && reference.isImporting());
}

View file

@ -187,42 +187,49 @@ void TestSharing::testSettingsSerialization_data()
void TestSharing::testPerDeviceMode()
{
QFETCH(QString, path);
QFETCH(QString, baseDir);
QFETCH(bool, expectedPerDevice);
KeeShareSettings::Reference reference;
reference.path = path;
reference.type = KeeShareSettings::SynchronizeWith;
QCOMPARE(reference.isPerDeviceMode(), expectedPerDevice);
QCOMPARE(reference.isPerDeviceMode(QDir(baseDir)), expectedPerDevice);
}
void TestSharing::testPerDeviceMode_data()
{
QTest::addColumn<QString>("path");
QTest::addColumn<QString>("baseDir");
QTest::addColumn<bool>("expectedPerDevice");
// Classic mode paths (file-based — don't need to exist on disk)
QTest::newRow("kdbx file") << "/some/path/share.kdbx" << false;
QTest::newRow("kdbx.share file") << "/some/path/share.kdbx.share" << false;
QTest::newRow("KDBX uppercase") << "/some/path/share.KDBX" << false;
QTest::newRow("KDBX.SHARE uppercase") << "/some/path/share.KDBX.SHARE" << false;
QTest::newRow("empty path") << "" << false;
QTest::newRow("nonexistent path") << "/nonexistent/path/nowhere" << false;
// Per-device mode paths (real directories on disk)
auto base = m_tempDir->path();
// Classic mode paths (file-based — don't need to exist on disk)
QTest::newRow("kdbx file") << "/some/path/share.kdbx" << base << false;
QTest::newRow("kdbx.share file") << "/some/path/share.kdbx.share" << base << false;
QTest::newRow("KDBX uppercase") << "/some/path/share.KDBX" << base << false;
QTest::newRow("KDBX.SHARE uppercase") << "/some/path/share.KDBX.SHARE" << base << false;
QTest::newRow("empty path") << "" << base << false;
QTest::newRow("nonexistent path") << "/nonexistent/path/nowhere" << base << false;
// Per-device mode paths (real directories on disk — absolute)
QDir(base).mkpath("syncdir");
QTest::newRow("directory path") << base + "/syncdir" << true;
QTest::newRow("directory path") << base + "/syncdir" << base << true;
QDir(base).mkpath("syncdir_slash");
QTest::newRow("directory trailing slash") << base + "/syncdir_slash/" << true;
QTest::newRow("directory trailing slash") << base + "/syncdir_slash/" << base << true;
QDir(base).mkpath("sub/shared");
QTest::newRow("nested directory") << base + "/sub/shared" << true;
QTest::newRow("nested directory") << base + "/sub/shared" << base << true;
QDir(base).mkpath("path.d/sync");
QTest::newRow("directory with dots") << base + "/path.d/sync" << true;
QTest::newRow("directory with dots") << base + "/path.d/sync" << base << true;
// Per-device mode with relative path (regression test: must resolve against baseDir)
QDir(base).mkpath("reldir");
QTest::newRow("relative directory") << "reldir" << base << true;
QTest::newRow("relative file") << "share.kdbx" << base << false;
}
void TestSharing::testPerDeviceModeImportExport()