1use std::{
4 collections::{BTreeMap, BTreeSet},
5 io::{Read, Seek, Write},
6};
7
8use super::{ObjectType, PackObjectId, PackReader, PackStats, StreamingPackBuilder, SyncData};
9use crate::{
10 object::{
11 ContentHash, EntryRedactions, ObjectSource, PartialTree, State, Tree, TreeEntryTarget,
12 },
13 store::{Result, StoreError},
14};
15
16pub(super) fn validate(
17 reader: &PackReader<'_>,
18 selected: &State,
19 max_decoded_bytes: u64,
20 references: &[crate::object::source_target::capture::ReferenceProof],
21 visibility: Option<&crate::object::thread_replication::CaptureVisibility>,
22) -> Result<Vec<PackObjectId>> {
23 Ok(validate_disclosure(
24 reader,
25 selected,
26 max_decoded_bytes,
27 references,
28 visibility,
29 false,
30 )?
31 .objects)
32}
33
34pub struct VisibleSourceClosure {
37 pub objects: Vec<PackObjectId>,
39 pub partial_trees: Vec<PartialTree>,
41}
42
43pub(super) fn validate_disclosure(
44 reader: &PackReader<'_>,
45 selected: &State,
46 max_decoded_bytes: u64,
47 references: &[crate::object::source_target::capture::ReferenceProof],
48 visibility: Option<&crate::object::thread_replication::CaptureVisibility>,
49 allow_partial: bool,
50) -> Result<VisibleSourceClosure> {
51 let canonical = selected.encode_current_msgpack()?;
52 let mut available = BTreeMap::new();
53 let mut trees = BTreeMap::new();
54 let mut partial_trees = Vec::new();
55 let mut decoded = 0_u64;
56 reader.visit_objects(|id, kind, data| {
57 decoded = decoded
58 .checked_add(data.len() as u64)
59 .ok_or_else(|| invalid("source pack size overflow"))?;
60 if decoded > max_decoded_bytes {
61 return Err(invalid("source pack decoded byte budget exceeded"));
62 }
63 if available.insert(id, kind).is_some() {
64 return Err(invalid("duplicate source pack object"));
65 }
66 match (id, kind) {
67 (PackObjectId::StateId(id), ObjectType::State)
68 if id == selected.id() && data == canonical => {}
69 (PackObjectId::Hash(hash), ObjectType::Blob)
70 if ContentHash::compute_typed("blob", data) == hash => {}
71 (PackObjectId::Hash(hash), ObjectType::Tree) => {
72 if allow_partial && crate::object::is_redacted_tree(data) {
73 let partial = crate::object::decode_redacted_projection(data)?;
74 partial.verify()?;
75 if partial.declared_root() != hash || !partial.has_redactions() {
76 return Err(invalid(
77 "partial source tree differs from its address or is complete",
78 ));
79 }
80 trees.insert(hash, partial.visible_tree()?);
81 partial_trees.push(partial);
82 return Ok(());
83 }
84 let tree = Tree::decode_canonical(data)
87 .map_err(|_| invalid("source pack requires complete canonical tree anchors"))?;
88 if tree.hash() != hash {
89 return Err(invalid("source tree hash differs from its address"));
90 }
91 trees.insert(hash, tree);
92 }
93 _ => {
94 return Err(invalid(
95 "source pack contains an unselected or incorrectly addressed object",
96 ));
97 }
98 }
99 Ok(())
100 })?;
101 let mut visited = BTreeSet::new();
102 let mut pending = vec![
103 (PackObjectId::StateId(selected.id()), ObjectType::State),
104 (PackObjectId::Hash(selected.tree), ObjectType::Tree),
105 ];
106 while let Some((id, kind)) = pending.pop() {
107 if available.get(&id) != Some(&kind) {
108 return Err(invalid("source closure is incomplete"));
109 }
110 if !visited.insert(id) {
111 continue;
112 }
113 if kind == ObjectType::Tree {
114 let PackObjectId::Hash(hash) = id else {
115 return Err(invalid("tree address is not a content hash"));
116 };
117 let tree = trees
118 .get(&hash)
119 .ok_or_else(|| invalid("source tree unavailable"))?;
120 for (hash, kind) in children(tree) {
121 pending.push((PackObjectId::Hash(hash), kind));
122 }
123 }
124 }
125 if let Some(visibility) = visibility {
126 visibility.validate(selected)?;
127 let mut leaves = BTreeMap::<ContentHash, BTreeSet<ContentHash>>::new();
130 for entry in &visibility.entries {
131 if !visited.contains(&PackObjectId::Hash(entry.tree_id)) {
132 return Err(invalid("entry visibility tree is outside selected source"));
133 }
134 let tree = trees
135 .get(&entry.tree_id)
136 .ok_or_else(|| invalid("entry visibility subject is not a tree"))?;
137 let actual = leaves.entry(entry.tree_id).or_insert_with(|| {
138 (0..tree.entries().len())
139 .filter_map(|index| tree.v4_leaf_hash_at(index))
140 .collect()
141 });
142 if !actual.contains(&entry.leaf_hash) {
143 return Err(invalid(
144 "entry visibility leaf is absent from selected salted tree",
145 ));
146 }
147 }
148 }
149 for reference in references {
150 let closure = crate::object::source_target::capture::closure(
151 &ReferenceReader(reader),
152 reference.descriptor,
153 &reference.scope,
154 reference.state,
155 )?;
156 for hash in closure.blobs.keys() {
157 let id = PackObjectId::Hash(*hash);
158 if available.get(&id) != Some(&ObjectType::Blob) {
159 return Err(invalid("reference closure is incomplete"));
160 }
161 visited.insert(id);
162 }
163 }
164 if visited.len() != available.len() {
165 return Err(invalid(
166 "source pack contains objects outside the selected revision",
167 ));
168 }
169 Ok(VisibleSourceClosure {
170 objects: visited.into_iter().collect(),
171 partial_trees,
172 })
173}
174fn invalid(message: &str) -> StoreError {
175 StoreError::InvalidObject(message.into())
176}
177
178pub fn build_source_pack<W: Write + Read + Seek + SyncData>(
187 builder: StreamingPackBuilder<W>,
188 source: &impl ObjectSource,
189 selected: &State,
190 max_objects: usize,
191 max_decoded_bytes: u64,
192) -> Result<(W, PackStats)> {
193 build_source_pack_with_references(
194 builder,
195 source,
196 selected,
197 &[],
198 max_objects,
199 max_decoded_bytes,
200 )
201}
202
203pub fn build_source_pack_with_references<W: Write + Read + Seek + SyncData>(
204 builder: StreamingPackBuilder<W>,
205 source: &impl ObjectSource,
206 selected: &State,
207 references: &[crate::object::source_target::capture::ReferenceProof],
208 max_objects: usize,
209 max_decoded_bytes: u64,
210) -> Result<(W, PackStats)> {
211 build_disclosure(
212 builder,
213 source,
214 selected,
215 references,
216 None,
217 max_objects,
218 max_decoded_bytes,
219 )
220 .map(|(output, stats, _)| (output, stats))
221}
222
223pub fn build_visible_source_pack<W: Write + Read + Seek + SyncData>(
229 builder: StreamingPackBuilder<W>,
230 source: &impl ObjectSource,
231 selected: &State,
232 references: &[crate::object::source_target::capture::ReferenceProof],
233 redactions: &EntryRedactions,
234 max_objects: usize,
235 max_decoded_bytes: u64,
236) -> Result<(W, PackStats, bool)> {
237 build_disclosure(
238 builder,
239 source,
240 selected,
241 references,
242 Some(redactions),
243 max_objects,
244 max_decoded_bytes,
245 )
246}
247
248fn build_disclosure<W: Write + Read + Seek + SyncData>(
249 mut builder: StreamingPackBuilder<W>,
250 source: &impl ObjectSource,
251 selected: &State,
252 references: &[crate::object::source_target::capture::ReferenceProof],
253 redactions: Option<&EntryRedactions>,
254 max_objects: usize,
255 max_decoded_bytes: u64,
256) -> Result<(W, PackStats, bool)> {
257 if max_objects < 2 {
258 return Err(invalid("source pack object budget exceeded"));
259 }
260 let canonical = selected.encode_current_msgpack()?;
261 let mut decoded = 0_u64;
262 charge_bytes(&mut decoded, canonical.len() as u64, max_decoded_bytes)?;
263 builder.add_id(
264 PackObjectId::StateId(selected.id()),
265 ObjectType::State,
266 &canonical,
267 )?;
268 let mut discovered = BTreeMap::from([(selected.tree, ObjectType::Tree)]);
269 let mut partial = false;
270 let mut pending = discovered.clone();
271 while let Some((hash, kind)) = pending.pop_first() {
272 match kind {
273 ObjectType::Tree => {
274 let tree = source
275 .get_tree(&hash)?
276 .ok_or_else(|| invalid("selected source tree is missing"))?;
277 if tree.hash() != hash {
278 return Err(invalid("source tree differs from its address"));
279 }
280 let full = tree.encode_canonical()?;
281 charge_bytes(&mut decoded, full.len() as u64, max_decoded_bytes)?;
284 let (canonical, visible_tree) = match redactions {
285 Some(redactions)
286 if (0..tree.entries().len())
287 .any(|index| !redactions.entry_visible(&tree, index)) =>
288 {
289 partial = true;
290 let partial = PartialTree::project(&tree, redactions.leaves())?;
291 (
292 crate::object::encode_redacted_projection(&partial)?,
293 partial.visible_tree()?,
294 )
295 }
296 _ => (full, tree),
297 };
298 for (hash, kind) in children(&visible_tree) {
299 if let Some(expected) = discovered.get(&hash) {
300 if *expected != kind {
301 return Err(invalid(
302 "source object is referenced with conflicting types",
303 ));
304 }
305 continue;
306 }
307 if discovered.len().saturating_add(1) >= max_objects {
310 return Err(invalid("source pack object budget exceeded"));
311 }
312 discovered.insert(hash, kind);
313 pending.insert(hash, kind);
314 }
315 builder.add_id(PackObjectId::Hash(hash), ObjectType::Tree, &canonical)?;
316 }
317 ObjectType::Blob => {
318 let length = source
319 .decoded_blob_len(&hash)?
320 .ok_or_else(|| invalid("selected source blob is missing"))?;
321 if length > max_decoded_bytes.saturating_sub(decoded) {
322 return Err(invalid("source pack decoded byte budget exceeded"));
323 }
324 let bytes = source
325 .get_blob_bytes(&hash)?
326 .ok_or_else(|| invalid("selected source blob is missing"))?;
327 if bytes.len() as u64 != length
328 || ContentHash::compute_typed("blob", &bytes) != hash
329 {
330 return Err(invalid(
331 "source blob differs from its address or declared size",
332 ));
333 }
334 charge_bytes(&mut decoded, length, max_decoded_bytes)?;
335 builder.add_id(PackObjectId::Hash(hash), ObjectType::Blob, bytes)?;
336 }
337 _ => return Err(invalid("unexpected source object type")),
338 }
339 }
340 if !partial {
341 for reference in references {
342 let closure = crate::object::source_target::capture::closure(
343 source,
344 reference.descriptor,
345 &reference.scope,
346 reference.state,
347 )?;
348 for (hash, bytes) in closure.blobs {
349 if let Some(kind) = discovered.get(&hash) {
350 if *kind != ObjectType::Blob {
351 return Err(invalid("reference object type conflict"));
352 }
353 continue;
354 }
355 if discovered.len().saturating_add(1) >= max_objects {
356 return Err(invalid("reference pack object budget exceeded"));
357 }
358 charge_bytes(&mut decoded, bytes.len() as u64, max_decoded_bytes)?;
359 discovered.insert(hash, ObjectType::Blob);
360 builder.add_id(PackObjectId::Hash(hash), ObjectType::Blob, bytes)?;
361 }
362 }
363 }
364 let (output, stats) = builder.finalize()?;
365 Ok((output, stats, !partial))
366}
367
368fn charge_bytes(decoded: &mut u64, length: u64, limit: u64) -> Result<()> {
369 *decoded = decoded
370 .checked_add(length)
371 .ok_or_else(|| invalid("source pack size overflow"))?;
372 if *decoded > limit {
373 return Err(invalid("source pack decoded byte budget exceeded"));
374 }
375 Ok(())
376}
377
378fn children(tree: &Tree) -> impl Iterator<Item = (ContentHash, ObjectType)> + '_ {
379 tree.entries()
380 .iter()
381 .filter_map(|entry| match entry.target() {
382 TreeEntryTarget::Tree { hash } => Some((*hash, ObjectType::Tree)),
383 TreeEntryTarget::Blob { hash, .. } | TreeEntryTarget::Symlink { hash } => {
384 Some((*hash, ObjectType::Blob))
385 }
386 TreeEntryTarget::Gitlink { .. } | TreeEntryTarget::Spoollink { .. } => None,
387 })
388}
389
390struct ReferenceReader<'a, 'b>(&'a PackReader<'b>);
391impl ObjectSource for ReferenceReader<'_, '_> {
392 fn get_tree(&self, _: &ContentHash) -> Result<Option<Tree>> {
393 Err(invalid("reference closure must not read source trees"))
394 }
395 fn get_state(&self, _: &crate::object::StateId) -> Result<Option<State>> {
396 Err(invalid("reference closure must not read source history"))
397 }
398 fn get_blob(&self, hash: &ContentHash) -> Result<Option<crate::object::Blob>> {
399 match self.0.get_hashed_object(hash)? {
400 Some((ObjectType::Blob, bytes)) => Ok(Some(crate::object::Blob::new(bytes))),
401 None => Ok(None),
402 _ => Err(invalid("reference object must be a blob")),
403 }
404 }
405 fn decoded_blob_len(&self, hash: &ContentHash) -> Result<Option<u64>> {
406 self.0.get_hashed_object_size(hash)
407 }
408}