Browse Source

very trivial merge test.

pull/3/head
Herbert Poul 4 years ago
parent
commit
76ba4aedbf
  1. 22
      lib/src/kdbx_entry.dart
  2. 4
      lib/src/kdbx_file.dart
  3. 12
      lib/src/kdbx_format.dart
  4. 2
      lib/src/kdbx_group.dart
  5. 70
      test/merge/kdbx_merge_test.dart

22
lib/src/kdbx_entry.dart

@ -77,6 +77,11 @@ extension KdbxEntryInternal on KdbxEntry {
other._overwriteNodes, other._overwriteNodes,
); );
// overwrite all strings // overwrite all strings
final stringsDiff = _diffMap(_strings, other._strings);
if (stringsDiff.isNotEmpty) {
overwriteContext.trackChange(this,
node: 'strings', debug: 'changed: ${stringsDiff.join(',')}');
}
_strings.clear(); _strings.clear();
_strings.addAll(other._strings); _strings.addAll(other._strings);
// overwrite all binaries // overwrite all binaries
@ -94,6 +99,17 @@ extension KdbxEntryInternal on KdbxEntry {
} }
} }
} }
List<String> _diffMap(Map<Object, Object> a, Map<Object, Object> b) {
final keys = {...a.keys, ...b.keys};
final ret = <String>[];
for (final key in keys) {
if (a[key] != b[key]) {
ret.add(key.toString());
}
}
return ret;
}
} }
class KdbxEntry extends KdbxObject { class KdbxEntry extends KdbxObject {
@ -337,9 +353,11 @@ class KdbxEntry extends KdbxObject {
void merge(MergeContext mergeContext, KdbxEntry other) { void merge(MergeContext mergeContext, KdbxEntry other) {
assertSameUuid(other, 'merge'); assertSameUuid(other, 'merge');
if (other.wasModifiedAfter(this)) { if (other.wasModifiedAfter(this)) {
_logger.finest('$this has incoming changes.');
// other object is newer, create new history entry and copy fields. // other object is newer, create new history entry and copy fields.
modify(() => _overwriteFrom(mergeContext, other)); modify(() => _overwriteFrom(mergeContext, other));
} else if (wasModifiedAfter(other)) { } else if (wasModifiedAfter(other)) {
_logger.finest('$this has outgoing changes.');
// we are newer. check if the old revision lives on in our history. // we are newer. check if the old revision lives on in our history.
final ourLastModificationTime = times.lastModificationTime.get(); final ourLastModificationTime = times.lastModificationTime.get();
final historyEntry = _findHistoryEntry(history, ourLastModificationTime); final historyEntry = _findHistoryEntry(history, ourLastModificationTime);
@ -348,6 +366,8 @@ class KdbxEntry extends KdbxObject {
// it to history. // it to history.
history.add(other.cloneInto(parent, toHistoryEntry: true)); history.add(other.cloneInto(parent, toHistoryEntry: true));
} }
} else {
_logger.finest('$this has no changes.');
} }
// copy missing history entries. // copy missing history entries.
for (final otherHistoryEntry in other.history) { for (final otherHistoryEntry in other.history) {
@ -367,6 +387,6 @@ class KdbxEntry extends KdbxObject {
@override @override
String toString() { String toString() {
return 'KdbxGroup{uuid=$uuid,name=$label}'; return 'KdbxEntry{uuid=$uuid,name=$label}';
} }
} }

4
lib/src/kdbx_file.dart

@ -123,12 +123,12 @@ class KdbxFile {
/// Merges the given file into this file. /// Merges the given file into this file.
/// Both files must have the same origin (ie. same root group UUID). /// Both files must have the same origin (ie. same root group UUID).
/// FIXME: THiS iS NOT YET FINISHED, DO NOT USE. /// 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) { if (other.body.rootGroup.uuid != body.rootGroup.uuid) {
throw KdbxUnsupportedException( throw KdbxUnsupportedException(
'Root groups of source and dest file do not match.'); 'Root groups of source and dest file do not match.');
} }
body.merge(other.body); return body.merge(other.body);
} }
} }

12
lib/src/kdbx_format.dart

@ -302,7 +302,7 @@ class KdbxBody extends KdbxNode {
concat([rootGroup.getAllGroups(), rootGroup.getAllEntries()]) concat([rootGroup.getAllGroups(), rootGroup.getAllEntries()])
.map((e) => MapEntry(e.uuid, e))); .map((e) => MapEntry(e.uuid, e)));
void merge(KdbxBody other) { MergeContext merge(KdbxBody other) {
// sync deleted objects. // sync deleted objects.
final deleted = final deleted =
Map.fromEntries(ctx._deletedObjects.map((e) => MapEntry(e.uuid, e))); Map.fromEntries(ctx._deletedObjects.map((e) => MapEntry(e.uuid, e)));
@ -341,7 +341,7 @@ class KdbxBody extends KdbxNode {
// FIXME do some cleanup. // FIXME do some cleanup.
_logger.info('Finished merging. ${mergeContext.debugChanges()}'); _logger.info('Finished merging:\n${mergeContext.debugChanges()}');
final incomingObjects = other._createObjectIndex(); final incomingObjects = other._createObjectIndex();
_logger.info('Merged: ${mergeContext.merged} vs. ' _logger.info('Merged: ${mergeContext.merged} vs. '
'(local objects: ${mergeContext.objectIndex.length}, ' '(local objects: ${mergeContext.objectIndex.length}, '
@ -351,6 +351,7 @@ class KdbxBody extends KdbxNode {
if (mergeContext.merged.keys.length != mergeContext.objectIndex.length) { if (mergeContext.merged.keys.length != mergeContext.objectIndex.length) {
// TODO figure out what went wrong. // TODO figure out what went wrong.
} }
return mergeContext;
} }
xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) { xml.XmlDocument generateXml(ProtectedSaltGenerator saltGenerator) {
@ -419,6 +420,10 @@ class MergeChange {
/// the name of the subnode of [object]. /// the name of the subnode of [object].
final String node; final String node;
final String debug; final String debug;
String debugString() {
return [node, debug].where((e) => e != null).join(' ');
}
} }
class MergeContext implements OverwriteContext { class MergeContext implements OverwriteContext {
@ -451,8 +456,7 @@ class MergeContext implements OverwriteContext {
return group.entries return group.entries
.map((e) => [ .map((e) => [
e.key.toString(), e.key.toString(),
': ', ...e.value.map((e) => e.debugString()),
...e.value.map((e) => e.toString()),
].join('\n ')) ].join('\n '))
.join('\n'); .join('\n');
} }

2
lib/src/kdbx_group.dart

@ -124,6 +124,7 @@ class KdbxGroup extends KdbxObject {
other._entries, other._entries,
importToHere: (other) => other.cloneInto(this), importToHere: (other) => other.cloneInto(this),
); );
mergeContext.markAsMerged(this);
} }
void _mergeSubObjects<T extends KdbxObject>( void _mergeSubObjects<T extends KdbxObject>(
@ -161,7 +162,6 @@ class KdbxGroup extends KdbxObject {
meObj.merge(mergeContext, otherObj); meObj.merge(mergeContext, otherObj);
} }
} }
mergeContext.markAsMerged(this);
} }
List<KdbxSubNode> get _overwriteNodes => [ List<KdbxSubNode> get _overwriteNodes => [

70
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<KdbxUuid>.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<KdbxUuid>.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;
}
Loading…
Cancel
Save