heddle_object_model/object/source_target/
capture.rs1use std::collections::BTreeMap;
3
4use serde::{Deserialize, Serialize};
5
6use super::{SourceFileCore, SourceSelector, SourceTargetCore};
7use crate::{
8 error::{HeddleError, Result},
9 object::{
10 CollaborationScope, ContentHash, ObjectSource, StateId,
11 source_target_map::{MapBudget, SourceTargetMap, SourceTargetMapStore},
12 },
13};
14
15pub const MAX_REFERENCE_OBJECTS: usize = 65_536;
16pub const MAX_REFERENCE_BYTES: usize = 32 * 1024 * 1024;
17pub const MAX_REFERENCE_OBJECT_BYTES: usize = 64 * 1024;
18
19#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
20#[serde(deny_unknown_fields)]
21pub struct SourceTargetSnapshot {
22 pub version: u16,
23 pub scope: CollaborationScope,
24 pub state: StateId,
25 pub collaboration_frontier: ContentHash,
29 pub files: Option<ContentHash>,
30 pub targets: Option<ContentHash>,
31}
32pub struct ReferenceProof {
35 pub descriptor: ContentHash,
36 pub scope: CollaborationScope,
37 pub state: StateId,
38}
39#[derive(Clone, Copy, Debug, PartialEq, Eq, Serialize, Deserialize)]
40#[serde(rename_all = "snake_case")]
41pub enum ResolutionStatus {
42 Resolved,
43 Ambiguous,
44 Deleted,
45}
46#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
47#[serde(deny_unknown_fields)]
48pub struct FileResolution {
49 pub core: SourceFileCore,
50 pub path: String,
51 pub blob: Option<ContentHash>,
52 pub status: ResolutionStatus,
53}
54#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
55#[serde(deny_unknown_fields)]
56pub struct TargetResolution {
57 pub core: SourceTargetCore,
58 pub selector: SourceSelector,
59 pub status: ResolutionStatus,
60}
61#[derive(Clone, Debug, PartialEq, Eq)]
62pub struct ResolvedSourceTarget {
63 pub scope: CollaborationScope,
64 pub state: StateId,
65 pub file: FileResolution,
66 pub target: TargetResolution,
67}
68pub fn invalid(message: impl std::fmt::Display) -> HeddleError {
69 HeddleError::InvalidObject(message.to_string())
70}
71pub fn encode(value: &impl Serialize) -> Result<Vec<u8>> {
72 Ok(rmp_serde::to_vec_named(value)?)
73}
74pub fn decode<T: serde::de::DeserializeOwned + Serialize>(bytes: &[u8]) -> Result<T> {
75 if bytes.len() > MAX_REFERENCE_OBJECT_BYTES {
76 return Err(invalid("reference object exceeds budget"));
77 }
78 let value: T = rmp_serde::from_slice(bytes)?;
79 if encode(&value)? != bytes {
80 return Err(invalid("noncanonical reference object"));
81 }
82 Ok(value)
83}
84impl SourceTargetSnapshot {
85 pub fn validate(&self, scope: &CollaborationScope, state: StateId) -> Result<()> {
86 if self.version != 1
87 || &self.scope != scope
88 || self.scope.spool.is_nil()
89 || self.scope.thread.is_none()
90 || self.state != state
91 {
92 return Err(invalid(
93 "reference snapshot scope, revision, or frontier mismatch",
94 ));
95 }
96 Ok(())
97 }
98}
99pub struct ReferenceClosure {
102 pub snapshot: SourceTargetSnapshot,
103 pub blobs: BTreeMap<ContentHash, Vec<u8>>,
104 pub files: BTreeMap<ContentHash, FileResolution>,
105 pub targets: BTreeMap<ContentHash, TargetResolution>,
106}
107
108pub fn resolve_target(
113 source: &impl ObjectSource,
114 descriptor: ContentHash,
115 scope: &CollaborationScope,
116 state: StateId,
117 target_id: ContentHash,
118) -> Result<Option<ResolvedSourceTarget>> {
119 let mut reader = Reader {
120 source,
121 blobs: BTreeMap::new(),
122 bytes: 0,
123 };
124 let snapshot: SourceTargetSnapshot =
125 decode(&reader.blob(descriptor, MAX_REFERENCE_OBJECT_BYTES)?)?;
126 snapshot.validate(scope, state)?;
127 let mut budget = MapBudget::new(
130 106,
131 106 * crate::object::source_target_map::MAX_NODE_BYTES,
132 0,
133 0,
134 );
135 let Some(target_hash) =
136 SourceTargetMap::get(&mut reader, snapshot.targets, target_id, &mut budget)
137 .map_err(invalid)?
138 else {
139 return Ok(None);
140 };
141 let target: TargetResolution = decode(&reader.blob(target_hash, MAX_REFERENCE_OBJECT_BYTES)?)?;
142 if target.core.id().map_err(invalid)? != target_id {
143 return Err(invalid("source target core identity mismatch"));
144 }
145 let Some(file_hash) =
146 SourceTargetMap::get(&mut reader, snapshot.files, target.core.file, &mut budget)
147 .map_err(invalid)?
148 else {
149 return Err(invalid("source target file binding missing"));
150 };
151 let file: FileResolution = decode(&reader.blob(file_hash, MAX_REFERENCE_OBJECT_BYTES)?)?;
152 if file.core.id().map_err(invalid)? != target.core.file || file.core.scope.spool != scope.spool
153 {
154 return Err(invalid("source file core identity or Spool mismatch"));
155 }
156 let mut current_file = file.core.clone();
157 current_file.path = file.path.clone();
158 current_file.id().map_err(invalid)?;
159 let mut current_target = target.core.clone();
160 current_target.selector = target.selector.clone();
161 current_target.id().map_err(invalid)?;
162 Ok(Some(ResolvedSourceTarget {
163 scope: snapshot.scope,
164 state,
165 file,
166 target,
167 }))
168}
169struct Reader<'a, S> {
170 source: &'a S,
171 blobs: BTreeMap<ContentHash, Vec<u8>>,
172 bytes: usize,
173}
174impl<S: ObjectSource> Reader<'_, S> {
175 fn blob(&mut self, hash: ContentHash, limit: usize) -> Result<Vec<u8>> {
176 if let Some(bytes) = self.blobs.get(&hash) {
177 if bytes.len() > limit {
178 return Err(invalid("reference read budget"));
179 }
180 return Ok(bytes.clone());
181 }
182 if self.blobs.len() >= MAX_REFERENCE_OBJECTS {
183 return Err(invalid("reference object count budget"));
184 }
185 let allowed = limit
186 .min(MAX_REFERENCE_OBJECT_BYTES)
187 .min(MAX_REFERENCE_BYTES.saturating_sub(self.bytes));
188 if self
189 .source
190 .decoded_blob_len(&hash)?
191 .is_none_or(|len| len > allowed as u64)
192 {
193 return Err(invalid("reference blob missing or exceeds budget"));
194 }
195 let blob = self
196 .source
197 .get_blob(&hash)?
198 .ok_or_else(|| invalid("reference blob missing"))?;
199 let bytes = blob.into_content();
200 if bytes.len() > allowed || ContentHash::compute_typed("blob", &bytes) != hash {
201 return Err(invalid("reference blob identity or budget mismatch"));
202 }
203 self.bytes += bytes.len();
204 self.blobs.insert(hash, bytes.clone());
205 Ok(bytes)
206 }
207}
208impl<S: ObjectSource> SourceTargetMapStore for Reader<'_, S> {
209 type Error = HeddleError;
210 fn read(&mut self, hash: ContentHash, max: usize) -> Result<Option<Vec<u8>>> {
211 self.blob(hash, max).map(Some)
212 }
213 fn write(&mut self, _: ContentHash, _: Vec<u8>) -> Result<()> {
214 Err(invalid("read-only reference closure"))
215 }
216}
217pub fn closure(
218 source: &impl ObjectSource,
219 descriptor: ContentHash,
220 scope: &CollaborationScope,
221 state: StateId,
222) -> Result<ReferenceClosure> {
223 let mut reader = Reader {
224 source,
225 blobs: BTreeMap::new(),
226 bytes: 0,
227 };
228 let snapshot: SourceTargetSnapshot =
229 decode(&reader.blob(descriptor, MAX_REFERENCE_OBJECT_BYTES)?)?;
230 snapshot.validate(scope, state)?;
231 let mut budget = MapBudget::new(MAX_REFERENCE_OBJECTS, MAX_REFERENCE_BYTES, 0, 0);
232 let file_entries = SourceTargetMap::entries(
233 &mut reader,
234 snapshot.files,
235 MAX_REFERENCE_OBJECTS,
236 &mut budget,
237 )
238 .map_err(invalid)?;
239 let target_entries = SourceTargetMap::entries(
240 &mut reader,
241 snapshot.targets,
242 MAX_REFERENCE_OBJECTS,
243 &mut budget,
244 )
245 .map_err(invalid)?;
246 let mut files = BTreeMap::new();
247 for (id, hash) in file_entries {
248 let file: FileResolution = decode(&reader.blob(hash, MAX_REFERENCE_OBJECT_BYTES)?)?;
249 if file.core.id().map_err(invalid)? != id || file.core.scope.spool != scope.spool {
250 return Err(invalid("source file core identity or Spool mismatch"));
251 }
252 let mut current = file.core.clone();
255 current.path = file.path.clone();
256 current.id().map_err(invalid)?;
257 files.insert(id, file);
258 }
259 let mut targets = BTreeMap::new();
260 for (id, hash) in target_entries {
261 let target: TargetResolution = decode(&reader.blob(hash, MAX_REFERENCE_OBJECT_BYTES)?)?;
262 if target.core.id().map_err(invalid)? != id || !files.contains_key(&target.core.file) {
263 return Err(invalid("source target identity or file closure mismatch"));
264 }
265 let mut current = target.core.clone();
266 current.selector = target.selector.clone();
267 current.id().map_err(invalid)?;
268 targets.insert(id, target);
269 }
270 Ok(ReferenceClosure {
271 snapshot,
272 blobs: reader.blobs,
273 files,
274 targets,
275 })
276}