diff --git a/lib/src/kdbx_meta.dart b/lib/src/kdbx_meta.dart index 4d1fbfb..32e5c7b 100644 --- a/lib/src/kdbx_meta.dart +++ b/lib/src/kdbx_meta.dart @@ -225,7 +225,12 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext { (customIcon) => XmlUtils.createNode(KdbxXml.NODE_ICON, [ XmlUtils.createTextNode(KdbxXml.NODE_UUID, customIcon.uuid.uuid), XmlUtils.createTextNode( - KdbxXml.NODE_DATA, base64.encode(customIcon.data)) + KdbxXml.NODE_DATA, base64.encode(customIcon.data)), + if (ctx.version > KdbxVersion.V4 && customIcon.name != null) + XmlUtils.createTextNode(KdbxXml.NODE_NAME, customIcon.name!), + if (ctx.version > KdbxVersion.V4 && customIcon.lastModified != null) + XmlUtils.createTextNode(KdbxXml.NODE_LAST_MODIFICATION_TIME, + DateTimeUtils.toBase64(customIcon.lastModified!)), ]), )), ); @@ -269,12 +274,14 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext { mergeKdbxMetaCustomDataWithDates( customData, other.customData, ctx, otherIsNewer); + mergeCustomIconsWithDates( + _customIcons, other._customIcons, ctx, otherIsNewer); // merge custom icons // Unused icons will be cleaned up later - //TODO: Use modified dates for better merging? - for (final otherCustomIcon in other._customIcons.values) { - _customIcons[otherCustomIcon.uuid] ??= otherCustomIcon; - } + // //TODO: Use modified dates for better merging? + // for (final otherCustomIcon in other._customIcons.values) { + // _customIcons[otherCustomIcon.uuid] ??= otherCustomIcon; + // } if (other.entryTemplatesGroupChanged.isAfter(entryTemplatesGroupChanged)) { entryTemplatesGroup.set(other.entryTemplatesGroup.get()); @@ -325,6 +332,36 @@ class KdbxMeta extends KdbxNode implements KdbxNodeContext { } } + void mergeCustomIconsWithDates( + Map local, + Map other, + MergeContext ctx, + bool assumeRemoteIsNewerWhenDatesMissing) { + 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) { + 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 && + otherItem.lastModified!.isAfter(existingItem.lastModified!)) { + local[otherKey] = otherItem; + } + } else if (!ctx.deletedObjects.containsKey(otherKey)) { + local[otherKey] = otherItem; + } + } + } + // Import changes in [other] into this meta data. void import(KdbxMeta other) { // import custom icons @@ -546,11 +583,16 @@ class BrowserDbSettings { } class KdbxCustomIcon { - KdbxCustomIcon({required this.uuid, required this.data}); + KdbxCustomIcon( + {required this.uuid, required this.data, this.name, this.lastModified}); /// uuid of the icon, must be unique within each file. final KdbxUuid uuid; /// Encoded png data of the image. will be base64 encoded into the kdbx file. final Uint8List data; + + final String? name; + + final DateTime? lastModified; } diff --git a/lib/src/kdbx_xml.dart b/lib/src/kdbx_xml.dart index 6ae10cd..1b6a324 100644 --- a/lib/src/kdbx_xml.dart +++ b/lib/src/kdbx_xml.dart @@ -44,6 +44,9 @@ class KdbxXml { /// CustomIcons >> Icon >> Data static const NODE_DATA = 'Data'; + /// CustomIcons >> Icon >> Name + static const NODE_NAME = 'Name'; + /// Used for objects UUID and CustomIcons static const NODE_UUID = 'UUID'; diff --git a/test/merge/kdbx_merge_test.dart b/test/merge/kdbx_merge_test.dart index 60d126b..8ad8a0c 100644 --- a/test/merge/kdbx_merge_test.dart +++ b/test/merge/kdbx_merge_test.dart @@ -75,6 +75,33 @@ 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]), + ); test('Local file custom data wins', () async { await withClock(fakeClock, () async { final file = await TestUtil.createRealFile(proceedSeconds); @@ -234,6 +261,79 @@ void main() { ), ); }); + + test('Local file custom icon wins', () async { + await withClock(fakeClock, () async { + final file = await TestUtil.createRealFile(proceedSeconds); + + final fileMod = await TestUtil.saveAndRead(file); + final fileReverse = await TestUtil.saveAndRead(file); + + fileMod.body.meta.addCustomIcon(icon4); + proceedSeconds(10); + file.body.meta.addCustomIcon(icon5); + ......................... + fileMod.body.meta.customData['custom2'] = + (value: 'custom value 3', lastModified: null); + + 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)); + + 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)); + }); + }); + + 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 fileReverse = await TestUtil.saveAndRead(file); + 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: time2)); + expect(file.body.meta.customData['custom2'], + (value: 'custom value 3', lastModified: time2)); + + 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)); + }); + }); }); group('Moving entries', () {