From 0197f5c8a9ecae66f83b561057c35fd59c936e5b Mon Sep 17 00:00:00 2001 From: luckyrat Date: Wed, 14 Feb 2024 11:32:04 +0000 Subject: [PATCH] wip --- lib/src/kdbx_meta.dart | 42 ++++-- test/icon/kdbx_customicon_test.dart | 4 + test/merge/kdbx_merge_test.dart | 198 +++++++++++++++++----------- 3 files changed, 153 insertions(+), 91 deletions(-) diff --git a/lib/src/kdbx_meta.dart b/lib/src/kdbx_meta.dart index 32e5c7b..50ef077 100644 --- a/lib/src/kdbx_meta.dart +++ b/lib/src/kdbx_meta.dart @@ -72,11 +72,20 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext { .singleElement(KdbxXml.NODE_CUSTOM_ICONS) ?.let((el) sync* { for (final iconNode in el.findElements(KdbxXml.NODE_ICON)) { + final lastModified = iconNode + .singleElement(KdbxXml.NODE_LAST_MODIFICATION_TIME) + ?.innerText; yield KdbxCustomIcon( uuid: KdbxUuid( iconNode.singleTextNode(KdbxXml.NODE_UUID)), - data: base64.decode( - iconNode.singleTextNode(KdbxXml.NODE_DATA))); + data: base64 + .decode(iconNode.singleTextNode(KdbxXml.NODE_DATA)), + name: iconNode + .singleElement(KdbxXml.NODE_NAME) + ?.innerText, + lastModified: lastModified != null + ? DateTimeUtils.fromBase64(lastModified) + : null); } }) .map((e) => MapEntry(e.uuid, e)) @@ -104,6 +113,10 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext { modify(() => _customIcons[customIcon.uuid] = customIcon); } + void modifyCustomIcon(KdbxCustomIcon customIcon) { + modify(() => _customIcons[customIcon.uuid] = customIcon); + } + void removeCustomIcon(KdbxUuid id) { if (!_customIcons.containsKey(id)) { return; @@ -274,8 +287,7 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext { mergeKdbxMetaCustomDataWithDates( customData, other.customData, ctx, otherIsNewer); - mergeCustomIconsWithDates( - _customIcons, other._customIcons, ctx, otherIsNewer); + mergeCustomIconsWithDates(_customIcons, other._customIcons, ctx); // merge custom icons // Unused icons will be cleaned up later // //TODO: Use modified dates for better merging? @@ -333,31 +345,33 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext { } void mergeCustomIconsWithDates( - Map local, - Map other, - MergeContext ctx, - bool assumeRemoteIsNewerWhenDatesMissing) { + Map local, + Map other, + MergeContext ctx, + ) { for (final entry in other.entries) { final otherKey = entry.key; final otherItem = entry.value; final existingItem = local[otherKey]; if (existingItem != null) { - if ((existingItem.lastModified == null || - otherItem.lastModified == null) && - assumeRemoteIsNewerWhenDatesMissing) { + if (existingItem.lastModified == null) { local[otherKey] = KdbxCustomIcon( uuid: otherItem.uuid, data: otherItem.data, lastModified: otherItem.lastModified ?? clock.now().toUtc(), name: otherItem.name, ); - } else if (existingItem.lastModified != null && - otherItem.lastModified != null && + } else if (otherItem.lastModified != null && otherItem.lastModified!.isAfter(existingItem.lastModified!)) { local[otherKey] = otherItem; } } else if (!ctx.deletedObjects.containsKey(otherKey)) { - local[otherKey] = otherItem; + local[otherKey] = KdbxCustomIcon( + uuid: otherItem.uuid, + data: otherItem.data, + lastModified: otherItem.lastModified ?? clock.now().toUtc(), + name: otherItem.name, + ); } } } diff --git a/test/icon/kdbx_customicon_test.dart b/test/icon/kdbx_customicon_test.dart index 3afa982..4741714 100644 --- a/test/icon/kdbx_customicon_test.dart +++ b/test/icon/kdbx_customicon_test.dart @@ -23,5 +23,9 @@ void main() { file.body.cleanup(); // actually deleted expect(file.body.meta.customIcons.length, 1); + expect( + file.body.deletedObjects.any((deletedObj) => + deletedObj.uuid.uuid == entry.customIconUuid.get()?.uuid), + true); }); } diff --git a/test/merge/kdbx_merge_test.dart b/test/merge/kdbx_merge_test.dart index 8ad8a0c..3032b42 100644 --- a/test/merge/kdbx_merge_test.dart +++ b/test/merge/kdbx_merge_test.dart @@ -75,33 +75,32 @@ void main() { //TODO: https://github.com/authpass/authpass/issues/335 group('Real merges', () { - - final icon1 = KdbxCustomIcon( - uuid: KdbxUuid.random(), - data: Uint8List.fromList([1,2,3]), - lastModified: fakeClock.now().toUtc(), - name: 'icon1', - ); - final icon2 = KdbxCustomIcon( - uuid: KdbxUuid.random(), - data: Uint8List.fromList([4,5,6]), - lastModified: fakeClock.now().add(const Duration(minutes: 5)).toUtc(), - name: 'icon2', - ); - final icon3 = KdbxCustomIcon( - uuid: KdbxUuid.random(), - data: Uint8List.fromList([7,8,9]), - lastModified: fakeClock.now().add(const Duration(minutes: 10)).toUtc(), - name: 'icon3', - ); - final icon4 = KdbxCustomIcon( - uuid: KdbxUuid.random(), - data: Uint8List.fromList([10,11,12]), - ); - final icon5 = KdbxCustomIcon( - uuid: KdbxUuid.random(), - data: Uint8List.fromList([13,14,15]), - ); + final icon1 = KdbxCustomIcon( + uuid: KdbxUuid.random(), + data: Uint8List.fromList([1, 2, 3]), + lastModified: fakeClock.now().toUtc(), + name: 'icon1', + ); + final icon2 = KdbxCustomIcon( + uuid: KdbxUuid.random(), + data: Uint8List.fromList([4, 5, 6]), + lastModified: fakeClock.now().add(const Duration(minutes: 5)).toUtc(), + name: 'icon2', + ); + final icon3 = KdbxCustomIcon( + uuid: KdbxUuid.random(), + data: Uint8List.fromList([7, 8, 9]), + lastModified: fakeClock.now().add(const Duration(minutes: 10)).toUtc(), + name: 'icon3', + ); + final icon4 = KdbxCustomIcon( + uuid: KdbxUuid.random(), + data: Uint8List.fromList([10, 11, 12]), + ); + final icon5 = KdbxCustomIcon( + uuid: KdbxUuid.random(), + data: Uint8List.fromList([13, 14, 15]), + ); test('Local file custom data wins', () async { await withClock(fakeClock, () async { final file = await TestUtil.createRealFile(proceedSeconds); @@ -262,76 +261,121 @@ void main() { ); }); - test('Local file custom icon wins', () async { + test('Local v4.0 file gets all custom icons', () async { await withClock(fakeClock, () async { final file = await TestUtil.createRealFile(proceedSeconds); - + file.header.upgradeMinor(4, 0); final fileMod = await TestUtil.saveAndRead(file); - final fileReverse = await TestUtil.saveAndRead(file); - fileMod.body.meta.addCustomIcon(icon4); + final allEntries = file.body.rootGroup.getAllEntries().values.toList(); + final entry1 = allEntries[0]; + + final allEntriesMod = + fileMod.body.rootGroup.getAllEntries().values.toList(); + final entry2Mod = allEntriesMod[1]; + + entry1.customIcon = icon4; proceedSeconds(10); - file.body.meta.addCustomIcon(icon5); - ......................... - fileMod.body.meta.customData['custom2'] = - (value: 'custom value 3', lastModified: null); + entry2Mod.customIcon = icon5; final file2 = await TestUtil.saveAndRead(fileMod); - final file2Reverse = await TestUtil.saveAndRead(fileMod); - final merge = file.merge(file2); - final set = Set.from(merge.merged.keys); - expect(set, hasLength(5)); - expect(file.body.meta.customData['custom1'], - (value: 'custom value 1', lastModified: null)); - expect(file.body.meta.customData['custom2'], - (value: 'custom value 3', lastModified: null)); + file.merge(file2); + final sutFile = await TestUtil.saveAndRead(file); + final sutIcon4 = sutFile.body.meta.customIcons[icon4.uuid]; + final sutIcon5 = sutFile.body.meta.customIcons[icon5.uuid]; + expect(sutIcon4?.uuid.uuid, icon4.uuid.uuid); + expect(sutIcon4?.lastModified, null); + expect(sutIcon5?.uuid.uuid, icon5.uuid.uuid); + expect(sutIcon5?.lastModified, null); + }); + }); + test( + 'Local v4.1 file gets all custom icons and new modified date for merged icon', + () async { + await withClock(fakeClock, () async { + final file = await TestUtil.createRealFile(proceedSeconds); + final fileMod = await TestUtil.saveAndRead(file); - final mergeReverse = file2Reverse.merge(fileReverse); - final setReverse = Set.from(mergeReverse.merged.keys); - expect(setReverse, hasLength(5)); - expect(file2Reverse.body.meta.customData['custom1'], - (value: 'custom value 2', lastModified: null)); - expect(file2Reverse.body.meta.customData['custom2'], - (value: 'custom value 3', lastModified: null)); + final allEntries = file.body.rootGroup.getAllEntries().values.toList(); + final entry1 = allEntries[0]; + final entry2 = allEntries[1]; + + final allEntriesMod = + fileMod.body.rootGroup.getAllEntries().values.toList(); + final entry2Mod = allEntriesMod[1]; + + entry1.customIcon = icon4; + entry2.customIcon = icon5; + proceedSeconds(10); + entry2Mod.customIcon = icon5; + + final file2 = await TestUtil.saveAndRead(fileMod); + + file.merge(file2); + final sutFile = await TestUtil.saveAndRead(file); + final sutIcon4 = sutFile.body.meta.customIcons[icon4.uuid]; + final sutIcon5 = sutFile.body.meta.customIcons[icon5.uuid]; + expect(sutIcon4?.uuid.uuid, icon4.uuid.uuid); + expect(sutIcon4?.lastModified, null); + expect(sutIcon5?.uuid.uuid, icon5.uuid.uuid); + expect(sutIcon5?.lastModified, isA()); }); }); test('Newer file custom icon wins', () async { await withClock(fakeClock, () async { final file = await TestUtil.createRealFile(proceedSeconds); - - final time1 = fakeClock.now().toUtc(); final fileMod = await TestUtil.saveAndRead(file); - fileMod.body.meta.customData['custom1'] = - (value: 'custom value 2', lastModified: time1); - proceedSeconds(10); - final time2 = fakeClock.now().toUtc(); - file.body.meta.customData['custom1'] = - (value: 'custom value 1', lastModified: time2); - fileMod.body.meta.customData['custom2'] = - (value: 'custom value 3', lastModified: time2); + final allEntries = file.body.rootGroup.getAllEntries().values.toList(); + final entry1 = allEntries[0]; + final entry2 = allEntries[1]; - final fileReverse = await TestUtil.saveAndRead(file); - final file2 = await TestUtil.saveAndRead(fileMod); - final file2Reverse = await TestUtil.saveAndRead(fileMod); + final allEntriesMod = + fileMod.body.rootGroup.getAllEntries().values.toList(); + final entry2Mod = allEntriesMod[1]; - final merge = file.merge(file2); - final set = Set.from(merge.merged.keys); - expect(set, hasLength(5)); - expect(file.body.meta.customData['custom1'], - (value: 'custom value 1', lastModified: time2)); - expect(file.body.meta.customData['custom2'], - (value: 'custom value 3', lastModified: time2)); + entry1.customIcon = icon1; + entry2.customIcon = icon2; + proceedSeconds(10); + entry2Mod.customIcon = icon3; + final iconModTime = + icon2.lastModified?.add(const Duration(minutes: 1)).toUtc(); + final iconModData = Uint8List.fromList([100, 101, 102]); + file.body.meta.modifyCustomIcon(KdbxCustomIcon( + uuid: icon2.uuid, + data: iconModData, + lastModified: iconModTime, + name: 'modified', + )); + + final fileTarget = await TestUtil.saveAndRead(file); + final file2 = await TestUtil.saveAndRead(fileMod); - final mergeReverse = file2Reverse.merge(fileReverse); - final setReverse = Set.from(mergeReverse.merged.keys); - expect(setReverse, hasLength(5)); - expect(file2Reverse.body.meta.customData['custom1'], - (value: 'custom value 1', lastModified: time2)); - expect(file2Reverse.body.meta.customData['custom2'], - (value: 'custom value 3', lastModified: time2)); + fileTarget.merge(file2); + final sutFile = await TestUtil.saveAndRead(fileTarget); + final sutIcon1 = sutFile.body.meta.customIcons[icon1.uuid]; + final sutIcon2 = sutFile.body.meta.customIcons[icon2.uuid]; + final sutIcon3 = sutFile.body.meta.customIcons[icon3.uuid]; + expect(sutIcon1?.uuid.uuid, icon1.uuid.uuid); + expect(sutIcon1?.lastModified, icon1.lastModified); + expect(sutIcon2?.uuid.uuid, icon2.uuid.uuid); + expect(sutIcon2?.lastModified, iconModTime); + expect(sutIcon2?.data, iconModData); + expect(sutIcon2?.name, 'modified'); + expect(sutIcon3?.uuid.uuid, icon3.uuid.uuid); + expect(sutIcon3?.lastModified, icon3.lastModified); + expect(sutIcon3?.name, icon3.name); + expect(sutIcon3?.data, icon3.data); + expect( + sutFile.body.rootGroup + .getAllEntries() + .values + .toList()[1] + .customIcon + ?.uuid, + icon3.uuid); }); }); });