use crate::Result;
use indexmap::{IndexMap, IndexSet};
use serde::{Deserialize, Serialize};
use sos_core::{
commit::{CommitHash, CommitState, Comparison},
AccountId, SecretId, VaultId,
};
use sos_core::{
device::DevicePublicKey,
events::{
patch::{
AccountDiff, AccountPatch, DeviceDiff, DevicePatch, FolderDiff,
FolderPatch,
},
AccountEvent, DeviceEvent, WriteEvent,
},
};
use sos_vault::Summary;
use std::collections::HashMap;
#[cfg(feature = "files")]
use sos_core::{
events::{
patch::{FileDiff, FilePatch},
FileEvent,
},
ExternalFile, ExternalFileName, SecretPath,
};
#[derive(Debug, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DebugTree {
pub account_id: AccountId,
pub folders: IndexSet<Summary>,
pub status: SyncStatus,
pub events: DebugEventLogs,
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct DebugEventLogs {
pub identity: DebugEvents,
pub account: DebugEvents,
pub device: DebugEvents,
#[cfg(feature = "files")]
pub file: DebugEvents,
pub folders: HashMap<VaultId, DebugEvents>,
}
#[derive(Debug, Default, Serialize, Deserialize)]
#[serde(default)]
pub struct DebugEvents {
#[serde(skip_serializing_if = "Vec::is_empty")]
pub leaves: Vec<CommitHash>,
pub length: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub root: Option<CommitHash>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct SyncPacket {
pub status: SyncStatus,
pub diff: SyncDiff,
pub compare: Option<SyncCompare>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum MaybeDiff<T> {
Diff(T),
Compare(Option<CommitState>),
}
#[derive(Default, Debug, Clone, PartialEq, Eq)]
pub struct SyncDiff {
pub identity: Option<MaybeDiff<FolderDiff>>,
pub account: Option<MaybeDiff<AccountDiff>>,
pub device: Option<MaybeDiff<DeviceDiff>>,
#[cfg(feature = "files")]
pub files: Option<MaybeDiff<FileDiff>>,
pub folders: IndexMap<VaultId, MaybeDiff<FolderDiff>>,
}
#[derive(Debug, Default, Clone, Eq, PartialEq, Serialize, Deserialize)]
pub struct SyncStatus {
pub root: CommitHash,
pub identity: CommitState,
pub account: CommitState,
pub device: CommitState,
#[cfg(feature = "files")]
pub files: Option<CommitState>,
pub folders: IndexMap<VaultId, CommitState>,
}
#[derive(Debug, Default, PartialEq, Eq)]
pub struct CreateSet {
pub identity: FolderPatch,
pub account: AccountPatch,
pub device: DevicePatch,
#[cfg(feature = "files")]
pub files: FilePatch,
pub folders: HashMap<VaultId, FolderPatch>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct UpdateSet {
pub identity: Option<FolderDiff>,
pub account: Option<AccountDiff>,
pub device: Option<DeviceDiff>,
#[cfg(feature = "files")]
pub files: Option<FileDiff>,
pub folders: HashMap<VaultId, FolderDiff>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct MergeOutcome {
pub changes: u64,
pub tracked: TrackedChanges,
#[doc(hidden)]
#[cfg(feature = "files")]
pub external_files: IndexSet<ExternalFile>,
}
#[derive(Debug, Default, Clone, PartialEq, Eq)]
pub struct TrackedChanges {
pub identity: IndexSet<TrackedFolderChange>,
pub device: IndexSet<TrackedDeviceChange>,
pub account: IndexSet<TrackedAccountChange>,
#[cfg(feature = "files")]
pub files: IndexSet<TrackedFileChange>,
pub folders: HashMap<VaultId, IndexSet<TrackedFolderChange>>,
}
impl TrackedChanges {
pub fn add_tracked_folder_changes(
&mut self,
folder_id: &VaultId,
changes: IndexSet<TrackedFolderChange>,
) {
if !changes.is_empty() {
self.folders.insert(*folder_id, changes);
}
}
pub async fn new_folder_records(
value: &FolderPatch,
) -> Result<IndexSet<TrackedFolderChange>> {
let events = value.into_events::<WriteEvent>().await?;
Self::new_folder_events(events).await
}
pub async fn new_folder_events(
events: Vec<WriteEvent>,
) -> Result<IndexSet<TrackedFolderChange>> {
let mut changes = IndexSet::new();
for event in events {
match event {
WriteEvent::CreateSecret(secret_id, _) => {
changes.insert(TrackedFolderChange::Created(secret_id));
}
WriteEvent::UpdateSecret(secret_id, _) => {
changes.insert(TrackedFolderChange::Updated(secret_id));
}
WriteEvent::DeleteSecret(secret_id) => {
let created = TrackedFolderChange::Created(secret_id);
let updated = TrackedFolderChange::Updated(secret_id);
let had_created = changes.shift_remove(&created);
changes.shift_remove(&updated);
if !had_created {
changes
.insert(TrackedFolderChange::Deleted(secret_id));
}
}
_ => {}
}
}
Ok(changes)
}
pub async fn new_account_records(
value: &AccountPatch,
) -> Result<IndexSet<TrackedAccountChange>> {
let events = value.into_events::<AccountEvent>().await?;
Self::new_account_events(events).await
}
pub async fn new_account_events(
events: Vec<AccountEvent>,
) -> Result<IndexSet<TrackedAccountChange>> {
let mut changes = IndexSet::new();
for event in events {
match event {
AccountEvent::CreateFolder(folder_id, _) => {
changes.insert(TrackedAccountChange::FolderCreated(
folder_id,
));
}
AccountEvent::RenameFolder(folder_id, _)
| AccountEvent::UpdateFolder(folder_id, _) => {
changes.insert(TrackedAccountChange::FolderUpdated(
folder_id,
));
}
AccountEvent::DeleteFolder(folder_id) => {
let created =
TrackedAccountChange::FolderCreated(folder_id);
let updated =
TrackedAccountChange::FolderUpdated(folder_id);
let had_created = changes.shift_remove(&created);
changes.shift_remove(&updated);
if !had_created {
changes.insert(TrackedAccountChange::FolderDeleted(
folder_id,
));
}
}
_ => {}
}
}
Ok(changes)
}
pub async fn new_device_records(
value: &DevicePatch,
) -> Result<IndexSet<TrackedDeviceChange>> {
let events = value.into_events::<DeviceEvent>().await?;
Self::new_device_events(events).await
}
pub async fn new_device_events(
events: Vec<DeviceEvent>,
) -> Result<IndexSet<TrackedDeviceChange>> {
let mut changes = IndexSet::new();
for event in events {
match event {
DeviceEvent::Trust(device) => {
changes.insert(TrackedDeviceChange::Trusted(
device.public_key().to_owned(),
));
}
DeviceEvent::Revoke(public_key) => {
let trusted = TrackedDeviceChange::Trusted(public_key);
let had_trusted = changes.shift_remove(&trusted);
if !had_trusted {
changes
.insert(TrackedDeviceChange::Revoked(public_key));
}
}
_ => {}
}
}
Ok(changes)
}
#[cfg(feature = "files")]
pub async fn new_file_records(
value: &FilePatch,
) -> Result<IndexSet<TrackedFileChange>> {
let events = value.into_events::<FileEvent>().await?;
Self::new_file_events(events).await
}
#[cfg(feature = "files")]
pub async fn new_file_events(
events: Vec<FileEvent>,
) -> Result<IndexSet<TrackedFileChange>> {
let mut changes = IndexSet::new();
for event in events {
match event {
FileEvent::CreateFile(owner, name) => {
changes.insert(TrackedFileChange::Created(owner, name));
}
FileEvent::MoveFile { name, from, dest } => {
changes.insert(TrackedFileChange::Moved {
name,
from,
dest,
});
}
FileEvent::DeleteFile(owner, name) => {
let created = TrackedFileChange::Created(owner, name);
let had_created = changes.shift_remove(&created);
let moved = changes.iter().find_map(|event| {
if let TrackedFileChange::Moved {
name: moved_name,
dest,
from,
} = event
{
if moved_name == &name && dest == &owner {
return Some(TrackedFileChange::Moved {
name: *moved_name,
from: *from,
dest: *dest,
});
}
}
None
});
if let Some(moved) = moved {
changes.shift_remove(&moved);
}
if !had_created {
changes
.insert(TrackedFileChange::Deleted(owner, name));
}
}
_ => {}
}
}
Ok(changes)
}
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum TrackedDeviceChange {
Trusted(DevicePublicKey),
Revoked(DevicePublicKey),
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum TrackedAccountChange {
FolderCreated(VaultId),
FolderUpdated(VaultId),
FolderDeleted(VaultId),
}
#[cfg(feature = "files")]
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum TrackedFileChange {
Created(SecretPath, ExternalFileName),
Moved {
name: ExternalFileName,
from: SecretPath,
dest: SecretPath,
},
Deleted(SecretPath, ExternalFileName),
}
#[derive(Debug, Clone, Hash, PartialEq, Eq)]
pub enum TrackedFolderChange {
Created(SecretId),
Updated(SecretId),
Deleted(SecretId),
}
#[derive(Debug, Default, Clone, Eq, PartialEq)]
pub struct SyncCompare {
pub identity: Option<Comparison>,
pub account: Option<Comparison>,
pub device: Option<Comparison>,
#[cfg(feature = "files")]
pub files: Option<Comparison>,
pub folders: IndexMap<VaultId, Comparison>,
}
impl SyncCompare {
pub fn maybe_conflict(&self) -> MaybeConflict {
MaybeConflict {
identity: self
.identity
.as_ref()
.map(|c| matches!(c, Comparison::Unknown))
.unwrap_or(false),
account: self
.account
.as_ref()
.map(|c| matches!(c, Comparison::Unknown))
.unwrap_or(false),
device: self
.device
.as_ref()
.map(|c| matches!(c, Comparison::Unknown))
.unwrap_or(false),
#[cfg(feature = "files")]
files: self
.files
.as_ref()
.map(|c| matches!(c, Comparison::Unknown))
.unwrap_or(false),
folders: self
.folders
.iter()
.map(|(k, v)| (*k, matches!(v, Comparison::Unknown)))
.collect(),
}
}
}
#[derive(Debug, Default, Eq, PartialEq)]
pub struct MaybeConflict {
pub identity: bool,
pub account: bool,
pub device: bool,
#[cfg(feature = "files")]
pub files: bool,
pub folders: IndexMap<VaultId, bool>,
}
impl MaybeConflict {
pub fn has_conflicts(&self) -> bool {
let mut has_conflicts = self.identity || self.account || self.device;
#[cfg(feature = "files")]
{
has_conflicts = has_conflicts || self.files;
}
for (_, value) in &self.folders {
has_conflicts = has_conflicts || *value;
if has_conflicts {
break;
}
}
has_conflicts
}
}