1use std::collections::HashSet;
12
13use crate::checksum::jenkins_lookup3;
14use crate::error::{Error, Result};
15use crate::io::Cursor;
16use crate::storage::Storage;
17
18const BTHD_SIGNATURE: [u8; 4] = *b"BTHD";
23const BTIN_SIGNATURE: [u8; 4] = *b"BTIN";
24const BTLF_SIGNATURE: [u8; 4] = *b"BTLF";
25const MAX_BTREE_V2_DEPTH: u16 = 64;
26
27#[derive(Debug, Clone)]
33pub struct BTreeV2Header {
34 pub btree_type: u8,
36 pub node_size: u32,
38 pub record_size: u16,
40 pub depth: u16,
42 pub split_percent: u8,
44 pub merge_percent: u8,
46 pub root_node_address: u64,
48 pub num_records_in_root: u16,
50 pub total_records: u64,
52}
53
54impl BTreeV2Header {
55 pub fn parse(cursor: &mut Cursor, offset_size: u8, length_size: u8) -> Result<Self> {
71 let start = cursor.position();
72
73 let sig = cursor.read_bytes(4)?;
74 if sig != BTHD_SIGNATURE {
75 return Err(Error::InvalidBTreeV2Signature { context: "header" });
76 }
77
78 let version = cursor.read_u8()?;
79 if version != 0 {
80 return Err(Error::UnsupportedBTreeVersion(version));
81 }
82
83 let btree_type = cursor.read_u8()?;
84 let node_size = cursor.read_u32_le()?;
85 let record_size = cursor.read_u16_le()?;
86 let depth = cursor.read_u16_le()?;
87 let split_percent = cursor.read_u8()?;
88 let merge_percent = cursor.read_u8()?;
89 let root_node_address = cursor.read_offset(offset_size)?;
90 let num_records_in_root = cursor.read_u16_le()?;
91 let total_records = cursor.read_length(length_size)?;
92
93 let checksum_end = cursor.position();
95 let stored_checksum = cursor.read_u32_le()?;
96
97 let computed = jenkins_lookup3(&cursor.data()[start as usize..checksum_end as usize]);
98 if computed != stored_checksum {
99 return Err(Error::ChecksumMismatch {
100 expected: stored_checksum,
101 actual: computed,
102 });
103 }
104
105 Ok(BTreeV2Header {
106 btree_type,
107 node_size,
108 record_size,
109 depth,
110 split_percent,
111 merge_percent,
112 root_node_address,
113 num_records_in_root,
114 total_records,
115 })
116 }
117
118 pub fn parse_at_storage(
120 storage: &dyn Storage,
121 address: u64,
122 offset_size: u8,
123 length_size: u8,
124 ) -> Result<Self> {
125 let header_len = 4
126 + 1
127 + 1
128 + 4
129 + 2
130 + 2
131 + 1
132 + 1
133 + usize::from(offset_size)
134 + 2
135 + usize::from(length_size)
136 + 4;
137 let bytes = storage.read_range(address, header_len)?;
138 let mut cursor = Cursor::new(bytes.as_ref());
139 Self::parse(&mut cursor, offset_size, length_size)
140 }
141}
142
143#[derive(Debug, Clone)]
151pub enum BTreeV2Record {
152 HugeIndirectNonFiltered {
154 address: u64,
155 length: u64,
156 object_id: u64,
157 },
158 HugeIndirectFiltered {
160 address: u64,
161 filtered_length: u64,
162 filter_mask: u32,
163 memory_length: u64,
164 object_id: u64,
165 },
166 HugeDirectNonFiltered { address: u64, length: u64 },
168 HugeDirectFiltered {
170 address: u64,
171 filtered_length: u64,
172 filter_mask: u32,
173 memory_length: u64,
174 },
175 LinkNameHash { hash: u32, heap_id: Vec<u8> },
177 CreationOrder { order: u64, heap_id: Vec<u8> },
179 AttributeNameHash {
181 hash: u32,
182 flags: u8,
183 creation_order: u32,
184 heap_id: Vec<u8>,
185 },
186 AttributeCreationOrder { order: u32, heap_id: Vec<u8> },
188 ChunkedNonFiltered { address: u64, offsets: Vec<u64> },
190 ChunkedFiltered {
192 address: u64,
193 chunk_size: u64,
194 filter_mask: u32,
195 offsets: Vec<u64>,
196 },
197 SharedMessageHeap {
199 hash: u32,
200 reference_count: u32,
201 heap_id: Vec<u8>,
202 },
203 SharedMessageObjectHeader {
205 hash: u32,
206 message_type: u16,
207 object_header_index: u16,
208 object_header_address: u64,
209 },
210 Unknown { record_type: u8, data: Vec<u8> },
212}
213
214#[allow(clippy::too_many_arguments)]
220fn parse_record(
221 cursor: &mut Cursor,
222 btree_type: u8,
223 record_size: u16,
224 offset_size: u8,
225 length_size: u8,
226 ndims: Option<u32>,
227 chunk_dims: &[u32],
228 heap_id_len: usize,
229) -> Result<BTreeV2Record> {
230 let record_start = cursor.position();
231
232 let record = match btree_type {
233 1 => BTreeV2Record::HugeIndirectNonFiltered {
235 address: cursor.read_offset(offset_size)?,
236 length: cursor.read_length(length_size)?,
237 object_id: cursor.read_length(length_size)?,
238 },
239
240 2 => BTreeV2Record::HugeIndirectFiltered {
242 address: cursor.read_offset(offset_size)?,
243 filtered_length: cursor.read_length(length_size)?,
244 filter_mask: cursor.read_u32_le()?,
245 memory_length: cursor.read_length(length_size)?,
246 object_id: cursor.read_length(length_size)?,
247 },
248
249 3 => BTreeV2Record::HugeDirectNonFiltered {
251 address: cursor.read_offset(offset_size)?,
252 length: cursor.read_length(length_size)?,
253 },
254
255 4 => BTreeV2Record::HugeDirectFiltered {
257 address: cursor.read_offset(offset_size)?,
258 filtered_length: cursor.read_length(length_size)?,
259 filter_mask: cursor.read_u32_le()?,
260 memory_length: cursor.read_length(length_size)?,
261 },
262
263 5 => {
265 let hash = cursor.read_u32_le()?;
266 let heap_id = cursor.read_bytes(heap_id_len)?.to_vec();
267 BTreeV2Record::LinkNameHash { hash, heap_id }
268 }
269
270 6 => {
272 let order = cursor.read_u64_le()?;
273 let heap_id = cursor.read_bytes(heap_id_len)?.to_vec();
274 BTreeV2Record::CreationOrder { order, heap_id }
275 }
276
277 7 => {
279 let location = cursor.read_u8()?;
280 cursor.skip(3)?;
281 let hash = cursor.read_u32_le()?;
282 match location {
283 0 => {
284 let reference_count = cursor.read_u32_le()?;
285 let heap_id = cursor.read_bytes(8)?.to_vec();
286 BTreeV2Record::SharedMessageHeap {
287 hash,
288 reference_count,
289 heap_id,
290 }
291 }
292 1 => {
293 let _reserved = cursor.read_u8()?;
294 let message_type = u16::from(cursor.read_u8()?);
295 let object_header_index = cursor.read_u16_le()?;
296 let object_header_address = cursor.read_offset(offset_size)?;
297 BTreeV2Record::SharedMessageObjectHeader {
298 hash,
299 message_type,
300 object_header_index,
301 object_header_address,
302 }
303 }
304 other => {
305 return Err(Error::InvalidData(format!(
306 "unknown SOHM B-tree record location: {other}"
307 )));
308 }
309 }
310 }
311
312 8 => {
314 let heap_id = cursor.read_bytes(heap_id_len)?.to_vec();
315 let flags = cursor.read_u8()?;
316 let creation_order = cursor.read_u32_le()?;
317 let hash = cursor.read_u32_le()?;
318 BTreeV2Record::AttributeNameHash {
319 hash,
320 flags,
321 creation_order,
322 heap_id,
323 }
324 }
325
326 9 => {
328 let heap_id = cursor.read_bytes(heap_id_len)?.to_vec();
329 let _flags = cursor.read_u8()?;
330 let order = cursor.read_u32_le()?;
331 BTreeV2Record::AttributeCreationOrder { order, heap_id }
332 }
333
334 10 => {
336 let offset_bytes_available = record_payload_len(
340 record_size,
341 offset_size as usize,
342 "B-tree v2 type-10 chunk record is shorter than its address",
343 )?;
344 let address = cursor.read_offset(offset_size)?;
345 let num_offsets = offset_bytes_available / 8;
346 let offsets = read_scaled_chunk_offsets(cursor, num_offsets, ndims, chunk_dims)?;
347 BTreeV2Record::ChunkedNonFiltered { address, offsets }
348 }
349
350 11 => {
352 let nbytes_size = length_size as usize;
354 let fixed_size = offset_size as usize + nbytes_size + 4; let remaining = record_payload_len(
356 record_size,
357 fixed_size,
358 "B-tree v2 type-11 chunk record is shorter than its fixed fields",
359 )?;
360 let address = cursor.read_offset(offset_size)?;
361 let chunk_size = cursor.read_length(length_size)?;
362 let filter_mask = cursor.read_u32_le()?;
363 let num_offsets = remaining / 8;
364 let offsets = read_scaled_chunk_offsets(cursor, num_offsets, ndims, chunk_dims)?;
365 BTreeV2Record::ChunkedFiltered {
366 address,
367 chunk_size,
368 filter_mask,
369 offsets,
370 }
371 }
372
373 _ => {
375 let data = cursor.read_bytes(record_size as usize)?.to_vec();
376 return Ok(BTreeV2Record::Unknown {
377 record_type: btree_type,
378 data,
379 });
380 }
381 };
382
383 let consumed = cursor.position() - record_start;
385 let record_size = u64::from(record_size);
386 if consumed > record_size {
387 return Err(Error::InvalidData(format!(
388 "B-tree v2 type-{btree_type} record consumed {consumed} bytes but record size is {record_size}"
389 )));
390 }
391 if consumed < record_size {
392 cursor.skip((record_size - consumed) as usize)?;
393 }
394
395 Ok(record)
396}
397
398fn record_payload_len(record_size: u16, fixed_size: usize, error_message: &str) -> Result<usize> {
399 (record_size as usize)
400 .checked_sub(fixed_size)
401 .ok_or_else(|| Error::InvalidData(error_message.into()))
402}
403
404fn read_scaled_chunk_offsets(
405 cursor: &mut Cursor,
406 num_offsets: usize,
407 ndims: Option<u32>,
408 chunk_dims: &[u32],
409) -> Result<Vec<u64>> {
410 if let Some(ndims) = ndims {
411 let ndims = usize::try_from(ndims)
412 .map_err(|_| Error::InvalidData("B-tree v2 chunk rank exceeds usize".into()))?;
413 if num_offsets != ndims {
414 return Err(Error::InvalidData(format!(
415 "B-tree v2 chunk record has {num_offsets} offsets for {ndims} dimensions"
416 )));
417 }
418 }
419
420 if !chunk_dims.is_empty() && num_offsets != chunk_dims.len() {
421 return Err(Error::InvalidData(format!(
422 "B-tree v2 chunk record has {num_offsets} offsets but {} chunk dimensions",
423 chunk_dims.len()
424 )));
425 }
426
427 let mut offsets = Vec::with_capacity(num_offsets);
428 for dim in 0..num_offsets {
429 let scaled = cursor.read_u64_le()?;
430 let chunk_extent = chunk_dims.get(dim).copied().unwrap_or(1);
431 let offset = scaled
432 .checked_mul(u64::from(chunk_extent))
433 .ok_or_else(|| Error::InvalidData("B-tree v2 chunk offset overflows u64".into()))?;
434 offsets.push(offset);
435 }
436 Ok(offsets)
437}
438
439fn record_matches_chunk_bounds(
440 record: &BTreeV2Record,
441 chunk_dims: &[u32],
442 chunk_bounds: Option<(&[u64], &[u64])>,
443) -> bool {
444 let Some((first_chunk, last_chunk)) = chunk_bounds else {
445 return true;
446 };
447
448 let offsets = match record {
449 BTreeV2Record::ChunkedNonFiltered { offsets, .. }
450 | BTreeV2Record::ChunkedFiltered { offsets, .. } => offsets,
451 _ => return true,
452 };
453
454 offsets.iter().enumerate().all(|(dim, offset)| {
455 let chunk_index = *offset / u64::from(chunk_dims[dim]);
456 chunk_index >= first_chunk[dim] && chunk_index <= last_chunk[dim]
457 })
458}
459
460fn record_matches_link_name_hash(record: &BTreeV2Record, target_hash: Option<u32>) -> bool {
461 match target_hash {
462 Some(target_hash) => {
463 matches!(record, BTreeV2Record::LinkNameHash { hash, .. } if *hash == target_hash)
464 }
465 None => true,
466 }
467}
468
469fn record_matches_query(
470 record: &BTreeV2Record,
471 chunk_dims: &[u32],
472 chunk_bounds: Option<(&[u64], &[u64])>,
473 link_name_hash: Option<u32>,
474) -> bool {
475 record_matches_link_name_hash(record, link_name_hash)
476 && record_matches_chunk_bounds(record, chunk_dims, chunk_bounds)
477}
478
479fn link_name_hash(record: &BTreeV2Record) -> Option<u32> {
480 match record {
481 BTreeV2Record::LinkNameHash { hash, .. } => Some(*hash),
482 _ => None,
483 }
484}
485
486fn child_may_match_link_name_hash(
487 records: &[BTreeV2Record],
488 child_index: usize,
489 target_hash: Option<u32>,
490) -> bool {
491 let Some(target_hash) = target_hash else {
492 return true;
493 };
494
495 let lower_matches = child_index == 0
496 || link_name_hash(&records[child_index - 1]).map_or(true, |hash| target_hash >= hash);
497 let upper_matches = child_index == records.len()
498 || link_name_hash(&records[child_index]).map_or(true, |hash| target_hash <= hash);
499 lower_matches && upper_matches
500}
501
502fn validate_btree_v2_depth(depth: u16) -> Result<()> {
503 if depth > MAX_BTREE_V2_DEPTH {
504 return Err(Error::InvalidData(format!(
505 "B-tree v2 depth {depth} exceeds traversal limit {MAX_BTREE_V2_DEPTH}"
506 )));
507 }
508 Ok(())
509}
510
511fn enter_btree_v2_node(visited: &mut HashSet<u64>, address: u64) -> Result<()> {
512 if !visited.insert(address) {
513 return Err(Error::InvalidData(format!(
514 "B-tree v2 traversal revisits node at offset {address:#x}"
515 )));
516 }
517 Ok(())
518}
519
520fn num_records_size(max_records: u64) -> usize {
527 if max_records <= 0xFF {
528 1
529 } else if max_records <= 0xFFFF {
530 2
531 } else if max_records <= 0xFFFF_FFFF {
532 4
533 } else {
534 8
535 }
536}
537
538fn max_leaf_records(node_size: u32, record_size: u16) -> u64 {
540 let overhead = 10u32;
542 if node_size <= overhead || record_size == 0 {
543 return 0;
544 }
545 ((node_size - overhead) / record_size as u32) as u64
546}
547
548fn max_internal_records(
553 node_size: u32,
554 record_size: u16,
555 offset_size: u8,
556 max_child_records: u64,
557) -> u64 {
558 let overhead = 10u32;
560 if node_size <= overhead || record_size == 0 {
561 return 0;
562 }
563 let available = (node_size - overhead) as u64;
564 let nrec_size = num_records_size(max_child_records) as u64;
566 let entry_size = record_size as u64 + offset_size as u64 + nrec_size;
567 let extra = offset_size as u64 + nrec_size;
571 if available <= extra {
572 return 0;
573 }
574 (available - extra) / entry_size
575}
576
577fn node_checksum_bounds(
578 cursor: &Cursor<'_>,
579 start: u64,
580 node_size: u32,
581 compact_end: u64,
582 context: &'static str,
583) -> Result<usize> {
584 let start = usize::try_from(start)
585 .map_err(|_| Error::InvalidData(format!("B-tree v2 {context} offset is too large")))?;
586 let node_size = usize::try_from(node_size)
587 .map_err(|_| Error::InvalidData(format!("B-tree v2 {context} size is too large")))?;
588 if node_size < 10 {
589 return Err(Error::InvalidData(format!(
590 "B-tree v2 {context} node is too small: {node_size} bytes"
591 )));
592 }
593
594 let checksum_pos = usize::try_from(compact_end)
595 .map_err(|_| Error::InvalidData(format!("B-tree v2 {context} checksum is too large")))?;
596 if checksum_pos < start {
597 return Err(Error::InvalidData(format!(
598 "B-tree v2 {context} payload starts before node"
599 )));
600 }
601 let node_end = start
602 .checked_add(node_size)
603 .ok_or_else(|| Error::InvalidData(format!("B-tree v2 {context} size overflow")))?;
604
605 if node_end > cursor.data().len() {
606 return Err(Error::UnexpectedEof {
607 offset: start as u64,
608 needed: node_size as u64,
609 available: cursor.data().len().saturating_sub(start) as u64,
610 });
611 }
612 let checksum_end = checksum_pos
613 .checked_add(4)
614 .ok_or_else(|| Error::InvalidData(format!("B-tree v2 {context} checksum overflow")))?;
615 if checksum_end > node_end {
616 return Err(Error::InvalidData(format!(
617 "B-tree v2 {context} contents exceed node size"
618 )));
619 }
620
621 Ok(checksum_pos)
622}
623
624fn verify_node_checksum(
625 cursor: &mut Cursor<'_>,
626 start: u64,
627 node_size: u32,
628 compact_end: u64,
629 context: &'static str,
630) -> Result<()> {
631 let checksum_pos = node_checksum_bounds(cursor, start, node_size, compact_end, context)?;
632 cursor.set_position(checksum_pos as u64);
633 let stored_checksum = cursor.read_u32_le()?;
634 let computed = jenkins_lookup3(&cursor.data()[start as usize..checksum_pos]);
635 if computed != stored_checksum {
636 return Err(Error::ChecksumMismatch {
637 expected: stored_checksum,
638 actual: computed,
639 });
640 }
641 Ok(())
642}
643
644#[allow(clippy::too_many_arguments)]
646fn parse_leaf_node(
647 cursor: &mut Cursor,
648 header: &BTreeV2Header,
649 offset_size: u8,
650 length_size: u8,
651 ndims: Option<u32>,
652 chunk_dims: &[u32],
653 chunk_bounds: Option<(&[u64], &[u64])>,
654 link_name_hash: Option<u32>,
655 num_records: u16,
656 heap_id_len: usize,
657 records: &mut Vec<BTreeV2Record>,
658) -> Result<()> {
659 let start = cursor.position();
660
661 let sig = cursor.read_bytes(4)?;
662 if sig != BTLF_SIGNATURE {
663 return Err(Error::InvalidBTreeV2Signature {
664 context: "leaf node",
665 });
666 }
667
668 let version = cursor.read_u8()?;
669 if version != 0 {
670 return Err(Error::UnsupportedBTreeVersion(version));
671 }
672
673 let node_type = cursor.read_u8()?;
674 if node_type != header.btree_type {
675 return Err(Error::InvalidData(format!(
676 "B-tree v2 leaf node type mismatch: header says {}, node says {}",
677 header.btree_type, node_type
678 )));
679 }
680
681 for _ in 0..num_records {
682 let record = parse_record(
683 cursor,
684 header.btree_type,
685 header.record_size,
686 offset_size,
687 length_size,
688 ndims,
689 chunk_dims,
690 heap_id_len,
691 )?;
692 if record_matches_query(&record, chunk_dims, chunk_bounds, link_name_hash) {
693 records.push(record);
694 }
695 }
696
697 verify_node_checksum(
698 cursor,
699 start,
700 header.node_size,
701 cursor.position(),
702 "leaf node",
703 )?;
704
705 Ok(())
706}
707
708#[allow(clippy::too_many_arguments)]
710fn parse_internal_node(
711 data: &[u8],
712 address: u64,
713 header: &BTreeV2Header,
714 offset_size: u8,
715 length_size: u8,
716 ndims: Option<u32>,
717 chunk_dims: &[u32],
718 chunk_bounds: Option<(&[u64], &[u64])>,
719 link_name_hash: Option<u32>,
720 num_records: u16,
721 depth: u16,
722 heap_id_len: usize,
723 visited: &mut HashSet<u64>,
724 records: &mut Vec<BTreeV2Record>,
725) -> Result<()> {
726 if depth == 0 {
727 return Err(Error::InvalidData(
728 "B-tree v2 internal node traversal reached depth zero".into(),
729 ));
730 }
731 enter_btree_v2_node(visited, address)?;
732
733 let mut cursor = Cursor::new(data);
734 cursor.set_position(address);
735
736 let start = cursor.position();
737
738 let sig = cursor.read_bytes(4)?;
739 if sig != BTIN_SIGNATURE {
740 return Err(Error::InvalidBTreeV2Signature {
741 context: "internal node",
742 });
743 }
744
745 let version = cursor.read_u8()?;
746 if version != 0 {
747 return Err(Error::UnsupportedBTreeVersion(version));
748 }
749
750 let node_type = cursor.read_u8()?;
751 if node_type != header.btree_type {
752 return Err(Error::InvalidData(format!(
753 "B-tree v2 internal node type mismatch: header says {}, node says {}",
754 header.btree_type, node_type
755 )));
756 }
757
758 let max_child_records = if depth == 1 {
761 max_leaf_records(header.node_size, header.record_size)
762 } else {
763 let leaf_max = max_leaf_records(header.node_size, header.record_size);
765 let mut prev_max = leaf_max;
766 for _ in 1..depth {
767 prev_max =
768 max_internal_records(header.node_size, header.record_size, offset_size, prev_max);
769 }
770 prev_max
771 };
772 let nrec_bytes = num_records_size(max_child_records);
773
774 let mut node_records = Vec::with_capacity(num_records as usize);
786 for _ in 0..num_records {
787 let record = parse_record(
788 &mut cursor,
789 header.btree_type,
790 header.record_size,
791 offset_size,
792 length_size,
793 ndims,
794 chunk_dims,
795 heap_id_len,
796 )?;
797 node_records.push(record);
798 }
799
800 let num_children = num_records as usize + 1;
802
803 let has_total_records = depth > 1;
806 let total_nrec_bytes = if has_total_records {
808 length_size as usize
811 } else {
812 0
813 };
814
815 let mut child_addresses = Vec::with_capacity(num_children);
816 let mut child_nrecords = Vec::with_capacity(num_children);
817
818 for _ in 0..num_children {
819 let child_addr = cursor.read_offset(offset_size)?;
820 child_addresses.push(child_addr);
821 let nrec = cursor.read_uvar(nrec_bytes)?;
822 child_nrecords.push(nrec as u16);
823 if has_total_records {
824 cursor.read_uvar(total_nrec_bytes)?;
826 }
827 }
828
829 let compact_end = cursor.position();
830 verify_node_checksum(
831 &mut cursor,
832 start,
833 header.node_size,
834 compact_end,
835 "internal node",
836 )?;
837
838 let child_depth = depth - 1;
841 for (i, record) in node_records.iter().enumerate() {
842 let child_addr = child_addresses[i];
843 if !Cursor::is_undefined_offset(child_addr, offset_size)
844 && child_may_match_link_name_hash(&node_records, i, link_name_hash)
845 {
846 let child_nrec = child_nrecords[i];
847 if child_depth == 0 {
848 enter_btree_v2_node(visited, child_addr)?;
849 let mut child_cursor = Cursor::new(data);
850 child_cursor.set_position(child_addr);
851 parse_leaf_node(
852 &mut child_cursor,
853 header,
854 offset_size,
855 length_size,
856 ndims,
857 chunk_dims,
858 chunk_bounds,
859 link_name_hash,
860 child_nrec,
861 heap_id_len,
862 records,
863 )?;
864 } else {
865 parse_internal_node(
866 data,
867 child_addr,
868 header,
869 offset_size,
870 length_size,
871 ndims,
872 chunk_dims,
873 chunk_bounds,
874 link_name_hash,
875 child_nrec,
876 child_depth,
877 heap_id_len,
878 visited,
879 records,
880 )?;
881 }
882 }
883
884 if record_matches_query(record, chunk_dims, chunk_bounds, link_name_hash) {
885 records.push(record.clone());
886 }
887 }
888
889 let final_child_index = node_records.len();
890 let final_child_addr = child_addresses[final_child_index];
891 if !Cursor::is_undefined_offset(final_child_addr, offset_size)
892 && child_may_match_link_name_hash(&node_records, final_child_index, link_name_hash)
893 {
894 let child_nrec = child_nrecords[final_child_index];
895 if child_depth == 0 {
896 enter_btree_v2_node(visited, final_child_addr)?;
897 let mut child_cursor = Cursor::new(data);
898 child_cursor.set_position(final_child_addr);
899 parse_leaf_node(
900 &mut child_cursor,
901 header,
902 offset_size,
903 length_size,
904 ndims,
905 chunk_dims,
906 chunk_bounds,
907 link_name_hash,
908 child_nrec,
909 heap_id_len,
910 records,
911 )?;
912 } else {
913 parse_internal_node(
914 data,
915 final_child_addr,
916 header,
917 offset_size,
918 length_size,
919 ndims,
920 chunk_dims,
921 chunk_bounds,
922 link_name_hash,
923 child_nrec,
924 child_depth,
925 heap_id_len,
926 visited,
927 records,
928 )?;
929 }
930 }
931
932 Ok(())
933}
934
935pub fn collect_btree_v2_records(
944 data: &[u8],
945 header: &BTreeV2Header,
946 offset_size: u8,
947 length_size: u8,
948 ndims: Option<u32>,
949 chunk_dims: &[u32],
950 chunk_bounds: Option<(&[u64], &[u64])>,
951) -> Result<Vec<BTreeV2Record>> {
952 collect_btree_v2_records_with_link_hash(
953 data,
954 header,
955 offset_size,
956 length_size,
957 ndims,
958 chunk_dims,
959 chunk_bounds,
960 None,
961 )
962}
963
964#[allow(clippy::too_many_arguments)]
965fn collect_btree_v2_records_with_link_hash(
966 data: &[u8],
967 header: &BTreeV2Header,
968 offset_size: u8,
969 length_size: u8,
970 ndims: Option<u32>,
971 chunk_dims: &[u32],
972 chunk_bounds: Option<(&[u64], &[u64])>,
973 link_name_hash: Option<u32>,
974) -> Result<Vec<BTreeV2Record>> {
975 if Cursor::is_undefined_offset(header.root_node_address, offset_size) {
976 return Ok(Vec::new());
977 }
978
979 if header.total_records == 0 || header.num_records_in_root == 0 {
980 return Ok(Vec::new());
981 }
982 validate_btree_v2_depth(header.depth)?;
983
984 let heap_id_len = compute_heap_id_len(header);
986
987 let mut records = Vec::new();
988 let mut visited = HashSet::new();
989
990 if header.depth == 0 {
991 enter_btree_v2_node(&mut visited, header.root_node_address)?;
993 let mut cursor = Cursor::new(data);
994 cursor.set_position(header.root_node_address);
995 parse_leaf_node(
996 &mut cursor,
997 header,
998 offset_size,
999 length_size,
1000 ndims,
1001 chunk_dims,
1002 chunk_bounds,
1003 link_name_hash,
1004 header.num_records_in_root,
1005 heap_id_len,
1006 &mut records,
1007 )?;
1008 } else {
1009 parse_internal_node(
1011 data,
1012 header.root_node_address,
1013 header,
1014 offset_size,
1015 length_size,
1016 ndims,
1017 chunk_dims,
1018 chunk_bounds,
1019 link_name_hash,
1020 header.num_records_in_root,
1021 header.depth,
1022 heap_id_len,
1023 &mut visited,
1024 &mut records,
1025 )?;
1026 }
1027
1028 Ok(records)
1029}
1030
1031#[cfg(test)]
1033pub(crate) fn collect_btree_v2_link_name_hash_records(
1034 data: &[u8],
1035 header: &BTreeV2Header,
1036 offset_size: u8,
1037 length_size: u8,
1038 target_hash: u32,
1039) -> Result<Vec<BTreeV2Record>> {
1040 collect_btree_v2_records_with_link_hash(
1041 data,
1042 header,
1043 offset_size,
1044 length_size,
1045 None,
1046 &[],
1047 None,
1048 Some(target_hash),
1049 )
1050}
1051
1052pub fn collect_btree_v2_records_storage(
1054 storage: &dyn Storage,
1055 header: &BTreeV2Header,
1056 offset_size: u8,
1057 length_size: u8,
1058 ndims: Option<u32>,
1059 chunk_dims: &[u32],
1060 chunk_bounds: Option<(&[u64], &[u64])>,
1061) -> Result<Vec<BTreeV2Record>> {
1062 collect_btree_v2_records_storage_with_link_hash(
1063 storage,
1064 header,
1065 offset_size,
1066 length_size,
1067 ndims,
1068 chunk_dims,
1069 chunk_bounds,
1070 None,
1071 )
1072}
1073
1074#[allow(clippy::too_many_arguments)]
1075fn collect_btree_v2_records_storage_with_link_hash(
1076 storage: &dyn Storage,
1077 header: &BTreeV2Header,
1078 offset_size: u8,
1079 length_size: u8,
1080 ndims: Option<u32>,
1081 chunk_dims: &[u32],
1082 chunk_bounds: Option<(&[u64], &[u64])>,
1083 link_name_hash: Option<u32>,
1084) -> Result<Vec<BTreeV2Record>> {
1085 if Cursor::is_undefined_offset(header.root_node_address, offset_size) {
1086 return Ok(Vec::new());
1087 }
1088
1089 if header.total_records == 0 || header.num_records_in_root == 0 {
1090 return Ok(Vec::new());
1091 }
1092 validate_btree_v2_depth(header.depth)?;
1093
1094 let heap_id_len = compute_heap_id_len(header);
1095 let mut records = Vec::new();
1096 let mut visited = HashSet::new();
1097
1098 if header.depth == 0 {
1099 enter_btree_v2_node(&mut visited, header.root_node_address)?;
1100 parse_leaf_node_storage(
1101 storage,
1102 header.root_node_address,
1103 header,
1104 offset_size,
1105 length_size,
1106 ndims,
1107 chunk_dims,
1108 chunk_bounds,
1109 link_name_hash,
1110 header.num_records_in_root,
1111 heap_id_len,
1112 &mut records,
1113 )?;
1114 } else {
1115 parse_internal_node_storage(
1116 storage,
1117 header.root_node_address,
1118 header,
1119 offset_size,
1120 length_size,
1121 ndims,
1122 chunk_dims,
1123 chunk_bounds,
1124 link_name_hash,
1125 header.num_records_in_root,
1126 header.depth,
1127 heap_id_len,
1128 &mut visited,
1129 &mut records,
1130 )?;
1131 }
1132
1133 Ok(records)
1134}
1135
1136pub(crate) fn collect_btree_v2_link_name_hash_records_storage(
1138 storage: &dyn Storage,
1139 header: &BTreeV2Header,
1140 offset_size: u8,
1141 length_size: u8,
1142 target_hash: u32,
1143) -> Result<Vec<BTreeV2Record>> {
1144 collect_btree_v2_records_storage_with_link_hash(
1145 storage,
1146 header,
1147 offset_size,
1148 length_size,
1149 None,
1150 &[],
1151 None,
1152 Some(target_hash),
1153 )
1154}
1155
1156#[allow(clippy::too_many_arguments)]
1157fn parse_leaf_node_storage(
1158 storage: &dyn Storage,
1159 address: u64,
1160 header: &BTreeV2Header,
1161 offset_size: u8,
1162 length_size: u8,
1163 ndims: Option<u32>,
1164 chunk_dims: &[u32],
1165 chunk_bounds: Option<(&[u64], &[u64])>,
1166 link_name_hash: Option<u32>,
1167 num_records: u16,
1168 heap_id_len: usize,
1169 records: &mut Vec<BTreeV2Record>,
1170) -> Result<()> {
1171 let node_len = usize::try_from(header.node_size).map_err(|_| {
1172 Error::InvalidData("B-tree v2 node size exceeds platform usize capacity".into())
1173 })?;
1174 let node_bytes = storage.read_range(address, node_len)?;
1175 let mut cursor = Cursor::new(node_bytes.as_ref());
1176 parse_leaf_node(
1177 &mut cursor,
1178 header,
1179 offset_size,
1180 length_size,
1181 ndims,
1182 chunk_dims,
1183 chunk_bounds,
1184 link_name_hash,
1185 num_records,
1186 heap_id_len,
1187 records,
1188 )
1189}
1190
1191#[allow(clippy::too_many_arguments)]
1192fn parse_internal_node_storage(
1193 storage: &dyn Storage,
1194 address: u64,
1195 header: &BTreeV2Header,
1196 offset_size: u8,
1197 length_size: u8,
1198 ndims: Option<u32>,
1199 chunk_dims: &[u32],
1200 chunk_bounds: Option<(&[u64], &[u64])>,
1201 link_name_hash: Option<u32>,
1202 num_records: u16,
1203 depth: u16,
1204 heap_id_len: usize,
1205 visited: &mut HashSet<u64>,
1206 records: &mut Vec<BTreeV2Record>,
1207) -> Result<()> {
1208 if depth == 0 {
1209 return Err(Error::InvalidData(
1210 "B-tree v2 internal node traversal reached depth zero".into(),
1211 ));
1212 }
1213 enter_btree_v2_node(visited, address)?;
1214
1215 let node_len = usize::try_from(header.node_size).map_err(|_| {
1216 Error::InvalidData("B-tree v2 node size exceeds platform usize capacity".into())
1217 })?;
1218 let node_bytes = storage.read_range(address, node_len)?;
1219 let mut cursor = Cursor::new(node_bytes.as_ref());
1220 let start = cursor.position();
1221
1222 let sig = cursor.read_bytes(4)?;
1223 if sig != BTIN_SIGNATURE {
1224 return Err(Error::InvalidBTreeV2Signature {
1225 context: "internal node",
1226 });
1227 }
1228
1229 let version = cursor.read_u8()?;
1230 if version != 0 {
1231 return Err(Error::UnsupportedBTreeVersion(version));
1232 }
1233
1234 let node_type = cursor.read_u8()?;
1235 if node_type != header.btree_type {
1236 return Err(Error::InvalidData(format!(
1237 "B-tree v2 internal node type mismatch: header says {}, node says {}",
1238 header.btree_type, node_type
1239 )));
1240 }
1241
1242 let max_child_records = if depth == 1 {
1243 max_leaf_records(header.node_size, header.record_size)
1244 } else {
1245 let leaf_max = max_leaf_records(header.node_size, header.record_size);
1246 let mut prev_max = leaf_max;
1247 for _ in 1..depth {
1248 prev_max =
1249 max_internal_records(header.node_size, header.record_size, offset_size, prev_max);
1250 }
1251 prev_max
1252 };
1253 let nrec_bytes = num_records_size(max_child_records);
1254
1255 let mut node_records = Vec::with_capacity(num_records as usize);
1256 for _ in 0..num_records {
1257 let record = parse_record(
1258 &mut cursor,
1259 header.btree_type,
1260 header.record_size,
1261 offset_size,
1262 length_size,
1263 ndims,
1264 chunk_dims,
1265 heap_id_len,
1266 )?;
1267 node_records.push(record);
1268 }
1269
1270 let num_children = usize::from(num_records) + 1;
1271 let has_total_records = depth > 1;
1272 let total_nrec_bytes = if has_total_records {
1273 usize::from(length_size)
1274 } else {
1275 0
1276 };
1277
1278 let mut child_addresses = Vec::with_capacity(num_children);
1279 let mut child_nrecords = Vec::with_capacity(num_children);
1280 for _ in 0..num_children {
1281 child_addresses.push(cursor.read_offset(offset_size)?);
1282 child_nrecords.push(cursor.read_uvar(nrec_bytes)? as u16);
1283 if has_total_records {
1284 cursor.read_uvar(total_nrec_bytes)?;
1285 }
1286 }
1287
1288 let compact_end = cursor.position();
1289 verify_node_checksum(
1290 &mut cursor,
1291 start,
1292 header.node_size,
1293 compact_end,
1294 "internal node",
1295 )?;
1296
1297 let child_depth = depth - 1;
1298 for (i, record) in node_records.iter().enumerate() {
1299 let child_addr = child_addresses[i];
1300 if !Cursor::is_undefined_offset(child_addr, offset_size)
1301 && child_may_match_link_name_hash(&node_records, i, link_name_hash)
1302 {
1303 let child_nrec = child_nrecords[i];
1304 if child_depth == 0 {
1305 enter_btree_v2_node(visited, child_addr)?;
1306 parse_leaf_node_storage(
1307 storage,
1308 child_addr,
1309 header,
1310 offset_size,
1311 length_size,
1312 ndims,
1313 chunk_dims,
1314 chunk_bounds,
1315 link_name_hash,
1316 child_nrec,
1317 heap_id_len,
1318 records,
1319 )?;
1320 } else {
1321 parse_internal_node_storage(
1322 storage,
1323 child_addr,
1324 header,
1325 offset_size,
1326 length_size,
1327 ndims,
1328 chunk_dims,
1329 chunk_bounds,
1330 link_name_hash,
1331 child_nrec,
1332 child_depth,
1333 heap_id_len,
1334 visited,
1335 records,
1336 )?;
1337 }
1338 }
1339
1340 if record_matches_query(record, chunk_dims, chunk_bounds, link_name_hash) {
1341 records.push(record.clone());
1342 }
1343 }
1344
1345 let final_child_index = node_records.len();
1346 let final_child_addr = child_addresses[final_child_index];
1347 if !Cursor::is_undefined_offset(final_child_addr, offset_size)
1348 && child_may_match_link_name_hash(&node_records, final_child_index, link_name_hash)
1349 {
1350 let child_nrec = child_nrecords[final_child_index];
1351 if child_depth == 0 {
1352 enter_btree_v2_node(visited, final_child_addr)?;
1353 parse_leaf_node_storage(
1354 storage,
1355 final_child_addr,
1356 header,
1357 offset_size,
1358 length_size,
1359 ndims,
1360 chunk_dims,
1361 chunk_bounds,
1362 link_name_hash,
1363 child_nrec,
1364 heap_id_len,
1365 records,
1366 )?;
1367 } else {
1368 parse_internal_node_storage(
1369 storage,
1370 final_child_addr,
1371 header,
1372 offset_size,
1373 length_size,
1374 ndims,
1375 chunk_dims,
1376 chunk_bounds,
1377 link_name_hash,
1378 child_nrec,
1379 child_depth,
1380 heap_id_len,
1381 visited,
1382 records,
1383 )?;
1384 }
1385 }
1386
1387 Ok(())
1388}
1389
1390fn compute_heap_id_len(header: &BTreeV2Header) -> usize {
1396 let rs = header.record_size as usize;
1397 match header.btree_type {
1398 5 => rs.saturating_sub(4), 6 => rs.saturating_sub(8), 8 => rs.saturating_sub(4 + 1 + 4), 9 => rs.saturating_sub(1 + 4), _ => 0,
1403 }
1404}
1405
1406#[cfg(test)]
1407mod tests {
1408 use super::*;
1409
1410 #[allow(clippy::too_many_arguments)]
1412 fn build_header(
1413 btree_type: u8,
1414 node_size: u32,
1415 record_size: u16,
1416 depth: u16,
1417 root_node_address: u64,
1418 num_records_in_root: u16,
1419 total_records: u64,
1420 offset_size: u8,
1421 length_size: u8,
1422 ) -> Vec<u8> {
1423 let mut buf = Vec::new();
1424 buf.extend_from_slice(b"BTHD");
1425 buf.push(0); buf.push(btree_type);
1427 buf.extend_from_slice(&node_size.to_le_bytes());
1428 buf.extend_from_slice(&record_size.to_le_bytes());
1429 buf.extend_from_slice(&depth.to_le_bytes());
1430 buf.push(75); buf.push(40); match offset_size {
1433 4 => buf.extend_from_slice(&(root_node_address as u32).to_le_bytes()),
1434 8 => buf.extend_from_slice(&root_node_address.to_le_bytes()),
1435 _ => panic!("unsupported"),
1436 }
1437 buf.extend_from_slice(&num_records_in_root.to_le_bytes());
1438 match length_size {
1439 4 => buf.extend_from_slice(&(total_records as u32).to_le_bytes()),
1440 8 => buf.extend_from_slice(&total_records.to_le_bytes()),
1441 _ => panic!("unsupported"),
1442 }
1443 let checksum = jenkins_lookup3(&buf);
1445 buf.extend_from_slice(&checksum.to_le_bytes());
1446 buf
1447 }
1448
1449 #[test]
1450 fn parse_header() {
1451 let data = build_header(5, 4096, 12, 0, 0x1000, 3, 3, 8, 8);
1452 let mut cursor = Cursor::new(&data);
1453 let hdr = BTreeV2Header::parse(&mut cursor, 8, 8).unwrap();
1454
1455 assert_eq!(hdr.btree_type, 5);
1456 assert_eq!(hdr.node_size, 4096);
1457 assert_eq!(hdr.record_size, 12);
1458 assert_eq!(hdr.depth, 0);
1459 assert_eq!(hdr.split_percent, 75);
1460 assert_eq!(hdr.merge_percent, 40);
1461 assert_eq!(hdr.root_node_address, 0x1000);
1462 assert_eq!(hdr.num_records_in_root, 3);
1463 assert_eq!(hdr.total_records, 3);
1464 }
1465
1466 #[test]
1467 fn bad_signature() {
1468 let mut data = build_header(5, 4096, 12, 0, 0x1000, 0, 0, 8, 8);
1469 data[0] = b'X';
1470 let mut cursor = Cursor::new(&data);
1471 assert!(matches!(
1472 BTreeV2Header::parse(&mut cursor, 8, 8),
1473 Err(Error::InvalidBTreeV2Signature { .. })
1474 ));
1475 }
1476
1477 #[test]
1478 fn bad_checksum() {
1479 let mut data = build_header(5, 4096, 12, 0, 0x1000, 0, 0, 8, 8);
1480 data[6] = 0xFF;
1482 let mut cursor = Cursor::new(&data);
1483 assert!(matches!(
1484 BTreeV2Header::parse(&mut cursor, 8, 8),
1485 Err(Error::ChecksumMismatch { .. })
1486 ));
1487 }
1488
1489 #[test]
1490 fn collect_empty_tree() {
1491 let header = BTreeV2Header {
1492 btree_type: 5,
1493 node_size: 4096,
1494 record_size: 12,
1495 depth: 0,
1496 split_percent: 75,
1497 merge_percent: 40,
1498 root_node_address: u64::MAX,
1499 num_records_in_root: 0,
1500 total_records: 0,
1501 };
1502 let data = vec![0u8; 100];
1503 let records = collect_btree_v2_records(&data, &header, 8, 8, None, &[], None).unwrap();
1504 assert!(records.is_empty());
1505 }
1506
1507 #[test]
1508 fn heap_id_len_uses_record_type_sizes() {
1509 let h5 = BTreeV2Header {
1511 btree_type: 5,
1512 record_size: 12,
1513 node_size: 0,
1514 depth: 0,
1515 split_percent: 0,
1516 merge_percent: 0,
1517 root_node_address: 0,
1518 num_records_in_root: 0,
1519 total_records: 0,
1520 };
1521 assert_eq!(compute_heap_id_len(&h5), 8);
1522
1523 let h8 = BTreeV2Header {
1525 btree_type: 8,
1526 record_size: 17,
1527 ..h5
1528 };
1529 assert_eq!(compute_heap_id_len(&h8), 8);
1530
1531 let h9 = BTreeV2Header {
1533 btree_type: 9,
1534 record_size: 13,
1535 ..h5
1536 };
1537 assert_eq!(compute_heap_id_len(&h9), 8);
1538 }
1539
1540 #[test]
1541 fn parse_attribute_name_record_uses_on_disk_field_order() {
1542 let heap_id = [0x00, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00];
1543 let mut data = heap_id.to_vec();
1544 data.push(0x02);
1545 data.extend_from_slice(&6u32.to_le_bytes());
1546 data.extend_from_slice(&0x0396_dda5u32.to_le_bytes());
1547 let mut cursor = Cursor::new(&data);
1548
1549 let record = parse_record(&mut cursor, 8, data.len() as u16, 8, 8, None, &[], 8).unwrap();
1550 match record {
1551 BTreeV2Record::AttributeNameHash {
1552 hash,
1553 flags,
1554 creation_order,
1555 heap_id: parsed_heap_id,
1556 } => {
1557 assert_eq!(parsed_heap_id, heap_id);
1558 assert_eq!(flags, 0x02);
1559 assert_eq!(creation_order, 6);
1560 assert_eq!(hash, 0x0396_dda5);
1561 }
1562 other => panic!("expected attribute-name record, got {other:?}"),
1563 }
1564 }
1565
1566 #[test]
1567 fn parse_attribute_creation_order_record_uses_on_disk_field_order() {
1568 let heap_id = [0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00];
1569 let mut data = heap_id.to_vec();
1570 data.push(0x02);
1571 data.extend_from_slice(&7u32.to_le_bytes());
1572 let mut cursor = Cursor::new(&data);
1573
1574 let record = parse_record(&mut cursor, 9, data.len() as u16, 8, 8, None, &[], 8).unwrap();
1575 match record {
1576 BTreeV2Record::AttributeCreationOrder {
1577 order,
1578 heap_id: parsed_heap_id,
1579 } => {
1580 assert_eq!(parsed_heap_id, heap_id);
1581 assert_eq!(order, 7);
1582 }
1583 other => panic!("expected attribute-creation-order record, got {other:?}"),
1584 }
1585 }
1586
1587 #[test]
1588 fn max_leaf_records_accounts_for_node_overhead() {
1589 assert_eq!(max_leaf_records(4096, 12), 340);
1592 }
1593
1594 #[test]
1595 fn record_count_size_scales_at_integer_boundaries() {
1596 assert_eq!(num_records_size(0), 1);
1597 assert_eq!(num_records_size(255), 1);
1598 assert_eq!(num_records_size(256), 2);
1599 assert_eq!(num_records_size(65535), 2);
1600 assert_eq!(num_records_size(65536), 4);
1601 }
1602
1603 #[test]
1604 fn parse_huge_indirect_record() {
1605 let mut data = Vec::new();
1606 data.extend_from_slice(&0x1234u64.to_le_bytes());
1607 data.extend_from_slice(&99u64.to_le_bytes());
1608 data.extend_from_slice(&7u64.to_le_bytes());
1609 let mut cursor = Cursor::new(&data);
1610
1611 let record = parse_record(&mut cursor, 1, data.len() as u16, 8, 8, None, &[], 0).unwrap();
1612 match record {
1613 BTreeV2Record::HugeIndirectNonFiltered {
1614 address,
1615 length,
1616 object_id,
1617 } => {
1618 assert_eq!(address, 0x1234);
1619 assert_eq!(length, 99);
1620 assert_eq!(object_id, 7);
1621 }
1622 other => panic!("expected huge record, got {:?}", other),
1623 }
1624 }
1625
1626 #[test]
1627 fn parse_shared_heap_record() {
1628 let mut data = Vec::new();
1629 data.push(0);
1630 data.extend_from_slice(&[0, 0, 0]);
1631 data.extend_from_slice(&0xAABB_CCDDu32.to_le_bytes());
1632 data.extend_from_slice(&3u32.to_le_bytes());
1633 data.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
1634 let mut cursor = Cursor::new(&data);
1635
1636 let record = parse_record(&mut cursor, 7, data.len() as u16, 8, 8, None, &[], 0).unwrap();
1637 match record {
1638 BTreeV2Record::SharedMessageHeap {
1639 hash,
1640 reference_count,
1641 heap_id,
1642 } => {
1643 assert_eq!(hash, 0xAABB_CCDD);
1644 assert_eq!(reference_count, 3);
1645 assert_eq!(heap_id, vec![1, 2, 3, 4, 5, 6, 7, 8]);
1646 }
1647 other => panic!("expected shared heap record, got {:?}", other),
1648 }
1649 }
1650
1651 #[test]
1652 fn parse_record_rejects_known_record_that_exceeds_record_size() {
1653 let mut data = Vec::new();
1654 data.extend_from_slice(&0x1234u64.to_le_bytes());
1655 data.extend_from_slice(&99u64.to_le_bytes());
1656 data.extend_from_slice(&7u64.to_le_bytes());
1657 let mut cursor = Cursor::new(&data);
1658
1659 let err = parse_record(&mut cursor, 1, 16, 8, 8, None, &[], 0).unwrap_err();
1660 assert!(
1661 matches!(err, Error::InvalidData(message) if message.contains("consumed 24 bytes but record size is 16"))
1662 );
1663 }
1664
1665 #[test]
1666 fn parse_chunk_record_rejects_size_shorter_than_address() {
1667 let mut data = Vec::new();
1668 data.extend_from_slice(&0x1234u64.to_le_bytes());
1669 let mut cursor = Cursor::new(&data);
1670
1671 let err = parse_record(&mut cursor, 10, 4, 8, 8, None, &[], 0).unwrap_err();
1672 assert!(
1673 matches!(err, Error::InvalidData(message) if message.contains("shorter than its address"))
1674 );
1675 assert_eq!(cursor.position(), 0);
1676 }
1677
1678 #[test]
1679 fn parse_filtered_chunk_record_rejects_size_shorter_than_fixed_fields() {
1680 let mut data = Vec::new();
1681 data.extend_from_slice(&0x1234u64.to_le_bytes());
1682 data.extend_from_slice(&99u64.to_le_bytes());
1683 data.extend_from_slice(&0xAu32.to_le_bytes());
1684 let mut cursor = Cursor::new(&data);
1685
1686 let err = parse_record(&mut cursor, 11, 16, 8, 8, None, &[], 0).unwrap_err();
1687 assert!(
1688 matches!(err, Error::InvalidData(message) if message.contains("shorter than its fixed fields"))
1689 );
1690 assert_eq!(cursor.position(), 0);
1691 }
1692
1693 #[test]
1694 fn parse_chunk_record_scales_offsets_by_chunk_dimensions() {
1695 let mut data = Vec::new();
1696 data.extend_from_slice(&0x1234u64.to_le_bytes());
1697 data.extend_from_slice(&2u64.to_le_bytes());
1698 data.extend_from_slice(&1u64.to_le_bytes());
1699 let mut cursor = Cursor::new(&data);
1700
1701 let record = parse_record(
1702 &mut cursor,
1703 10,
1704 data.len() as u16,
1705 8,
1706 8,
1707 Some(2),
1708 &[5, 7],
1709 0,
1710 )
1711 .unwrap();
1712 match record {
1713 BTreeV2Record::ChunkedNonFiltered { address, offsets } => {
1714 assert_eq!(address, 0x1234);
1715 assert_eq!(offsets, vec![10, 7]);
1716 }
1717 other => panic!("expected non-filtered chunk record, got {:?}", other),
1718 }
1719 }
1720
1721 #[test]
1722 fn parse_filtered_chunk_record_scales_offsets_by_chunk_dimensions() {
1723 let mut data = Vec::new();
1724 data.extend_from_slice(&0x1234u64.to_le_bytes());
1725 data.extend_from_slice(&99u64.to_le_bytes());
1726 data.extend_from_slice(&0xAu32.to_le_bytes());
1727 data.extend_from_slice(&3u64.to_le_bytes());
1728 data.extend_from_slice(&4u64.to_le_bytes());
1729 let mut cursor = Cursor::new(&data);
1730
1731 let record = parse_record(
1732 &mut cursor,
1733 11,
1734 data.len() as u16,
1735 8,
1736 8,
1737 Some(2),
1738 &[5, 7],
1739 0,
1740 )
1741 .unwrap();
1742 match record {
1743 BTreeV2Record::ChunkedFiltered {
1744 address,
1745 chunk_size,
1746 filter_mask,
1747 offsets,
1748 } => {
1749 assert_eq!(address, 0x1234);
1750 assert_eq!(chunk_size, 99);
1751 assert_eq!(filter_mask, 0xA);
1752 assert_eq!(offsets, vec![15, 28]);
1753 }
1754 other => panic!("expected filtered chunk record, got {:?}", other),
1755 }
1756 }
1757
1758 #[test]
1759 fn parse_leaf_with_type5_records() {
1760 let record_size: u16 = 12;
1763 let node_size: u32 = 4096;
1764
1765 let header = BTreeV2Header {
1766 btree_type: 5,
1767 node_size,
1768 record_size,
1769 depth: 0,
1770 split_percent: 75,
1771 merge_percent: 40,
1772 root_node_address: 0, num_records_in_root: 2,
1774 total_records: 2,
1775 };
1776
1777 let mut leaf = Vec::new();
1779 leaf.extend_from_slice(b"BTLF"); leaf.push(0); leaf.push(5); leaf.extend_from_slice(&0xAABBCCDDu32.to_le_bytes());
1785 leaf.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
1786
1787 leaf.extend_from_slice(&0x11223344u32.to_le_bytes());
1789 leaf.extend_from_slice(&[9, 10, 11, 12, 13, 14, 15, 16]);
1790
1791 let checksum = jenkins_lookup3(&leaf);
1794 leaf.extend_from_slice(&checksum.to_le_bytes());
1795 leaf.resize(node_size as usize, 0);
1796
1797 let mut records = Vec::new();
1798 let mut cursor = Cursor::new(&leaf);
1799 parse_leaf_node(
1800 &mut cursor,
1801 &header,
1802 8,
1803 8,
1804 None,
1805 &[],
1806 None,
1807 None,
1808 2,
1809 8, &mut records,
1811 )
1812 .unwrap();
1813
1814 assert_eq!(records.len(), 2);
1815 match &records[0] {
1816 BTreeV2Record::LinkNameHash { hash, heap_id } => {
1817 assert_eq!(*hash, 0xAABBCCDD);
1818 assert_eq!(heap_id, &[1, 2, 3, 4, 5, 6, 7, 8]);
1819 }
1820 _ => panic!("expected LinkNameHash"),
1821 }
1822 match &records[1] {
1823 BTreeV2Record::LinkNameHash { hash, heap_id } => {
1824 assert_eq!(*hash, 0x11223344);
1825 assert_eq!(heap_id, &[9, 10, 11, 12, 13, 14, 15, 16]);
1826 }
1827 _ => panic!("expected LinkNameHash"),
1828 }
1829 }
1830
1831 fn build_type5_record(hash: u32, heap_id: [u8; 8]) -> Vec<u8> {
1832 let mut record = Vec::new();
1833 record.extend_from_slice(&hash.to_le_bytes());
1834 record.extend_from_slice(&heap_id);
1835 record
1836 }
1837
1838 fn build_type5_leaf(records: &[(u32, [u8; 8])], node_size: usize) -> Vec<u8> {
1839 let mut leaf = Vec::new();
1840 leaf.extend_from_slice(b"BTLF");
1841 leaf.push(0);
1842 leaf.push(5);
1843 for &(hash, heap_id) in records {
1844 leaf.extend_from_slice(&build_type5_record(hash, heap_id));
1845 }
1846 let checksum = jenkins_lookup3(&leaf);
1847 leaf.extend_from_slice(&checksum.to_le_bytes());
1848 leaf.resize(node_size, 0);
1849 leaf
1850 }
1851
1852 fn build_type5_internal(
1853 records: &[(u32, [u8; 8])],
1854 children: &[(u64, u8)],
1855 node_size: usize,
1856 ) -> Vec<u8> {
1857 assert_eq!(children.len(), records.len() + 1);
1858
1859 let mut node = Vec::new();
1860 node.extend_from_slice(b"BTIN");
1861 node.push(0);
1862 node.push(5);
1863 for &(hash, heap_id) in records {
1864 node.extend_from_slice(&build_type5_record(hash, heap_id));
1865 }
1866 for &(address, child_records) in children {
1867 node.extend_from_slice(&address.to_le_bytes());
1868 node.push(child_records);
1869 }
1870 let checksum = jenkins_lookup3(&node);
1871 node.extend_from_slice(&checksum.to_le_bytes());
1872 node.resize(node_size, 0);
1873 node
1874 }
1875
1876 fn link_hashes(records: &[BTreeV2Record]) -> Vec<u32> {
1877 records
1878 .iter()
1879 .map(|record| match record {
1880 BTreeV2Record::LinkNameHash { hash, .. } => *hash,
1881 other => panic!("expected LinkNameHash, got {other:?}"),
1882 })
1883 .collect()
1884 }
1885
1886 #[test]
1887 fn collect_depth_one_tree_includes_internal_records_in_order() {
1888 let node_size = 128usize;
1892 let left_addr = node_size as u64;
1893 let right_addr = (node_size * 2) as u64;
1894 let header = BTreeV2Header {
1895 btree_type: 5,
1896 node_size: node_size as u32,
1897 record_size: 12,
1898 depth: 1,
1899 split_percent: 75,
1900 merge_percent: 40,
1901 root_node_address: 0,
1902 num_records_in_root: 1,
1903 total_records: 3,
1904 };
1905
1906 let root = build_type5_internal(
1907 &[(20, [20; 8])],
1908 &[(left_addr, 1), (right_addr, 1)],
1909 node_size,
1910 );
1911 let left_leaf = build_type5_leaf(&[(10, [10; 8])], node_size);
1912 let right_leaf = build_type5_leaf(&[(30, [30; 8])], node_size);
1913
1914 let mut data = vec![0; node_size * 3];
1915 data[0..node_size].copy_from_slice(&root);
1916 data[node_size..node_size * 2].copy_from_slice(&left_leaf);
1917 data[node_size * 2..node_size * 3].copy_from_slice(&right_leaf);
1918
1919 let records = collect_btree_v2_records(&data, &header, 8, 8, None, &[], None).unwrap();
1920 assert_eq!(link_hashes(&records), vec![10, 20, 30]);
1921
1922 let storage = crate::storage::BytesStorage::new(data);
1923 let records =
1924 collect_btree_v2_records_storage(&storage, &header, 8, 8, None, &[], None).unwrap();
1925 assert_eq!(link_hashes(&records), vec![10, 20, 30]);
1926 }
1927
1928 #[test]
1929 fn collect_rejects_depth_above_traversal_limit() {
1930 let header = BTreeV2Header {
1931 btree_type: 5,
1932 node_size: 128,
1933 record_size: 12,
1934 depth: MAX_BTREE_V2_DEPTH + 1,
1935 split_percent: 75,
1936 merge_percent: 40,
1937 root_node_address: 0,
1938 num_records_in_root: 1,
1939 total_records: 1,
1940 };
1941 let data = vec![0; 128];
1942
1943 let err = collect_btree_v2_records(&data, &header, 8, 8, None, &[], None).unwrap_err();
1944 assert!(
1945 matches!(err, Error::InvalidData(message) if message.contains("exceeds traversal limit"))
1946 );
1947 }
1948
1949 #[test]
1950 fn collect_rejects_revisited_leaf_node() {
1951 let node_size = 128usize;
1952 let leaf_addr = node_size as u64;
1953 let header = BTreeV2Header {
1954 btree_type: 5,
1955 node_size: node_size as u32,
1956 record_size: 12,
1957 depth: 1,
1958 split_percent: 75,
1959 merge_percent: 40,
1960 root_node_address: 0,
1961 num_records_in_root: 1,
1962 total_records: 3,
1963 };
1964
1965 let root = build_type5_internal(
1966 &[(20, [20; 8])],
1967 &[(leaf_addr, 1), (leaf_addr, 1)],
1968 node_size,
1969 );
1970 let leaf = build_type5_leaf(&[(10, [10; 8])], node_size);
1971
1972 let mut data = vec![0; node_size * 2];
1973 data[0..node_size].copy_from_slice(&root);
1974 data[node_size..node_size * 2].copy_from_slice(&leaf);
1975
1976 let err = collect_btree_v2_records(&data, &header, 8, 8, None, &[], None).unwrap_err();
1977 assert!(matches!(err, Error::InvalidData(message) if message.contains("revisits node")));
1978
1979 let storage = crate::storage::BytesStorage::new(data);
1980 let err =
1981 collect_btree_v2_records_storage(&storage, &header, 8, 8, None, &[], None).unwrap_err();
1982 assert!(matches!(err, Error::InvalidData(message) if message.contains("revisits node")));
1983 }
1984
1985 #[test]
1986 fn collect_link_name_hash_records_filters_leaf_records() {
1987 let record_size: u16 = 12;
1988 let node_size: u32 = 4096;
1989 let header = BTreeV2Header {
1990 btree_type: 5,
1991 node_size,
1992 record_size,
1993 depth: 0,
1994 split_percent: 75,
1995 merge_percent: 40,
1996 root_node_address: 0,
1997 num_records_in_root: 2,
1998 total_records: 2,
1999 };
2000
2001 let mut leaf = Vec::new();
2002 leaf.extend_from_slice(b"BTLF");
2003 leaf.push(0);
2004 leaf.push(5);
2005 leaf.extend_from_slice(&0x1111_1111u32.to_le_bytes());
2006 leaf.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
2007 leaf.extend_from_slice(&0x2222_2222u32.to_le_bytes());
2008 leaf.extend_from_slice(&[9, 10, 11, 12, 13, 14, 15, 16]);
2009 let checksum = jenkins_lookup3(&leaf);
2010 leaf.extend_from_slice(&checksum.to_le_bytes());
2011 leaf.resize(node_size as usize, 0);
2012
2013 let records =
2014 collect_btree_v2_link_name_hash_records(&leaf, &header, 8, 8, 0x2222_2222).unwrap();
2015 assert_eq!(records.len(), 1);
2016 match &records[0] {
2017 BTreeV2Record::LinkNameHash { hash, heap_id } => {
2018 assert_eq!(*hash, 0x2222_2222);
2019 assert_eq!(heap_id, &[9, 10, 11, 12, 13, 14, 15, 16]);
2020 }
2021 other => panic!("expected LinkNameHash, got {other:?}"),
2022 }
2023 }
2024
2025 #[test]
2026 fn child_link_hash_filter_prunes_disjoint_hash_ranges() {
2027 let records = vec![
2028 BTreeV2Record::LinkNameHash {
2029 hash: 10,
2030 heap_id: vec![],
2031 },
2032 BTreeV2Record::LinkNameHash {
2033 hash: 20,
2034 heap_id: vec![],
2035 },
2036 ];
2037
2038 assert!(child_may_match_link_name_hash(&records, 0, Some(10)));
2039 assert!(!child_may_match_link_name_hash(&records, 0, Some(15)));
2040 assert!(child_may_match_link_name_hash(&records, 1, Some(15)));
2041 assert!(!child_may_match_link_name_hash(&records, 2, Some(15)));
2042 assert!(child_may_match_link_name_hash(&records, 2, Some(20)));
2043 }
2044}