1use sley_core::{ObjectFormat as GitObjectFormat, ObjectId as GitObjectId};
10
11use super::{
12 ContentHash, EntryType, FileMode, PartialTree, PartialTreeLeaf, SpoolId, StateId, Tree,
13 TreeEntry, TreeError, TreeScheme,
14 tree::{git_format_from_tag, git_format_to_tag},
15 tree_git_layout::{apply_layout_trailer, layout_trailer_len, split_layout_flags},
16 tree_stream::TreeStreamError,
17};
18
19pub const TREE_ENCODING_VERSION: u8 = 4;
21pub const TREE_BLOCK_ENCODING_VERSION: u8 = 5;
23pub const TREE_SALTED_ENCODING_VERSION: u8 = 1;
25pub const TREE_REDACTED_ENCODING_VERSION: u8 = 1;
27pub const TREE_CANONICAL_MAGIC: &[u8; 4] = b"HTR4";
29pub const TREE_LEAN_MAGIC: &[u8; 4] = b"HLR1";
31pub const TREE_DELTA_MAGIC: &[u8; 4] = b"HDC1";
33pub const TREE_SALTED_MAGIC: &[u8; 4] = b"HSR1";
36pub const TREE_REDACTED_MAGIC: &[u8; 4] = b"HRT1";
39pub const TREE_LEAN_ENCODING_VERSION: u8 = 6;
41pub const TREE_DELTA_ENCODING_VERSION: u8 = 1;
43pub const TREE_DELTA_HEADER_LEN: usize = 59;
45pub const TREE_DELTA_ANCHOR_INTERVAL: u8 = 128;
47pub const TREE_DELTA_MAX_OPS: usize = 512;
49pub const TREE_HEADER_LEN: usize = 4 + 1 + 32 + 8 + 8 + 8;
51pub const TREE_BLOCK_MIN_ENTRIES: usize = 18;
53
54pub(crate) const TREE_BLOCK_ENTRIES: usize = 256;
55pub(crate) const TREE_BLOCK_PREAMBLE_LEN: usize = 16;
56pub(crate) const TREE_BLOCK_INDEX_LEN: usize = 24;
57const TREE_BLOCK_CODEC_ZSTD: u8 = 1;
58const TREE_BLOCK_MAX_ENTRY_FRAME_LEN: usize =
62 4 + 1 + 1 + 2 + u16::MAX as usize + 2 + u16::MAX as usize + 32;
63pub(crate) const TREE_BLOCK_MAX_RAW_LEN: usize =
64 TREE_BLOCK_ENTRIES * TREE_BLOCK_MAX_ENTRY_FRAME_LEN;
65const TREE_BLOCK_MAX_EXPANSION_RATIO: usize = 128 * 1024;
69
70#[derive(Clone, Copy, Debug, PartialEq, Eq)]
71pub(crate) struct TreeBlockHeader {
72 pub block_entries: usize,
73 pub block_count: usize,
74 pub entry_count: u64,
75 pub raw_payload_len: u64,
76 pub index_end: u64,
77}
78
79#[derive(Clone, Copy, Debug, PartialEq, Eq)]
80pub(crate) struct TreeBlockIndex {
81 pub raw_offset: u64,
82 pub stored_offset: u64,
83 pub stored_len: usize,
84 pub raw_len: usize,
85}
86
87#[derive(Clone, Debug, PartialEq, Eq)]
89pub struct TreeHeader {
90 pub version: u8,
91 pub tree_id: ContentHash,
92 pub entry_count: u64,
93 pub payload_len: u64,
94 pub logical_len: u64,
95}
96
97pub fn is_canonical_tree(bytes: &[u8]) -> bool {
99 bytes.starts_with(TREE_CANONICAL_MAGIC)
100}
101
102pub fn is_lean_tree(bytes: &[u8]) -> bool {
104 bytes.starts_with(TREE_LEAN_MAGIC)
105}
106
107pub fn is_delta_tree(bytes: &[u8]) -> bool {
109 bytes.starts_with(TREE_DELTA_MAGIC)
110}
111
112pub fn is_salted_tree(bytes: &[u8]) -> bool {
114 bytes.starts_with(TREE_SALTED_MAGIC)
115}
116
117pub fn is_redacted_tree(bytes: &[u8]) -> bool {
120 bytes.starts_with(TREE_REDACTED_MAGIC)
121}
122
123pub fn is_streamable_tree(bytes: &[u8]) -> bool {
125 is_canonical_tree(bytes) || is_lean_tree(bytes)
126}
127
128impl Tree {
129 pub fn encode_canonical(&self) -> Result<Vec<u8>, TreeStreamError> {
133 if self.scheme() == TreeScheme::V4Salted {
134 return self.encode_salted_v4();
135 }
136 self.validate()?;
137 let tree_id = self.hash();
138 let mut payload = Vec::new();
139 let mut logical_len = 0u64;
140 for (index, entry) in self.entries().iter().enumerate() {
141 let source_position = self.source_position_at(index);
142 logical_len = logical_len
143 .checked_add(entry.encoded_len(source_position) as u64)
144 .ok_or_else(|| TreeStreamError::Malformed("logical length overflow".into()))?;
145 let frame = encode_entry_frame(entry, source_position)?;
146 let frame_len = u32::try_from(frame.len()).map_err(|_| {
147 TreeStreamError::Malformed(format!("entry '{}' frame exceeds u32", entry.name()))
148 })?;
149 payload.extend_from_slice(&frame_len.to_le_bytes());
150 payload.extend_from_slice(&frame);
151 }
152 let mut out = Vec::with_capacity(TREE_HEADER_LEN + payload.len());
153 out.extend_from_slice(TREE_CANONICAL_MAGIC);
154 out.push(TREE_ENCODING_VERSION);
155 out.extend_from_slice(tree_id.as_bytes());
156 out.extend_from_slice(&(self.len() as u64).to_le_bytes());
157 out.extend_from_slice(&(payload.len() as u64).to_le_bytes());
158 out.extend_from_slice(&logical_len.to_le_bytes());
159 out.extend_from_slice(&payload);
160 Ok(out)
161 }
162
163 pub fn decode_canonical(data: &[u8]) -> Result<Self, TreeStreamError> {
167 if is_salted_tree(data) {
168 return decode_salted_v4(data);
169 }
170 if is_redacted_tree(data) {
171 return Err(TreeStreamError::Malformed(
172 "HRT1 redacted projection cannot be decoded as a full tree".into(),
173 ));
174 }
175 let header = decode_header(data)?;
176 if header.version == TREE_BLOCK_ENCODING_VERSION {
177 return Self::decode_canonical_streamed(data);
178 }
179 let expected_len = TREE_HEADER_LEN as u64 + header.payload_len;
180 if (data.len() as u64) < expected_len {
181 return Err(TreeStreamError::TruncatedFrame {
182 offset: data.len() as u64,
183 });
184 }
185 if (data.len() as u64) > expected_len {
186 return Err(TreeStreamError::TrailingBytes {
187 extra: data.len() as u64 - expected_len,
188 });
189 }
190 let mut entries = Vec::new();
191 let mut positions = SourcePositions::default();
192 let mut offset = TREE_HEADER_LEN;
193 let payload_end = data.len();
194 for _ in 0..header.entry_count {
195 let (entry, position, consumed) = decode_entry_at(data, offset, payload_end)?;
196 positions.push(entries.len(), position)?;
197 entries.push(entry);
198 offset += consumed;
199 }
200 if offset != payload_end {
201 return Err(TreeStreamError::TrailingBytes {
202 extra: (payload_end - offset) as u64,
203 });
204 }
205 let tree = Tree::try_from_decoded_layout(entries, positions.finish())?;
206 let found = tree.hash();
207 if found != header.tree_id {
208 return Err(TreeStreamError::HashMismatch {
209 expected: header.tree_id,
210 found,
211 });
212 }
213 if tree
214 .entries()
215 .iter()
216 .enumerate()
217 .map(|(index, entry)| entry.encoded_len(tree.source_position_at(index)) as u64)
218 .sum::<u64>()
219 != header.logical_len
220 {
221 return Err(TreeStreamError::Malformed(
222 "declared logical length does not match entries".into(),
223 ));
224 }
225 Ok(tree)
226 }
227
228 pub fn encode_canonical_blocked(
231 &self,
232 level: i32,
233 min_size: usize,
234 ) -> Result<Vec<u8>, TreeStreamError> {
235 let raw = self.encode_canonical()?;
236 if self.scheme() == TreeScheme::V4Salted || raw.len() < min_size {
239 return Ok(raw);
240 }
241 let blocked = encode_blocked_htr4(&raw, level)?;
242 if blocked.len() < raw.len() {
243 Ok(blocked)
244 } else {
245 Ok(raw)
246 }
247 }
248}
249
250pub fn decode_header(data: &[u8]) -> Result<TreeHeader, TreeStreamError> {
252 if data.len() < TREE_HEADER_LEN {
253 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
254 }
255 if !is_canonical_tree(data) {
256 return Err(TreeStreamError::Malformed(
257 "bytes are not a canonical HTR4 tree".into(),
258 ));
259 }
260 let version = data[4];
261 if version != TREE_ENCODING_VERSION && version != TREE_BLOCK_ENCODING_VERSION {
262 return Err(TreeStreamError::UnsupportedVersion { found: version });
263 }
264 let tree_id = ContentHash::from_bytes(
265 data[5..37]
266 .try_into()
267 .map_err(|_| TreeStreamError::Malformed("tree id slice is not 32 bytes".into()))?,
268 );
269 let entry_count = u64::from_le_bytes(
270 data[37..45]
271 .try_into()
272 .map_err(|_| TreeStreamError::Malformed("entry count slice is not 8 bytes".into()))?,
273 );
274 let payload_len =
275 u64::from_le_bytes(data[45..53].try_into().map_err(|_| {
276 TreeStreamError::Malformed("payload length slice is not 8 bytes".into())
277 })?);
278 let logical_len =
279 u64::from_le_bytes(data[53..61].try_into().map_err(|_| {
280 TreeStreamError::Malformed("logical length slice is not 8 bytes".into())
281 })?);
282 Ok(TreeHeader {
283 version,
284 tree_id,
285 entry_count,
286 payload_len,
287 logical_len,
288 })
289}
290
291pub(crate) fn decode_block_header(
292 header: &TreeHeader,
293 preamble: &[u8],
294 object_len: u64,
295) -> Result<TreeBlockHeader, TreeStreamError> {
296 if header.version != TREE_BLOCK_ENCODING_VERSION {
297 return Err(TreeStreamError::Malformed(
298 "tree does not use block compression".into(),
299 ));
300 }
301 if preamble.len() != TREE_BLOCK_PREAMBLE_LEN {
302 return Err(TreeStreamError::TruncatedFrame {
303 offset: TREE_HEADER_LEN as u64,
304 });
305 }
306 if preamble[0] != TREE_BLOCK_CODEC_ZSTD {
307 return Err(TreeStreamError::Malformed(format!(
308 "unsupported tree block codec {}",
309 preamble[0]
310 )));
311 }
312 if preamble[1] != 0 {
313 return Err(TreeStreamError::Malformed(
314 "unsupported tree block flags".into(),
315 ));
316 }
317 let block_entries = u16::from_le_bytes([preamble[2], preamble[3]]) as usize;
318 if block_entries == 0 {
319 return Err(TreeStreamError::Malformed(
320 "tree block size must be nonzero".into(),
321 ));
322 }
323 if block_entries > TREE_BLOCK_ENTRIES {
324 return Err(TreeStreamError::Malformed(format!(
325 "tree block entry count {block_entries} exceeds maximum {TREE_BLOCK_ENTRIES}"
326 )));
327 }
328 let block_count = u32::from_le_bytes(
329 preamble[4..8]
330 .try_into()
331 .map_err(|_| TreeStreamError::Malformed("invalid tree block count".into()))?,
332 ) as usize;
333 let raw_payload_len = u64::from_le_bytes(
334 preamble[8..16]
335 .try_into()
336 .map_err(|_| TreeStreamError::Malformed("invalid raw tree payload length".into()))?,
337 );
338 let expected_blocks = header.entry_count.div_ceil(block_entries as u64);
339 if block_count as u64 != expected_blocks {
340 return Err(TreeStreamError::Malformed(
341 "tree block count does not match entry count".into(),
342 ));
343 }
344 let index_bytes = block_count
345 .checked_mul(TREE_BLOCK_INDEX_LEN)
346 .ok_or_else(|| TreeStreamError::Malformed("tree block index length overflow".into()))?;
347 let index_end = TREE_HEADER_LEN
348 .checked_add(TREE_BLOCK_PREAMBLE_LEN)
349 .and_then(|len| len.checked_add(index_bytes))
350 .ok_or_else(|| TreeStreamError::Malformed("tree block index end overflow".into()))?
351 as u64;
352 let expected_object_len = (TREE_HEADER_LEN as u64)
353 .checked_add(header.payload_len)
354 .ok_or_else(|| TreeStreamError::Malformed("tree object length overflow".into()))?;
355 if index_end > expected_object_len || expected_object_len != object_len {
356 return Err(TreeStreamError::TruncatedFrame { offset: object_len });
357 }
358 Ok(TreeBlockHeader {
359 block_entries,
360 block_count,
361 entry_count: header.entry_count,
362 raw_payload_len,
363 index_end,
364 })
365}
366
367pub(crate) fn decode_block_index(
368 bytes: &[u8],
369 block: usize,
370 block_header: &TreeBlockHeader,
371 object_len: u64,
372) -> Result<TreeBlockIndex, TreeStreamError> {
373 if bytes.len() != TREE_BLOCK_INDEX_LEN || block >= block_header.block_count {
374 return Err(TreeStreamError::Malformed(
375 "invalid tree block index entry".into(),
376 ));
377 }
378 let raw_offset = u64::from_le_bytes(
379 bytes[0..8]
380 .try_into()
381 .map_err(|_| TreeStreamError::Malformed("invalid tree block raw offset".into()))?,
382 );
383 let stored_offset = u64::from_le_bytes(
384 bytes[8..16]
385 .try_into()
386 .map_err(|_| TreeStreamError::Malformed("invalid tree block stored offset".into()))?,
387 );
388 let stored_len = u32::from_le_bytes(
389 bytes[16..20]
390 .try_into()
391 .map_err(|_| TreeStreamError::Malformed("invalid tree block stored length".into()))?,
392 ) as usize;
393 let raw_len = u32::from_le_bytes(
394 bytes[20..24]
395 .try_into()
396 .map_err(|_| TreeStreamError::Malformed("invalid tree block raw length".into()))?,
397 ) as usize;
398 let first_entry = (block as u64)
399 .checked_mul(block_header.block_entries as u64)
400 .ok_or_else(|| TreeStreamError::Malformed("tree block ordinal overflow".into()))?;
401 let entries = block_header
402 .entry_count
403 .checked_sub(first_entry)
404 .ok_or_else(|| TreeStreamError::Malformed("tree block starts past entries".into()))?
405 .min(block_header.block_entries as u64) as usize;
406 let max_raw_len = entries
407 .checked_mul(TREE_BLOCK_MAX_ENTRY_FRAME_LEN)
408 .ok_or_else(|| TreeStreamError::Malformed("tree block raw length overflow".into()))?;
409 validate_block_lengths(stored_len, raw_len, max_raw_len)?;
410 if stored_offset < block_header.index_end {
411 return Err(TreeStreamError::Malformed(
412 "invalid empty or overlapping tree block".into(),
413 ));
414 }
415 if stored_offset
416 .checked_add(stored_len as u64)
417 .is_none_or(|end| end > object_len)
418 {
419 return Err(TreeStreamError::TruncatedFrame {
420 offset: stored_offset,
421 });
422 }
423 Ok(TreeBlockIndex {
424 raw_offset,
425 stored_offset,
426 stored_len,
427 raw_len,
428 })
429}
430
431fn validate_block_lengths(
432 stored_len: usize,
433 raw_len: usize,
434 max_raw_len: usize,
435) -> Result<(), TreeStreamError> {
436 if raw_len > max_raw_len {
437 return Err(TreeStreamError::Malformed(format!(
438 "tree block raw length {raw_len} exceeds maximum {max_raw_len}"
439 )));
440 }
441 if stored_len == 0 || raw_len == 0 {
442 return Err(TreeStreamError::Malformed(
443 "invalid empty or overlapping tree block".into(),
444 ));
445 }
446 let expansion_limit = (stored_len as u64) * (TREE_BLOCK_MAX_EXPANSION_RATIO as u64);
447 if raw_len as u64 > expansion_limit {
448 return Err(TreeStreamError::Malformed(format!(
449 "tree block raw length {raw_len} exceeds {TREE_BLOCK_MAX_EXPANSION_RATIO}:1 expansion limit for {stored_len} stored bytes"
450 )));
451 }
452 Ok(())
453}
454
455pub(crate) fn decode_block_payload(
456 stored: &[u8],
457 raw_len: usize,
458) -> Result<Vec<u8>, TreeStreamError> {
459 validate_block_lengths(stored.len(), raw_len, TREE_BLOCK_MAX_RAW_LEN)?;
460 if stored.len() == raw_len {
461 return Ok(stored.to_vec());
462 }
463 let anchor_end = block_anchor_end(stored)?;
464 if anchor_end >= raw_len {
465 return Err(TreeStreamError::Malformed(
466 "compressed tree block has no tail".into(),
467 ));
468 }
469 let tail = decompress_block_tail(&stored[anchor_end..], raw_len - anchor_end)?;
470 let mut raw = Vec::with_capacity(raw_len);
471 raw.extend_from_slice(&stored[..anchor_end]);
472 raw.extend_from_slice(&tail);
473 if raw.len() != raw_len {
474 return Err(TreeStreamError::Malformed(
475 "decoded tree block length mismatch".into(),
476 ));
477 }
478 Ok(raw)
479}
480
481pub(crate) fn block_anchor_end(stored: &[u8]) -> Result<usize, TreeStreamError> {
482 let len_bytes = stored
483 .get(..4)
484 .ok_or(TreeStreamError::TruncatedFrame { offset: 0 })?;
485 let frame_len = u32::from_le_bytes(
486 len_bytes
487 .try_into()
488 .map_err(|_| TreeStreamError::Malformed("invalid anchor frame length".into()))?,
489 ) as usize;
490 let end = 4usize
491 .checked_add(frame_len)
492 .ok_or(TreeStreamError::TruncatedFrame { offset: 0 })?;
493 if end > stored.len() {
494 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
495 }
496 Ok(end)
497}
498
499fn encode_blocked_htr4(raw: &[u8], level: i32) -> Result<Vec<u8>, TreeStreamError> {
500 let header = decode_header(raw)?;
501 if header.version != TREE_ENCODING_VERSION {
502 return Err(TreeStreamError::Malformed(
503 "block encoder requires raw HTR4".into(),
504 ));
505 }
506 let ranges = entry_frame_ranges(raw, header.entry_count)?;
507 let block_count = ranges.len().div_ceil(TREE_BLOCK_ENTRIES);
508 let block_count_u32 = u32::try_from(block_count)
509 .map_err(|_| TreeStreamError::Malformed("tree has too many blocks".into()))?;
510 let mut blocks = Vec::with_capacity(block_count);
511 for chunk in ranges.chunks(TREE_BLOCK_ENTRIES) {
512 let (start, anchor_end) = *chunk
513 .first()
514 .ok_or_else(|| TreeStreamError::Malformed("empty tree block".into()))?;
515 let end = chunk
516 .last()
517 .map(|range| range.1)
518 .ok_or_else(|| TreeStreamError::Malformed("empty tree block".into()))?;
519 let block = &raw[start..end];
520 let compressed_tail = compress_block_tail(&raw[anchor_end..end], level)?;
521 if anchor_end - start + compressed_tail.len() < block.len() {
522 let mut stored = Vec::with_capacity(anchor_end - start + compressed_tail.len());
523 stored.extend_from_slice(&raw[start..anchor_end]);
524 stored.extend_from_slice(&compressed_tail);
525 blocks.push(stored);
526 } else {
527 blocks.push(block.to_vec());
528 }
529 }
530
531 let index_bytes = block_count
532 .checked_mul(TREE_BLOCK_INDEX_LEN)
533 .ok_or_else(|| TreeStreamError::Malformed("tree block index length overflow".into()))?;
534 let stored_payload_len = TREE_BLOCK_PREAMBLE_LEN
535 .checked_add(index_bytes)
536 .and_then(|len| {
537 blocks
538 .iter()
539 .try_fold(len, |total, block| total.checked_add(block.len()))
540 })
541 .ok_or_else(|| TreeStreamError::Malformed("blocked tree length overflow".into()))?;
542 let mut out = Vec::with_capacity(TREE_HEADER_LEN + stored_payload_len);
543 out.extend_from_slice(TREE_CANONICAL_MAGIC);
544 out.push(TREE_BLOCK_ENCODING_VERSION);
545 out.extend_from_slice(header.tree_id.as_bytes());
546 out.extend_from_slice(&header.entry_count.to_le_bytes());
547 out.extend_from_slice(&(stored_payload_len as u64).to_le_bytes());
548 out.extend_from_slice(&header.logical_len.to_le_bytes());
549 out.push(TREE_BLOCK_CODEC_ZSTD);
550 out.push(0);
551 out.extend_from_slice(&(TREE_BLOCK_ENTRIES as u16).to_le_bytes());
552 out.extend_from_slice(&block_count_u32.to_le_bytes());
553 out.extend_from_slice(&header.payload_len.to_le_bytes());
554
555 let mut stored_offset = TREE_HEADER_LEN
556 .checked_add(TREE_BLOCK_PREAMBLE_LEN)
557 .and_then(|len| len.checked_add(index_bytes))
558 .ok_or_else(|| TreeStreamError::Malformed("tree block offset overflow".into()))?;
559 for (block, stored) in blocks.iter().enumerate() {
560 let first_entry = block * TREE_BLOCK_ENTRIES;
561 let chunk = &ranges[first_entry..(first_entry + TREE_BLOCK_ENTRIES).min(ranges.len())];
562 let raw_offset = chunk[0].0 - TREE_HEADER_LEN;
563 let raw_len = chunk[chunk.len() - 1].1 - chunk[0].0;
564 let stored_len = u32::try_from(stored.len())
565 .map_err(|_| TreeStreamError::Malformed("stored tree block exceeds u32".into()))?;
566 let raw_len = u32::try_from(raw_len)
567 .map_err(|_| TreeStreamError::Malformed("raw tree block exceeds u32".into()))?;
568 out.extend_from_slice(&(raw_offset as u64).to_le_bytes());
569 out.extend_from_slice(&(stored_offset as u64).to_le_bytes());
570 out.extend_from_slice(&stored_len.to_le_bytes());
571 out.extend_from_slice(&raw_len.to_le_bytes());
572 stored_offset = stored_offset
573 .checked_add(stored.len())
574 .ok_or_else(|| TreeStreamError::Malformed("tree block offset overflow".into()))?;
575 }
576 for block in blocks {
577 out.extend_from_slice(&block);
578 }
579 Ok(out)
580}
581
582fn entry_frame_ranges(
583 raw: &[u8],
584 entry_count: u64,
585) -> Result<Vec<(usize, usize)>, TreeStreamError> {
586 let count = usize::try_from(entry_count)
587 .map_err(|_| TreeStreamError::Malformed("tree entry count exceeds usize".into()))?;
588 let mut ranges = Vec::with_capacity(count);
589 let mut offset = TREE_HEADER_LEN;
590 for _ in 0..count {
591 let len_bytes = raw
592 .get(offset..offset + 4)
593 .ok_or(TreeStreamError::TruncatedFrame {
594 offset: offset as u64,
595 })?;
596 let frame_len = u32::from_le_bytes(
597 len_bytes
598 .try_into()
599 .map_err(|_| TreeStreamError::Malformed("invalid frame length".into()))?,
600 ) as usize;
601 let end = offset
602 .checked_add(4)
603 .and_then(|start| start.checked_add(frame_len))
604 .ok_or(TreeStreamError::TruncatedFrame {
605 offset: offset as u64,
606 })?;
607 if end > raw.len() {
608 return Err(TreeStreamError::TruncatedFrame {
609 offset: offset as u64,
610 });
611 }
612 ranges.push((offset, end));
613 offset = end;
614 }
615 if offset != raw.len() {
616 return Err(TreeStreamError::TrailingBytes {
617 extra: raw.len().abs_diff(offset) as u64,
618 });
619 }
620 Ok(ranges)
621}
622
623#[cfg(feature = "zstd")]
624fn compress_block_tail(data: &[u8], level: i32) -> Result<Vec<u8>, TreeStreamError> {
625 zstd::bulk::compress(data, level)
626 .map_err(|error| TreeStreamError::Compression(error.to_string()))
627}
628
629#[cfg(not(feature = "zstd"))]
630fn compress_block_tail(_data: &[u8], _level: i32) -> Result<Vec<u8>, TreeStreamError> {
631 Err(TreeStreamError::Compression(
632 "zstd support is not compiled into this build".into(),
633 ))
634}
635
636#[cfg(feature = "zstd")]
637fn decompress_block_tail(data: &[u8], capacity: usize) -> Result<Vec<u8>, TreeStreamError> {
638 if capacity > TREE_BLOCK_MAX_RAW_LEN {
639 return Err(TreeStreamError::Malformed(format!(
640 "tree block tail length {capacity} exceeds maximum {TREE_BLOCK_MAX_RAW_LEN}"
641 )));
642 }
643 let decoded = zstd::bulk::decompress(data, capacity)
644 .map_err(|error| TreeStreamError::Compression(error.to_string()))?;
645 if decoded.len() != capacity {
646 return Err(TreeStreamError::Malformed(
647 "decoded tree block tail length mismatch".into(),
648 ));
649 }
650 Ok(decoded)
651}
652
653#[cfg(not(feature = "zstd"))]
654fn decompress_block_tail(_data: &[u8], _capacity: usize) -> Result<Vec<u8>, TreeStreamError> {
655 Err(TreeStreamError::Compression(
656 "zstd support is not compiled into this build".into(),
657 ))
658}
659
660#[derive(Clone, Debug, PartialEq, Eq)]
662pub enum TreeDeltaOp {
663 Remove(String),
664 Upsert(TreeEntry),
665}
666
667impl TreeDeltaOp {
668 pub fn name(&self) -> &str {
669 match self {
670 Self::Remove(name) => name,
671 Self::Upsert(entry) => entry.name(),
672 }
673 }
674}
675
676#[derive(Clone, Copy, Debug, PartialEq, Eq)]
678pub struct TreeDeltaHeader {
679 pub anchor: ContentHash,
680 pub result_count: usize,
681 pub op_count: usize,
682 pub first_op_count: usize,
683 pub first_base_count: usize,
684 pub first_end: usize,
685 pub hundred_op_count: usize,
686 pub hundred_base_count: usize,
687 pub hundred_end: usize,
688}
689
690impl Tree {
691 pub fn encode_lean(&self) -> Result<Vec<u8>, TreeStreamError> {
695 if self.scheme() == TreeScheme::V4Salted {
696 return Err(TreeStreamError::Malformed(
699 "cannot encode a v4 salted tree as HLR1 lean; use HSR1".into(),
700 ));
701 }
702 if self.has_git_layout() {
703 return Err(TreeStreamError::Malformed(
705 "cannot encode a tree with a git source layout as HLR1 lean; use HTR4".into(),
706 ));
707 }
708 self.validate()?;
709 encode_lean_entries(self.entries())
710 }
711
712 pub fn decode_lean(data: &[u8], expected: ContentHash) -> Result<Self, TreeStreamError> {
714 let (entries, consumed) = decode_lean_entries(data, usize::MAX)?;
715 if consumed != data.len() {
716 return Err(TreeStreamError::TrailingBytes {
717 extra: data.len().abs_diff(consumed) as u64,
718 });
719 }
720 let tree = Tree::try_from_decoded_entries(entries)?;
721 let found = tree.hash();
722 if found != expected {
723 return Err(TreeStreamError::HashMismatch { expected, found });
724 }
725 Ok(tree)
726 }
727}
728
729pub fn decode_lean_prefix(
732 data: &[u8],
733 wanted: usize,
734) -> Result<(Vec<TreeEntry>, usize), TreeStreamError> {
735 decode_lean_entries(data, wanted)
736}
737
738fn encode_lean_entries(entries: &[TreeEntry]) -> Result<Vec<u8>, TreeStreamError> {
739 let mut out = Vec::new();
740 out.extend_from_slice(TREE_LEAN_MAGIC);
741 put_varint(entries.len(), &mut out);
742 let mut previous = "";
743 for entry in entries {
744 encode_lean_entry(entry, previous, &mut out)?;
745 previous = entry.name();
746 }
747 Ok(out)
748}
749
750fn decode_lean_entries(
751 data: &[u8],
752 wanted: usize,
753) -> Result<(Vec<TreeEntry>, usize), TreeStreamError> {
754 if !is_lean_tree(data) {
755 return Err(TreeStreamError::Malformed(
756 "bytes are not an HLR1 tree anchor".into(),
757 ));
758 }
759 let mut offset = TREE_LEAN_MAGIC.len();
760 let count = take_varint(data, &mut offset)?;
761 let wanted = wanted.min(count);
762 let mut entries = Vec::with_capacity(wanted);
763 let mut previous = String::new();
764 for _ in 0..wanted {
765 let entry = decode_compact_entry(data, &mut offset, &previous)?;
766 if !previous.is_empty() && previous.as_str() >= entry.name() {
767 return Err(TreeError::InvalidStructure(
768 "entries must be strictly sorted by name".into(),
769 )
770 .into());
771 }
772 previous = entry.name().to_string();
773 entries.push(entry);
774 }
775 if wanted == count && offset != data.len() {
776 return Err(TreeStreamError::TrailingBytes {
777 extra: data.len().abs_diff(offset) as u64,
778 });
779 }
780 Ok((entries, offset))
781}
782
783fn put_varint(mut value: usize, out: &mut Vec<u8>) {
784 while value >= 0x80 {
785 out.push((value as u8) | 0x80);
786 value >>= 7;
787 }
788 out.push(value as u8);
789}
790
791pub(crate) fn take_varint(bytes: &[u8], offset: &mut usize) -> Result<usize, TreeStreamError> {
792 let mut value = 0usize;
793 for shift in (0..usize::BITS).step_by(7) {
794 let byte = *bytes.get(*offset).ok_or(TreeStreamError::TruncatedFrame {
795 offset: *offset as u64,
796 })?;
797 *offset += 1;
798 value |= ((byte & 0x7f) as usize)
799 .checked_shl(shift)
800 .ok_or_else(|| TreeStreamError::Malformed("varint overflow".into()))?;
801 if byte & 0x80 == 0 {
802 return Ok(value);
803 }
804 }
805 Err(TreeStreamError::Malformed("varint overflow".into()))
806}
807
808fn shared_prefix(left: &str, right: &str) -> usize {
809 left.as_bytes()
810 .iter()
811 .zip(right.as_bytes())
812 .take_while(|(left, right)| left == right)
813 .count()
814}
815
816pub fn encode_lean_entry(
819 entry: &TreeEntry,
820 previous_name: &str,
821 out: &mut Vec<u8>,
822) -> Result<(), TreeStreamError> {
823 if entry.raw_git_mode().is_some() {
824 return Err(TreeStreamError::Malformed(format!(
825 "compact entry '{}' cannot carry a raw git mode",
826 entry.name()
827 )));
828 }
829 out.push((entry.mode().to_byte() << 3) | entry.entry_type().to_byte());
830 let prefix = shared_prefix(previous_name, entry.name());
831 put_varint(prefix, out);
832 put_varint(entry.name().len() - prefix, out);
833 out.extend_from_slice(&entry.name().as_bytes()[prefix..]);
834 match entry.entry_type() {
835 EntryType::Blob | EntryType::Tree | EntryType::Symlink => {
836 let hash = entry.content_hash().ok_or_else(|| {
837 TreeStreamError::Malformed("compact entry is missing its content hash".into())
838 })?;
839 out.extend_from_slice(hash.as_bytes());
840 }
841 EntryType::Gitlink => {
842 let target = entry.gitlink_target().ok_or_else(|| {
843 TreeStreamError::Malformed("compact gitlink is missing its target".into())
844 })?;
845 out.push(git_format_to_tag(target.format()));
846 out.extend_from_slice(target.as_bytes());
847 }
848 EntryType::Spoollink => {
849 let (spool, state) = entry.spoollink_target().ok_or_else(|| {
850 TreeStreamError::Malformed("compact spoollink is missing its target".into())
851 })?;
852 put_varint(spool.as_str().len(), out);
853 out.extend_from_slice(spool.as_str().as_bytes());
854 out.extend_from_slice(state.as_bytes());
855 }
856 }
857 Ok(())
858}
859
860pub(crate) fn decode_compact_entry(
861 bytes: &[u8],
862 offset: &mut usize,
863 previous_name: &str,
864) -> Result<TreeEntry, TreeStreamError> {
865 let tag = *bytes.get(*offset).ok_or(TreeStreamError::TruncatedFrame {
866 offset: *offset as u64,
867 })?;
868 *offset += 1;
869 let mode = FileMode::from_byte(tag >> 3).ok_or_else(|| {
870 TreeStreamError::Malformed(format!("invalid compact entry mode {}", tag >> 3))
871 })?;
872 let kind = EntryType::from_byte(tag & 0x07).ok_or_else(|| {
873 TreeStreamError::Malformed(format!("invalid compact entry kind {}", tag & 0x07))
874 })?;
875 let prefix = take_varint(bytes, offset)?;
876 let suffix_len = take_varint(bytes, offset)?;
877 if prefix > previous_name.len() {
878 return Err(TreeStreamError::Malformed(
879 "compact name prefix exceeds predecessor".into(),
880 ));
881 }
882 let suffix_end = offset
883 .checked_add(suffix_len)
884 .ok_or_else(|| TreeStreamError::Malformed("compact name length overflow".into()))?;
885 let suffix = bytes
886 .get(*offset..suffix_end)
887 .ok_or(TreeStreamError::TruncatedFrame {
888 offset: *offset as u64,
889 })?;
890 let mut name = previous_name.as_bytes()[..prefix].to_vec();
891 name.extend_from_slice(suffix);
892 let name = String::from_utf8(name)
893 .map_err(|_| TreeStreamError::Malformed("compact entry name is not UTF-8".into()))?;
894 *offset = suffix_end;
895 let entry = match kind {
896 EntryType::Blob | EntryType::Tree | EntryType::Symlink => {
897 let end = offset
898 .checked_add(32)
899 .ok_or_else(|| TreeStreamError::Malformed("compact hash overflow".into()))?;
900 let hash = ContentHash::from_bytes(
901 bytes
902 .get(*offset..end)
903 .ok_or(TreeStreamError::TruncatedFrame {
904 offset: *offset as u64,
905 })?
906 .try_into()
907 .map_err(|_| {
908 TreeStreamError::Malformed("compact hash is not 32 bytes".into())
909 })?,
910 );
911 *offset = end;
912 match kind {
913 EntryType::Blob => TreeEntry::file(name, hash, mode == FileMode::Executable)?,
914 EntryType::Tree => TreeEntry::directory(name, hash)?,
915 EntryType::Symlink => TreeEntry::symlink(name, hash)?,
916 EntryType::Gitlink | EntryType::Spoollink => {
917 return Err(TreeStreamError::Malformed(
918 "invalid compact content-addressed kind".into(),
919 ));
920 }
921 }
922 }
923 EntryType::Gitlink => {
924 let format_tag = *bytes.get(*offset).ok_or(TreeStreamError::TruncatedFrame {
925 offset: *offset as u64,
926 })?;
927 *offset += 1;
928 let format = git_format_from_tag(format_tag)?;
929 let oid_len = match format {
930 GitObjectFormat::Sha1 => 20,
931 GitObjectFormat::Sha256 => 32,
932 };
933 let end = offset.checked_add(oid_len).ok_or_else(|| {
934 TreeStreamError::Malformed("compact gitlink length overflow".into())
935 })?;
936 let target = GitObjectId::from_raw(
937 format,
938 bytes
939 .get(*offset..end)
940 .ok_or(TreeStreamError::TruncatedFrame {
941 offset: *offset as u64,
942 })?,
943 )
944 .map_err(|error| {
945 TreeStreamError::Malformed(format!("invalid compact gitlink: {error}"))
946 })?;
947 *offset = end;
948 TreeEntry::gitlink(name, target)?
949 }
950 EntryType::Spoollink => {
951 let spool_len = take_varint(bytes, offset)?;
952 let spool_end = offset.checked_add(spool_len).ok_or_else(|| {
953 TreeStreamError::Malformed("compact spool length overflow".into())
954 })?;
955 let spool = std::str::from_utf8(bytes.get(*offset..spool_end).ok_or(
956 TreeStreamError::TruncatedFrame {
957 offset: *offset as u64,
958 },
959 )?)
960 .map_err(|_| TreeStreamError::Malformed("compact spool id is not UTF-8".into()))?;
961 *offset = spool_end;
962 let state_end = offset
963 .checked_add(32)
964 .ok_or_else(|| TreeStreamError::Malformed("compact state overflow".into()))?;
965 let state = StateId::from_bytes(
966 bytes
967 .get(*offset..state_end)
968 .ok_or(TreeStreamError::TruncatedFrame {
969 offset: *offset as u64,
970 })?
971 .try_into()
972 .map_err(|_| {
973 TreeStreamError::Malformed("compact state is not 32 bytes".into())
974 })?,
975 );
976 *offset = state_end;
977 let spool_id = SpoolId::parse(spool).map_err(|error| {
978 TreeStreamError::Malformed(format!("invalid compact spool id: {error}"))
979 })?;
980 TreeEntry::spoollink(name, spool_id, state)?
981 }
982 };
983 if entry.mode() != mode {
984 return Err(TreeStreamError::Malformed(format!(
985 "compact entry kind/mode mismatch for {}: {kind:?}/{mode:?}",
986 entry.name()
987 )));
988 }
989 Ok(entry)
990}
991
992pub fn tree_delta(anchor: &Tree, current: &Tree) -> Vec<TreeDeltaOp> {
994 let mut ops = Vec::new();
995 let mut anchor_index = 0usize;
996 let mut current_index = 0usize;
997 while anchor_index < anchor.len() || current_index < current.len() {
998 match (
999 anchor.entries().get(anchor_index),
1000 current.entries().get(current_index),
1001 ) {
1002 (Some(anchor_entry), Some(current_entry)) => {
1003 match anchor_entry.name().cmp(current_entry.name()) {
1004 std::cmp::Ordering::Less => {
1005 ops.push(TreeDeltaOp::Remove(anchor_entry.name().to_string()));
1006 anchor_index += 1;
1007 }
1008 std::cmp::Ordering::Greater => {
1009 ops.push(TreeDeltaOp::Upsert(current_entry.clone()));
1010 current_index += 1;
1011 }
1012 std::cmp::Ordering::Equal => {
1013 if anchor_entry != current_entry {
1014 ops.push(TreeDeltaOp::Upsert(current_entry.clone()));
1015 }
1016 anchor_index += 1;
1017 current_index += 1;
1018 }
1019 }
1020 }
1021 (Some(anchor_entry), None) => {
1022 ops.push(TreeDeltaOp::Remove(anchor_entry.name().to_string()));
1023 anchor_index += 1;
1024 }
1025 (None, Some(current_entry)) => {
1026 ops.push(TreeDeltaOp::Upsert(current_entry.clone()));
1027 current_index += 1;
1028 }
1029 (None, None) => break,
1030 }
1031 }
1032 ops
1033}
1034
1035pub fn apply_tree_delta(anchor: &Tree, ops: &[TreeDeltaOp]) -> Result<Tree, TreeStreamError> {
1037 if anchor.scheme() == TreeScheme::V4Salted {
1038 return Err(TreeStreamError::Malformed(
1042 "cannot apply an HDC1 delta to a v4 salted anchor".into(),
1043 ));
1044 }
1045 let mut entries = Vec::with_capacity(anchor.len().saturating_add(ops.len()));
1046 let mut anchor_index = 0usize;
1047 let mut op_index = 0usize;
1048 while anchor_index < anchor.len() || op_index < ops.len() {
1049 match (anchor.entries().get(anchor_index), ops.get(op_index)) {
1050 (Some(anchor_entry), Some(op)) => match anchor_entry.name().cmp(op.name()) {
1051 std::cmp::Ordering::Less => {
1052 entries.push(anchor_entry.clone());
1053 anchor_index += 1;
1054 }
1055 std::cmp::Ordering::Greater => {
1056 if let TreeDeltaOp::Upsert(entry) = op {
1057 entries.push(entry.clone());
1058 }
1059 op_index += 1;
1060 }
1061 std::cmp::Ordering::Equal => {
1062 if let TreeDeltaOp::Upsert(entry) = op {
1063 entries.push(entry.clone());
1064 }
1065 anchor_index += 1;
1066 op_index += 1;
1067 }
1068 },
1069 (Some(anchor_entry), None) => {
1070 entries.push(anchor_entry.clone());
1071 anchor_index += 1;
1072 }
1073 (None, Some(op)) => {
1074 if let TreeDeltaOp::Upsert(entry) = op {
1075 entries.push(entry.clone());
1076 }
1077 op_index += 1;
1078 }
1079 (None, None) => break,
1080 }
1081 }
1082 Tree::try_from_decoded_entries(entries).map_err(TreeStreamError::from)
1083}
1084
1085fn delta_prefix_counts(
1086 anchor: &Tree,
1087 current: &Tree,
1088 ops: &[TreeDeltaOp],
1089 count: usize,
1090) -> Result<(u16, u16), TreeStreamError> {
1091 if current.is_empty() {
1092 return Ok((0, 0));
1093 }
1094 let boundary = current.entries()[count.min(current.len()) - 1].name();
1095 let op_count = ops.partition_point(|op| op.name() <= boundary);
1096 let base_count = anchor
1097 .entries()
1098 .partition_point(|entry| entry.name() <= boundary);
1099 Ok((
1100 u16::try_from(op_count)
1101 .map_err(|_| TreeStreamError::Malformed("delta porch op count exceeds u16".into()))?,
1102 u16::try_from(base_count).map_err(|_| {
1103 TreeStreamError::Malformed("delta porch anchor count exceeds u16".into())
1104 })?,
1105 ))
1106}
1107
1108pub fn encode_tree_delta(
1110 anchor_id: ContentHash,
1111 anchor: &Tree,
1112 current: &Tree,
1113 ops: &[TreeDeltaOp],
1114) -> Result<Vec<u8>, TreeStreamError> {
1115 if anchor.scheme() == TreeScheme::V4Salted || current.scheme() == TreeScheme::V4Salted {
1116 return Err(TreeStreamError::Malformed(
1119 "cannot encode a v4 salted tree as an HDC1 delta; use HSR1".into(),
1120 ));
1121 }
1122 if anchor.has_git_layout() || current.has_git_layout() {
1123 return Err(TreeStreamError::Malformed(
1125 "cannot encode a tree with a git source layout as an HDC1 delta; use HTR4".into(),
1126 ));
1127 }
1128 anchor.validate()?;
1129 current.validate()?;
1130 if anchor.hash() != anchor_id {
1131 return Err(TreeStreamError::HashMismatch {
1132 expected: anchor_id,
1133 found: anchor.hash(),
1134 });
1135 }
1136 if ops.len() > TREE_DELTA_MAX_OPS {
1137 return Err(TreeStreamError::Malformed(format!(
1138 "tree delta has {} operations; maximum is {TREE_DELTA_MAX_OPS}",
1139 ops.len()
1140 )));
1141 }
1142 if apply_tree_delta(anchor, ops)? != *current {
1143 return Err(TreeStreamError::Malformed(
1144 "tree delta operations do not reconstruct the result".into(),
1145 ));
1146 }
1147 let result_count = u32::try_from(current.len())
1148 .map_err(|_| TreeStreamError::Malformed("delta result count exceeds u32".into()))?;
1149 let op_count = u16::try_from(ops.len())
1150 .map_err(|_| TreeStreamError::Malformed("delta operation count exceeds u16".into()))?;
1151 let (first_ops, first_base) = delta_prefix_counts(anchor, current, ops, 1)?;
1152 let (hundred_ops, hundred_base) = delta_prefix_counts(anchor, current, ops, 100)?;
1153 let mut body = Vec::new();
1154 let mut ends = Vec::with_capacity(ops.len());
1155 let mut previous = "";
1156 for op in ops {
1157 match op {
1158 TreeDeltaOp::Remove(name) => {
1159 body.push(0);
1160 let prefix = shared_prefix(previous, name);
1161 put_varint(prefix, &mut body);
1162 put_varint(name.len() - prefix, &mut body);
1163 body.extend_from_slice(&name.as_bytes()[prefix..]);
1164 }
1165 TreeDeltaOp::Upsert(entry) => {
1166 body.push(1);
1167 encode_lean_entry(entry, previous, &mut body)?;
1168 }
1169 }
1170 if !previous.is_empty() && previous >= op.name() {
1171 return Err(TreeStreamError::Malformed(
1172 "tree delta operations must be strictly sorted".into(),
1173 ));
1174 }
1175 previous = op.name();
1176 ends.push(body.len());
1177 }
1178 let end_for = |count: u16| -> Result<u32, TreeStreamError> {
1179 let end = if count == 0 {
1180 TREE_DELTA_HEADER_LEN
1181 } else {
1182 TREE_DELTA_HEADER_LEN
1183 .checked_add(*ends.get(count as usize - 1).ok_or_else(|| {
1184 TreeStreamError::Malformed("delta porch exceeds operation count".into())
1185 })?)
1186 .ok_or_else(|| TreeStreamError::Malformed("delta porch offset overflow".into()))?
1187 };
1188 u32::try_from(end)
1189 .map_err(|_| TreeStreamError::Malformed("delta porch offset exceeds u32".into()))
1190 };
1191 let mut out = Vec::with_capacity(TREE_DELTA_HEADER_LEN + body.len());
1192 out.extend_from_slice(TREE_DELTA_MAGIC);
1193 out.push(TREE_DELTA_ENCODING_VERSION);
1194 out.extend_from_slice(anchor_id.as_bytes());
1195 out.extend_from_slice(&result_count.to_le_bytes());
1196 out.extend_from_slice(&op_count.to_le_bytes());
1197 out.extend_from_slice(&first_ops.to_le_bytes());
1198 out.extend_from_slice(&first_base.to_le_bytes());
1199 out.extend_from_slice(&end_for(first_ops)?.to_le_bytes());
1200 out.extend_from_slice(&hundred_ops.to_le_bytes());
1201 out.extend_from_slice(&hundred_base.to_le_bytes());
1202 out.extend_from_slice(&end_for(hundred_ops)?.to_le_bytes());
1203 if out.len() != TREE_DELTA_HEADER_LEN {
1204 return Err(TreeStreamError::Malformed(
1205 "internal HDC1 header length mismatch".into(),
1206 ));
1207 }
1208 out.extend_from_slice(&body);
1209 Ok(out)
1210}
1211
1212pub fn decode_tree_delta_header(data: &[u8]) -> Result<TreeDeltaHeader, TreeStreamError> {
1214 decode_tree_delta_header_prefix(data, data.len())
1215}
1216
1217pub fn decode_tree_delta_header_prefix(
1220 data: &[u8],
1221 object_len: usize,
1222) -> Result<TreeDeltaHeader, TreeStreamError> {
1223 if data.len() < TREE_DELTA_HEADER_LEN {
1224 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
1225 }
1226 if !is_delta_tree(data) {
1227 return Err(TreeStreamError::Malformed(
1228 "bytes are not an HDC1 tree delta".into(),
1229 ));
1230 }
1231 if data[4] != TREE_DELTA_ENCODING_VERSION {
1232 return Err(TreeStreamError::UnsupportedVersion { found: data[4] });
1233 }
1234 let anchor = ContentHash::from_bytes(
1235 data[5..37]
1236 .try_into()
1237 .map_err(|_| TreeStreamError::Malformed("delta anchor hash is not 32 bytes".into()))?,
1238 );
1239 let header = TreeDeltaHeader {
1240 anchor,
1241 result_count: read_u32_at(data, 37)? as usize,
1242 op_count: read_u16_at(data, 41)? as usize,
1243 first_op_count: read_u16_at(data, 43)? as usize,
1244 first_base_count: read_u16_at(data, 45)? as usize,
1245 first_end: read_u32_at(data, 47)? as usize,
1246 hundred_op_count: read_u16_at(data, 51)? as usize,
1247 hundred_base_count: read_u16_at(data, 53)? as usize,
1248 hundred_end: read_u32_at(data, 55)? as usize,
1249 };
1250 if header.op_count > TREE_DELTA_MAX_OPS
1251 || header.first_op_count > header.op_count
1252 || header.hundred_op_count > header.op_count
1253 || header.first_end < TREE_DELTA_HEADER_LEN
1254 || header.hundred_end < header.first_end
1255 || header.hundred_end > object_len
1256 {
1257 return Err(TreeStreamError::Malformed(
1258 "invalid HDC1 porch or operation bounds".into(),
1259 ));
1260 }
1261 Ok(header)
1262}
1263
1264pub fn decode_tree_delta_ops(
1267 data: &[u8],
1268 wanted: usize,
1269) -> Result<(TreeDeltaHeader, Vec<TreeDeltaOp>, usize), TreeStreamError> {
1270 decode_tree_delta_ops_prefix(data, data.len(), wanted)
1271}
1272
1273pub fn decode_tree_delta_ops_prefix(
1275 data: &[u8],
1276 object_len: usize,
1277 wanted: usize,
1278) -> Result<(TreeDeltaHeader, Vec<TreeDeltaOp>, usize), TreeStreamError> {
1279 let header = decode_tree_delta_header_prefix(data, object_len)?;
1280 if wanted > header.op_count {
1281 return Err(TreeStreamError::Malformed(
1282 "partial delta operation count exceeds object".into(),
1283 ));
1284 }
1285 let mut offset = TREE_DELTA_HEADER_LEN;
1286 let mut previous = String::new();
1287 let mut ops = Vec::with_capacity(wanted);
1288 for _ in 0..wanted {
1289 let opcode = *data.get(offset).ok_or(TreeStreamError::TruncatedFrame {
1290 offset: offset as u64,
1291 })?;
1292 offset += 1;
1293 let op =
1294 match opcode {
1295 0 => {
1296 let prefix = take_varint(data, &mut offset)?;
1297 let suffix_len = take_varint(data, &mut offset)?;
1298 if prefix > previous.len() {
1299 return Err(TreeStreamError::Malformed(
1300 "delta name prefix exceeds predecessor".into(),
1301 ));
1302 }
1303 let end = offset.checked_add(suffix_len).ok_or_else(|| {
1304 TreeStreamError::Malformed("delta name length overflow".into())
1305 })?;
1306 let mut name = previous.as_bytes()[..prefix].to_vec();
1307 name.extend_from_slice(data.get(offset..end).ok_or(
1308 TreeStreamError::TruncatedFrame {
1309 offset: offset as u64,
1310 },
1311 )?);
1312 offset = end;
1313 TreeDeltaOp::Remove(String::from_utf8(name).map_err(|_| {
1314 TreeStreamError::Malformed("delta name is not UTF-8".into())
1315 })?)
1316 }
1317 1 => TreeDeltaOp::Upsert(decode_compact_entry(data, &mut offset, &previous)?),
1318 _ => {
1319 return Err(TreeStreamError::Malformed(format!(
1320 "invalid tree delta opcode {opcode}"
1321 )));
1322 }
1323 };
1324 if !previous.is_empty() && previous.as_str() >= op.name() {
1325 return Err(TreeStreamError::Malformed(
1326 "tree delta operations must be strictly sorted".into(),
1327 ));
1328 }
1329 previous = op.name().to_string();
1330 ops.push(op);
1331 }
1332 if (wanted == header.first_op_count && offset != header.first_end)
1333 || (wanted == header.hundred_op_count && offset != header.hundred_end)
1334 {
1335 return Err(TreeStreamError::Malformed(
1336 "HDC1 porch offset does not match decoded operations".into(),
1337 ));
1338 }
1339 Ok((header, ops, offset))
1340}
1341
1342pub fn decode_tree_delta(
1345 data: &[u8],
1346 anchor: &Tree,
1347 expected: ContentHash,
1348) -> Result<Tree, TreeStreamError> {
1349 let header = decode_tree_delta_header(data)?;
1350 let (decoded_header, ops, consumed) = decode_tree_delta_ops(data, header.op_count)?;
1351 if consumed != data.len() {
1352 return Err(TreeStreamError::TrailingBytes {
1353 extra: data.len().abs_diff(consumed) as u64,
1354 });
1355 }
1356 let anchor_found = anchor.hash();
1357 if anchor_found != decoded_header.anchor {
1358 return Err(TreeStreamError::HashMismatch {
1359 expected: decoded_header.anchor,
1360 found: anchor_found,
1361 });
1362 }
1363 let tree = apply_tree_delta(anchor, &ops)?;
1364 if tree.len() != decoded_header.result_count {
1365 return Err(TreeStreamError::Malformed(
1366 "delta result count does not match reconstructed tree".into(),
1367 ));
1368 }
1369 let found = tree.hash();
1370 if found != expected {
1371 return Err(TreeStreamError::HashMismatch { expected, found });
1372 }
1373 Ok(tree)
1374}
1375
1376fn read_u16_at(data: &[u8], offset: usize) -> Result<u16, TreeStreamError> {
1377 Ok(u16::from_le_bytes(
1378 data.get(offset..offset + 2)
1379 .ok_or(TreeStreamError::TruncatedFrame {
1380 offset: offset as u64,
1381 })?
1382 .try_into()
1383 .map_err(|_| TreeStreamError::Malformed("invalid u16 field".into()))?,
1384 ))
1385}
1386
1387fn read_u32_at(data: &[u8], offset: usize) -> Result<u32, TreeStreamError> {
1388 Ok(u32::from_le_bytes(
1389 data.get(offset..offset + 4)
1390 .ok_or(TreeStreamError::TruncatedFrame {
1391 offset: offset as u64,
1392 })?
1393 .try_into()
1394 .map_err(|_| TreeStreamError::Malformed("invalid u32 field".into()))?,
1395 ))
1396}
1397
1398pub(crate) fn encode_entry_frame(
1402 entry: &TreeEntry,
1403 source_position: Option<u32>,
1404) -> Result<Vec<u8>, TreeStreamError> {
1405 let name = entry.name().as_bytes();
1406 let name_len = u16::try_from(name.len()).map_err(|_| {
1407 TreeStreamError::Malformed(format!("entry name '{}' exceeds u16", entry.name()))
1408 })?;
1409 let mut frame = Vec::new();
1410 frame.push(entry.mode().to_byte() | entry.layout_flags(source_position));
1411 frame.push(entry.entry_type().to_byte());
1412 frame.extend_from_slice(&name_len.to_le_bytes());
1413 frame.extend_from_slice(name);
1414 encode_target(&mut frame, entry)?;
1415 entry.write_layout_trailer(source_position, |bytes| frame.extend_from_slice(bytes));
1416 Ok(frame)
1417}
1418
1419#[derive(Default)]
1422pub(crate) struct SourcePositions {
1423 positions: Vec<u32>,
1424}
1425
1426impl SourcePositions {
1427 pub(crate) fn push(
1428 &mut self,
1429 index: usize,
1430 position: Option<u32>,
1431 ) -> Result<(), TreeStreamError> {
1432 match position {
1433 Some(position) if self.positions.len() == index => {
1434 self.positions.push(position);
1435 Ok(())
1436 }
1437 None if self.positions.is_empty() => Ok(()),
1438 _ => Err(TreeStreamError::Malformed(
1439 "source positions must be recorded on every entry or none".into(),
1440 )),
1441 }
1442 }
1443
1444 pub(crate) fn finish(self) -> Vec<u32> {
1445 self.positions
1446 }
1447}
1448
1449pub(crate) fn decode_entry_at(
1450 data: &[u8],
1451 offset: usize,
1452 payload_end: usize,
1453) -> Result<(TreeEntry, Option<u32>, usize), TreeStreamError> {
1454 if offset + 4 > payload_end {
1455 return Err(TreeStreamError::TruncatedFrame {
1456 offset: offset as u64,
1457 });
1458 }
1459 let frame_len = u32::from_le_bytes(
1460 data[offset..offset + 4]
1461 .try_into()
1462 .map_err(|_| TreeStreamError::Malformed("frame length slice is not 4 bytes".into()))?,
1463 ) as usize;
1464 let frame_start = offset + 4;
1465 let frame_end = frame_start
1466 .checked_add(frame_len)
1467 .ok_or(TreeStreamError::TruncatedFrame {
1468 offset: offset as u64,
1469 })?;
1470 if frame_end > payload_end {
1471 return Err(TreeStreamError::TruncatedFrame {
1472 offset: offset as u64,
1473 });
1474 }
1475 let (entry, position) = decode_entry_frame(&data[frame_start..frame_end])?;
1476 Ok((entry, position, 4 + frame_len))
1477}
1478
1479pub(crate) fn decode_entry_frame(
1481 frame: &[u8],
1482) -> Result<(TreeEntry, Option<u32>), TreeStreamError> {
1483 if frame.len() < 4 {
1484 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
1485 }
1486 let (flags, mode_byte) = split_layout_flags(frame[0]);
1487 let mode = FileMode::from_byte(mode_byte).ok_or_else(|| {
1488 TreeStreamError::Malformed(format!("malformed tree entry mode {}", frame[0]))
1489 })?;
1490 let kind = EntryType::from_byte(frame[1]).ok_or_else(|| {
1491 TreeStreamError::Malformed(format!("malformed tree entry kind {}", frame[1]))
1492 })?;
1493 let name_len = u16::from_le_bytes([frame[2], frame[3]]) as usize;
1494 let name_end = 4 + name_len;
1495 if frame.len() < name_end {
1496 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
1497 }
1498 let name = std::str::from_utf8(&frame[4..name_end])
1499 .map_err(|_| TreeStreamError::Malformed("tree entry name is not UTF-8".into()))?
1500 .to_string();
1501 let target_end = frame
1502 .len()
1503 .checked_sub(layout_trailer_len(flags))
1504 .filter(|end| *end >= name_end)
1505 .ok_or(TreeStreamError::TruncatedFrame { offset: 0 })?;
1506 let entry = decode_target(name, kind, mode, &frame[name_end..target_end])?;
1507 if entry.mode() != mode {
1508 return Err(TreeStreamError::Malformed(format!(
1509 "tree kind/mode mismatch for {}: {kind:?}/{mode:?}",
1510 entry.name()
1511 )));
1512 }
1513 Ok(apply_layout_trailer(entry, flags, &frame[target_end..])?)
1514}
1515
1516fn encode_target(frame: &mut Vec<u8>, entry: &TreeEntry) -> Result<(), TreeStreamError> {
1517 match entry.entry_type() {
1518 EntryType::Blob | EntryType::Tree | EntryType::Symlink => {
1519 frame.extend_from_slice(entry.require_content_hash().as_bytes());
1520 }
1521 EntryType::Gitlink => {
1522 let target = entry.gitlink_target().ok_or_else(|| {
1523 TreeStreamError::Malformed("gitlink entry is missing target".into())
1524 })?;
1525 frame.push(git_format_to_tag(target.format()));
1526 frame.extend_from_slice(target.as_bytes());
1527 }
1528 EntryType::Spoollink => {
1529 let (spool, state) = entry.spoollink_target().ok_or_else(|| {
1530 TreeStreamError::Malformed("spoollink entry is missing target".into())
1531 })?;
1532 let spool_bytes = spool.as_str().as_bytes();
1533 let spool_len = u16::try_from(spool_bytes.len())
1534 .map_err(|_| TreeStreamError::Malformed("spool id exceeds u16".into()))?;
1535 frame.extend_from_slice(&spool_len.to_le_bytes());
1536 frame.extend_from_slice(spool_bytes);
1537 frame.extend_from_slice(state.as_bytes());
1538 }
1539 }
1540 Ok(())
1541}
1542
1543fn decode_target(
1544 name: String,
1545 kind: EntryType,
1546 mode: FileMode,
1547 payload: &[u8],
1548) -> Result<TreeEntry, TreeStreamError> {
1549 match kind {
1550 EntryType::Blob => TreeEntry::file(name, take_hash(payload)?, mode == FileMode::Executable)
1551 .map_err(TreeStreamError::from),
1552 EntryType::Tree => {
1553 TreeEntry::directory(name, take_hash(payload)?).map_err(TreeStreamError::from)
1554 }
1555 EntryType::Symlink => {
1556 TreeEntry::symlink(name, take_hash(payload)?).map_err(TreeStreamError::from)
1557 }
1558 EntryType::Gitlink => decode_gitlink(name, payload),
1559 EntryType::Spoollink => decode_spoollink(name, payload),
1560 }
1561}
1562
1563fn take_hash(payload: &[u8]) -> Result<ContentHash, TreeStreamError> {
1564 let bytes: [u8; 32] = payload
1565 .try_into()
1566 .map_err(|_| TreeStreamError::Malformed("malformed tree entry object id".into()))?;
1567 Ok(ContentHash::from_bytes(bytes))
1568}
1569
1570fn decode_gitlink(name: String, payload: &[u8]) -> Result<TreeEntry, TreeStreamError> {
1571 if payload.is_empty() {
1572 return Err(TreeStreamError::Malformed(
1573 "malformed tree entry object id".into(),
1574 ));
1575 }
1576 let format = git_format_from_tag(payload[0])?;
1577 let oid = &payload[1..];
1578 let expected = match format {
1579 GitObjectFormat::Sha1 => 20,
1580 GitObjectFormat::Sha256 => 32,
1581 };
1582 if oid.len() != expected {
1583 return Err(TreeStreamError::Malformed(
1584 "malformed tree entry object id".into(),
1585 ));
1586 }
1587 let target = GitObjectId::from_raw(format, oid)
1588 .map_err(|err| TreeError::InvalidStructure(format!("invalid gitlink target: {err}")))?;
1589 TreeEntry::gitlink(name, target).map_err(TreeStreamError::from)
1590}
1591
1592fn decode_spoollink(name: String, payload: &[u8]) -> Result<TreeEntry, TreeStreamError> {
1593 if payload.len() < 2 {
1594 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
1595 }
1596 let spool_len = u16::from_le_bytes([payload[0], payload[1]]) as usize;
1597 let spool_end = 2 + spool_len;
1598 let state_end = spool_end + 32;
1599 if payload.len() != state_end {
1600 return Err(TreeStreamError::Malformed(
1601 "malformed tree entry object id".into(),
1602 ));
1603 }
1604 let spool = std::str::from_utf8(&payload[2..spool_end])
1605 .map_err(|_| TreeStreamError::Malformed("spool id is not UTF-8".into()))?;
1606 let spool_id = SpoolId::parse(spool)
1607 .map_err(|err| TreeStreamError::Malformed(format!("invalid spool id: {err}")))?;
1608 let state =
1609 StateId::from_bytes(payload[spool_end..state_end].try_into().map_err(|_| {
1610 TreeStreamError::Malformed("spoollink state id is not 32 bytes".into())
1611 })?);
1612 TreeEntry::spoollink(name, spool_id, state).map_err(TreeStreamError::from)
1613}
1614
1615pub const TREE_REDACTED_HEADER_LEN: usize = 4 + 1 + 32 + 8;
1619
1620fn encode_salted_entry_frame(
1624 entry: &TreeEntry,
1625 salt: &[u8; 32],
1626) -> Result<Vec<u8>, TreeStreamError> {
1627 let inner = encode_entry_frame(entry, None)?;
1628 let mut frame = Vec::with_capacity(32 + inner.len());
1629 frame.extend_from_slice(salt);
1630 frame.extend_from_slice(&inner);
1631 Ok(frame)
1632}
1633
1634pub(crate) fn decode_salted_entry_frame(
1635 frame: &[u8],
1636) -> Result<([u8; 32], TreeEntry), TreeStreamError> {
1637 if frame.len() < 32 {
1638 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
1639 }
1640 let salt: [u8; 32] = frame[..32]
1641 .try_into()
1642 .map_err(|_| TreeStreamError::Malformed("salted frame salt is not 32 bytes".into()))?;
1643 let (entry, position) = decode_entry_frame(&frame[32..])?;
1644 if position.is_some() {
1645 return Err(TreeStreamError::Malformed(
1646 "salted v4 entries do not record a git source position".into(),
1647 ));
1648 }
1649 Ok((salt, entry))
1650}
1651
1652impl Tree {
1653 pub(crate) fn encode_salted_v4(&self) -> Result<Vec<u8>, TreeStreamError> {
1655 if self.scheme() != TreeScheme::V4Salted {
1656 return Err(TreeStreamError::Malformed(
1657 "HSR1 encoding requires a v4 salted tree".into(),
1658 ));
1659 }
1660 self.validate()?;
1661 let declared_root = self.hash();
1662 let mut payload = Vec::new();
1663 let mut logical_len = 0u64;
1664 for (entry, salt) in self.entries().iter().zip(self.salts().iter()) {
1665 logical_len = logical_len
1666 .checked_add(entry.encoded_len(None) as u64)
1667 .ok_or_else(|| TreeStreamError::Malformed("logical length overflow".into()))?;
1668 let frame = encode_salted_entry_frame(entry, salt)?;
1669 let frame_len = u32::try_from(frame.len()).map_err(|_| {
1670 TreeStreamError::Malformed(format!("entry '{}' frame exceeds u32", entry.name()))
1671 })?;
1672 payload.extend_from_slice(&frame_len.to_le_bytes());
1673 payload.extend_from_slice(&frame);
1674 }
1675 let mut out = Vec::with_capacity(TREE_HEADER_LEN + payload.len());
1676 out.extend_from_slice(TREE_SALTED_MAGIC);
1677 out.push(TREE_SALTED_ENCODING_VERSION);
1678 out.extend_from_slice(declared_root.as_bytes());
1679 out.extend_from_slice(&(self.len() as u64).to_le_bytes());
1680 out.extend_from_slice(&(payload.len() as u64).to_le_bytes());
1681 out.extend_from_slice(&logical_len.to_le_bytes());
1682 out.extend_from_slice(&payload);
1683 Ok(out)
1684 }
1685}
1686
1687pub fn decode_salted_v4(data: &[u8]) -> Result<Tree, TreeStreamError> {
1689 if !is_salted_tree(data) {
1690 return Err(TreeStreamError::Malformed(
1691 "bytes are not an HSR1 salted tree".into(),
1692 ));
1693 }
1694 if data.len() < TREE_HEADER_LEN {
1695 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
1696 }
1697 let version = data[4];
1698 if version != TREE_SALTED_ENCODING_VERSION {
1699 return Err(TreeStreamError::UnsupportedVersion { found: version });
1700 }
1701 let declared_root = ContentHash::from_bytes(
1702 data[5..37]
1703 .try_into()
1704 .map_err(|_| TreeStreamError::Malformed("tree id slice is not 32 bytes".into()))?,
1705 );
1706 let entry_count = u64::from_le_bytes(
1707 data[37..45]
1708 .try_into()
1709 .map_err(|_| TreeStreamError::Malformed("entry count slice is not 8 bytes".into()))?,
1710 );
1711 let payload_len =
1712 u64::from_le_bytes(data[45..53].try_into().map_err(|_| {
1713 TreeStreamError::Malformed("payload length slice is not 8 bytes".into())
1714 })?);
1715 let logical_len =
1716 u64::from_le_bytes(data[53..61].try_into().map_err(|_| {
1717 TreeStreamError::Malformed("logical length slice is not 8 bytes".into())
1718 })?);
1719 let expected_len = TREE_HEADER_LEN as u64 + payload_len;
1720 if (data.len() as u64) < expected_len {
1721 return Err(TreeStreamError::TruncatedFrame {
1722 offset: data.len() as u64,
1723 });
1724 }
1725 if (data.len() as u64) > expected_len {
1726 return Err(TreeStreamError::TrailingBytes {
1727 extra: data.len() as u64 - expected_len,
1728 });
1729 }
1730 let mut entries = Vec::new();
1731 let mut salts = Vec::new();
1732 let mut offset = TREE_HEADER_LEN;
1733 let payload_end = data.len();
1734 for _ in 0..entry_count {
1735 if offset + 4 > payload_end {
1736 return Err(TreeStreamError::TruncatedFrame {
1737 offset: offset as u64,
1738 });
1739 }
1740 let frame_len =
1741 u32::from_le_bytes(data[offset..offset + 4].try_into().map_err(|_| {
1742 TreeStreamError::Malformed("frame length slice is not 4 bytes".into())
1743 })?) as usize;
1744 let frame_start = offset + 4;
1745 let frame_end =
1746 frame_start
1747 .checked_add(frame_len)
1748 .ok_or(TreeStreamError::TruncatedFrame {
1749 offset: offset as u64,
1750 })?;
1751 if frame_end > payload_end {
1752 return Err(TreeStreamError::TruncatedFrame {
1753 offset: offset as u64,
1754 });
1755 }
1756 let (salt, entry) = decode_salted_entry_frame(&data[frame_start..frame_end])?;
1757 entries.push(entry);
1758 salts.push(salt);
1759 offset = frame_end;
1760 }
1761 if offset != payload_end {
1762 return Err(TreeStreamError::TrailingBytes {
1763 extra: (payload_end - offset) as u64,
1764 });
1765 }
1766 let tree = Tree::try_from_decoded_entries_salted_v4(entries, salts)?;
1767 let found = tree.hash();
1768 if found != declared_root {
1769 return Err(TreeStreamError::HashMismatch {
1770 expected: declared_root,
1771 found,
1772 });
1773 }
1774 if tree
1775 .entries()
1776 .iter()
1777 .map(|entry| entry.encoded_len(None) as u64)
1778 .sum::<u64>()
1779 != logical_len
1780 {
1781 return Err(TreeStreamError::Malformed(
1782 "declared logical length does not match entries".into(),
1783 ));
1784 }
1785 Ok(tree)
1786}
1787
1788pub fn encode_redacted_projection(partial: &PartialTree) -> Result<Vec<u8>, TreeStreamError> {
1792 let mut visibles: Vec<(&TreeEntry, &[u8; 32])> = Vec::new();
1793 let mut redacted: Vec<ContentHash> = Vec::new();
1794 for leaf in partial.leaves() {
1795 match leaf {
1796 PartialTreeLeaf::Visible { entry, salt } => visibles.push((entry, salt)),
1797 PartialTreeLeaf::Redacted { leaf_hash } => redacted.push(*leaf_hash),
1798 }
1799 }
1800 visibles.sort_by(|a, b| a.0.name().cmp(b.0.name()));
1801 redacted.sort_unstable();
1802 let entry_count = visibles.len() + redacted.len();
1803 let mut out = Vec::new();
1804 out.extend_from_slice(TREE_REDACTED_MAGIC);
1805 out.push(TREE_REDACTED_ENCODING_VERSION);
1806 out.extend_from_slice(partial.declared_root().as_bytes());
1807 out.extend_from_slice(&(entry_count as u64).to_le_bytes());
1808 for (entry, salt) in visibles {
1809 out.push(0);
1810 let frame = encode_salted_entry_frame(entry, salt)?;
1811 let frame_len = u32::try_from(frame.len()).map_err(|_| {
1812 TreeStreamError::Malformed(format!("entry '{}' frame exceeds u32", entry.name()))
1813 })?;
1814 out.extend_from_slice(&frame_len.to_le_bytes());
1815 out.extend_from_slice(&frame);
1816 }
1817 for leaf_hash in redacted {
1818 out.push(1);
1819 out.extend_from_slice(leaf_hash.as_bytes());
1820 }
1821 Ok(out)
1822}
1823
1824pub fn decode_redacted_projection(data: &[u8]) -> Result<PartialTree, TreeStreamError> {
1827 if !is_redacted_tree(data) {
1828 return Err(TreeStreamError::Malformed(
1829 "bytes are not an HRT1 redacted projection".into(),
1830 ));
1831 }
1832 if data.len() < TREE_REDACTED_HEADER_LEN {
1833 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
1834 }
1835 let version = data[4];
1836 if version != TREE_REDACTED_ENCODING_VERSION {
1837 return Err(TreeStreamError::UnsupportedVersion { found: version });
1838 }
1839 let declared_root = ContentHash::from_bytes(
1840 data[5..37]
1841 .try_into()
1842 .map_err(|_| TreeStreamError::Malformed("tree id slice is not 32 bytes".into()))?,
1843 );
1844 let entry_count = u64::from_le_bytes(
1845 data[37..45]
1846 .try_into()
1847 .map_err(|_| TreeStreamError::Malformed("entry count slice is not 8 bytes".into()))?,
1848 );
1849 let mut offset = TREE_REDACTED_HEADER_LEN;
1850 let mut leaves = Vec::new();
1851 let mut prev_visible_name: Option<String> = None;
1852 let mut prev_redacted: Option<ContentHash> = None;
1853 let mut seen_redacted = false;
1854 for _ in 0..entry_count {
1855 let flag = *data.get(offset).ok_or(TreeStreamError::TruncatedFrame {
1856 offset: offset as u64,
1857 })?;
1858 offset += 1;
1859 match flag {
1860 0 => {
1861 if seen_redacted {
1862 return Err(TreeStreamError::Malformed(
1863 "HRT1 visible frame follows a redacted leaf".into(),
1864 ));
1865 }
1866 if offset + 4 > data.len() {
1867 return Err(TreeStreamError::TruncatedFrame {
1868 offset: offset as u64,
1869 });
1870 }
1871 let frame_len =
1872 u32::from_le_bytes(data[offset..offset + 4].try_into().map_err(|_| {
1873 TreeStreamError::Malformed("frame length slice is not 4 bytes".into())
1874 })?) as usize;
1875 let frame_start = offset + 4;
1876 let frame_end =
1877 frame_start
1878 .checked_add(frame_len)
1879 .ok_or(TreeStreamError::TruncatedFrame {
1880 offset: offset as u64,
1881 })?;
1882 if frame_end > data.len() {
1883 return Err(TreeStreamError::TruncatedFrame {
1884 offset: offset as u64,
1885 });
1886 }
1887 let (salt, entry) = decode_salted_entry_frame(&data[frame_start..frame_end])?;
1888 if let Some(previous) = &prev_visible_name
1889 && previous.as_str() >= entry.name()
1890 {
1891 return Err(TreeStreamError::Malformed(
1892 "HRT1 visible frames must be strictly sorted by name".into(),
1893 ));
1894 }
1895 prev_visible_name = Some(entry.name().to_string());
1896 leaves.push(PartialTreeLeaf::Visible { entry, salt });
1897 offset = frame_end;
1898 }
1899 1 => {
1900 seen_redacted = true;
1901 let end = offset
1902 .checked_add(32)
1903 .ok_or(TreeStreamError::TruncatedFrame {
1904 offset: offset as u64,
1905 })?;
1906 let leaf_hash = ContentHash::from_bytes(
1907 data.get(offset..end)
1908 .ok_or(TreeStreamError::TruncatedFrame {
1909 offset: offset as u64,
1910 })?
1911 .try_into()
1912 .map_err(|_| {
1913 TreeStreamError::Malformed("redacted leaf hash is not 32 bytes".into())
1914 })?,
1915 );
1916 if let Some(previous) = prev_redacted
1917 && previous >= leaf_hash
1918 {
1919 return Err(TreeStreamError::Malformed(
1920 "HRT1 redacted leaves must be strictly sorted by leaf hash".into(),
1921 ));
1922 }
1923 prev_redacted = Some(leaf_hash);
1924 leaves.push(PartialTreeLeaf::Redacted { leaf_hash });
1925 offset = end;
1926 }
1927 other => {
1928 return Err(TreeStreamError::Malformed(format!(
1929 "invalid HRT1 leaf flag {other}"
1930 )));
1931 }
1932 }
1933 }
1934 if offset != data.len() {
1935 return Err(TreeStreamError::TrailingBytes {
1936 extra: (data.len() - offset) as u64,
1937 });
1938 }
1939 PartialTree::from_leaves_verified(declared_root, leaves).map_err(TreeStreamError::from)
1940}
1941
1942#[cfg(all(test, feature = "zstd"))]
1943mod block_tests {
1944 use super::*;
1945
1946 fn fixture(entries: usize) -> Tree {
1947 Tree::from_entries(
1948 (0..entries)
1949 .map(|index| {
1950 TreeEntry::file(
1951 format!("module_{index:04}.rs"),
1952 ContentHash::compute(format!("blob-{index}").as_bytes()),
1953 false,
1954 )
1955 .expect("fixture entry")
1956 })
1957 .collect(),
1958 )
1959 }
1960
1961 fn maximum_size_block_fixture() -> Tree {
1962 let name_prefix = "n".repeat(u16::MAX as usize - 5);
1963 let spool_id = SpoolId::parse(format!("s/{}", "s".repeat(u16::MAX as usize - 2)))
1964 .expect("maximum-size spool id");
1965 assert_eq!(spool_id.as_str().len(), u16::MAX as usize);
1966 Tree::from_entries(
1967 (0..TREE_BLOCK_ENTRIES)
1968 .map(|index| {
1969 let name = format!("{name_prefix}{index:05}");
1970 assert_eq!(name.len(), u16::MAX as usize);
1971 TreeEntry::spoollink(name, spool_id.clone(), StateId::from_bytes([7; 32]))
1972 .expect("maximum-size fixture entry")
1973 })
1974 .collect(),
1975 )
1976 }
1977
1978 #[test]
1979 fn blocked_encoder_falls_back_for_the_complete_object() {
1980 let tree = fixture(1);
1981 let encoded = tree.encode_canonical_blocked(3, 0).expect("encode");
1982 assert_eq!(encoded[4], TREE_ENCODING_VERSION);
1983 }
1984
1985 #[test]
1986 fn final_single_entry_block_is_stored_raw() {
1987 let tree = fixture(TREE_BLOCK_ENTRIES + 1);
1988 let encoded = tree.encode_canonical_blocked(3, 0).expect("encode");
1989 assert_eq!(encoded[4], TREE_BLOCK_ENCODING_VERSION);
1990 let header = decode_header(&encoded).expect("header");
1991 let preamble = &encoded[TREE_HEADER_LEN..TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN];
1992 let block_header =
1993 decode_block_header(&header, preamble, encoded.len() as u64).expect("block header");
1994 let second_index = TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN + TREE_BLOCK_INDEX_LEN;
1995 let index = decode_block_index(
1996 &encoded[second_index..second_index + TREE_BLOCK_INDEX_LEN],
1997 1,
1998 &block_header,
1999 encoded.len() as u64,
2000 )
2001 .expect("second block");
2002 assert_eq!(index.stored_len, index.raw_len);
2003 }
2004
2005 #[test]
2006 fn trailing_bytes_in_a_raw_block_are_rejected() {
2007 let tree = fixture(TREE_BLOCK_ENTRIES + 1);
2008 let mut encoded = tree.encode_canonical_blocked(3, 0).expect("encode");
2009 let second_index = TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN + TREE_BLOCK_INDEX_LEN;
2010 let stored_len_offset = second_index + 16;
2011 let stored_len = u32::from_le_bytes(
2012 encoded[stored_len_offset..stored_len_offset + 4]
2013 .try_into()
2014 .expect("stored length"),
2015 );
2016 encoded[stored_len_offset..stored_len_offset + 4]
2017 .copy_from_slice(&(stored_len + 1).to_le_bytes());
2018 let payload_len =
2019 u64::from_le_bytes(encoded[45..53].try_into().expect("stored payload length"));
2020 encoded[45..53].copy_from_slice(&(payload_len + 1).to_le_bytes());
2021 encoded.push(0);
2022
2023 assert!(Tree::decode_canonical(&encoded).is_err());
2024 }
2025
2026 #[test]
2027 fn block_raw_length_above_encoder_ceiling_is_rejected_in_the_index() {
2028 let tree = fixture(TREE_BLOCK_ENTRIES);
2029 let encoded = tree.encode_canonical_blocked(3, 0).expect("encode");
2030 let header = decode_header(&encoded).expect("header");
2031 let preamble = &encoded[TREE_HEADER_LEN..TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN];
2032 let block_header =
2033 decode_block_header(&header, preamble, encoded.len() as u64).expect("block header");
2034 let index_offset = TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN;
2035 let mut index: [u8; TREE_BLOCK_INDEX_LEN] = encoded
2036 [index_offset..index_offset + TREE_BLOCK_INDEX_LEN]
2037 .try_into()
2038 .expect("index");
2039 index[20..24].copy_from_slice(&u32::MAX.to_le_bytes());
2040
2041 let error = decode_block_index(&index, 0, &block_header, encoded.len() as u64)
2042 .expect_err("attacker-controlled raw length must fail at the index boundary");
2043 assert!(
2044 matches!(error, TreeStreamError::Malformed(ref message) if message.contains("raw length") && message.contains("exceeds maximum")),
2045 "unexpected error: {error}"
2046 );
2047 }
2048
2049 #[test]
2050 fn block_entry_count_above_encoder_limit_is_rejected() {
2051 let tree = fixture(TREE_BLOCK_ENTRIES);
2052 let mut encoded = tree.encode_canonical_blocked(3, 0).expect("encode");
2053 let block_entries_offset = TREE_HEADER_LEN + 2;
2054 encoded[block_entries_offset..block_entries_offset + 2]
2055 .copy_from_slice(&u16::MAX.to_le_bytes());
2056
2057 let error = Tree::decode_canonical(&encoded)
2058 .expect_err("block entry count above the encoder limit must fail");
2059 assert!(
2060 matches!(error, TreeStreamError::Malformed(ref message) if message.contains("entry count") && message.contains("exceeds maximum")),
2061 "unexpected error: {error}"
2062 );
2063 }
2064
2065 #[test]
2066 fn short_final_block_uses_its_actual_entry_count_for_the_ceiling() {
2067 let tree = fixture(TREE_BLOCK_ENTRIES + 1);
2068 let encoded = tree.encode_canonical_blocked(3, 0).expect("encode");
2069 let header = decode_header(&encoded).expect("header");
2070 let preamble = &encoded[TREE_HEADER_LEN..TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN];
2071 let block_header =
2072 decode_block_header(&header, preamble, encoded.len() as u64).expect("block header");
2073 let index_offset = TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN + TREE_BLOCK_INDEX_LEN;
2074 let mut index: [u8; TREE_BLOCK_INDEX_LEN] = encoded
2075 [index_offset..index_offset + TREE_BLOCK_INDEX_LEN]
2076 .try_into()
2077 .expect("final index");
2078 let one_entry_max_plus_one =
2079 u32::try_from(TREE_BLOCK_MAX_ENTRY_FRAME_LEN + 1).expect("one over final block");
2080 index[20..24].copy_from_slice(&one_entry_max_plus_one.to_le_bytes());
2081
2082 let error = decode_block_index(&index, 1, &block_header, encoded.len() as u64)
2083 .expect_err("short final block must use its actual structural ceiling");
2084 let expected_max = format!("maximum {TREE_BLOCK_MAX_ENTRY_FRAME_LEN}");
2085 assert!(
2086 matches!(error, TreeStreamError::Malformed(ref message) if message.contains("raw length") && message.contains(&expected_max)),
2087 "unexpected error: {error}"
2088 );
2089 }
2090
2091 #[test]
2092 fn block_raw_length_above_zstd_expansion_bound_is_rejected() {
2093 let stored_offset = 128u64;
2094 let stored_len = 1u32;
2095 let raw_len = u32::try_from(TREE_BLOCK_MAX_EXPANSION_RATIO + 1).expect("raw length");
2096 let mut index = [0u8; TREE_BLOCK_INDEX_LEN];
2097 index[8..16].copy_from_slice(&stored_offset.to_le_bytes());
2098 index[16..20].copy_from_slice(&stored_len.to_le_bytes());
2099 index[20..24].copy_from_slice(&raw_len.to_le_bytes());
2100 let block_header = TreeBlockHeader {
2101 block_entries: TREE_BLOCK_ENTRIES,
2102 block_count: 1,
2103 entry_count: 1,
2104 raw_payload_len: raw_len as u64,
2105 index_end: stored_offset,
2106 };
2107
2108 let error = decode_block_index(&index, 0, &block_header, stored_offset + stored_len as u64)
2109 .expect_err("excessive zstd expansion must fail at the index boundary");
2110 assert!(
2111 matches!(error, TreeStreamError::Malformed(ref message) if message.contains("expansion limit")),
2112 "unexpected error: {error}"
2113 );
2114 }
2115
2116 #[test]
2117 fn payload_and_zstd_helpers_recheck_the_structural_ceiling() {
2118 let over_limit = TREE_BLOCK_MAX_RAW_LEN + 1;
2119 let payload_error = decode_block_payload(&[0], over_limit)
2120 .expect_err("payload helper must reject an unchecked raw length");
2121 assert!(
2122 matches!(payload_error, TreeStreamError::Malformed(ref message) if message.contains("raw length")),
2123 "unexpected error: {payload_error}"
2124 );
2125 let zstd_error = decompress_block_tail(&[], over_limit)
2126 .expect_err("zstd helper must cap its output independently");
2127 assert!(
2128 matches!(zstd_error, TreeStreamError::Malformed(ref message) if message.contains("tail length")),
2129 "unexpected error: {zstd_error}"
2130 );
2131 }
2132
2133 #[test]
2134 fn maximum_size_encoder_block_round_trips_and_one_over_is_rejected() {
2135 let tree = maximum_size_block_fixture();
2136 let raw = tree.encode_canonical().expect("encode raw");
2137 assert_eq!(raw.len() - TREE_HEADER_LEN, TREE_BLOCK_MAX_RAW_LEN);
2138
2139 let blocked = tree.encode_canonical_blocked(3, 0).expect("encode blocked");
2140 assert_eq!(blocked[4], TREE_BLOCK_ENCODING_VERSION);
2141 let header = decode_header(&blocked).expect("header");
2142 let preamble = &blocked[TREE_HEADER_LEN..TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN];
2143 let block_header =
2144 decode_block_header(&header, preamble, blocked.len() as u64).expect("block header");
2145 let index_offset = TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN;
2146 let index = decode_block_index(
2147 &blocked[index_offset..index_offset + TREE_BLOCK_INDEX_LEN],
2148 0,
2149 &block_header,
2150 blocked.len() as u64,
2151 )
2152 .expect("maximum-size block index");
2153 assert_eq!(index.raw_len, TREE_BLOCK_MAX_RAW_LEN);
2154
2155 let decoded = Tree::decode_canonical(&blocked).expect("decode maximum-size block");
2156 assert_eq!(decoded, tree);
2157 assert_eq!(decoded.encode_canonical().expect("re-encode decoded"), raw);
2158 assert_eq!(
2159 decoded
2160 .encode_canonical_blocked(3, 0)
2161 .expect("re-encode blocked"),
2162 blocked
2163 );
2164
2165 let mut over_limit = blocked;
2166 let raw_len_offset = index_offset + 20;
2167 let one_over = u32::try_from(TREE_BLOCK_MAX_RAW_LEN + 1).expect("one over");
2168 over_limit[raw_len_offset..raw_len_offset + 4].copy_from_slice(&one_over.to_le_bytes());
2169 let error = Tree::decode_canonical(&over_limit)
2170 .expect_err("one byte above the block ceiling must fail");
2171 assert!(
2172 matches!(error, TreeStreamError::Malformed(ref message) if message.contains("raw length")),
2173 "unexpected error: {error}"
2174 );
2175 }
2176}