appcore_sync/sync/
revision.rs1use super::{SyncError, SyncResult};
18use serde::{Deserialize, Serialize};
19use std::collections::BTreeSet;
20
21pub const MAX_REVISION_CHAIN_V1: usize = 1_024;
23
24#[derive(Clone, PartialEq, Eq, Serialize, Deserialize)]
26#[serde(deny_unknown_fields)]
27pub struct ObjectRevisionV1 {
28 pub object_id: String,
30 pub revision_id: String,
32 pub base_revision_id: Option<String>,
34 pub event_id: String,
36}
37
38impl std::fmt::Debug for ObjectRevisionV1 {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 f.debug_struct("ObjectRevisionV1").finish_non_exhaustive()
41 }
42}
43
44impl ObjectRevisionV1 {
45 pub fn validate(&self) -> SyncResult<()> {
47 for id in [&self.object_id, &self.revision_id, &self.event_id] {
48 validate_id(id)?;
49 }
50 if let Some(base) = &self.base_revision_id {
51 validate_id(base)?;
52 if base == &self.revision_id {
53 return invalid();
54 }
55 }
56 Ok(())
57 }
58}
59
60#[derive(Clone)]
63pub struct RevisionChainV1 {
64 revisions: Vec<ObjectRevisionV1>,
65}
66
67impl RevisionChainV1 {
68 pub fn new(revisions: Vec<ObjectRevisionV1>) -> SyncResult<Self> {
70 if revisions.is_empty() || revisions.len() > MAX_REVISION_CHAIN_V1 {
71 return invalid();
72 }
73 let mut ids = BTreeSet::new();
74 let mut events = BTreeSet::new();
75 for (index, revision) in revisions.iter().enumerate() {
76 revision.validate()?;
77 if revision.object_id != revisions[0].object_id
78 || !ids.insert(&revision.revision_id)
79 || !events.insert(&revision.event_id)
80 || revisions[..index].iter().any(|previous| {
81 previous.base_revision_id.as_ref() == Some(&revision.revision_id)
82 })
83 {
84 return invalid();
85 }
86 if index > 0
87 && revision.base_revision_id.as_ref() != Some(&revisions[index - 1].revision_id)
88 {
89 return invalid();
90 }
91 }
92 Ok(Self { revisions })
93 }
94
95 fn position(&self, revision: &ObjectRevisionV1) -> SyncResult<Option<usize>> {
96 let position = self.revisions.iter().position(|entry| {
97 entry.revision_id == revision.revision_id || entry.event_id == revision.event_id
98 });
99 if position.is_some_and(|index| self.revisions[index] != *revision) {
100 return invalid();
101 }
102 Ok(position)
103 }
104}
105
106#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
108#[serde(rename_all = "snake_case")]
109#[non_exhaustive]
110pub enum RevisionDecisionV1 {
111 AlreadyApplied,
113 AncestorEcho,
115 Applicable,
117 ConcurrentBranch,
119 SnapshotRequired,
121}
122
123pub fn decide_object_revision_v1(
127 local: &ObjectRevisionV1,
128 remote: &ObjectRevisionV1,
129 chain: &RevisionChainV1,
130) -> SyncResult<RevisionDecisionV1> {
131 local.validate()?;
132 remote.validate()?;
133 if local.object_id != remote.object_id || local.object_id != chain.revisions[0].object_id {
134 return invalid();
135 }
136 let left = chain.position(local)?;
137 let right = chain.position(remote)?;
138 if local.revision_id == remote.revision_id || local.event_id == remote.event_id {
139 return if local == remote {
140 Ok(RevisionDecisionV1::AlreadyApplied)
141 } else {
142 invalid()
143 };
144 }
145 if let (Some(left), Some(right)) = (left, right) {
146 return Ok(if right < left {
147 RevisionDecisionV1::AncestorEcho
148 } else {
149 RevisionDecisionV1::Applicable
150 });
151 }
152 if remote.base_revision_id.as_ref() == Some(&local.revision_id) {
153 return Ok(RevisionDecisionV1::Applicable);
154 }
155 if local.base_revision_id.is_some() && local.base_revision_id == remote.base_revision_id {
156 return Ok(RevisionDecisionV1::ConcurrentBranch);
157 }
158 Ok(RevisionDecisionV1::SnapshotRequired)
159}
160
161pub fn validate_checkpoint_head_v1(checkpoint: u64, observed_head: u64) -> SyncResult<()> {
163 if checkpoint > observed_head {
164 return Err(SyncError::InvalidSequence(checkpoint));
165 }
166 Ok(())
167}
168
169fn validate_id(id: &str) -> SyncResult<()> {
170 if id.is_empty() || id.len() > 128 || id.chars().any(char::is_control) {
171 return invalid();
172 }
173 Ok(())
174}
175
176fn invalid<T>() -> SyncResult<T> {
177 Err(SyncError::InvalidSyncMessage(
178 "invalid object revision evidence",
179 ))
180}