sos_sync/
types.rs

1//! Core types for the synchronization primitives.
2use crate::Result;
3use indexmap::{IndexMap, IndexSet};
4use serde::{Deserialize, Serialize};
5use sos_core::{
6    commit::{CommitHash, CommitState, Comparison},
7    SecretId, VaultId,
8};
9use sos_core::{
10    device::DevicePublicKey,
11    events::{
12        patch::{
13            AccountDiff, AccountPatch, DeviceDiff, DevicePatch, FolderDiff,
14            FolderPatch,
15        },
16        AccountEvent, DeviceEvent, WriteEvent,
17    },
18};
19use std::collections::HashMap;
20
21#[cfg(feature = "files")]
22use sos_core::{
23    events::{
24        patch::{FileDiff, FilePatch},
25        FileEvent,
26    },
27    ExternalFile, ExternalFileName, SecretPath,
28};
29
30/// Combined sync status, diff and comparisons.
31#[derive(Debug, Default, Clone, PartialEq, Eq)]
32pub struct SyncPacket {
33    /// Sync status.
34    pub status: SyncStatus,
35    /// Sync diff.
36    pub diff: SyncDiff,
37    /// Sync comparisons.
38    pub compare: Option<SyncCompare>,
39}
40
41/// Diff of events or conflict information.
42#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum MaybeDiff<T> {
44    /// Diff of local changes to send to the remote.
45    Diff(T),
46    /// Local needs to compare it's state with remote.
47    // The additional `Option` wrapper is required because
48    // the files event log may not exist.
49    Compare(Option<CommitState>),
50}
51
52/// Diff between all events logs on local and remote.
53#[derive(Default, Debug, Clone, PartialEq, Eq)]
54pub struct SyncDiff {
55    /// Diff of the identity vault event logs.
56    pub identity: Option<MaybeDiff<FolderDiff>>,
57    /// Diff of the account event log.
58    pub account: Option<MaybeDiff<AccountDiff>>,
59    /// Diff of the device event log.
60    pub device: Option<MaybeDiff<DeviceDiff>>,
61    /// Diff of the files event log.
62    #[cfg(feature = "files")]
63    pub files: Option<MaybeDiff<FileDiff>>,
64    /// Diff for folders in the account.
65    pub folders: IndexMap<VaultId, MaybeDiff<FolderDiff>>,
66}
67
68/// Provides a status overview of an account.
69///
70/// Intended to be used during a synchronization protocol.
71#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
72pub struct SyncStatus {
73    /// Computed root of all event log roots.
74    pub root: CommitHash,
75    /// Identity vault commit state.
76    pub identity: CommitState,
77    /// Account log commit state.
78    pub account: CommitState,
79    /// Device log commit state.
80    pub device: CommitState,
81    /// Files log commit state.
82    #[cfg(feature = "files")]
83    pub files: Option<CommitState>,
84    /// Commit proofs for the account folders.
85    pub folders: IndexMap<VaultId, CommitState>,
86}
87
88/// Collection of patches for an account.
89#[derive(Debug, Default, PartialEq, Eq)]
90pub struct CreateSet {
91    /// Identity vault event logs.
92    pub identity: FolderPatch,
93    /// Account event logs.
94    pub account: AccountPatch,
95    /// Device event logs.
96    pub device: DevicePatch,
97    /// File event logs.
98    #[cfg(feature = "files")]
99    pub files: FilePatch,
100    /// Folders to be imported into the new account.
101    pub folders: HashMap<VaultId, FolderPatch>,
102}
103
104/// Set of updates to the folders in an account.
105///
106/// Used to destructively update folders in an account;
107/// the identity and folders are entire event
108/// logs so that the account state can be overwritten in the
109/// case of events such as changing encryption cipher, changing
110/// folder password or compacing the events in a folder.
111#[derive(Debug, Default, Clone, PartialEq, Eq)]
112pub struct UpdateSet {
113    /// Identity folder event logs.
114    pub identity: Option<FolderDiff>,
115    /// Account event log.
116    pub account: Option<AccountDiff>,
117    /// Device event log.
118    pub device: Option<DeviceDiff>,
119    /// Files event log.
120    #[cfg(feature = "files")]
121    pub files: Option<FileDiff>,
122    /// Folders to be updated.
123    pub folders: HashMap<VaultId, FolderDiff>,
124}
125
126/// Outcome of a merge operation.
127#[derive(Debug, Default, Clone, PartialEq, Eq)]
128pub struct MergeOutcome {
129    /// Total number of changes made during a merge.
130    ///
131    /// Will often be different to the number of tracked changes
132    /// as tracked changes are normalized.
133    pub changes: u64,
134
135    /// Tracked changes made during a merge.
136    ///
137    /// These events can be used by client implementations
138    /// to react to changes on other devices but they are not
139    /// an exact representation of what was merged as tracked
140    /// changes are normalized.
141    ///
142    /// For example, a create secret followed by a deletion of
143    /// the same secret will result in both events being omitted.
144    ///
145    /// Tracked changes are normalized for all event types.
146    ///
147    /// Not all events are tracked, for example, renaming a folder
148    /// triggers events on the account event log and also on the
149    /// folder but only the account level events are tracked.
150    pub tracked: TrackedChanges,
151
152    /// Collection of external files detected when merging
153    /// file events logs, must never be serialized over
154    /// the wire.
155    ///
156    /// Used after merge to update the file transfer queue.
157    #[doc(hidden)]
158    #[cfg(feature = "files")]
159    pub external_files: IndexSet<ExternalFile>,
160}
161
162/// Changes tracking during a merge operation.
163#[derive(Debug, Default, Clone, PartialEq, Eq)]
164pub struct TrackedChanges {
165    /// Changes made to the identity folder.
166    pub identity: IndexSet<TrackedFolderChange>,
167
168    /// Changes made to the devices collection.
169    pub device: IndexSet<TrackedDeviceChange>,
170
171    /// Changes made to the account.
172    pub account: IndexSet<TrackedAccountChange>,
173
174    /// Changes to the files log.
175    #[cfg(feature = "files")]
176    pub files: IndexSet<TrackedFileChange>,
177
178    /// Change made to each folder.
179    pub folders: HashMap<VaultId, IndexSet<TrackedFolderChange>>,
180}
181
182impl TrackedChanges {
183    /// Add tracked folder changes only when
184    /// the set of tracked changes is not empty.
185    pub fn add_tracked_folder_changes(
186        &mut self,
187        folder_id: &VaultId,
188        changes: IndexSet<TrackedFolderChange>,
189    ) {
190        if !changes.is_empty() {
191            self.folders.insert(*folder_id, changes);
192        }
193    }
194
195    /// Create a new set of tracked changes to a folder from a patch.
196    pub async fn new_folder_records(
197        value: &FolderPatch,
198    ) -> Result<IndexSet<TrackedFolderChange>> {
199        let events = value.into_events::<WriteEvent>().await?;
200        Self::new_folder_events(events).await
201    }
202
203    /// Create a new set of tracked changes from a
204    /// collection of folder events.
205    pub async fn new_folder_events(
206        events: Vec<WriteEvent>,
207    ) -> Result<IndexSet<TrackedFolderChange>> {
208        let mut changes = IndexSet::new();
209        for event in events {
210            match event {
211                WriteEvent::CreateSecret(secret_id, _) => {
212                    changes.insert(TrackedFolderChange::Created(secret_id));
213                }
214                WriteEvent::UpdateSecret(secret_id, _) => {
215                    changes.insert(TrackedFolderChange::Updated(secret_id));
216                }
217                WriteEvent::DeleteSecret(secret_id) => {
218                    let created = TrackedFolderChange::Created(secret_id);
219                    let updated = TrackedFolderChange::Updated(secret_id);
220                    let had_created = changes.shift_remove(&created);
221                    changes.shift_remove(&updated);
222                    if !had_created {
223                        changes
224                            .insert(TrackedFolderChange::Deleted(secret_id));
225                    }
226                }
227                _ => {}
228            }
229        }
230        Ok(changes)
231    }
232
233    /// Create a new set of tracked changes to an account from a patch.
234    pub async fn new_account_records(
235        value: &AccountPatch,
236    ) -> Result<IndexSet<TrackedAccountChange>> {
237        let events = value.into_events::<AccountEvent>().await?;
238        Self::new_account_events(events).await
239    }
240
241    /// Create a new set of tracked changes from a
242    /// collection of account events.
243    pub async fn new_account_events(
244        events: Vec<AccountEvent>,
245    ) -> Result<IndexSet<TrackedAccountChange>> {
246        let mut changes = IndexSet::new();
247        for event in events {
248            match event {
249                AccountEvent::CreateFolder(folder_id, _) => {
250                    changes.insert(TrackedAccountChange::FolderCreated(
251                        folder_id,
252                    ));
253                }
254                AccountEvent::RenameFolder(folder_id, _)
255                | AccountEvent::UpdateFolder(folder_id, _) => {
256                    changes.insert(TrackedAccountChange::FolderUpdated(
257                        folder_id,
258                    ));
259                }
260                AccountEvent::DeleteFolder(folder_id) => {
261                    let created =
262                        TrackedAccountChange::FolderCreated(folder_id);
263                    let updated =
264                        TrackedAccountChange::FolderUpdated(folder_id);
265                    let had_created = changes.shift_remove(&created);
266                    changes.shift_remove(&updated);
267
268                    if !had_created {
269                        changes.insert(TrackedAccountChange::FolderDeleted(
270                            folder_id,
271                        ));
272                    }
273                }
274                _ => {}
275            }
276        }
277        Ok(changes)
278    }
279
280    /// Create a new set of tracked changes to a device from a patch.
281    pub async fn new_device_records(
282        value: &DevicePatch,
283    ) -> Result<IndexSet<TrackedDeviceChange>> {
284        let events = value.into_events::<DeviceEvent>().await?;
285        Self::new_device_events(events).await
286    }
287
288    /// Create a new set of tracked changes from a
289    /// collection of device events.
290    pub async fn new_device_events(
291        events: Vec<DeviceEvent>,
292    ) -> Result<IndexSet<TrackedDeviceChange>> {
293        let mut changes = IndexSet::new();
294        for event in events {
295            match event {
296                DeviceEvent::Trust(device) => {
297                    changes.insert(TrackedDeviceChange::Trusted(
298                        device.public_key().to_owned(),
299                    ));
300                }
301                DeviceEvent::Revoke(public_key) => {
302                    let trusted = TrackedDeviceChange::Trusted(public_key);
303                    let had_trusted = changes.shift_remove(&trusted);
304                    if !had_trusted {
305                        changes
306                            .insert(TrackedDeviceChange::Revoked(public_key));
307                    }
308                }
309                _ => {}
310            }
311        }
312        Ok(changes)
313    }
314
315    /// Create a new set of tracked changes to a file from a patch.
316    #[cfg(feature = "files")]
317    pub async fn new_file_records(
318        value: &FilePatch,
319    ) -> Result<IndexSet<TrackedFileChange>> {
320        let events = value.into_events::<FileEvent>().await?;
321        Self::new_file_events(events).await
322    }
323
324    /// Create a new set of tracked changes from a
325    /// collection of file events.
326    #[cfg(feature = "files")]
327    pub async fn new_file_events(
328        events: Vec<FileEvent>,
329    ) -> Result<IndexSet<TrackedFileChange>> {
330        let mut changes = IndexSet::new();
331        for event in events {
332            match event {
333                FileEvent::CreateFile(owner, name) => {
334                    changes.insert(TrackedFileChange::Created(owner, name));
335                }
336                FileEvent::MoveFile { name, from, dest } => {
337                    changes.insert(TrackedFileChange::Moved {
338                        name,
339                        from,
340                        dest,
341                    });
342                }
343                FileEvent::DeleteFile(owner, name) => {
344                    let created = TrackedFileChange::Created(owner, name);
345                    let had_created = changes.shift_remove(&created);
346
347                    let moved = changes.iter().find_map(|event| {
348                        if let TrackedFileChange::Moved {
349                            name: moved_name,
350                            dest,
351                            from,
352                        } = event
353                        {
354                            if moved_name == &name && dest == &owner {
355                                return Some(TrackedFileChange::Moved {
356                                    name: *moved_name,
357                                    from: *from,
358                                    dest: *dest,
359                                });
360                            }
361                        }
362                        None
363                    });
364                    if let Some(moved) = moved {
365                        changes.shift_remove(&moved);
366                    }
367
368                    if !had_created {
369                        changes
370                            .insert(TrackedFileChange::Deleted(owner, name));
371                    }
372                }
373                _ => {}
374            }
375        }
376        Ok(changes)
377    }
378}
379
380/// Change made to a device.
381#[derive(Debug, Clone, Hash, PartialEq, Eq)]
382pub enum TrackedDeviceChange {
383    /// Device was trusted.
384    Trusted(DevicePublicKey),
385    /// Device was revoked.
386    Revoked(DevicePublicKey),
387}
388
389/// Change made to an account.
390#[derive(Debug, Clone, Hash, PartialEq, Eq)]
391pub enum TrackedAccountChange {
392    /// Folder was added.
393    FolderCreated(VaultId),
394    /// Folder was updated.
395    FolderUpdated(VaultId),
396    /// Folder was deleted.
397    FolderDeleted(VaultId),
398}
399
400/// Change made to file event logs.
401#[cfg(feature = "files")]
402#[derive(Debug, Clone, Hash, PartialEq, Eq)]
403pub enum TrackedFileChange {
404    /// File was created in the log.
405    Created(SecretPath, ExternalFileName),
406    /// File was moved in the log.
407    Moved {
408        /// File name.
409        name: ExternalFileName,
410        /// From identifiers.
411        from: SecretPath,
412        /// Destination identifiers.
413        dest: SecretPath,
414    },
415    /// File was deleted in the log.
416    Deleted(SecretPath, ExternalFileName),
417}
418
419/// Change made to a folder.
420#[derive(Debug, Clone, Hash, PartialEq, Eq)]
421pub enum TrackedFolderChange {
422    /// Secret was created.
423    Created(SecretId),
424    /// Secret was updated.
425    Updated(SecretId),
426    /// Secret was deleted.
427    Deleted(SecretId),
428}
429
430/// Collection of comparisons for an account.
431///
432/// When a local account does not contain the proof for
433/// a remote event log if will interrogate the server to
434/// compare it's proof with the remote tree.
435///
436/// The server will reply with comparison(s) so that the local
437/// account can determine if the trees have completely diverged
438/// or whether it can attempt to automatically merge
439/// partially diverged trees.
440#[derive(Debug, Default, Clone, Eq, PartialEq)]
441pub struct SyncCompare {
442    /// Identity vault comparison.
443    pub identity: Option<Comparison>,
444    /// Account log comparison.
445    pub account: Option<Comparison>,
446    /// Device log comparison.
447    pub device: Option<Comparison>,
448    /// Files log comparison.
449    #[cfg(feature = "files")]
450    pub files: Option<Comparison>,
451    /// Comparisons for the account folders.
452    pub folders: IndexMap<VaultId, Comparison>,
453}
454
455impl SyncCompare {
456    /// Determine if this comparison might conflict.
457    pub fn maybe_conflict(&self) -> MaybeConflict {
458        MaybeConflict {
459            identity: self
460                .identity
461                .as_ref()
462                .map(|c| matches!(c, Comparison::Unknown))
463                .unwrap_or(false),
464            account: self
465                .account
466                .as_ref()
467                .map(|c| matches!(c, Comparison::Unknown))
468                .unwrap_or(false),
469            device: self
470                .device
471                .as_ref()
472                .map(|c| matches!(c, Comparison::Unknown))
473                .unwrap_or(false),
474            #[cfg(feature = "files")]
475            files: self
476                .files
477                .as_ref()
478                .map(|c| matches!(c, Comparison::Unknown))
479                .unwrap_or(false),
480            folders: self
481                .folders
482                .iter()
483                .map(|(k, v)| (*k, matches!(v, Comparison::Unknown)))
484                .collect(),
485        }
486    }
487}
488
489/// Information about possible conflicts.
490#[derive(Debug, Default, Eq, PartialEq)]
491pub struct MaybeConflict {
492    /// Whether the identity folder might be conflicted.
493    pub identity: bool,
494    /// Whether the account log might be conflicted.
495    pub account: bool,
496    /// Whether the device log might be conflicted.
497    pub device: bool,
498    /// Whether the files log might be conflicted.
499    #[cfg(feature = "files")]
500    pub files: bool,
501    /// Account folders that might be conflicted.
502    pub folders: IndexMap<VaultId, bool>,
503}
504
505impl MaybeConflict {
506    /// Check for any conflicts.
507    pub fn has_conflicts(&self) -> bool {
508        let mut has_conflicts = self.identity || self.account || self.device;
509
510        #[cfg(feature = "files")]
511        {
512            has_conflicts = has_conflicts || self.files;
513        }
514
515        for (_, value) in &self.folders {
516            has_conflicts = has_conflicts || *value;
517            if has_conflicts {
518                break;
519            }
520        }
521
522        has_conflicts
523    }
524}