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