1use 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#[derive(Debug, Default, Clone, PartialEq, Eq)]
32pub struct SyncPacket {
33 pub status: SyncStatus,
35 pub diff: SyncDiff,
37 pub compare: Option<SyncCompare>,
39}
40
41#[derive(Debug, Clone, PartialEq, Eq)]
43pub enum MaybeDiff<T> {
44 Diff(T),
46 Compare(Option<CommitState>),
50}
51
52#[derive(Default, Debug, Clone, PartialEq, Eq)]
54pub struct SyncDiff {
55 pub identity: Option<MaybeDiff<FolderDiff>>,
57 pub account: Option<MaybeDiff<AccountDiff>>,
59 pub device: Option<MaybeDiff<DeviceDiff>>,
61 #[cfg(feature = "files")]
63 pub files: Option<MaybeDiff<FileDiff>>,
64 pub folders: IndexMap<VaultId, MaybeDiff<FolderDiff>>,
66}
67
68#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
72pub struct SyncStatus {
73 pub root: CommitHash,
75 pub identity: CommitState,
77 pub account: CommitState,
79 pub device: CommitState,
81 #[cfg(feature = "files")]
83 pub files: Option<CommitState>,
84 pub folders: IndexMap<VaultId, CommitState>,
86}
87
88#[derive(Debug, Default, PartialEq, Eq)]
90pub struct CreateSet {
91 pub identity: FolderPatch,
93 pub account: AccountPatch,
95 pub device: DevicePatch,
97 #[cfg(feature = "files")]
99 pub files: FilePatch,
100 pub folders: HashMap<VaultId, FolderPatch>,
102}
103
104#[derive(Debug, Default, Clone, PartialEq, Eq)]
112pub struct UpdateSet {
113 pub identity: Option<FolderDiff>,
115 pub account: Option<AccountDiff>,
117 pub device: Option<DeviceDiff>,
119 #[cfg(feature = "files")]
121 pub files: Option<FileDiff>,
122 pub folders: HashMap<VaultId, FolderDiff>,
124}
125
126#[derive(Debug, Default, Clone, PartialEq, Eq)]
128pub struct MergeOutcome {
129 pub changes: u64,
134
135 pub tracked: TrackedChanges,
151
152 #[doc(hidden)]
158 #[cfg(feature = "files")]
159 pub external_files: IndexSet<ExternalFile>,
160}
161
162#[derive(Debug, Default, Clone, PartialEq, Eq)]
164pub struct TrackedChanges {
165 pub identity: IndexSet<TrackedFolderChange>,
167
168 pub device: IndexSet<TrackedDeviceChange>,
170
171 pub account: IndexSet<TrackedAccountChange>,
173
174 #[cfg(feature = "files")]
176 pub files: IndexSet<TrackedFileChange>,
177
178 pub folders: HashMap<VaultId, IndexSet<TrackedFolderChange>>,
180}
181
182impl TrackedChanges {
183 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 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 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 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 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 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 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 #[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 #[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#[derive(Debug, Clone, Hash, PartialEq, Eq)]
382pub enum TrackedDeviceChange {
383 Trusted(DevicePublicKey),
385 Revoked(DevicePublicKey),
387}
388
389#[derive(Debug, Clone, Hash, PartialEq, Eq)]
391pub enum TrackedAccountChange {
392 FolderCreated(VaultId),
394 FolderUpdated(VaultId),
396 FolderDeleted(VaultId),
398}
399
400#[cfg(feature = "files")]
402#[derive(Debug, Clone, Hash, PartialEq, Eq)]
403pub enum TrackedFileChange {
404 Created(SecretPath, ExternalFileName),
406 Moved {
408 name: ExternalFileName,
410 from: SecretPath,
412 dest: SecretPath,
414 },
415 Deleted(SecretPath, ExternalFileName),
417}
418
419#[derive(Debug, Clone, Hash, PartialEq, Eq)]
421pub enum TrackedFolderChange {
422 Created(SecretId),
424 Updated(SecretId),
426 Deleted(SecretId),
428}
429
430#[derive(Debug, Default, Clone, Eq, PartialEq)]
441pub struct SyncCompare {
442 pub identity: Option<Comparison>,
444 pub account: Option<Comparison>,
446 pub device: Option<Comparison>,
448 #[cfg(feature = "files")]
450 pub files: Option<Comparison>,
451 pub folders: IndexMap<VaultId, Comparison>,
453}
454
455impl SyncCompare {
456 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#[derive(Debug, Default, Eq, PartialEq)]
491pub struct MaybeConflict {
492 pub identity: bool,
494 pub account: bool,
496 pub device: bool,
498 #[cfg(feature = "files")]
500 pub files: bool,
501 pub folders: IndexMap<VaultId, bool>,
503}
504
505impl MaybeConflict {
506 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}