Skip to content

Commit 9a878d8

Browse files
committed
feat: add merge feature
1 parent 3b2456f commit 9a878d8

File tree

3 files changed

+852
-3
lines changed

3 files changed

+852
-3
lines changed

src/db/entry.rs

+126
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,11 @@
11
use std::collections::HashMap;
2+
use std::{thread, time};
23

34
use chrono::NaiveDateTime;
45
use secstr::SecStr;
56
use uuid::Uuid;
67

8+
use crate::db::group::MergeLog;
79
use crate::db::{Color, CustomData, Times};
810

911
#[cfg(feature = "totp")]
@@ -41,6 +43,64 @@ impl Entry {
4143
..Default::default()
4244
}
4345
}
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+
}
44104
}
45105

46106
impl<'a> Entry {
@@ -227,6 +287,8 @@ pub struct History {
227287
}
228288
impl History {
229289
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?
230292
if entry.history.is_some() {
231293
// Remove the history from the new history entry to avoid having
232294
// an exponential number of history entries.
@@ -238,6 +300,70 @@ impl History {
238300
pub fn get_entries(&self) -> &Vec<Entry> {
239301
&self.entries
240302
}
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+
}
241367
}
242368

243369
#[cfg(test)]

0 commit comments

Comments
 (0)