1use sley::{ObjectFormat as GitObjectFormat, ObjectId as GitObjectId};
10
11use super::{
12 ContentHash, EntryType, FileMode, SpoolId, StateId, Tree, TreeEntry, TreeError,
13 tree::{git_format_from_tag, git_format_to_tag},
14 tree_stream::TreeStreamError,
15};
16
17pub const TREE_ENCODING_VERSION: u8 = 4;
19pub const TREE_BLOCK_ENCODING_VERSION: u8 = 5;
21pub const TREE_CANONICAL_MAGIC: &[u8; 4] = b"HTR4";
23pub const TREE_LEAN_MAGIC: &[u8; 4] = b"HLR1";
25pub const TREE_DELTA_MAGIC: &[u8; 4] = b"HDC1";
27pub const TREE_LEAN_ENCODING_VERSION: u8 = 6;
29pub const TREE_DELTA_ENCODING_VERSION: u8 = 1;
31pub const TREE_DELTA_HEADER_LEN: usize = 59;
33pub const TREE_DELTA_ANCHOR_INTERVAL: u8 = 128;
35pub const TREE_DELTA_MAX_OPS: usize = 512;
37pub const TREE_HEADER_LEN: usize = 4 + 1 + 32 + 8 + 8 + 8;
39pub const TREE_BLOCK_MIN_ENTRIES: usize = 18;
41
42pub(crate) const TREE_BLOCK_ENTRIES: usize = 256;
43pub(crate) const TREE_BLOCK_PREAMBLE_LEN: usize = 16;
44pub(crate) const TREE_BLOCK_INDEX_LEN: usize = 24;
45const TREE_BLOCK_CODEC_ZSTD: u8 = 1;
46const TREE_BLOCK_MAX_ENTRY_FRAME_LEN: usize =
50 4 + 1 + 1 + 2 + u16::MAX as usize + 2 + u16::MAX as usize + 32;
51pub(crate) const TREE_BLOCK_MAX_RAW_LEN: usize =
52 TREE_BLOCK_ENTRIES * TREE_BLOCK_MAX_ENTRY_FRAME_LEN;
53const TREE_BLOCK_MAX_EXPANSION_RATIO: usize = 128 * 1024;
57
58#[derive(Clone, Copy, Debug, PartialEq, Eq)]
59pub(crate) struct TreeBlockHeader {
60 pub block_entries: usize,
61 pub block_count: usize,
62 pub entry_count: u64,
63 pub raw_payload_len: u64,
64 pub index_end: u64,
65}
66
67#[derive(Clone, Copy, Debug, PartialEq, Eq)]
68pub(crate) struct TreeBlockIndex {
69 pub raw_offset: u64,
70 pub stored_offset: u64,
71 pub stored_len: usize,
72 pub raw_len: usize,
73}
74
75#[derive(Clone, Debug, PartialEq, Eq)]
77pub struct TreeHeader {
78 pub version: u8,
79 pub tree_id: ContentHash,
80 pub entry_count: u64,
81 pub payload_len: u64,
82 pub logical_len: u64,
83}
84
85pub fn is_canonical_tree(bytes: &[u8]) -> bool {
87 bytes.starts_with(TREE_CANONICAL_MAGIC)
88}
89
90pub fn is_lean_tree(bytes: &[u8]) -> bool {
92 bytes.starts_with(TREE_LEAN_MAGIC)
93}
94
95pub fn is_delta_tree(bytes: &[u8]) -> bool {
97 bytes.starts_with(TREE_DELTA_MAGIC)
98}
99
100pub fn is_streamable_tree(bytes: &[u8]) -> bool {
102 is_canonical_tree(bytes) || is_lean_tree(bytes)
103}
104
105impl Tree {
106 pub fn encode_canonical(&self) -> Result<Vec<u8>, TreeStreamError> {
108 self.validate()?;
109 let tree_id = self.hash();
110 let mut payload = Vec::new();
111 let mut logical_len = 0u64;
112 for entry in self.entries() {
113 logical_len = logical_len
114 .checked_add(entry.encoded_len() as u64)
115 .ok_or_else(|| TreeStreamError::Malformed("logical length overflow".into()))?;
116 let frame = encode_entry_frame(entry)?;
117 let frame_len = u32::try_from(frame.len()).map_err(|_| {
118 TreeStreamError::Malformed(format!("entry '{}' frame exceeds u32", entry.name()))
119 })?;
120 payload.extend_from_slice(&frame_len.to_le_bytes());
121 payload.extend_from_slice(&frame);
122 }
123 let mut out = Vec::with_capacity(TREE_HEADER_LEN + payload.len());
124 out.extend_from_slice(TREE_CANONICAL_MAGIC);
125 out.push(TREE_ENCODING_VERSION);
126 out.extend_from_slice(tree_id.as_bytes());
127 out.extend_from_slice(&(self.len() as u64).to_le_bytes());
128 out.extend_from_slice(&(payload.len() as u64).to_le_bytes());
129 out.extend_from_slice(&logical_len.to_le_bytes());
130 out.extend_from_slice(&payload);
131 Ok(out)
132 }
133
134 pub fn decode_canonical(data: &[u8]) -> Result<Self, TreeStreamError> {
136 let header = decode_header(data)?;
137 if header.version == TREE_BLOCK_ENCODING_VERSION {
138 return Self::decode_canonical_streamed(data);
139 }
140 let expected_len = TREE_HEADER_LEN as u64 + header.payload_len;
141 if (data.len() as u64) < expected_len {
142 return Err(TreeStreamError::TruncatedFrame {
143 offset: data.len() as u64,
144 });
145 }
146 if (data.len() as u64) > expected_len {
147 return Err(TreeStreamError::TrailingBytes {
148 extra: data.len() as u64 - expected_len,
149 });
150 }
151 let mut entries = Vec::new();
152 let mut offset = TREE_HEADER_LEN;
153 let payload_end = data.len();
154 for _ in 0..header.entry_count {
155 let (entry, consumed) = decode_entry_at(data, offset, payload_end)?;
156 entries.push(entry);
157 offset += consumed;
158 }
159 if offset != payload_end {
160 return Err(TreeStreamError::TrailingBytes {
161 extra: (payload_end - offset) as u64,
162 });
163 }
164 let tree = Tree::try_from_decoded_entries(entries)?;
165 let found = tree.hash();
166 if found != header.tree_id {
167 return Err(TreeStreamError::HashMismatch {
168 expected: header.tree_id,
169 found,
170 });
171 }
172 if tree
173 .entries()
174 .iter()
175 .map(|entry| entry.encoded_len() as u64)
176 .sum::<u64>()
177 != header.logical_len
178 {
179 return Err(TreeStreamError::Malformed(
180 "declared logical length does not match entries".into(),
181 ));
182 }
183 Ok(tree)
184 }
185
186 pub fn encode_canonical_blocked(
189 &self,
190 level: i32,
191 min_size: usize,
192 ) -> Result<Vec<u8>, TreeStreamError> {
193 let raw = self.encode_canonical()?;
194 if raw.len() < min_size {
195 return Ok(raw);
196 }
197 let blocked = encode_blocked_htr4(&raw, level)?;
198 if blocked.len() < raw.len() {
199 Ok(blocked)
200 } else {
201 Ok(raw)
202 }
203 }
204}
205
206pub fn decode_header(data: &[u8]) -> Result<TreeHeader, TreeStreamError> {
208 if data.len() < TREE_HEADER_LEN {
209 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
210 }
211 if !is_canonical_tree(data) {
212 return Err(TreeStreamError::Malformed(
213 "bytes are not a canonical HTR4 tree".into(),
214 ));
215 }
216 let version = data[4];
217 if version != TREE_ENCODING_VERSION && version != TREE_BLOCK_ENCODING_VERSION {
218 return Err(TreeStreamError::UnsupportedVersion { found: version });
219 }
220 let tree_id = ContentHash::from_bytes(
221 data[5..37]
222 .try_into()
223 .map_err(|_| TreeStreamError::Malformed("tree id slice is not 32 bytes".into()))?,
224 );
225 let entry_count = u64::from_le_bytes(
226 data[37..45]
227 .try_into()
228 .map_err(|_| TreeStreamError::Malformed("entry count slice is not 8 bytes".into()))?,
229 );
230 let payload_len =
231 u64::from_le_bytes(data[45..53].try_into().map_err(|_| {
232 TreeStreamError::Malformed("payload length slice is not 8 bytes".into())
233 })?);
234 let logical_len =
235 u64::from_le_bytes(data[53..61].try_into().map_err(|_| {
236 TreeStreamError::Malformed("logical length slice is not 8 bytes".into())
237 })?);
238 Ok(TreeHeader {
239 version,
240 tree_id,
241 entry_count,
242 payload_len,
243 logical_len,
244 })
245}
246
247pub(crate) fn decode_block_header(
248 header: &TreeHeader,
249 preamble: &[u8],
250 object_len: u64,
251) -> Result<TreeBlockHeader, TreeStreamError> {
252 if header.version != TREE_BLOCK_ENCODING_VERSION {
253 return Err(TreeStreamError::Malformed(
254 "tree does not use block compression".into(),
255 ));
256 }
257 if preamble.len() != TREE_BLOCK_PREAMBLE_LEN {
258 return Err(TreeStreamError::TruncatedFrame {
259 offset: TREE_HEADER_LEN as u64,
260 });
261 }
262 if preamble[0] != TREE_BLOCK_CODEC_ZSTD {
263 return Err(TreeStreamError::Malformed(format!(
264 "unsupported tree block codec {}",
265 preamble[0]
266 )));
267 }
268 if preamble[1] != 0 {
269 return Err(TreeStreamError::Malformed(
270 "unsupported tree block flags".into(),
271 ));
272 }
273 let block_entries = u16::from_le_bytes([preamble[2], preamble[3]]) as usize;
274 if block_entries == 0 {
275 return Err(TreeStreamError::Malformed(
276 "tree block size must be nonzero".into(),
277 ));
278 }
279 if block_entries > TREE_BLOCK_ENTRIES {
280 return Err(TreeStreamError::Malformed(format!(
281 "tree block entry count {block_entries} exceeds maximum {TREE_BLOCK_ENTRIES}"
282 )));
283 }
284 let block_count = u32::from_le_bytes(
285 preamble[4..8]
286 .try_into()
287 .map_err(|_| TreeStreamError::Malformed("invalid tree block count".into()))?,
288 ) as usize;
289 let raw_payload_len = u64::from_le_bytes(
290 preamble[8..16]
291 .try_into()
292 .map_err(|_| TreeStreamError::Malformed("invalid raw tree payload length".into()))?,
293 );
294 let expected_blocks = header.entry_count.div_ceil(block_entries as u64);
295 if block_count as u64 != expected_blocks {
296 return Err(TreeStreamError::Malformed(
297 "tree block count does not match entry count".into(),
298 ));
299 }
300 let index_bytes = block_count
301 .checked_mul(TREE_BLOCK_INDEX_LEN)
302 .ok_or_else(|| TreeStreamError::Malformed("tree block index length overflow".into()))?;
303 let index_end = TREE_HEADER_LEN
304 .checked_add(TREE_BLOCK_PREAMBLE_LEN)
305 .and_then(|len| len.checked_add(index_bytes))
306 .ok_or_else(|| TreeStreamError::Malformed("tree block index end overflow".into()))?
307 as u64;
308 let expected_object_len = (TREE_HEADER_LEN as u64)
309 .checked_add(header.payload_len)
310 .ok_or_else(|| TreeStreamError::Malformed("tree object length overflow".into()))?;
311 if index_end > expected_object_len || expected_object_len != object_len {
312 return Err(TreeStreamError::TruncatedFrame { offset: object_len });
313 }
314 Ok(TreeBlockHeader {
315 block_entries,
316 block_count,
317 entry_count: header.entry_count,
318 raw_payload_len,
319 index_end,
320 })
321}
322
323pub(crate) fn decode_block_index(
324 bytes: &[u8],
325 block: usize,
326 block_header: &TreeBlockHeader,
327 object_len: u64,
328) -> Result<TreeBlockIndex, TreeStreamError> {
329 if bytes.len() != TREE_BLOCK_INDEX_LEN || block >= block_header.block_count {
330 return Err(TreeStreamError::Malformed(
331 "invalid tree block index entry".into(),
332 ));
333 }
334 let raw_offset = u64::from_le_bytes(
335 bytes[0..8]
336 .try_into()
337 .map_err(|_| TreeStreamError::Malformed("invalid tree block raw offset".into()))?,
338 );
339 let stored_offset = u64::from_le_bytes(
340 bytes[8..16]
341 .try_into()
342 .map_err(|_| TreeStreamError::Malformed("invalid tree block stored offset".into()))?,
343 );
344 let stored_len = u32::from_le_bytes(
345 bytes[16..20]
346 .try_into()
347 .map_err(|_| TreeStreamError::Malformed("invalid tree block stored length".into()))?,
348 ) as usize;
349 let raw_len = u32::from_le_bytes(
350 bytes[20..24]
351 .try_into()
352 .map_err(|_| TreeStreamError::Malformed("invalid tree block raw length".into()))?,
353 ) as usize;
354 let first_entry = (block as u64)
355 .checked_mul(block_header.block_entries as u64)
356 .ok_or_else(|| TreeStreamError::Malformed("tree block ordinal overflow".into()))?;
357 let entries = block_header
358 .entry_count
359 .checked_sub(first_entry)
360 .ok_or_else(|| TreeStreamError::Malformed("tree block starts past entries".into()))?
361 .min(block_header.block_entries as u64) as usize;
362 let max_raw_len = entries
363 .checked_mul(TREE_BLOCK_MAX_ENTRY_FRAME_LEN)
364 .ok_or_else(|| TreeStreamError::Malformed("tree block raw length overflow".into()))?;
365 validate_block_lengths(stored_len, raw_len, max_raw_len)?;
366 if stored_offset < block_header.index_end {
367 return Err(TreeStreamError::Malformed(
368 "invalid empty or overlapping tree block".into(),
369 ));
370 }
371 if stored_offset
372 .checked_add(stored_len as u64)
373 .is_none_or(|end| end > object_len)
374 {
375 return Err(TreeStreamError::TruncatedFrame {
376 offset: stored_offset,
377 });
378 }
379 Ok(TreeBlockIndex {
380 raw_offset,
381 stored_offset,
382 stored_len,
383 raw_len,
384 })
385}
386
387fn validate_block_lengths(
388 stored_len: usize,
389 raw_len: usize,
390 max_raw_len: usize,
391) -> Result<(), TreeStreamError> {
392 if raw_len > max_raw_len {
393 return Err(TreeStreamError::Malformed(format!(
394 "tree block raw length {raw_len} exceeds maximum {max_raw_len}"
395 )));
396 }
397 if stored_len == 0 || raw_len == 0 {
398 return Err(TreeStreamError::Malformed(
399 "invalid empty or overlapping tree block".into(),
400 ));
401 }
402 let expansion_limit = (stored_len as u64) * (TREE_BLOCK_MAX_EXPANSION_RATIO as u64);
403 if raw_len as u64 > expansion_limit {
404 return Err(TreeStreamError::Malformed(format!(
405 "tree block raw length {raw_len} exceeds {TREE_BLOCK_MAX_EXPANSION_RATIO}:1 expansion limit for {stored_len} stored bytes"
406 )));
407 }
408 Ok(())
409}
410
411pub(crate) fn decode_block_payload(
412 stored: &[u8],
413 raw_len: usize,
414) -> Result<Vec<u8>, TreeStreamError> {
415 validate_block_lengths(stored.len(), raw_len, TREE_BLOCK_MAX_RAW_LEN)?;
416 if stored.len() == raw_len {
417 return Ok(stored.to_vec());
418 }
419 let anchor_end = block_anchor_end(stored)?;
420 if anchor_end >= raw_len {
421 return Err(TreeStreamError::Malformed(
422 "compressed tree block has no tail".into(),
423 ));
424 }
425 let tail = decompress_block_tail(&stored[anchor_end..], raw_len - anchor_end)?;
426 let mut raw = Vec::with_capacity(raw_len);
427 raw.extend_from_slice(&stored[..anchor_end]);
428 raw.extend_from_slice(&tail);
429 if raw.len() != raw_len {
430 return Err(TreeStreamError::Malformed(
431 "decoded tree block length mismatch".into(),
432 ));
433 }
434 Ok(raw)
435}
436
437pub(crate) fn block_anchor_end(stored: &[u8]) -> Result<usize, TreeStreamError> {
438 let len_bytes = stored
439 .get(..4)
440 .ok_or(TreeStreamError::TruncatedFrame { offset: 0 })?;
441 let frame_len = u32::from_le_bytes(
442 len_bytes
443 .try_into()
444 .map_err(|_| TreeStreamError::Malformed("invalid anchor frame length".into()))?,
445 ) as usize;
446 let end = 4usize
447 .checked_add(frame_len)
448 .ok_or(TreeStreamError::TruncatedFrame { offset: 0 })?;
449 if end > stored.len() {
450 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
451 }
452 Ok(end)
453}
454
455fn encode_blocked_htr4(raw: &[u8], level: i32) -> Result<Vec<u8>, TreeStreamError> {
456 let header = decode_header(raw)?;
457 if header.version != TREE_ENCODING_VERSION {
458 return Err(TreeStreamError::Malformed(
459 "block encoder requires raw HTR4".into(),
460 ));
461 }
462 let ranges = entry_frame_ranges(raw, header.entry_count)?;
463 let block_count = ranges.len().div_ceil(TREE_BLOCK_ENTRIES);
464 let block_count_u32 = u32::try_from(block_count)
465 .map_err(|_| TreeStreamError::Malformed("tree has too many blocks".into()))?;
466 let mut blocks = Vec::with_capacity(block_count);
467 for chunk in ranges.chunks(TREE_BLOCK_ENTRIES) {
468 let (start, anchor_end) = *chunk
469 .first()
470 .ok_or_else(|| TreeStreamError::Malformed("empty tree block".into()))?;
471 let end = chunk
472 .last()
473 .map(|range| range.1)
474 .ok_or_else(|| TreeStreamError::Malformed("empty tree block".into()))?;
475 let block = &raw[start..end];
476 let compressed_tail = compress_block_tail(&raw[anchor_end..end], level)?;
477 if anchor_end - start + compressed_tail.len() < block.len() {
478 let mut stored = Vec::with_capacity(anchor_end - start + compressed_tail.len());
479 stored.extend_from_slice(&raw[start..anchor_end]);
480 stored.extend_from_slice(&compressed_tail);
481 blocks.push(stored);
482 } else {
483 blocks.push(block.to_vec());
484 }
485 }
486
487 let index_bytes = block_count
488 .checked_mul(TREE_BLOCK_INDEX_LEN)
489 .ok_or_else(|| TreeStreamError::Malformed("tree block index length overflow".into()))?;
490 let stored_payload_len = TREE_BLOCK_PREAMBLE_LEN
491 .checked_add(index_bytes)
492 .and_then(|len| {
493 blocks
494 .iter()
495 .try_fold(len, |total, block| total.checked_add(block.len()))
496 })
497 .ok_or_else(|| TreeStreamError::Malformed("blocked tree length overflow".into()))?;
498 let mut out = Vec::with_capacity(TREE_HEADER_LEN + stored_payload_len);
499 out.extend_from_slice(TREE_CANONICAL_MAGIC);
500 out.push(TREE_BLOCK_ENCODING_VERSION);
501 out.extend_from_slice(header.tree_id.as_bytes());
502 out.extend_from_slice(&header.entry_count.to_le_bytes());
503 out.extend_from_slice(&(stored_payload_len as u64).to_le_bytes());
504 out.extend_from_slice(&header.logical_len.to_le_bytes());
505 out.push(TREE_BLOCK_CODEC_ZSTD);
506 out.push(0);
507 out.extend_from_slice(&(TREE_BLOCK_ENTRIES as u16).to_le_bytes());
508 out.extend_from_slice(&block_count_u32.to_le_bytes());
509 out.extend_from_slice(&header.payload_len.to_le_bytes());
510
511 let mut stored_offset = TREE_HEADER_LEN
512 .checked_add(TREE_BLOCK_PREAMBLE_LEN)
513 .and_then(|len| len.checked_add(index_bytes))
514 .ok_or_else(|| TreeStreamError::Malformed("tree block offset overflow".into()))?;
515 for (block, stored) in blocks.iter().enumerate() {
516 let first_entry = block * TREE_BLOCK_ENTRIES;
517 let chunk = &ranges[first_entry..(first_entry + TREE_BLOCK_ENTRIES).min(ranges.len())];
518 let raw_offset = chunk[0].0 - TREE_HEADER_LEN;
519 let raw_len = chunk[chunk.len() - 1].1 - chunk[0].0;
520 let stored_len = u32::try_from(stored.len())
521 .map_err(|_| TreeStreamError::Malformed("stored tree block exceeds u32".into()))?;
522 let raw_len = u32::try_from(raw_len)
523 .map_err(|_| TreeStreamError::Malformed("raw tree block exceeds u32".into()))?;
524 out.extend_from_slice(&(raw_offset as u64).to_le_bytes());
525 out.extend_from_slice(&(stored_offset as u64).to_le_bytes());
526 out.extend_from_slice(&stored_len.to_le_bytes());
527 out.extend_from_slice(&raw_len.to_le_bytes());
528 stored_offset = stored_offset
529 .checked_add(stored.len())
530 .ok_or_else(|| TreeStreamError::Malformed("tree block offset overflow".into()))?;
531 }
532 for block in blocks {
533 out.extend_from_slice(&block);
534 }
535 Ok(out)
536}
537
538fn entry_frame_ranges(
539 raw: &[u8],
540 entry_count: u64,
541) -> Result<Vec<(usize, usize)>, TreeStreamError> {
542 let count = usize::try_from(entry_count)
543 .map_err(|_| TreeStreamError::Malformed("tree entry count exceeds usize".into()))?;
544 let mut ranges = Vec::with_capacity(count);
545 let mut offset = TREE_HEADER_LEN;
546 for _ in 0..count {
547 let len_bytes = raw
548 .get(offset..offset + 4)
549 .ok_or(TreeStreamError::TruncatedFrame {
550 offset: offset as u64,
551 })?;
552 let frame_len = u32::from_le_bytes(
553 len_bytes
554 .try_into()
555 .map_err(|_| TreeStreamError::Malformed("invalid frame length".into()))?,
556 ) as usize;
557 let end = offset
558 .checked_add(4)
559 .and_then(|start| start.checked_add(frame_len))
560 .ok_or(TreeStreamError::TruncatedFrame {
561 offset: offset as u64,
562 })?;
563 if end > raw.len() {
564 return Err(TreeStreamError::TruncatedFrame {
565 offset: offset as u64,
566 });
567 }
568 ranges.push((offset, end));
569 offset = end;
570 }
571 if offset != raw.len() {
572 return Err(TreeStreamError::TrailingBytes {
573 extra: raw.len().abs_diff(offset) as u64,
574 });
575 }
576 Ok(ranges)
577}
578
579#[cfg(feature = "zstd")]
580fn compress_block_tail(data: &[u8], level: i32) -> Result<Vec<u8>, TreeStreamError> {
581 zstd::bulk::compress(data, level)
582 .map_err(|error| TreeStreamError::Compression(error.to_string()))
583}
584
585#[cfg(not(feature = "zstd"))]
586fn compress_block_tail(_data: &[u8], _level: i32) -> Result<Vec<u8>, TreeStreamError> {
587 Err(TreeStreamError::Compression(
588 "zstd support is not compiled into this build".into(),
589 ))
590}
591
592#[cfg(feature = "zstd")]
593fn decompress_block_tail(data: &[u8], capacity: usize) -> Result<Vec<u8>, TreeStreamError> {
594 if capacity > TREE_BLOCK_MAX_RAW_LEN {
595 return Err(TreeStreamError::Malformed(format!(
596 "tree block tail length {capacity} exceeds maximum {TREE_BLOCK_MAX_RAW_LEN}"
597 )));
598 }
599 let decoded = zstd::bulk::decompress(data, capacity)
600 .map_err(|error| TreeStreamError::Compression(error.to_string()))?;
601 if decoded.len() != capacity {
602 return Err(TreeStreamError::Malformed(
603 "decoded tree block tail length mismatch".into(),
604 ));
605 }
606 Ok(decoded)
607}
608
609#[cfg(not(feature = "zstd"))]
610fn decompress_block_tail(_data: &[u8], _capacity: usize) -> Result<Vec<u8>, TreeStreamError> {
611 Err(TreeStreamError::Compression(
612 "zstd support is not compiled into this build".into(),
613 ))
614}
615
616#[derive(Clone, Debug, PartialEq, Eq)]
618pub enum TreeDeltaOp {
619 Remove(String),
620 Upsert(TreeEntry),
621}
622
623impl TreeDeltaOp {
624 pub fn name(&self) -> &str {
625 match self {
626 Self::Remove(name) => name,
627 Self::Upsert(entry) => entry.name(),
628 }
629 }
630}
631
632#[derive(Clone, Copy, Debug, PartialEq, Eq)]
634pub struct TreeDeltaHeader {
635 pub anchor: ContentHash,
636 pub result_count: usize,
637 pub op_count: usize,
638 pub first_op_count: usize,
639 pub first_base_count: usize,
640 pub first_end: usize,
641 pub hundred_op_count: usize,
642 pub hundred_base_count: usize,
643 pub hundred_end: usize,
644}
645
646impl Tree {
647 pub fn encode_lean(&self) -> Result<Vec<u8>, TreeStreamError> {
651 self.validate()?;
652 encode_lean_entries(self.entries())
653 }
654
655 pub fn decode_lean(data: &[u8], expected: ContentHash) -> Result<Self, TreeStreamError> {
657 let (entries, consumed) = decode_lean_entries(data, usize::MAX)?;
658 if consumed != data.len() {
659 return Err(TreeStreamError::TrailingBytes {
660 extra: data.len().abs_diff(consumed) as u64,
661 });
662 }
663 let tree = Tree::try_from_decoded_entries(entries)?;
664 let found = tree.hash();
665 if found != expected {
666 return Err(TreeStreamError::HashMismatch { expected, found });
667 }
668 Ok(tree)
669 }
670}
671
672pub fn decode_lean_prefix(
675 data: &[u8],
676 wanted: usize,
677) -> Result<(Vec<TreeEntry>, usize), TreeStreamError> {
678 decode_lean_entries(data, wanted)
679}
680
681fn encode_lean_entries(entries: &[TreeEntry]) -> Result<Vec<u8>, TreeStreamError> {
682 let mut out = Vec::new();
683 out.extend_from_slice(TREE_LEAN_MAGIC);
684 put_varint(entries.len(), &mut out);
685 let mut previous = "";
686 for entry in entries {
687 encode_lean_entry(entry, previous, &mut out)?;
688 previous = entry.name();
689 }
690 Ok(out)
691}
692
693fn decode_lean_entries(
694 data: &[u8],
695 wanted: usize,
696) -> Result<(Vec<TreeEntry>, usize), TreeStreamError> {
697 if !is_lean_tree(data) {
698 return Err(TreeStreamError::Malformed(
699 "bytes are not an HLR1 tree anchor".into(),
700 ));
701 }
702 let mut offset = TREE_LEAN_MAGIC.len();
703 let count = take_varint(data, &mut offset)?;
704 let wanted = wanted.min(count);
705 let mut entries = Vec::with_capacity(wanted);
706 let mut previous = String::new();
707 for _ in 0..wanted {
708 let entry = decode_compact_entry(data, &mut offset, &previous)?;
709 if !previous.is_empty() && previous.as_str() >= entry.name() {
710 return Err(TreeError::InvalidStructure(
711 "entries must be strictly sorted by name".into(),
712 )
713 .into());
714 }
715 previous = entry.name().to_string();
716 entries.push(entry);
717 }
718 if wanted == count && offset != data.len() {
719 return Err(TreeStreamError::TrailingBytes {
720 extra: data.len().abs_diff(offset) as u64,
721 });
722 }
723 Ok((entries, offset))
724}
725
726fn put_varint(mut value: usize, out: &mut Vec<u8>) {
727 while value >= 0x80 {
728 out.push((value as u8) | 0x80);
729 value >>= 7;
730 }
731 out.push(value as u8);
732}
733
734pub(crate) fn take_varint(bytes: &[u8], offset: &mut usize) -> Result<usize, TreeStreamError> {
735 let mut value = 0usize;
736 for shift in (0..usize::BITS).step_by(7) {
737 let byte = *bytes.get(*offset).ok_or(TreeStreamError::TruncatedFrame {
738 offset: *offset as u64,
739 })?;
740 *offset += 1;
741 value |= ((byte & 0x7f) as usize)
742 .checked_shl(shift)
743 .ok_or_else(|| TreeStreamError::Malformed("varint overflow".into()))?;
744 if byte & 0x80 == 0 {
745 return Ok(value);
746 }
747 }
748 Err(TreeStreamError::Malformed("varint overflow".into()))
749}
750
751fn shared_prefix(left: &str, right: &str) -> usize {
752 left.as_bytes()
753 .iter()
754 .zip(right.as_bytes())
755 .take_while(|(left, right)| left == right)
756 .count()
757}
758
759pub fn encode_lean_entry(
762 entry: &TreeEntry,
763 previous_name: &str,
764 out: &mut Vec<u8>,
765) -> Result<(), TreeStreamError> {
766 out.push((entry.mode().to_byte() << 3) | entry.entry_type().to_byte());
767 let prefix = shared_prefix(previous_name, entry.name());
768 put_varint(prefix, out);
769 put_varint(entry.name().len() - prefix, out);
770 out.extend_from_slice(&entry.name().as_bytes()[prefix..]);
771 match entry.entry_type() {
772 EntryType::Blob | EntryType::Tree | EntryType::Symlink => {
773 let hash = entry.content_hash().ok_or_else(|| {
774 TreeStreamError::Malformed("compact entry is missing its content hash".into())
775 })?;
776 out.extend_from_slice(hash.as_bytes());
777 }
778 EntryType::Gitlink => {
779 let target = entry.gitlink_target().ok_or_else(|| {
780 TreeStreamError::Malformed("compact gitlink is missing its target".into())
781 })?;
782 out.push(git_format_to_tag(target.format()));
783 out.extend_from_slice(target.as_bytes());
784 }
785 EntryType::Spoollink => {
786 let (spool, state) = entry.spoollink_target().ok_or_else(|| {
787 TreeStreamError::Malformed("compact spoollink is missing its target".into())
788 })?;
789 put_varint(spool.as_str().len(), out);
790 out.extend_from_slice(spool.as_str().as_bytes());
791 out.extend_from_slice(state.as_bytes());
792 }
793 }
794 Ok(())
795}
796
797pub(crate) fn decode_compact_entry(
798 bytes: &[u8],
799 offset: &mut usize,
800 previous_name: &str,
801) -> Result<TreeEntry, TreeStreamError> {
802 let tag = *bytes.get(*offset).ok_or(TreeStreamError::TruncatedFrame {
803 offset: *offset as u64,
804 })?;
805 *offset += 1;
806 let mode = FileMode::from_byte(tag >> 3).ok_or_else(|| {
807 TreeStreamError::Malformed(format!("invalid compact entry mode {}", tag >> 3))
808 })?;
809 let kind = EntryType::from_byte(tag & 0x07).ok_or_else(|| {
810 TreeStreamError::Malformed(format!("invalid compact entry kind {}", tag & 0x07))
811 })?;
812 let prefix = take_varint(bytes, offset)?;
813 let suffix_len = take_varint(bytes, offset)?;
814 if prefix > previous_name.len() {
815 return Err(TreeStreamError::Malformed(
816 "compact name prefix exceeds predecessor".into(),
817 ));
818 }
819 let suffix_end = offset
820 .checked_add(suffix_len)
821 .ok_or_else(|| TreeStreamError::Malformed("compact name length overflow".into()))?;
822 let suffix = bytes
823 .get(*offset..suffix_end)
824 .ok_or(TreeStreamError::TruncatedFrame {
825 offset: *offset as u64,
826 })?;
827 let mut name = previous_name.as_bytes()[..prefix].to_vec();
828 name.extend_from_slice(suffix);
829 let name = String::from_utf8(name)
830 .map_err(|_| TreeStreamError::Malformed("compact entry name is not UTF-8".into()))?;
831 *offset = suffix_end;
832 let entry = match kind {
833 EntryType::Blob | EntryType::Tree | EntryType::Symlink => {
834 let end = offset
835 .checked_add(32)
836 .ok_or_else(|| TreeStreamError::Malformed("compact hash overflow".into()))?;
837 let hash = ContentHash::from_bytes(
838 bytes
839 .get(*offset..end)
840 .ok_or(TreeStreamError::TruncatedFrame {
841 offset: *offset as u64,
842 })?
843 .try_into()
844 .map_err(|_| {
845 TreeStreamError::Malformed("compact hash is not 32 bytes".into())
846 })?,
847 );
848 *offset = end;
849 match kind {
850 EntryType::Blob => TreeEntry::file(name, hash, mode == FileMode::Executable)?,
851 EntryType::Tree => TreeEntry::directory(name, hash)?,
852 EntryType::Symlink => TreeEntry::symlink(name, hash)?,
853 EntryType::Gitlink | EntryType::Spoollink => {
854 return Err(TreeStreamError::Malformed(
855 "invalid compact content-addressed kind".into(),
856 ));
857 }
858 }
859 }
860 EntryType::Gitlink => {
861 let format_tag = *bytes.get(*offset).ok_or(TreeStreamError::TruncatedFrame {
862 offset: *offset as u64,
863 })?;
864 *offset += 1;
865 let format = git_format_from_tag(format_tag)?;
866 let oid_len = match format {
867 GitObjectFormat::Sha1 => 20,
868 GitObjectFormat::Sha256 => 32,
869 };
870 let end = offset.checked_add(oid_len).ok_or_else(|| {
871 TreeStreamError::Malformed("compact gitlink length overflow".into())
872 })?;
873 let target = GitObjectId::from_raw(
874 format,
875 bytes
876 .get(*offset..end)
877 .ok_or(TreeStreamError::TruncatedFrame {
878 offset: *offset as u64,
879 })?,
880 )
881 .map_err(|error| {
882 TreeStreamError::Malformed(format!("invalid compact gitlink: {error}"))
883 })?;
884 *offset = end;
885 TreeEntry::gitlink(name, target)?
886 }
887 EntryType::Spoollink => {
888 let spool_len = take_varint(bytes, offset)?;
889 let spool_end = offset.checked_add(spool_len).ok_or_else(|| {
890 TreeStreamError::Malformed("compact spool length overflow".into())
891 })?;
892 let spool = std::str::from_utf8(bytes.get(*offset..spool_end).ok_or(
893 TreeStreamError::TruncatedFrame {
894 offset: *offset as u64,
895 },
896 )?)
897 .map_err(|_| TreeStreamError::Malformed("compact spool id is not UTF-8".into()))?;
898 *offset = spool_end;
899 let state_end = offset
900 .checked_add(32)
901 .ok_or_else(|| TreeStreamError::Malformed("compact state overflow".into()))?;
902 let state = StateId::from_bytes(
903 bytes
904 .get(*offset..state_end)
905 .ok_or(TreeStreamError::TruncatedFrame {
906 offset: *offset as u64,
907 })?
908 .try_into()
909 .map_err(|_| {
910 TreeStreamError::Malformed("compact state is not 32 bytes".into())
911 })?,
912 );
913 *offset = state_end;
914 let spool_id = SpoolId::parse(spool).map_err(|error| {
915 TreeStreamError::Malformed(format!("invalid compact spool id: {error}"))
916 })?;
917 TreeEntry::spoollink(name, spool_id, state)?
918 }
919 };
920 if entry.mode() != mode {
921 return Err(TreeStreamError::Malformed(format!(
922 "compact entry kind/mode mismatch for {}: {kind:?}/{mode:?}",
923 entry.name()
924 )));
925 }
926 Ok(entry)
927}
928
929pub fn tree_delta(anchor: &Tree, current: &Tree) -> Vec<TreeDeltaOp> {
931 let mut ops = Vec::new();
932 let mut anchor_index = 0usize;
933 let mut current_index = 0usize;
934 while anchor_index < anchor.len() || current_index < current.len() {
935 match (
936 anchor.entries().get(anchor_index),
937 current.entries().get(current_index),
938 ) {
939 (Some(anchor_entry), Some(current_entry)) => {
940 match anchor_entry.name().cmp(current_entry.name()) {
941 std::cmp::Ordering::Less => {
942 ops.push(TreeDeltaOp::Remove(anchor_entry.name().to_string()));
943 anchor_index += 1;
944 }
945 std::cmp::Ordering::Greater => {
946 ops.push(TreeDeltaOp::Upsert(current_entry.clone()));
947 current_index += 1;
948 }
949 std::cmp::Ordering::Equal => {
950 if anchor_entry != current_entry {
951 ops.push(TreeDeltaOp::Upsert(current_entry.clone()));
952 }
953 anchor_index += 1;
954 current_index += 1;
955 }
956 }
957 }
958 (Some(anchor_entry), None) => {
959 ops.push(TreeDeltaOp::Remove(anchor_entry.name().to_string()));
960 anchor_index += 1;
961 }
962 (None, Some(current_entry)) => {
963 ops.push(TreeDeltaOp::Upsert(current_entry.clone()));
964 current_index += 1;
965 }
966 (None, None) => break,
967 }
968 }
969 ops
970}
971
972pub fn apply_tree_delta(anchor: &Tree, ops: &[TreeDeltaOp]) -> Result<Tree, TreeStreamError> {
974 let mut entries = Vec::with_capacity(anchor.len().saturating_add(ops.len()));
975 let mut anchor_index = 0usize;
976 let mut op_index = 0usize;
977 while anchor_index < anchor.len() || op_index < ops.len() {
978 match (anchor.entries().get(anchor_index), ops.get(op_index)) {
979 (Some(anchor_entry), Some(op)) => match anchor_entry.name().cmp(op.name()) {
980 std::cmp::Ordering::Less => {
981 entries.push(anchor_entry.clone());
982 anchor_index += 1;
983 }
984 std::cmp::Ordering::Greater => {
985 if let TreeDeltaOp::Upsert(entry) = op {
986 entries.push(entry.clone());
987 }
988 op_index += 1;
989 }
990 std::cmp::Ordering::Equal => {
991 if let TreeDeltaOp::Upsert(entry) = op {
992 entries.push(entry.clone());
993 }
994 anchor_index += 1;
995 op_index += 1;
996 }
997 },
998 (Some(anchor_entry), None) => {
999 entries.push(anchor_entry.clone());
1000 anchor_index += 1;
1001 }
1002 (None, Some(op)) => {
1003 if let TreeDeltaOp::Upsert(entry) = op {
1004 entries.push(entry.clone());
1005 }
1006 op_index += 1;
1007 }
1008 (None, None) => break,
1009 }
1010 }
1011 Tree::try_from_decoded_entries(entries).map_err(TreeStreamError::from)
1012}
1013
1014fn delta_prefix_counts(
1015 anchor: &Tree,
1016 current: &Tree,
1017 ops: &[TreeDeltaOp],
1018 count: usize,
1019) -> Result<(u16, u16), TreeStreamError> {
1020 if current.is_empty() {
1021 return Ok((0, 0));
1022 }
1023 let boundary = current.entries()[count.min(current.len()) - 1].name();
1024 let op_count = ops.partition_point(|op| op.name() <= boundary);
1025 let base_count = anchor
1026 .entries()
1027 .partition_point(|entry| entry.name() <= boundary);
1028 Ok((
1029 u16::try_from(op_count)
1030 .map_err(|_| TreeStreamError::Malformed("delta porch op count exceeds u16".into()))?,
1031 u16::try_from(base_count).map_err(|_| {
1032 TreeStreamError::Malformed("delta porch anchor count exceeds u16".into())
1033 })?,
1034 ))
1035}
1036
1037pub fn encode_tree_delta(
1039 anchor_id: ContentHash,
1040 anchor: &Tree,
1041 current: &Tree,
1042 ops: &[TreeDeltaOp],
1043) -> Result<Vec<u8>, TreeStreamError> {
1044 anchor.validate()?;
1045 current.validate()?;
1046 if anchor.hash() != anchor_id {
1047 return Err(TreeStreamError::HashMismatch {
1048 expected: anchor_id,
1049 found: anchor.hash(),
1050 });
1051 }
1052 if ops.len() > TREE_DELTA_MAX_OPS {
1053 return Err(TreeStreamError::Malformed(format!(
1054 "tree delta has {} operations; maximum is {TREE_DELTA_MAX_OPS}",
1055 ops.len()
1056 )));
1057 }
1058 if apply_tree_delta(anchor, ops)? != *current {
1059 return Err(TreeStreamError::Malformed(
1060 "tree delta operations do not reconstruct the result".into(),
1061 ));
1062 }
1063 let result_count = u32::try_from(current.len())
1064 .map_err(|_| TreeStreamError::Malformed("delta result count exceeds u32".into()))?;
1065 let op_count = u16::try_from(ops.len())
1066 .map_err(|_| TreeStreamError::Malformed("delta operation count exceeds u16".into()))?;
1067 let (first_ops, first_base) = delta_prefix_counts(anchor, current, ops, 1)?;
1068 let (hundred_ops, hundred_base) = delta_prefix_counts(anchor, current, ops, 100)?;
1069 let mut body = Vec::new();
1070 let mut ends = Vec::with_capacity(ops.len());
1071 let mut previous = "";
1072 for op in ops {
1073 match op {
1074 TreeDeltaOp::Remove(name) => {
1075 body.push(0);
1076 let prefix = shared_prefix(previous, name);
1077 put_varint(prefix, &mut body);
1078 put_varint(name.len() - prefix, &mut body);
1079 body.extend_from_slice(&name.as_bytes()[prefix..]);
1080 }
1081 TreeDeltaOp::Upsert(entry) => {
1082 body.push(1);
1083 encode_lean_entry(entry, previous, &mut body)?;
1084 }
1085 }
1086 if !previous.is_empty() && previous >= op.name() {
1087 return Err(TreeStreamError::Malformed(
1088 "tree delta operations must be strictly sorted".into(),
1089 ));
1090 }
1091 previous = op.name();
1092 ends.push(body.len());
1093 }
1094 let end_for = |count: u16| -> Result<u32, TreeStreamError> {
1095 let end = if count == 0 {
1096 TREE_DELTA_HEADER_LEN
1097 } else {
1098 TREE_DELTA_HEADER_LEN
1099 .checked_add(*ends.get(count as usize - 1).ok_or_else(|| {
1100 TreeStreamError::Malformed("delta porch exceeds operation count".into())
1101 })?)
1102 .ok_or_else(|| TreeStreamError::Malformed("delta porch offset overflow".into()))?
1103 };
1104 u32::try_from(end)
1105 .map_err(|_| TreeStreamError::Malformed("delta porch offset exceeds u32".into()))
1106 };
1107 let mut out = Vec::with_capacity(TREE_DELTA_HEADER_LEN + body.len());
1108 out.extend_from_slice(TREE_DELTA_MAGIC);
1109 out.push(TREE_DELTA_ENCODING_VERSION);
1110 out.extend_from_slice(anchor_id.as_bytes());
1111 out.extend_from_slice(&result_count.to_le_bytes());
1112 out.extend_from_slice(&op_count.to_le_bytes());
1113 out.extend_from_slice(&first_ops.to_le_bytes());
1114 out.extend_from_slice(&first_base.to_le_bytes());
1115 out.extend_from_slice(&end_for(first_ops)?.to_le_bytes());
1116 out.extend_from_slice(&hundred_ops.to_le_bytes());
1117 out.extend_from_slice(&hundred_base.to_le_bytes());
1118 out.extend_from_slice(&end_for(hundred_ops)?.to_le_bytes());
1119 if out.len() != TREE_DELTA_HEADER_LEN {
1120 return Err(TreeStreamError::Malformed(
1121 "internal HDC1 header length mismatch".into(),
1122 ));
1123 }
1124 out.extend_from_slice(&body);
1125 Ok(out)
1126}
1127
1128pub fn decode_tree_delta_header(data: &[u8]) -> Result<TreeDeltaHeader, TreeStreamError> {
1130 decode_tree_delta_header_prefix(data, data.len())
1131}
1132
1133pub fn decode_tree_delta_header_prefix(
1136 data: &[u8],
1137 object_len: usize,
1138) -> Result<TreeDeltaHeader, TreeStreamError> {
1139 if data.len() < TREE_DELTA_HEADER_LEN {
1140 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
1141 }
1142 if !is_delta_tree(data) {
1143 return Err(TreeStreamError::Malformed(
1144 "bytes are not an HDC1 tree delta".into(),
1145 ));
1146 }
1147 if data[4] != TREE_DELTA_ENCODING_VERSION {
1148 return Err(TreeStreamError::UnsupportedVersion { found: data[4] });
1149 }
1150 let anchor = ContentHash::from_bytes(
1151 data[5..37]
1152 .try_into()
1153 .map_err(|_| TreeStreamError::Malformed("delta anchor hash is not 32 bytes".into()))?,
1154 );
1155 let header = TreeDeltaHeader {
1156 anchor,
1157 result_count: read_u32_at(data, 37)? as usize,
1158 op_count: read_u16_at(data, 41)? as usize,
1159 first_op_count: read_u16_at(data, 43)? as usize,
1160 first_base_count: read_u16_at(data, 45)? as usize,
1161 first_end: read_u32_at(data, 47)? as usize,
1162 hundred_op_count: read_u16_at(data, 51)? as usize,
1163 hundred_base_count: read_u16_at(data, 53)? as usize,
1164 hundred_end: read_u32_at(data, 55)? as usize,
1165 };
1166 if header.op_count > TREE_DELTA_MAX_OPS
1167 || header.first_op_count > header.op_count
1168 || header.hundred_op_count > header.op_count
1169 || header.first_end < TREE_DELTA_HEADER_LEN
1170 || header.hundred_end < header.first_end
1171 || header.hundred_end > object_len
1172 {
1173 return Err(TreeStreamError::Malformed(
1174 "invalid HDC1 porch or operation bounds".into(),
1175 ));
1176 }
1177 Ok(header)
1178}
1179
1180pub fn decode_tree_delta_ops(
1183 data: &[u8],
1184 wanted: usize,
1185) -> Result<(TreeDeltaHeader, Vec<TreeDeltaOp>, usize), TreeStreamError> {
1186 decode_tree_delta_ops_prefix(data, data.len(), wanted)
1187}
1188
1189pub fn decode_tree_delta_ops_prefix(
1191 data: &[u8],
1192 object_len: usize,
1193 wanted: usize,
1194) -> Result<(TreeDeltaHeader, Vec<TreeDeltaOp>, usize), TreeStreamError> {
1195 let header = decode_tree_delta_header_prefix(data, object_len)?;
1196 if wanted > header.op_count {
1197 return Err(TreeStreamError::Malformed(
1198 "partial delta operation count exceeds object".into(),
1199 ));
1200 }
1201 let mut offset = TREE_DELTA_HEADER_LEN;
1202 let mut previous = String::new();
1203 let mut ops = Vec::with_capacity(wanted);
1204 for _ in 0..wanted {
1205 let opcode = *data.get(offset).ok_or(TreeStreamError::TruncatedFrame {
1206 offset: offset as u64,
1207 })?;
1208 offset += 1;
1209 let op =
1210 match opcode {
1211 0 => {
1212 let prefix = take_varint(data, &mut offset)?;
1213 let suffix_len = take_varint(data, &mut offset)?;
1214 if prefix > previous.len() {
1215 return Err(TreeStreamError::Malformed(
1216 "delta name prefix exceeds predecessor".into(),
1217 ));
1218 }
1219 let end = offset.checked_add(suffix_len).ok_or_else(|| {
1220 TreeStreamError::Malformed("delta name length overflow".into())
1221 })?;
1222 let mut name = previous.as_bytes()[..prefix].to_vec();
1223 name.extend_from_slice(data.get(offset..end).ok_or(
1224 TreeStreamError::TruncatedFrame {
1225 offset: offset as u64,
1226 },
1227 )?);
1228 offset = end;
1229 TreeDeltaOp::Remove(String::from_utf8(name).map_err(|_| {
1230 TreeStreamError::Malformed("delta name is not UTF-8".into())
1231 })?)
1232 }
1233 1 => TreeDeltaOp::Upsert(decode_compact_entry(data, &mut offset, &previous)?),
1234 _ => {
1235 return Err(TreeStreamError::Malformed(format!(
1236 "invalid tree delta opcode {opcode}"
1237 )));
1238 }
1239 };
1240 if !previous.is_empty() && previous.as_str() >= op.name() {
1241 return Err(TreeStreamError::Malformed(
1242 "tree delta operations must be strictly sorted".into(),
1243 ));
1244 }
1245 previous = op.name().to_string();
1246 ops.push(op);
1247 }
1248 if (wanted == header.first_op_count && offset != header.first_end)
1249 || (wanted == header.hundred_op_count && offset != header.hundred_end)
1250 {
1251 return Err(TreeStreamError::Malformed(
1252 "HDC1 porch offset does not match decoded operations".into(),
1253 ));
1254 }
1255 Ok((header, ops, offset))
1256}
1257
1258pub fn decode_tree_delta(
1261 data: &[u8],
1262 anchor: &Tree,
1263 expected: ContentHash,
1264) -> Result<Tree, TreeStreamError> {
1265 let header = decode_tree_delta_header(data)?;
1266 let (decoded_header, ops, consumed) = decode_tree_delta_ops(data, header.op_count)?;
1267 if consumed != data.len() {
1268 return Err(TreeStreamError::TrailingBytes {
1269 extra: data.len().abs_diff(consumed) as u64,
1270 });
1271 }
1272 let anchor_found = anchor.hash();
1273 if anchor_found != decoded_header.anchor {
1274 return Err(TreeStreamError::HashMismatch {
1275 expected: decoded_header.anchor,
1276 found: anchor_found,
1277 });
1278 }
1279 let tree = apply_tree_delta(anchor, &ops)?;
1280 if tree.len() != decoded_header.result_count {
1281 return Err(TreeStreamError::Malformed(
1282 "delta result count does not match reconstructed tree".into(),
1283 ));
1284 }
1285 let found = tree.hash();
1286 if found != expected {
1287 return Err(TreeStreamError::HashMismatch { expected, found });
1288 }
1289 Ok(tree)
1290}
1291
1292fn read_u16_at(data: &[u8], offset: usize) -> Result<u16, TreeStreamError> {
1293 Ok(u16::from_le_bytes(
1294 data.get(offset..offset + 2)
1295 .ok_or(TreeStreamError::TruncatedFrame {
1296 offset: offset as u64,
1297 })?
1298 .try_into()
1299 .map_err(|_| TreeStreamError::Malformed("invalid u16 field".into()))?,
1300 ))
1301}
1302
1303fn read_u32_at(data: &[u8], offset: usize) -> Result<u32, TreeStreamError> {
1304 Ok(u32::from_le_bytes(
1305 data.get(offset..offset + 4)
1306 .ok_or(TreeStreamError::TruncatedFrame {
1307 offset: offset as u64,
1308 })?
1309 .try_into()
1310 .map_err(|_| TreeStreamError::Malformed("invalid u32 field".into()))?,
1311 ))
1312}
1313
1314pub(crate) fn encode_entry_frame(entry: &TreeEntry) -> Result<Vec<u8>, TreeStreamError> {
1315 let name = entry.name().as_bytes();
1316 let name_len = u16::try_from(name.len()).map_err(|_| {
1317 TreeStreamError::Malformed(format!("entry name '{}' exceeds u16", entry.name()))
1318 })?;
1319 let mut frame = Vec::new();
1320 frame.push(entry.mode().to_byte());
1321 frame.push(entry.entry_type().to_byte());
1322 frame.extend_from_slice(&name_len.to_le_bytes());
1323 frame.extend_from_slice(name);
1324 encode_target(&mut frame, entry)?;
1325 Ok(frame)
1326}
1327
1328pub(crate) fn decode_entry_at(
1329 data: &[u8],
1330 offset: usize,
1331 payload_end: usize,
1332) -> Result<(TreeEntry, usize), TreeStreamError> {
1333 if offset + 4 > payload_end {
1334 return Err(TreeStreamError::TruncatedFrame {
1335 offset: offset as u64,
1336 });
1337 }
1338 let frame_len = u32::from_le_bytes(
1339 data[offset..offset + 4]
1340 .try_into()
1341 .map_err(|_| TreeStreamError::Malformed("frame length slice is not 4 bytes".into()))?,
1342 ) as usize;
1343 let frame_start = offset + 4;
1344 let frame_end = frame_start
1345 .checked_add(frame_len)
1346 .ok_or(TreeStreamError::TruncatedFrame {
1347 offset: offset as u64,
1348 })?;
1349 if frame_end > payload_end {
1350 return Err(TreeStreamError::TruncatedFrame {
1351 offset: offset as u64,
1352 });
1353 }
1354 let entry = decode_entry_frame(&data[frame_start..frame_end])?;
1355 Ok((entry, 4 + frame_len))
1356}
1357
1358pub(crate) fn decode_entry_frame(frame: &[u8]) -> Result<TreeEntry, TreeStreamError> {
1359 if frame.len() < 4 {
1360 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
1361 }
1362 let mode = FileMode::from_byte(frame[0]).ok_or_else(|| {
1363 TreeStreamError::Malformed(format!("malformed tree entry mode {}", frame[0]))
1364 })?;
1365 let kind = EntryType::from_byte(frame[1]).ok_or_else(|| {
1366 TreeStreamError::Malformed(format!("malformed tree entry kind {}", frame[1]))
1367 })?;
1368 let name_len = u16::from_le_bytes([frame[2], frame[3]]) as usize;
1369 let name_end = 4 + name_len;
1370 if frame.len() < name_end {
1371 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
1372 }
1373 let name = std::str::from_utf8(&frame[4..name_end])
1374 .map_err(|_| TreeStreamError::Malformed("tree entry name is not UTF-8".into()))?
1375 .to_string();
1376 let entry = decode_target(name, kind, mode, &frame[name_end..])?;
1377 if entry.mode() != mode {
1378 return Err(TreeStreamError::Malformed(format!(
1379 "tree kind/mode mismatch for {}: {kind:?}/{mode:?}",
1380 entry.name()
1381 )));
1382 }
1383 Ok(entry)
1384}
1385
1386fn encode_target(frame: &mut Vec<u8>, entry: &TreeEntry) -> Result<(), TreeStreamError> {
1387 match entry.entry_type() {
1388 EntryType::Blob | EntryType::Tree | EntryType::Symlink => {
1389 frame.extend_from_slice(entry.require_content_hash().as_bytes());
1390 }
1391 EntryType::Gitlink => {
1392 let target = entry.gitlink_target().ok_or_else(|| {
1393 TreeStreamError::Malformed("gitlink entry is missing target".into())
1394 })?;
1395 frame.push(git_format_to_tag(target.format()));
1396 frame.extend_from_slice(target.as_bytes());
1397 }
1398 EntryType::Spoollink => {
1399 let (spool, state) = entry.spoollink_target().ok_or_else(|| {
1400 TreeStreamError::Malformed("spoollink entry is missing target".into())
1401 })?;
1402 let spool_bytes = spool.as_str().as_bytes();
1403 let spool_len = u16::try_from(spool_bytes.len())
1404 .map_err(|_| TreeStreamError::Malformed("spool id exceeds u16".into()))?;
1405 frame.extend_from_slice(&spool_len.to_le_bytes());
1406 frame.extend_from_slice(spool_bytes);
1407 frame.extend_from_slice(state.as_bytes());
1408 }
1409 }
1410 Ok(())
1411}
1412
1413fn decode_target(
1414 name: String,
1415 kind: EntryType,
1416 mode: FileMode,
1417 payload: &[u8],
1418) -> Result<TreeEntry, TreeStreamError> {
1419 match kind {
1420 EntryType::Blob => TreeEntry::file(name, take_hash(payload)?, mode == FileMode::Executable)
1421 .map_err(TreeStreamError::from),
1422 EntryType::Tree => {
1423 TreeEntry::directory(name, take_hash(payload)?).map_err(TreeStreamError::from)
1424 }
1425 EntryType::Symlink => {
1426 TreeEntry::symlink(name, take_hash(payload)?).map_err(TreeStreamError::from)
1427 }
1428 EntryType::Gitlink => decode_gitlink(name, payload),
1429 EntryType::Spoollink => decode_spoollink(name, payload),
1430 }
1431}
1432
1433fn take_hash(payload: &[u8]) -> Result<ContentHash, TreeStreamError> {
1434 let bytes: [u8; 32] = payload
1435 .try_into()
1436 .map_err(|_| TreeStreamError::Malformed("malformed tree entry object id".into()))?;
1437 Ok(ContentHash::from_bytes(bytes))
1438}
1439
1440fn decode_gitlink(name: String, payload: &[u8]) -> Result<TreeEntry, TreeStreamError> {
1441 if payload.is_empty() {
1442 return Err(TreeStreamError::Malformed(
1443 "malformed tree entry object id".into(),
1444 ));
1445 }
1446 let format = git_format_from_tag(payload[0])?;
1447 let oid = &payload[1..];
1448 let expected = match format {
1449 GitObjectFormat::Sha1 => 20,
1450 GitObjectFormat::Sha256 => 32,
1451 };
1452 if oid.len() != expected {
1453 return Err(TreeStreamError::Malformed(
1454 "malformed tree entry object id".into(),
1455 ));
1456 }
1457 let target = GitObjectId::from_raw(format, oid)
1458 .map_err(|err| TreeError::InvalidStructure(format!("invalid gitlink target: {err}")))?;
1459 TreeEntry::gitlink(name, target).map_err(TreeStreamError::from)
1460}
1461
1462fn decode_spoollink(name: String, payload: &[u8]) -> Result<TreeEntry, TreeStreamError> {
1463 if payload.len() < 2 {
1464 return Err(TreeStreamError::TruncatedFrame { offset: 0 });
1465 }
1466 let spool_len = u16::from_le_bytes([payload[0], payload[1]]) as usize;
1467 let spool_end = 2 + spool_len;
1468 let state_end = spool_end + 32;
1469 if payload.len() != state_end {
1470 return Err(TreeStreamError::Malformed(
1471 "malformed tree entry object id".into(),
1472 ));
1473 }
1474 let spool = std::str::from_utf8(&payload[2..spool_end])
1475 .map_err(|_| TreeStreamError::Malformed("spool id is not UTF-8".into()))?;
1476 let spool_id = SpoolId::parse(spool)
1477 .map_err(|err| TreeStreamError::Malformed(format!("invalid spool id: {err}")))?;
1478 let state =
1479 StateId::from_bytes(payload[spool_end..state_end].try_into().map_err(|_| {
1480 TreeStreamError::Malformed("spoollink state id is not 32 bytes".into())
1481 })?);
1482 TreeEntry::spoollink(name, spool_id, state).map_err(TreeStreamError::from)
1483}
1484
1485#[cfg(all(test, feature = "zstd"))]
1486mod block_tests {
1487 use super::*;
1488
1489 fn fixture(entries: usize) -> Tree {
1490 Tree::from_entries(
1491 (0..entries)
1492 .map(|index| {
1493 TreeEntry::file(
1494 format!("module_{index:04}.rs"),
1495 ContentHash::compute(format!("blob-{index}").as_bytes()),
1496 false,
1497 )
1498 .expect("fixture entry")
1499 })
1500 .collect(),
1501 )
1502 }
1503
1504 fn maximum_size_block_fixture() -> Tree {
1505 let name_prefix = "n".repeat(u16::MAX as usize - 5);
1506 let spool_id = SpoolId::parse(format!("s/{}", "s".repeat(u16::MAX as usize - 2)))
1507 .expect("maximum-size spool id");
1508 assert_eq!(spool_id.as_str().len(), u16::MAX as usize);
1509 Tree::from_entries(
1510 (0..TREE_BLOCK_ENTRIES)
1511 .map(|index| {
1512 let name = format!("{name_prefix}{index:05}");
1513 assert_eq!(name.len(), u16::MAX as usize);
1514 TreeEntry::spoollink(name, spool_id.clone(), StateId::from_bytes([7; 32]))
1515 .expect("maximum-size fixture entry")
1516 })
1517 .collect(),
1518 )
1519 }
1520
1521 #[test]
1522 fn blocked_encoder_falls_back_for_the_complete_object() {
1523 let tree = fixture(1);
1524 let encoded = tree.encode_canonical_blocked(3, 0).expect("encode");
1525 assert_eq!(encoded[4], TREE_ENCODING_VERSION);
1526 }
1527
1528 #[test]
1529 fn final_single_entry_block_is_stored_raw() {
1530 let tree = fixture(TREE_BLOCK_ENTRIES + 1);
1531 let encoded = tree.encode_canonical_blocked(3, 0).expect("encode");
1532 assert_eq!(encoded[4], TREE_BLOCK_ENCODING_VERSION);
1533 let header = decode_header(&encoded).expect("header");
1534 let preamble = &encoded[TREE_HEADER_LEN..TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN];
1535 let block_header =
1536 decode_block_header(&header, preamble, encoded.len() as u64).expect("block header");
1537 let second_index = TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN + TREE_BLOCK_INDEX_LEN;
1538 let index = decode_block_index(
1539 &encoded[second_index..second_index + TREE_BLOCK_INDEX_LEN],
1540 1,
1541 &block_header,
1542 encoded.len() as u64,
1543 )
1544 .expect("second block");
1545 assert_eq!(index.stored_len, index.raw_len);
1546 }
1547
1548 #[test]
1549 fn trailing_bytes_in_a_raw_block_are_rejected() {
1550 let tree = fixture(TREE_BLOCK_ENTRIES + 1);
1551 let mut encoded = tree.encode_canonical_blocked(3, 0).expect("encode");
1552 let second_index = TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN + TREE_BLOCK_INDEX_LEN;
1553 let stored_len_offset = second_index + 16;
1554 let stored_len = u32::from_le_bytes(
1555 encoded[stored_len_offset..stored_len_offset + 4]
1556 .try_into()
1557 .expect("stored length"),
1558 );
1559 encoded[stored_len_offset..stored_len_offset + 4]
1560 .copy_from_slice(&(stored_len + 1).to_le_bytes());
1561 let payload_len =
1562 u64::from_le_bytes(encoded[45..53].try_into().expect("stored payload length"));
1563 encoded[45..53].copy_from_slice(&(payload_len + 1).to_le_bytes());
1564 encoded.push(0);
1565
1566 assert!(Tree::decode_canonical(&encoded).is_err());
1567 }
1568
1569 #[test]
1570 fn block_raw_length_above_encoder_ceiling_is_rejected_in_the_index() {
1571 let tree = fixture(TREE_BLOCK_ENTRIES);
1572 let encoded = tree.encode_canonical_blocked(3, 0).expect("encode");
1573 let header = decode_header(&encoded).expect("header");
1574 let preamble = &encoded[TREE_HEADER_LEN..TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN];
1575 let block_header =
1576 decode_block_header(&header, preamble, encoded.len() as u64).expect("block header");
1577 let index_offset = TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN;
1578 let mut index: [u8; TREE_BLOCK_INDEX_LEN] = encoded
1579 [index_offset..index_offset + TREE_BLOCK_INDEX_LEN]
1580 .try_into()
1581 .expect("index");
1582 index[20..24].copy_from_slice(&u32::MAX.to_le_bytes());
1583
1584 let error = decode_block_index(&index, 0, &block_header, encoded.len() as u64)
1585 .expect_err("attacker-controlled raw length must fail at the index boundary");
1586 assert!(
1587 matches!(error, TreeStreamError::Malformed(ref message) if message.contains("raw length") && message.contains("exceeds maximum")),
1588 "unexpected error: {error}"
1589 );
1590 }
1591
1592 #[test]
1593 fn block_entry_count_above_encoder_limit_is_rejected() {
1594 let tree = fixture(TREE_BLOCK_ENTRIES);
1595 let mut encoded = tree.encode_canonical_blocked(3, 0).expect("encode");
1596 let block_entries_offset = TREE_HEADER_LEN + 2;
1597 encoded[block_entries_offset..block_entries_offset + 2]
1598 .copy_from_slice(&u16::MAX.to_le_bytes());
1599
1600 let error = Tree::decode_canonical(&encoded)
1601 .expect_err("block entry count above the encoder limit must fail");
1602 assert!(
1603 matches!(error, TreeStreamError::Malformed(ref message) if message.contains("entry count") && message.contains("exceeds maximum")),
1604 "unexpected error: {error}"
1605 );
1606 }
1607
1608 #[test]
1609 fn short_final_block_uses_its_actual_entry_count_for_the_ceiling() {
1610 let tree = fixture(TREE_BLOCK_ENTRIES + 1);
1611 let encoded = tree.encode_canonical_blocked(3, 0).expect("encode");
1612 let header = decode_header(&encoded).expect("header");
1613 let preamble = &encoded[TREE_HEADER_LEN..TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN];
1614 let block_header =
1615 decode_block_header(&header, preamble, encoded.len() as u64).expect("block header");
1616 let index_offset = TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN + TREE_BLOCK_INDEX_LEN;
1617 let mut index: [u8; TREE_BLOCK_INDEX_LEN] = encoded
1618 [index_offset..index_offset + TREE_BLOCK_INDEX_LEN]
1619 .try_into()
1620 .expect("final index");
1621 let one_entry_max_plus_one =
1622 u32::try_from(TREE_BLOCK_MAX_ENTRY_FRAME_LEN + 1).expect("one over final block");
1623 index[20..24].copy_from_slice(&one_entry_max_plus_one.to_le_bytes());
1624
1625 let error = decode_block_index(&index, 1, &block_header, encoded.len() as u64)
1626 .expect_err("short final block must use its actual structural ceiling");
1627 let expected_max = format!("maximum {TREE_BLOCK_MAX_ENTRY_FRAME_LEN}");
1628 assert!(
1629 matches!(error, TreeStreamError::Malformed(ref message) if message.contains("raw length") && message.contains(&expected_max)),
1630 "unexpected error: {error}"
1631 );
1632 }
1633
1634 #[test]
1635 fn block_raw_length_above_zstd_expansion_bound_is_rejected() {
1636 let stored_offset = 128u64;
1637 let stored_len = 1u32;
1638 let raw_len = u32::try_from(TREE_BLOCK_MAX_EXPANSION_RATIO + 1).expect("raw length");
1639 let mut index = [0u8; TREE_BLOCK_INDEX_LEN];
1640 index[8..16].copy_from_slice(&stored_offset.to_le_bytes());
1641 index[16..20].copy_from_slice(&stored_len.to_le_bytes());
1642 index[20..24].copy_from_slice(&raw_len.to_le_bytes());
1643 let block_header = TreeBlockHeader {
1644 block_entries: TREE_BLOCK_ENTRIES,
1645 block_count: 1,
1646 entry_count: 1,
1647 raw_payload_len: raw_len as u64,
1648 index_end: stored_offset,
1649 };
1650
1651 let error = decode_block_index(&index, 0, &block_header, stored_offset + stored_len as u64)
1652 .expect_err("excessive zstd expansion must fail at the index boundary");
1653 assert!(
1654 matches!(error, TreeStreamError::Malformed(ref message) if message.contains("expansion limit")),
1655 "unexpected error: {error}"
1656 );
1657 }
1658
1659 #[test]
1660 fn payload_and_zstd_helpers_recheck_the_structural_ceiling() {
1661 let over_limit = TREE_BLOCK_MAX_RAW_LEN + 1;
1662 let payload_error = decode_block_payload(&[0], over_limit)
1663 .expect_err("payload helper must reject an unchecked raw length");
1664 assert!(
1665 matches!(payload_error, TreeStreamError::Malformed(ref message) if message.contains("raw length")),
1666 "unexpected error: {payload_error}"
1667 );
1668 let zstd_error = decompress_block_tail(&[], over_limit)
1669 .expect_err("zstd helper must cap its output independently");
1670 assert!(
1671 matches!(zstd_error, TreeStreamError::Malformed(ref message) if message.contains("tail length")),
1672 "unexpected error: {zstd_error}"
1673 );
1674 }
1675
1676 #[test]
1677 fn maximum_size_encoder_block_round_trips_and_one_over_is_rejected() {
1678 let tree = maximum_size_block_fixture();
1679 let raw = tree.encode_canonical().expect("encode raw");
1680 assert_eq!(raw.len() - TREE_HEADER_LEN, TREE_BLOCK_MAX_RAW_LEN);
1681
1682 let blocked = tree.encode_canonical_blocked(3, 0).expect("encode blocked");
1683 assert_eq!(blocked[4], TREE_BLOCK_ENCODING_VERSION);
1684 let header = decode_header(&blocked).expect("header");
1685 let preamble = &blocked[TREE_HEADER_LEN..TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN];
1686 let block_header =
1687 decode_block_header(&header, preamble, blocked.len() as u64).expect("block header");
1688 let index_offset = TREE_HEADER_LEN + TREE_BLOCK_PREAMBLE_LEN;
1689 let index = decode_block_index(
1690 &blocked[index_offset..index_offset + TREE_BLOCK_INDEX_LEN],
1691 0,
1692 &block_header,
1693 blocked.len() as u64,
1694 )
1695 .expect("maximum-size block index");
1696 assert_eq!(index.raw_len, TREE_BLOCK_MAX_RAW_LEN);
1697
1698 let decoded = Tree::decode_canonical(&blocked).expect("decode maximum-size block");
1699 assert_eq!(decoded, tree);
1700 assert_eq!(decoded.encode_canonical().expect("re-encode decoded"), raw);
1701 assert_eq!(
1702 decoded
1703 .encode_canonical_blocked(3, 0)
1704 .expect("re-encode blocked"),
1705 blocked
1706 );
1707
1708 let mut over_limit = blocked;
1709 let raw_len_offset = index_offset + 20;
1710 let one_over = u32::try_from(TREE_BLOCK_MAX_RAW_LEN + 1).expect("one over");
1711 over_limit[raw_len_offset..raw_len_offset + 4].copy_from_slice(&one_over.to_le_bytes());
1712 let error = Tree::decode_canonical(&over_limit)
1713 .expect_err("one byte above the block ceiling must fail");
1714 assert!(
1715 matches!(error, TreeStreamError::Malformed(ref message) if message.contains("raw length")),
1716 "unexpected error: {error}"
1717 );
1718 }
1719}