1
1
use std:: collections:: HashMap ;
2
2
3
- use chrono:: NaiveDateTime ;
4
3
use secstr:: SecStr ;
5
4
use uuid:: Uuid ;
6
5
6
+ #[ cfg( feature = "_merge" ) ]
7
+ use crate :: db:: merge:: { MergeError , MergeLog } ;
8
+ #[ cfg( all( test, feature = "_merge" ) ) ]
9
+ use std:: { thread, time} ;
10
+
7
11
use crate :: db:: { Color , CustomData , Times } ;
8
12
9
13
#[ cfg( feature = "totp" ) ]
@@ -41,6 +45,131 @@ impl Entry {
41
45
..Default :: default ( )
42
46
}
43
47
}
48
+
49
+ #[ cfg( feature = "_merge" ) ]
50
+ pub ( crate ) fn merge ( & self , other : & Entry ) -> Result < ( Option < Entry > , MergeLog ) , MergeError > {
51
+ let mut log = MergeLog :: default ( ) ;
52
+
53
+ let source_last_modification = match other. times . get_last_modification ( ) {
54
+ Some ( t) => * t,
55
+ None => {
56
+ log. warnings . push ( format ! (
57
+ "Entry {} did not have a last modification timestamp" ,
58
+ other. uuid
59
+ ) ) ;
60
+ Times :: epoch ( )
61
+ }
62
+ } ;
63
+ let destination_last_modification = match self . times . get_last_modification ( ) {
64
+ Some ( t) => * t,
65
+ None => {
66
+ log. warnings . push ( format ! (
67
+ "Entry {} did not have a last modification timestamp" ,
68
+ self . uuid
69
+ ) ) ;
70
+ Times :: now ( )
71
+ }
72
+ } ;
73
+
74
+ if destination_last_modification == source_last_modification {
75
+ if !self . has_diverged_from ( & other) {
76
+ // This should never happen.
77
+ // This means that an entry was updated without updating the last modification
78
+ // timestamp.
79
+ return Err ( MergeError :: EntryModificationTimeNotUpdated (
80
+ other. uuid . to_string ( ) ,
81
+ ) ) ;
82
+ }
83
+ return Ok ( ( None , log) ) ;
84
+ }
85
+
86
+ let ( mut merged_entry, entry_merge_log) = match destination_last_modification > source_last_modification
87
+ {
88
+ true => self . merge_history ( other) ?,
89
+ false => other. clone ( ) . merge_history ( & self ) ?,
90
+ } ;
91
+
92
+ // The location changed timestamp is handled separately when merging two databases.
93
+ if let Some ( location_changed_timestamp) = self . times . get_location_changed ( ) {
94
+ merged_entry
95
+ . times
96
+ . set_location_changed ( * location_changed_timestamp) ;
97
+ }
98
+
99
+ return Ok ( ( Some ( merged_entry) , entry_merge_log) ) ;
100
+ }
101
+
102
+ #[ cfg( feature = "_merge" ) ]
103
+ pub ( crate ) fn merge_history ( & self , other : & Entry ) -> Result < ( Entry , MergeLog ) , MergeError > {
104
+ let mut log = MergeLog :: default ( ) ;
105
+
106
+ let mut source_history = match & other. history {
107
+ Some ( h) => h. clone ( ) ,
108
+ None => {
109
+ log. warnings . push ( format ! (
110
+ "Entry {} from source database had no history." ,
111
+ self . uuid
112
+ ) ) ;
113
+ History :: default ( )
114
+ }
115
+ } ;
116
+ let mut destination_history = match & self . history {
117
+ Some ( h) => h. clone ( ) ,
118
+ None => {
119
+ log. warnings . push ( format ! (
120
+ "Entry {} from destination database had no history." ,
121
+ self . uuid
122
+ ) ) ;
123
+ History :: default ( )
124
+ }
125
+ } ;
126
+ let mut response = self . clone ( ) ;
127
+
128
+ if other. has_uncommitted_changes ( ) {
129
+ log. warnings . push ( format ! (
130
+ "Entry {} from source database has uncommitted changes." ,
131
+ self . uuid
132
+ ) ) ;
133
+ source_history. add_entry ( other. clone ( ) ) ;
134
+ }
135
+
136
+ // TODO we should probably check for uncommitted changes in the destination
137
+ // database here too for consistency.
138
+
139
+ let history_merge_log = destination_history. merge_with ( & source_history) ?;
140
+ response. history = Some ( destination_history) ;
141
+
142
+ Ok ( ( response, log. merge_with ( & history_merge_log) ) )
143
+ }
144
+
145
+ #[ cfg( all( test, feature = "_merge" ) ) ]
146
+ // Convenience function used in unit tests, to make sure that:
147
+ // 1. The history gets updated after changing a field
148
+ // 2. We wait a second before commiting the changes so that the timestamp is not the same
149
+ // as it previously was. This is necessary since the timestamps in the KDBX format
150
+ // do not preserve the msecs.
151
+ pub ( crate ) fn set_field_and_commit ( & mut self , field_name : & str , field_value : & str ) {
152
+ self . fields . insert (
153
+ field_name. to_string ( ) ,
154
+ Value :: Unprotected ( field_value. to_string ( ) ) ,
155
+ ) ;
156
+ thread:: sleep ( time:: Duration :: from_secs ( 1 ) ) ;
157
+ self . update_history ( ) ;
158
+ }
159
+
160
+ #[ cfg( feature = "_merge" ) ]
161
+ // Convenience function used in when merging two entries
162
+ pub ( crate ) fn has_diverged_from ( & self , other_entry : & Entry ) -> bool {
163
+ let new_times = Times :: default ( ) ;
164
+
165
+ let mut self_without_times = self . clone ( ) ;
166
+ self_without_times. times = new_times. clone ( ) ;
167
+
168
+ let mut other_without_times = other_entry. clone ( ) ;
169
+ other_without_times. times = new_times. clone ( ) ;
170
+
171
+ !self_without_times. eq ( & other_without_times)
172
+ }
44
173
}
45
174
46
175
impl < ' a > Entry {
@@ -148,16 +277,14 @@ impl<'a> Entry {
148
277
return true ;
149
278
}
150
279
280
+ let new_times = Times :: default ( ) ;
281
+
151
282
let mut sanitized_entry = self . clone ( ) ;
152
- sanitized_entry
153
- . times
154
- . set_last_modification ( NaiveDateTime :: default ( ) ) ;
283
+ sanitized_entry. times = new_times. clone ( ) ;
155
284
sanitized_entry. history . take ( ) ;
156
285
157
286
let mut last_history_entry = history. entries . get ( 0 ) . unwrap ( ) . clone ( ) ;
158
- last_history_entry
159
- . times
160
- . set_last_modification ( NaiveDateTime :: default ( ) ) ;
287
+ last_history_entry. times = new_times. clone ( ) ;
161
288
last_history_entry. history . take ( ) ;
162
289
163
290
if sanitized_entry. eq ( & last_history_entry) {
@@ -225,6 +352,8 @@ pub struct History {
225
352
}
226
353
impl History {
227
354
pub fn add_entry ( & mut self , mut entry : Entry ) {
355
+ // DISCUSS: should we make sure that the last modification time is not the same
356
+ // or older than the entry at the top of the history?
228
357
if entry. history . is_some ( ) {
229
358
// Remove the history from the new history entry to avoid having
230
359
// an exponential number of history entries.
@@ -236,6 +365,70 @@ impl History {
236
365
pub fn get_entries ( & self ) -> & Vec < Entry > {
237
366
& self . entries
238
367
}
368
+
369
+ #[ cfg( all( test, feature = "_merge" ) ) ]
370
+ // Determines if the entries of the history are
371
+ // ordered by last modification time.
372
+ pub ( crate ) fn is_ordered ( & self ) -> bool {
373
+ let mut last_modification_time: Option < & chrono:: NaiveDateTime > = None ;
374
+ for entry in & self . entries {
375
+ if last_modification_time. is_none ( ) {
376
+ last_modification_time = entry. times . get_last_modification ( ) ;
377
+ }
378
+
379
+ let entry_modification_time = entry. times . get_last_modification ( ) . unwrap ( ) ;
380
+ // FIXME should we also handle equal modification times??
381
+ if last_modification_time. unwrap ( ) < entry_modification_time {
382
+ return false ;
383
+ }
384
+ last_modification_time = Some ( entry_modification_time) ;
385
+ }
386
+ true
387
+ }
388
+
389
+ // Merge both histories together.
390
+ #[ cfg( feature = "_merge" ) ]
391
+ pub ( crate ) fn merge_with ( & mut self , other : & History ) -> Result < MergeLog , MergeError > {
392
+ let mut log = MergeLog :: default ( ) ;
393
+ let mut new_history_entries: HashMap < chrono:: NaiveDateTime , Entry > = HashMap :: new ( ) ;
394
+
395
+ for history_entry in & self . entries {
396
+ let modification_time = history_entry. times . get_last_modification ( ) . unwrap ( ) ;
397
+ if new_history_entries. contains_key ( modification_time) {
398
+ return Err ( MergeError :: DuplicateHistoryEntries (
399
+ modification_time. to_string ( ) ,
400
+ history_entry. uuid . to_string ( ) ,
401
+ ) ) ;
402
+ }
403
+ new_history_entries. insert ( modification_time. clone ( ) , history_entry. clone ( ) ) ;
404
+ }
405
+
406
+ for history_entry in & other. entries {
407
+ let modification_time = history_entry. times . get_last_modification ( ) . unwrap ( ) ;
408
+ let existing_history_entry = new_history_entries. get ( modification_time) ;
409
+ if let Some ( existing_history_entry) = existing_history_entry {
410
+ if existing_history_entry. has_diverged_from ( & history_entry) {
411
+ log. warnings . push ( format ! (
412
+ "History entries for {} have the same modification timestamp but were not the same." ,
413
+ existing_history_entry. uuid
414
+ ) ) ;
415
+ }
416
+ } else {
417
+ new_history_entries. insert ( modification_time. clone ( ) , history_entry. clone ( ) ) ;
418
+ }
419
+ }
420
+
421
+ let mut all_modification_times: Vec < & chrono:: NaiveDateTime > = new_history_entries. keys ( ) . collect ( ) ;
422
+ all_modification_times. sort ( ) ;
423
+ all_modification_times. reverse ( ) ;
424
+ let mut new_entries: Vec < Entry > = vec ! [ ] ;
425
+ for modification_time in & all_modification_times {
426
+ new_entries. push ( new_history_entries. get ( & modification_time) . unwrap ( ) . clone ( ) ) ;
427
+ }
428
+
429
+ self . entries = new_entries;
430
+ Ok ( log)
431
+ }
239
432
}
240
433
241
434
#[ cfg( test) ]
0 commit comments