diff --git a/lib/src/kdbx_entry.dart b/lib/src/kdbx_entry.dart index edd5628..338f3e0 100644 --- a/lib/src/kdbx_entry.dart +++ b/lib/src/kdbx_entry.dart @@ -77,6 +77,11 @@ extension KdbxEntryInternal on KdbxEntry { other._overwriteNodes, ); // overwrite all strings + final stringsDiff = _diffMap(_strings, other._strings); + if (stringsDiff.isNotEmpty) { + overwriteContext.trackChange(this, + node: 'strings', debug: 'changed: ${stringsDiff.join(',')}'); + } _strings.clear(); _strings.addAll(other._strings); // overwrite all binaries @@ -94,6 +99,17 @@ extension KdbxEntryInternal on KdbxEntry { } } } + + List _diffMap(Map a, Map b) { + final keys = {...a.keys, ...b.keys}; + final ret = []; + for (final key in keys) { + if (a[key] != b[key]) { + ret.add(key.toString()); + } + } + return ret; + } } class KdbxEntry extends KdbxObject { @@ -337,9 +353,11 @@ class KdbxEntry extends KdbxObject { void merge(MergeContext mergeContext, KdbxEntry other) { assertSameUuid(other, 'merge'); if (other.wasModifiedAfter(this)) { + _logger.finest('$this has incoming changes.'); // other object is newer, create new history entry and copy fields. modify(() => _overwriteFrom(mergeContext, other)); } else if (wasModifiedAfter(other)) { + _logger.finest('$this has outgoing changes.'); // we are newer. check if the old revision lives on in our history. final ourLastModificationTime = times.lastModificationTime.get(); final historyEntry = _findHistoryEntry(history, ourLastModificationTime); @@ -348,6 +366,8 @@ class KdbxEntry extends KdbxObject { // it to history. history.add(other.cloneInto(parent, toHistoryEntry: true)); } + } else { + _logger.finest('$this has no changes.'); } // copy missing history entries. for (final otherHistoryEntry in other.history) { @@ -367,6 +387,6 @@ class KdbxEntry extends KdbxObject { @override String toString() { - return 'KdbxGroup{uuid=$uuid,name=$label}'; + return 'KdbxEntry{uuid=$uuid,name=$label}'; } } diff --git a/lib/src/kdbx_file.dart b/lib/src/kdbx_file.dart index 3477872..888e422 100644 --- a/lib/src/kdbx_file.dart +++ b/lib/src/kdbx_file.dart @@ -123,12 +123,12 @@ class KdbxFile { /// Merges the given file into this file. /// Both files must have the same origin (ie. same root group UUID). /// FIXME: THiS iS NOT YET FINISHED, DO NOT USE. - void merge(KdbxFile other) { + MergeContext merge(KdbxFile other) { if (other.body.rootGroup.uuid != body.rootGroup.uuid) { throw KdbxUnsupportedException( 'Root groups of source and dest file do not match.'); } - body.merge(other.body); + return body.merge(other.body); } } diff --git a/lib/src/kdbx_format.dart b/lib/src/kdbx_format.dart index 96bff0b..46a55c9 100644 --- a/lib/src/kdbx_format.dart +++ b/lib/src/kdbx_format.dart @@ -302,7 +302,7 @@ class KdbxBody extends KdbxNode { concat([rootGroup.getAllGroups(), rootGroup.getAllEntries()]) .map((e) => MapEntry(e.uuid, e))); - void merge(KdbxBody other) { + MergeContext merge(KdbxBody other) { // sync deleted objects. final deleted = Map.fromEntries(ctx._deletedObjects.map((e) => MapEntry(e.uuid, e))); @@ -341,7 +341,7 @@ class KdbxBody extends KdbxNode { // FIXME do some cleanup. - _logger.info('Finished merging. ${mergeContext.debugChanges()}'); + _logger.info('Finished merging:\n${mergeContext.debugChanges()}'); final incomingObjects = other._createObjectIndex(); _logger.info('Merged: ${mergeContext.merged} vs. ' '(local objects: ${mergeContext.objectIndex.length}, ' @@ -351,6 +351,7 @@ class KdbxBody extends KdbxNode { if (mergeContext.merged.keys.length != mergeContext.objectIndex.length) { // TODO figure out what went wrong. } + return mergeContext; } xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) { @@ -419,6 +420,10 @@ class MergeChange { /// the name of the subnode of [object]. final String node; final String debug; + + String debugString() { + return [node, debug].where((e) => e != null).join(' '); + } } class MergeContext implements OverwriteContext { @@ -451,8 +456,7 @@ class MergeContext implements OverwriteContext { return group.entries .map((e) => [ e.key.toString(), - ': ', - ...e.value.map((e) => e.toString()), + ...e.value.map((e) => e.debugString()), ].join('\n ')) .join('\n'); } diff --git a/lib/src/kdbx_group.dart b/lib/src/kdbx_group.dart index 2af4ea6..4c1d8a7 100644 --- a/lib/src/kdbx_group.dart +++ b/lib/src/kdbx_group.dart @@ -124,6 +124,7 @@ class KdbxGroup extends KdbxObject { other._entries, importToHere: (other) => other.cloneInto(this), ); + mergeContext.markAsMerged(this); } void _mergeSubObjects( @@ -161,7 +162,6 @@ class KdbxGroup extends KdbxObject { meObj.merge(mergeContext, otherObj); } } - mergeContext.markAsMerged(this); } List get _overwriteNodes => [ diff --git a/test/merge/kdbx_merge_test.dart b/test/merge/kdbx_merge_test.dart new file mode 100644 index 0000000..a758fd8 --- /dev/null +++ b/test/merge/kdbx_merge_test.dart @@ -0,0 +1,70 @@ +import 'package:clock/clock.dart'; +import 'package:kdbx/kdbx.dart'; +import 'package:test/test.dart'; + +import '../internal/test_utils.dart'; +import 'package:logging/logging.dart'; + +final _logger = Logger('kdbx_merge_test'); + +void main() { + TestUtil.setupLogging(); + DateTime now = DateTime.fromMillisecondsSinceEpoch(0); + + final fakeClock = Clock(() => now); + final kdbxFormat = TestUtil.kdbxFormat(); + void proceedSeconds(int seconds) { + now = now.add(Duration(seconds: seconds)); + } + + setUp(() { + DateTime.fromMillisecondsSinceEpoch(0); + }); + group('Simple merges', () { + test('Noop merge', () async { + final file = kdbxFormat.create( + Credentials.composite(ProtectedValue.fromString('asdf'), null), + 'example'); + _createEntry(file, file.body.rootGroup, 'test1', 'test1'); + final file2 = await TestUtil.saveAndRead(file); + final merge = file.merge(file2); + final set = Set.from(merge.merged.keys); + expect(set, hasLength(2)); + expect(merge.changes, isEmpty); + }); + test('Username change', () async { + await withClock(fakeClock, () async { + final file = kdbxFormat.create( + Credentials.composite(ProtectedValue.fromString('asdf'), null), + 'example'); + _createEntry(file, file.body.rootGroup, 'test1', 'test1'); + + final fileMod = await TestUtil.saveAndRead(file); + proceedSeconds(10); + + fileMod.body.rootGroup.entries.first + .setString(KdbxKey('UserName'), PlainValue('changed.')); + _logger.info('mod date: ' + + fileMod.body.rootGroup.entries.first.times.lastModificationTime + .get() + .toString()); + final file2 = await TestUtil.saveAndRead(fileMod); + + _logger.info('\n\n\nstarting merge.\n'); + final merge = file.merge(file2); + final set = Set.from(merge.merged.keys); + expect(set, hasLength(2)); + expect(merge.changes, hasLength(1)); + }); + }); + }); +} + +KdbxEntry _createEntry( + KdbxFile file, KdbxGroup group, String username, String password) { + final entry = KdbxEntry.create(file, group); + group.addEntry(entry); + entry.setString(KdbxKey('UserName'), PlainValue(username)); + entry.setString(KdbxKey('Password'), ProtectedValue.fromString(password)); + return entry; +}