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