1
1
use std:: collections:: HashMap ;
2
+ use std:: { thread, time} ;
2
3
3
4
use chrono:: NaiveDateTime ;
4
5
use secstr:: SecStr ;
5
6
use uuid:: Uuid ;
6
7
8
+ use crate :: db:: group:: MergeLog ;
7
9
use crate :: db:: { Color , CustomData , Times } ;
8
10
9
11
#[ cfg( feature = "totp" ) ]
@@ -41,6 +43,64 @@ impl Entry {
41
43
..Default :: default ( )
42
44
}
43
45
}
46
+
47
+ pub ( crate ) fn merge ( & self , other : & Entry ) -> Result < ( Entry , MergeLog ) , String > {
48
+ let mut log = MergeLog :: default ( ) ;
49
+
50
+ let mut source_history = match & other. history {
51
+ Some ( h) => h. clone ( ) ,
52
+ None => {
53
+ log. warnings . push ( format ! (
54
+ "Entry {} from source database had no history." ,
55
+ self . uuid
56
+ ) ) ;
57
+ History :: default ( )
58
+ }
59
+ } ;
60
+ let mut destination_history = match & self . history {
61
+ Some ( h) => h. clone ( ) ,
62
+ None => {
63
+ log. warnings . push ( format ! (
64
+ "Entry {} from destination database had no history." ,
65
+ self . uuid
66
+ ) ) ;
67
+ History :: default ( )
68
+ }
69
+ } ;
70
+ let mut history_merge_log: MergeLog = MergeLog :: default ( ) ;
71
+
72
+ let mut response = self . clone ( ) ;
73
+
74
+ if other. has_uncommitted_changes ( ) {
75
+ log. warnings . push ( format ! (
76
+ "Entry {} from source database has uncommitted changes." ,
77
+ self . uuid
78
+ ) ) ;
79
+ source_history. add_entry ( other. clone ( ) ) ;
80
+ }
81
+
82
+ // TODO we should probably check for uncommitted changes in the destination
83
+ // database here too for consistency.
84
+
85
+ history_merge_log = destination_history. merge_with ( & source_history) ?;
86
+ response. history = Some ( destination_history) ;
87
+
88
+ Ok ( ( response, log. merge_with ( & history_merge_log) ) )
89
+ }
90
+
91
+ // Convenience function used in unit tests, to make sure that:
92
+ // 1. The history gets updated after changing a field
93
+ // 2. We wait a second before commiting the changes so that the timestamp is not the same
94
+ // as it previously was. This is necessary since the timestamps in the KDBX format
95
+ // do not preserve the msecs.
96
+ pub ( crate ) fn set_field_and_commit ( & mut self , field_name : & str , field_value : & str ) {
97
+ self . fields . insert (
98
+ field_name. to_string ( ) ,
99
+ Value :: Unprotected ( field_value. to_string ( ) ) ,
100
+ ) ;
101
+ thread:: sleep ( time:: Duration :: from_secs ( 1 ) ) ;
102
+ self . update_history ( ) ;
103
+ }
44
104
}
45
105
46
106
impl < ' a > Entry {
@@ -227,6 +287,8 @@ pub struct History {
227
287
}
228
288
impl History {
229
289
pub fn add_entry ( & mut self , mut entry : Entry ) {
290
+ // DISCUSS: should we make sure that the last modification time is not the same
291
+ // or older than the entry at the top of the history?
230
292
if entry. history . is_some ( ) {
231
293
// Remove the history from the new history entry to avoid having
232
294
// an exponential number of history entries.
@@ -238,6 +300,70 @@ impl History {
238
300
pub fn get_entries ( & self ) -> & Vec < Entry > {
239
301
& self . entries
240
302
}
303
+
304
+ // Determines if the entries of the history are
305
+ // ordered by last modification time.
306
+ pub ( crate ) fn is_ordered ( & self ) -> bool {
307
+ let mut last_modification_time: Option < & NaiveDateTime > = None ;
308
+ for entry in & self . entries {
309
+ if last_modification_time. is_none ( ) {
310
+ last_modification_time = entry. times . get_last_modification ( ) ;
311
+ }
312
+
313
+ let entry_modification_time = entry. times . get_last_modification ( ) . unwrap ( ) ;
314
+ // FIXME should we also handle equal modification times??
315
+ if last_modification_time. unwrap ( ) < entry_modification_time {
316
+ return false ;
317
+ }
318
+ last_modification_time = Some ( entry_modification_time) ;
319
+ }
320
+ true
321
+ }
322
+
323
+ // Merge both histories together.
324
+ pub ( crate ) fn merge_with ( & mut self , other : & History ) -> Result < MergeLog , String > {
325
+ let mut log = MergeLog :: default ( ) ;
326
+ let mut new_history_entries: HashMap < NaiveDateTime , Entry > = HashMap :: new ( ) ;
327
+
328
+ for history_entry in & self . entries {
329
+ let modification_time = history_entry. times . get_last_modification ( ) . unwrap ( ) ;
330
+ if new_history_entries. contains_key ( modification_time) {
331
+ return Err ( format ! (
332
+ "Found history entries with the same timestamp ({}) for entry {}." ,
333
+ modification_time, history_entry. uuid,
334
+ ) ) ;
335
+ }
336
+ new_history_entries. insert ( modification_time. clone ( ) , history_entry. clone ( ) ) ;
337
+ }
338
+
339
+ for history_entry in & other. entries {
340
+ let modification_time = history_entry. times . get_last_modification ( ) . unwrap ( ) ;
341
+ let existing_history_entry = new_history_entries. get ( modification_time) ;
342
+ if let Some ( existing_history_entry) = existing_history_entry {
343
+ if !existing_history_entry. eq ( & history_entry) {
344
+ log. warnings . push ( format ! ( "History entries for {} have the same modification timestamp but were not the same." , existing_history_entry. uuid) ) ;
345
+ }
346
+ } else {
347
+ new_history_entries. insert ( modification_time. clone ( ) , history_entry. clone ( ) ) ;
348
+ }
349
+ }
350
+
351
+ let mut all_modification_times: Vec < & NaiveDateTime > = new_history_entries. keys ( ) . collect ( ) ;
352
+ all_modification_times. sort ( ) ;
353
+ all_modification_times. reverse ( ) ;
354
+ let mut new_entries: Vec < Entry > = vec ! [ ] ;
355
+ for modification_time in & all_modification_times {
356
+ new_entries. push ( new_history_entries. get ( & modification_time) . unwrap ( ) . clone ( ) ) ;
357
+ }
358
+
359
+ self . entries = new_entries;
360
+ if !self . is_ordered ( ) {
361
+ // TODO this should be unit tested.
362
+ return Err ( "The resulting history is not ordered." . to_string ( ) ) ;
363
+ }
364
+
365
+ Ok ( log)
366
+ }
241
367
}
242
368
243
369
#[ cfg( test) ]
0 commit comments