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 => {
356 let ndims_val = ndims
357 .map(|n| n as usize)
358 .or_else(|| (!chunk_dims.is_empty()).then_some(chunk_dims.len()))
359 .ok_or_else(|| {
360 Error::InvalidData(
361 "B-tree v2 type-11 chunk record needs a known dimensionality".to_string(),
362 )
363 })?;
364 let offsets_bytes = ndims_val
365 .checked_mul(8)
366 .ok_or_else(|| Error::InvalidData("chunk offset bytes overflow".to_string()))?;
367 let fixed_size = offset_size as usize + 4 + offsets_bytes;
368 let chunk_size_len =
369 (record_size as usize)
370 .checked_sub(fixed_size)
371 .ok_or_else(|| {
372 Error::InvalidData(
373 "B-tree v2 type-11 chunk record is shorter than its fixed fields"
374 .to_string(),
375 )
376 })?;
377 if chunk_size_len == 0 || chunk_size_len > 8 {
378 return Err(Error::InvalidData(format!(
379 "B-tree v2 type-11 chunk-size field width {chunk_size_len} is out of range"
380 )));
381 }
382 let address = cursor.read_offset(offset_size)?;
383 let chunk_size = cursor.read_length(chunk_size_len as u8)?;
384 let filter_mask = cursor.read_u32_le()?;
385 let offsets = read_scaled_chunk_offsets(cursor, ndims_val, ndims, chunk_dims)?;
386 BTreeV2Record::ChunkedFiltered {
387 address,
388 chunk_size,
389 filter_mask,
390 offsets,
391 }
392 }
393
394 _ => {
396 let data = cursor.read_bytes(record_size as usize)?.to_vec();
397 return Ok(BTreeV2Record::Unknown {
398 record_type: btree_type,
399 data,
400 });
401 }
402 };
403
404 let consumed = cursor.position() - record_start;
406 let record_size = u64::from(record_size);
407 if consumed > record_size {
408 return Err(Error::InvalidData(format!(
409 "B-tree v2 type-{btree_type} record consumed {consumed} bytes but record size is {record_size}"
410 )));
411 }
412 if consumed < record_size {
413 cursor.skip((record_size - consumed) as usize)?;
414 }
415
416 Ok(record)
417}
418
419fn record_payload_len(record_size: u16, fixed_size: usize, error_message: &str) -> Result<usize> {
420 (record_size as usize)
421 .checked_sub(fixed_size)
422 .ok_or_else(|| Error::InvalidData(error_message.into()))
423}
424
425fn read_scaled_chunk_offsets(
426 cursor: &mut Cursor,
427 num_offsets: usize,
428 ndims: Option<u32>,
429 chunk_dims: &[u32],
430) -> Result<Vec<u64>> {
431 if let Some(ndims) = ndims {
432 let ndims = usize::try_from(ndims)
433 .map_err(|_| Error::InvalidData("B-tree v2 chunk rank exceeds usize".into()))?;
434 if num_offsets != ndims {
435 return Err(Error::InvalidData(format!(
436 "B-tree v2 chunk record has {num_offsets} offsets for {ndims} dimensions"
437 )));
438 }
439 }
440
441 if !chunk_dims.is_empty() && num_offsets != chunk_dims.len() {
442 return Err(Error::InvalidData(format!(
443 "B-tree v2 chunk record has {num_offsets} offsets but {} chunk dimensions",
444 chunk_dims.len()
445 )));
446 }
447
448 let mut offsets = Vec::with_capacity(num_offsets);
449 for dim in 0..num_offsets {
450 let scaled = cursor.read_u64_le()?;
451 let chunk_extent = chunk_dims.get(dim).copied().unwrap_or(1);
452 let offset = scaled
453 .checked_mul(u64::from(chunk_extent))
454 .ok_or_else(|| Error::InvalidData("B-tree v2 chunk offset overflows u64".into()))?;
455 offsets.push(offset);
456 }
457 Ok(offsets)
458}
459
460fn record_matches_chunk_bounds(
461 record: &BTreeV2Record,
462 chunk_dims: &[u32],
463 chunk_bounds: Option<(&[u64], &[u64])>,
464) -> bool {
465 let Some((first_chunk, last_chunk)) = chunk_bounds else {
466 return true;
467 };
468
469 let offsets = match record {
470 BTreeV2Record::ChunkedNonFiltered { offsets, .. }
471 | BTreeV2Record::ChunkedFiltered { offsets, .. } => offsets,
472 _ => return true,
473 };
474
475 offsets.iter().enumerate().all(|(dim, offset)| {
476 let chunk_index = *offset / u64::from(chunk_dims[dim]);
477 chunk_index >= first_chunk[dim] && chunk_index <= last_chunk[dim]
478 })
479}
480
481fn record_matches_link_name_hash(record: &BTreeV2Record, target_hash: Option<u32>) -> bool {
482 match target_hash {
483 Some(target_hash) => {
484 matches!(record, BTreeV2Record::LinkNameHash { hash, .. } if *hash == target_hash)
485 }
486 None => true,
487 }
488}
489
490fn record_matches_query(
491 record: &BTreeV2Record,
492 chunk_dims: &[u32],
493 chunk_bounds: Option<(&[u64], &[u64])>,
494 link_name_hash: Option<u32>,
495) -> bool {
496 record_matches_link_name_hash(record, link_name_hash)
497 && record_matches_chunk_bounds(record, chunk_dims, chunk_bounds)
498}
499
500fn link_name_hash(record: &BTreeV2Record) -> Option<u32> {
501 match record {
502 BTreeV2Record::LinkNameHash { hash, .. } => Some(*hash),
503 _ => None,
504 }
505}
506
507fn child_may_match_link_name_hash(
508 records: &[BTreeV2Record],
509 child_index: usize,
510 target_hash: Option<u32>,
511) -> bool {
512 let Some(target_hash) = target_hash else {
513 return true;
514 };
515
516 let lower_matches = child_index == 0
517 || link_name_hash(&records[child_index - 1]).map_or(true, |hash| target_hash >= hash);
518 let upper_matches = child_index == records.len()
519 || link_name_hash(&records[child_index]).map_or(true, |hash| target_hash <= hash);
520 lower_matches && upper_matches
521}
522
523fn validate_btree_v2_depth(depth: u16) -> Result<()> {
524 if depth > MAX_BTREE_V2_DEPTH {
525 return Err(Error::InvalidData(format!(
526 "B-tree v2 depth {depth} exceeds traversal limit {MAX_BTREE_V2_DEPTH}"
527 )));
528 }
529 Ok(())
530}
531
532fn enter_btree_v2_node(visited: &mut HashSet<u64>, address: u64) -> Result<()> {
533 if !visited.insert(address) {
534 return Err(Error::InvalidData(format!(
535 "B-tree v2 traversal revisits node at offset {address:#x}"
536 )));
537 }
538 Ok(())
539}
540
541fn num_records_size(max_records: u64) -> usize {
548 if max_records <= 0xFF {
549 1
550 } else if max_records <= 0xFFFF {
551 2
552 } else if max_records <= 0xFFFF_FFFF {
553 4
554 } else {
555 8
556 }
557}
558
559fn max_leaf_records(node_size: u32, record_size: u16) -> u64 {
561 let overhead = 10u32;
563 if node_size <= overhead || record_size == 0 {
564 return 0;
565 }
566 ((node_size - overhead) / record_size as u32) as u64
567}
568
569fn max_internal_records(
574 node_size: u32,
575 record_size: u16,
576 offset_size: u8,
577 max_child_records: u64,
578) -> u64 {
579 let overhead = 10u32;
581 if node_size <= overhead || record_size == 0 {
582 return 0;
583 }
584 let available = (node_size - overhead) as u64;
585 let nrec_size = num_records_size(max_child_records) as u64;
587 let entry_size = record_size as u64 + offset_size as u64 + nrec_size;
588 let extra = offset_size as u64 + nrec_size;
592 if available <= extra {
593 return 0;
594 }
595 (available - extra) / entry_size
596}
597
598fn node_checksum_bounds(
599 cursor: &Cursor<'_>,
600 start: u64,
601 node_size: u32,
602 compact_end: u64,
603 context: &'static str,
604) -> Result<usize> {
605 let start = usize::try_from(start)
606 .map_err(|_| Error::InvalidData(format!("B-tree v2 {context} offset is too large")))?;
607 let node_size = usize::try_from(node_size)
608 .map_err(|_| Error::InvalidData(format!("B-tree v2 {context} size is too large")))?;
609 if node_size < 10 {
610 return Err(Error::InvalidData(format!(
611 "B-tree v2 {context} node is too small: {node_size} bytes"
612 )));
613 }
614
615 let checksum_pos = usize::try_from(compact_end)
616 .map_err(|_| Error::InvalidData(format!("B-tree v2 {context} checksum is too large")))?;
617 if checksum_pos < start {
618 return Err(Error::InvalidData(format!(
619 "B-tree v2 {context} payload starts before node"
620 )));
621 }
622 let node_end = start
623 .checked_add(node_size)
624 .ok_or_else(|| Error::InvalidData(format!("B-tree v2 {context} size overflow")))?;
625
626 if node_end > cursor.data().len() {
627 return Err(Error::UnexpectedEof {
628 offset: start as u64,
629 needed: node_size as u64,
630 available: cursor.data().len().saturating_sub(start) as u64,
631 });
632 }
633 let checksum_end = checksum_pos
634 .checked_add(4)
635 .ok_or_else(|| Error::InvalidData(format!("B-tree v2 {context} checksum overflow")))?;
636 if checksum_end > node_end {
637 return Err(Error::InvalidData(format!(
638 "B-tree v2 {context} contents exceed node size"
639 )));
640 }
641
642 Ok(checksum_pos)
643}
644
645fn verify_node_checksum(
646 cursor: &mut Cursor<'_>,
647 start: u64,
648 node_size: u32,
649 compact_end: u64,
650 context: &'static str,
651) -> Result<()> {
652 let checksum_pos = node_checksum_bounds(cursor, start, node_size, compact_end, context)?;
653 cursor.set_position(checksum_pos as u64);
654 let stored_checksum = cursor.read_u32_le()?;
655 let computed = jenkins_lookup3(&cursor.data()[start as usize..checksum_pos]);
656 if computed != stored_checksum {
657 return Err(Error::ChecksumMismatch {
658 expected: stored_checksum,
659 actual: computed,
660 });
661 }
662 Ok(())
663}
664
665#[allow(clippy::too_many_arguments)]
667fn parse_leaf_node(
668 cursor: &mut Cursor,
669 header: &BTreeV2Header,
670 offset_size: u8,
671 length_size: u8,
672 ndims: Option<u32>,
673 chunk_dims: &[u32],
674 chunk_bounds: Option<(&[u64], &[u64])>,
675 link_name_hash: Option<u32>,
676 num_records: u16,
677 heap_id_len: usize,
678 records: &mut Vec<BTreeV2Record>,
679) -> Result<()> {
680 let start = cursor.position();
681
682 let sig = cursor.read_bytes(4)?;
683 if sig != BTLF_SIGNATURE {
684 return Err(Error::InvalidBTreeV2Signature {
685 context: "leaf node",
686 });
687 }
688
689 let version = cursor.read_u8()?;
690 if version != 0 {
691 return Err(Error::UnsupportedBTreeVersion(version));
692 }
693
694 let node_type = cursor.read_u8()?;
695 if node_type != header.btree_type {
696 return Err(Error::InvalidData(format!(
697 "B-tree v2 leaf node type mismatch: header says {}, node says {}",
698 header.btree_type, node_type
699 )));
700 }
701
702 for _ in 0..num_records {
703 let record = parse_record(
704 cursor,
705 header.btree_type,
706 header.record_size,
707 offset_size,
708 length_size,
709 ndims,
710 chunk_dims,
711 heap_id_len,
712 )?;
713 if record_matches_query(&record, chunk_dims, chunk_bounds, link_name_hash) {
714 records.push(record);
715 }
716 }
717
718 verify_node_checksum(
719 cursor,
720 start,
721 header.node_size,
722 cursor.position(),
723 "leaf node",
724 )?;
725
726 Ok(())
727}
728
729#[allow(clippy::too_many_arguments)]
731fn parse_internal_node(
732 data: &[u8],
733 address: u64,
734 header: &BTreeV2Header,
735 offset_size: u8,
736 length_size: u8,
737 ndims: Option<u32>,
738 chunk_dims: &[u32],
739 chunk_bounds: Option<(&[u64], &[u64])>,
740 link_name_hash: Option<u32>,
741 num_records: u16,
742 depth: u16,
743 heap_id_len: usize,
744 visited: &mut HashSet<u64>,
745 records: &mut Vec<BTreeV2Record>,
746) -> Result<()> {
747 if depth == 0 {
748 return Err(Error::InvalidData(
749 "B-tree v2 internal node traversal reached depth zero".into(),
750 ));
751 }
752 enter_btree_v2_node(visited, address)?;
753
754 let mut cursor = Cursor::new(data);
755 cursor.set_position(address);
756
757 let start = cursor.position();
758
759 let sig = cursor.read_bytes(4)?;
760 if sig != BTIN_SIGNATURE {
761 return Err(Error::InvalidBTreeV2Signature {
762 context: "internal node",
763 });
764 }
765
766 let version = cursor.read_u8()?;
767 if version != 0 {
768 return Err(Error::UnsupportedBTreeVersion(version));
769 }
770
771 let node_type = cursor.read_u8()?;
772 if node_type != header.btree_type {
773 return Err(Error::InvalidData(format!(
774 "B-tree v2 internal node type mismatch: header says {}, node says {}",
775 header.btree_type, node_type
776 )));
777 }
778
779 let max_child_records = if depth == 1 {
782 max_leaf_records(header.node_size, header.record_size)
783 } else {
784 let leaf_max = max_leaf_records(header.node_size, header.record_size);
786 let mut prev_max = leaf_max;
787 for _ in 1..depth {
788 prev_max =
789 max_internal_records(header.node_size, header.record_size, offset_size, prev_max);
790 }
791 prev_max
792 };
793 let nrec_bytes = num_records_size(max_child_records);
794
795 let mut node_records = Vec::with_capacity(num_records as usize);
807 for _ in 0..num_records {
808 let record = parse_record(
809 &mut cursor,
810 header.btree_type,
811 header.record_size,
812 offset_size,
813 length_size,
814 ndims,
815 chunk_dims,
816 heap_id_len,
817 )?;
818 node_records.push(record);
819 }
820
821 let num_children = num_records as usize + 1;
823
824 let has_total_records = depth > 1;
827 let total_nrec_bytes = if has_total_records {
829 length_size as usize
832 } else {
833 0
834 };
835
836 let mut child_addresses = Vec::with_capacity(num_children);
837 let mut child_nrecords = Vec::with_capacity(num_children);
838
839 for _ in 0..num_children {
840 let child_addr = cursor.read_offset(offset_size)?;
841 child_addresses.push(child_addr);
842 let nrec = cursor.read_uvar(nrec_bytes)?;
843 child_nrecords.push(nrec as u16);
844 if has_total_records {
845 cursor.read_uvar(total_nrec_bytes)?;
847 }
848 }
849
850 let compact_end = cursor.position();
851 verify_node_checksum(
852 &mut cursor,
853 start,
854 header.node_size,
855 compact_end,
856 "internal node",
857 )?;
858
859 let child_depth = depth - 1;
862 for (i, record) in node_records.iter().enumerate() {
863 let child_addr = child_addresses[i];
864 if !Cursor::is_undefined_offset(child_addr, offset_size)
865 && child_may_match_link_name_hash(&node_records, i, link_name_hash)
866 {
867 let child_nrec = child_nrecords[i];
868 if child_depth == 0 {
869 enter_btree_v2_node(visited, child_addr)?;
870 let mut child_cursor = Cursor::new(data);
871 child_cursor.set_position(child_addr);
872 parse_leaf_node(
873 &mut child_cursor,
874 header,
875 offset_size,
876 length_size,
877 ndims,
878 chunk_dims,
879 chunk_bounds,
880 link_name_hash,
881 child_nrec,
882 heap_id_len,
883 records,
884 )?;
885 } else {
886 parse_internal_node(
887 data,
888 child_addr,
889 header,
890 offset_size,
891 length_size,
892 ndims,
893 chunk_dims,
894 chunk_bounds,
895 link_name_hash,
896 child_nrec,
897 child_depth,
898 heap_id_len,
899 visited,
900 records,
901 )?;
902 }
903 }
904
905 if record_matches_query(record, chunk_dims, chunk_bounds, link_name_hash) {
906 records.push(record.clone());
907 }
908 }
909
910 let final_child_index = node_records.len();
911 let final_child_addr = child_addresses[final_child_index];
912 if !Cursor::is_undefined_offset(final_child_addr, offset_size)
913 && child_may_match_link_name_hash(&node_records, final_child_index, link_name_hash)
914 {
915 let child_nrec = child_nrecords[final_child_index];
916 if child_depth == 0 {
917 enter_btree_v2_node(visited, final_child_addr)?;
918 let mut child_cursor = Cursor::new(data);
919 child_cursor.set_position(final_child_addr);
920 parse_leaf_node(
921 &mut child_cursor,
922 header,
923 offset_size,
924 length_size,
925 ndims,
926 chunk_dims,
927 chunk_bounds,
928 link_name_hash,
929 child_nrec,
930 heap_id_len,
931 records,
932 )?;
933 } else {
934 parse_internal_node(
935 data,
936 final_child_addr,
937 header,
938 offset_size,
939 length_size,
940 ndims,
941 chunk_dims,
942 chunk_bounds,
943 link_name_hash,
944 child_nrec,
945 child_depth,
946 heap_id_len,
947 visited,
948 records,
949 )?;
950 }
951 }
952
953 Ok(())
954}
955
956pub fn collect_btree_v2_records(
965 data: &[u8],
966 header: &BTreeV2Header,
967 offset_size: u8,
968 length_size: u8,
969 ndims: Option<u32>,
970 chunk_dims: &[u32],
971 chunk_bounds: Option<(&[u64], &[u64])>,
972) -> Result<Vec<BTreeV2Record>> {
973 collect_btree_v2_records_with_link_hash(
974 data,
975 header,
976 offset_size,
977 length_size,
978 ndims,
979 chunk_dims,
980 chunk_bounds,
981 None,
982 )
983}
984
985#[allow(clippy::too_many_arguments)]
986fn collect_btree_v2_records_with_link_hash(
987 data: &[u8],
988 header: &BTreeV2Header,
989 offset_size: u8,
990 length_size: u8,
991 ndims: Option<u32>,
992 chunk_dims: &[u32],
993 chunk_bounds: Option<(&[u64], &[u64])>,
994 link_name_hash: Option<u32>,
995) -> Result<Vec<BTreeV2Record>> {
996 if Cursor::is_undefined_offset(header.root_node_address, offset_size) {
997 return Ok(Vec::new());
998 }
999
1000 if header.total_records == 0 || header.num_records_in_root == 0 {
1001 return Ok(Vec::new());
1002 }
1003 validate_btree_v2_depth(header.depth)?;
1004
1005 let heap_id_len = compute_heap_id_len(header);
1007
1008 let mut records = Vec::new();
1009 let mut visited = HashSet::new();
1010
1011 if header.depth == 0 {
1012 enter_btree_v2_node(&mut visited, header.root_node_address)?;
1014 let mut cursor = Cursor::new(data);
1015 cursor.set_position(header.root_node_address);
1016 parse_leaf_node(
1017 &mut cursor,
1018 header,
1019 offset_size,
1020 length_size,
1021 ndims,
1022 chunk_dims,
1023 chunk_bounds,
1024 link_name_hash,
1025 header.num_records_in_root,
1026 heap_id_len,
1027 &mut records,
1028 )?;
1029 } else {
1030 parse_internal_node(
1032 data,
1033 header.root_node_address,
1034 header,
1035 offset_size,
1036 length_size,
1037 ndims,
1038 chunk_dims,
1039 chunk_bounds,
1040 link_name_hash,
1041 header.num_records_in_root,
1042 header.depth,
1043 heap_id_len,
1044 &mut visited,
1045 &mut records,
1046 )?;
1047 }
1048
1049 Ok(records)
1050}
1051
1052#[cfg(test)]
1054pub(crate) fn collect_btree_v2_link_name_hash_records(
1055 data: &[u8],
1056 header: &BTreeV2Header,
1057 offset_size: u8,
1058 length_size: u8,
1059 target_hash: u32,
1060) -> Result<Vec<BTreeV2Record>> {
1061 collect_btree_v2_records_with_link_hash(
1062 data,
1063 header,
1064 offset_size,
1065 length_size,
1066 None,
1067 &[],
1068 None,
1069 Some(target_hash),
1070 )
1071}
1072
1073pub fn collect_btree_v2_records_storage(
1075 storage: &dyn Storage,
1076 header: &BTreeV2Header,
1077 offset_size: u8,
1078 length_size: u8,
1079 ndims: Option<u32>,
1080 chunk_dims: &[u32],
1081 chunk_bounds: Option<(&[u64], &[u64])>,
1082) -> Result<Vec<BTreeV2Record>> {
1083 collect_btree_v2_records_storage_with_link_hash(
1084 storage,
1085 header,
1086 offset_size,
1087 length_size,
1088 ndims,
1089 chunk_dims,
1090 chunk_bounds,
1091 None,
1092 )
1093}
1094
1095#[allow(clippy::too_many_arguments)]
1096fn collect_btree_v2_records_storage_with_link_hash(
1097 storage: &dyn Storage,
1098 header: &BTreeV2Header,
1099 offset_size: u8,
1100 length_size: u8,
1101 ndims: Option<u32>,
1102 chunk_dims: &[u32],
1103 chunk_bounds: Option<(&[u64], &[u64])>,
1104 link_name_hash: Option<u32>,
1105) -> Result<Vec<BTreeV2Record>> {
1106 if Cursor::is_undefined_offset(header.root_node_address, offset_size) {
1107 return Ok(Vec::new());
1108 }
1109
1110 if header.total_records == 0 || header.num_records_in_root == 0 {
1111 return Ok(Vec::new());
1112 }
1113 validate_btree_v2_depth(header.depth)?;
1114
1115 let heap_id_len = compute_heap_id_len(header);
1116 let mut records = Vec::new();
1117 let mut visited = HashSet::new();
1118
1119 if header.depth == 0 {
1120 enter_btree_v2_node(&mut visited, header.root_node_address)?;
1121 parse_leaf_node_storage(
1122 storage,
1123 header.root_node_address,
1124 header,
1125 offset_size,
1126 length_size,
1127 ndims,
1128 chunk_dims,
1129 chunk_bounds,
1130 link_name_hash,
1131 header.num_records_in_root,
1132 heap_id_len,
1133 &mut records,
1134 )?;
1135 } else {
1136 parse_internal_node_storage(
1137 storage,
1138 header.root_node_address,
1139 header,
1140 offset_size,
1141 length_size,
1142 ndims,
1143 chunk_dims,
1144 chunk_bounds,
1145 link_name_hash,
1146 header.num_records_in_root,
1147 header.depth,
1148 heap_id_len,
1149 &mut visited,
1150 &mut records,
1151 )?;
1152 }
1153
1154 Ok(records)
1155}
1156
1157pub(crate) fn collect_btree_v2_link_name_hash_records_storage(
1159 storage: &dyn Storage,
1160 header: &BTreeV2Header,
1161 offset_size: u8,
1162 length_size: u8,
1163 target_hash: u32,
1164) -> Result<Vec<BTreeV2Record>> {
1165 collect_btree_v2_records_storage_with_link_hash(
1166 storage,
1167 header,
1168 offset_size,
1169 length_size,
1170 None,
1171 &[],
1172 None,
1173 Some(target_hash),
1174 )
1175}
1176
1177#[allow(clippy::too_many_arguments)]
1178fn parse_leaf_node_storage(
1179 storage: &dyn Storage,
1180 address: u64,
1181 header: &BTreeV2Header,
1182 offset_size: u8,
1183 length_size: u8,
1184 ndims: Option<u32>,
1185 chunk_dims: &[u32],
1186 chunk_bounds: Option<(&[u64], &[u64])>,
1187 link_name_hash: Option<u32>,
1188 num_records: u16,
1189 heap_id_len: usize,
1190 records: &mut Vec<BTreeV2Record>,
1191) -> Result<()> {
1192 let node_len = usize::try_from(header.node_size).map_err(|_| {
1193 Error::InvalidData("B-tree v2 node size exceeds platform usize capacity".into())
1194 })?;
1195 let node_bytes = storage.read_range(address, node_len)?;
1196 let mut cursor = Cursor::new(node_bytes.as_ref());
1197 parse_leaf_node(
1198 &mut cursor,
1199 header,
1200 offset_size,
1201 length_size,
1202 ndims,
1203 chunk_dims,
1204 chunk_bounds,
1205 link_name_hash,
1206 num_records,
1207 heap_id_len,
1208 records,
1209 )
1210}
1211
1212#[allow(clippy::too_many_arguments)]
1213fn parse_internal_node_storage(
1214 storage: &dyn Storage,
1215 address: u64,
1216 header: &BTreeV2Header,
1217 offset_size: u8,
1218 length_size: u8,
1219 ndims: Option<u32>,
1220 chunk_dims: &[u32],
1221 chunk_bounds: Option<(&[u64], &[u64])>,
1222 link_name_hash: Option<u32>,
1223 num_records: u16,
1224 depth: u16,
1225 heap_id_len: usize,
1226 visited: &mut HashSet<u64>,
1227 records: &mut Vec<BTreeV2Record>,
1228) -> Result<()> {
1229 if depth == 0 {
1230 return Err(Error::InvalidData(
1231 "B-tree v2 internal node traversal reached depth zero".into(),
1232 ));
1233 }
1234 enter_btree_v2_node(visited, address)?;
1235
1236 let node_len = usize::try_from(header.node_size).map_err(|_| {
1237 Error::InvalidData("B-tree v2 node size exceeds platform usize capacity".into())
1238 })?;
1239 let node_bytes = storage.read_range(address, node_len)?;
1240 let mut cursor = Cursor::new(node_bytes.as_ref());
1241 let start = cursor.position();
1242
1243 let sig = cursor.read_bytes(4)?;
1244 if sig != BTIN_SIGNATURE {
1245 return Err(Error::InvalidBTreeV2Signature {
1246 context: "internal node",
1247 });
1248 }
1249
1250 let version = cursor.read_u8()?;
1251 if version != 0 {
1252 return Err(Error::UnsupportedBTreeVersion(version));
1253 }
1254
1255 let node_type = cursor.read_u8()?;
1256 if node_type != header.btree_type {
1257 return Err(Error::InvalidData(format!(
1258 "B-tree v2 internal node type mismatch: header says {}, node says {}",
1259 header.btree_type, node_type
1260 )));
1261 }
1262
1263 let max_child_records = if depth == 1 {
1264 max_leaf_records(header.node_size, header.record_size)
1265 } else {
1266 let leaf_max = max_leaf_records(header.node_size, header.record_size);
1267 let mut prev_max = leaf_max;
1268 for _ in 1..depth {
1269 prev_max =
1270 max_internal_records(header.node_size, header.record_size, offset_size, prev_max);
1271 }
1272 prev_max
1273 };
1274 let nrec_bytes = num_records_size(max_child_records);
1275
1276 let mut node_records = Vec::with_capacity(num_records as usize);
1277 for _ in 0..num_records {
1278 let record = parse_record(
1279 &mut cursor,
1280 header.btree_type,
1281 header.record_size,
1282 offset_size,
1283 length_size,
1284 ndims,
1285 chunk_dims,
1286 heap_id_len,
1287 )?;
1288 node_records.push(record);
1289 }
1290
1291 let num_children = usize::from(num_records) + 1;
1292 let has_total_records = depth > 1;
1293 let total_nrec_bytes = if has_total_records {
1294 usize::from(length_size)
1295 } else {
1296 0
1297 };
1298
1299 let mut child_addresses = Vec::with_capacity(num_children);
1300 let mut child_nrecords = Vec::with_capacity(num_children);
1301 for _ in 0..num_children {
1302 child_addresses.push(cursor.read_offset(offset_size)?);
1303 child_nrecords.push(cursor.read_uvar(nrec_bytes)? as u16);
1304 if has_total_records {
1305 cursor.read_uvar(total_nrec_bytes)?;
1306 }
1307 }
1308
1309 let compact_end = cursor.position();
1310 verify_node_checksum(
1311 &mut cursor,
1312 start,
1313 header.node_size,
1314 compact_end,
1315 "internal node",
1316 )?;
1317
1318 let child_depth = depth - 1;
1319 for (i, record) in node_records.iter().enumerate() {
1320 let child_addr = child_addresses[i];
1321 if !Cursor::is_undefined_offset(child_addr, offset_size)
1322 && child_may_match_link_name_hash(&node_records, i, link_name_hash)
1323 {
1324 let child_nrec = child_nrecords[i];
1325 if child_depth == 0 {
1326 enter_btree_v2_node(visited, child_addr)?;
1327 parse_leaf_node_storage(
1328 storage,
1329 child_addr,
1330 header,
1331 offset_size,
1332 length_size,
1333 ndims,
1334 chunk_dims,
1335 chunk_bounds,
1336 link_name_hash,
1337 child_nrec,
1338 heap_id_len,
1339 records,
1340 )?;
1341 } else {
1342 parse_internal_node_storage(
1343 storage,
1344 child_addr,
1345 header,
1346 offset_size,
1347 length_size,
1348 ndims,
1349 chunk_dims,
1350 chunk_bounds,
1351 link_name_hash,
1352 child_nrec,
1353 child_depth,
1354 heap_id_len,
1355 visited,
1356 records,
1357 )?;
1358 }
1359 }
1360
1361 if record_matches_query(record, chunk_dims, chunk_bounds, link_name_hash) {
1362 records.push(record.clone());
1363 }
1364 }
1365
1366 let final_child_index = node_records.len();
1367 let final_child_addr = child_addresses[final_child_index];
1368 if !Cursor::is_undefined_offset(final_child_addr, offset_size)
1369 && child_may_match_link_name_hash(&node_records, final_child_index, link_name_hash)
1370 {
1371 let child_nrec = child_nrecords[final_child_index];
1372 if child_depth == 0 {
1373 enter_btree_v2_node(visited, final_child_addr)?;
1374 parse_leaf_node_storage(
1375 storage,
1376 final_child_addr,
1377 header,
1378 offset_size,
1379 length_size,
1380 ndims,
1381 chunk_dims,
1382 chunk_bounds,
1383 link_name_hash,
1384 child_nrec,
1385 heap_id_len,
1386 records,
1387 )?;
1388 } else {
1389 parse_internal_node_storage(
1390 storage,
1391 final_child_addr,
1392 header,
1393 offset_size,
1394 length_size,
1395 ndims,
1396 chunk_dims,
1397 chunk_bounds,
1398 link_name_hash,
1399 child_nrec,
1400 child_depth,
1401 heap_id_len,
1402 visited,
1403 records,
1404 )?;
1405 }
1406 }
1407
1408 Ok(())
1409}
1410
1411fn compute_heap_id_len(header: &BTreeV2Header) -> usize {
1417 let rs = header.record_size as usize;
1418 match header.btree_type {
1419 5 => rs.saturating_sub(4), 6 => rs.saturating_sub(8), 8 => rs.saturating_sub(4 + 1 + 4), 9 => rs.saturating_sub(1 + 4), _ => 0,
1424 }
1425}
1426
1427#[cfg(test)]
1428mod tests {
1429 use super::*;
1430
1431 #[allow(clippy::too_many_arguments)]
1433 fn build_header(
1434 btree_type: u8,
1435 node_size: u32,
1436 record_size: u16,
1437 depth: u16,
1438 root_node_address: u64,
1439 num_records_in_root: u16,
1440 total_records: u64,
1441 offset_size: u8,
1442 length_size: u8,
1443 ) -> Vec<u8> {
1444 let mut buf = Vec::new();
1445 buf.extend_from_slice(b"BTHD");
1446 buf.push(0); buf.push(btree_type);
1448 buf.extend_from_slice(&node_size.to_le_bytes());
1449 buf.extend_from_slice(&record_size.to_le_bytes());
1450 buf.extend_from_slice(&depth.to_le_bytes());
1451 buf.push(75); buf.push(40); match offset_size {
1454 4 => buf.extend_from_slice(&(root_node_address as u32).to_le_bytes()),
1455 8 => buf.extend_from_slice(&root_node_address.to_le_bytes()),
1456 _ => panic!("unsupported"),
1457 }
1458 buf.extend_from_slice(&num_records_in_root.to_le_bytes());
1459 match length_size {
1460 4 => buf.extend_from_slice(&(total_records as u32).to_le_bytes()),
1461 8 => buf.extend_from_slice(&total_records.to_le_bytes()),
1462 _ => panic!("unsupported"),
1463 }
1464 let checksum = jenkins_lookup3(&buf);
1466 buf.extend_from_slice(&checksum.to_le_bytes());
1467 buf
1468 }
1469
1470 #[test]
1471 fn parse_header() {
1472 let data = build_header(5, 4096, 12, 0, 0x1000, 3, 3, 8, 8);
1473 let mut cursor = Cursor::new(&data);
1474 let hdr = BTreeV2Header::parse(&mut cursor, 8, 8).unwrap();
1475
1476 assert_eq!(hdr.btree_type, 5);
1477 assert_eq!(hdr.node_size, 4096);
1478 assert_eq!(hdr.record_size, 12);
1479 assert_eq!(hdr.depth, 0);
1480 assert_eq!(hdr.split_percent, 75);
1481 assert_eq!(hdr.merge_percent, 40);
1482 assert_eq!(hdr.root_node_address, 0x1000);
1483 assert_eq!(hdr.num_records_in_root, 3);
1484 assert_eq!(hdr.total_records, 3);
1485 }
1486
1487 #[test]
1488 fn bad_signature() {
1489 let mut data = build_header(5, 4096, 12, 0, 0x1000, 0, 0, 8, 8);
1490 data[0] = b'X';
1491 let mut cursor = Cursor::new(&data);
1492 assert!(matches!(
1493 BTreeV2Header::parse(&mut cursor, 8, 8),
1494 Err(Error::InvalidBTreeV2Signature { .. })
1495 ));
1496 }
1497
1498 #[test]
1499 fn bad_checksum() {
1500 let mut data = build_header(5, 4096, 12, 0, 0x1000, 0, 0, 8, 8);
1501 data[6] = 0xFF;
1503 let mut cursor = Cursor::new(&data);
1504 assert!(matches!(
1505 BTreeV2Header::parse(&mut cursor, 8, 8),
1506 Err(Error::ChecksumMismatch { .. })
1507 ));
1508 }
1509
1510 #[test]
1511 fn collect_empty_tree() {
1512 let header = BTreeV2Header {
1513 btree_type: 5,
1514 node_size: 4096,
1515 record_size: 12,
1516 depth: 0,
1517 split_percent: 75,
1518 merge_percent: 40,
1519 root_node_address: u64::MAX,
1520 num_records_in_root: 0,
1521 total_records: 0,
1522 };
1523 let data = vec![0u8; 100];
1524 let records = collect_btree_v2_records(&data, &header, 8, 8, None, &[], None).unwrap();
1525 assert!(records.is_empty());
1526 }
1527
1528 #[test]
1529 fn heap_id_len_uses_record_type_sizes() {
1530 let h5 = BTreeV2Header {
1532 btree_type: 5,
1533 record_size: 12,
1534 node_size: 0,
1535 depth: 0,
1536 split_percent: 0,
1537 merge_percent: 0,
1538 root_node_address: 0,
1539 num_records_in_root: 0,
1540 total_records: 0,
1541 };
1542 assert_eq!(compute_heap_id_len(&h5), 8);
1543
1544 let h8 = BTreeV2Header {
1546 btree_type: 8,
1547 record_size: 17,
1548 ..h5
1549 };
1550 assert_eq!(compute_heap_id_len(&h8), 8);
1551
1552 let h9 = BTreeV2Header {
1554 btree_type: 9,
1555 record_size: 13,
1556 ..h5
1557 };
1558 assert_eq!(compute_heap_id_len(&h9), 8);
1559 }
1560
1561 #[test]
1562 fn parse_attribute_name_record_uses_on_disk_field_order() {
1563 let heap_id = [0x00, 0xbe, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00];
1564 let mut data = heap_id.to_vec();
1565 data.push(0x02);
1566 data.extend_from_slice(&6u32.to_le_bytes());
1567 data.extend_from_slice(&0x0396_dda5u32.to_le_bytes());
1568 let mut cursor = Cursor::new(&data);
1569
1570 let record = parse_record(&mut cursor, 8, data.len() as u16, 8, 8, None, &[], 8).unwrap();
1571 match record {
1572 BTreeV2Record::AttributeNameHash {
1573 hash,
1574 flags,
1575 creation_order,
1576 heap_id: parsed_heap_id,
1577 } => {
1578 assert_eq!(parsed_heap_id, heap_id);
1579 assert_eq!(flags, 0x02);
1580 assert_eq!(creation_order, 6);
1581 assert_eq!(hash, 0x0396_dda5);
1582 }
1583 other => panic!("expected attribute-name record, got {other:?}"),
1584 }
1585 }
1586
1587 #[test]
1588 fn parse_attribute_creation_order_record_uses_on_disk_field_order() {
1589 let heap_id = [0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x1c, 0x00];
1590 let mut data = heap_id.to_vec();
1591 data.push(0x02);
1592 data.extend_from_slice(&7u32.to_le_bytes());
1593 let mut cursor = Cursor::new(&data);
1594
1595 let record = parse_record(&mut cursor, 9, data.len() as u16, 8, 8, None, &[], 8).unwrap();
1596 match record {
1597 BTreeV2Record::AttributeCreationOrder {
1598 order,
1599 heap_id: parsed_heap_id,
1600 } => {
1601 assert_eq!(parsed_heap_id, heap_id);
1602 assert_eq!(order, 7);
1603 }
1604 other => panic!("expected attribute-creation-order record, got {other:?}"),
1605 }
1606 }
1607
1608 #[test]
1609 fn max_leaf_records_accounts_for_node_overhead() {
1610 assert_eq!(max_leaf_records(4096, 12), 340);
1613 }
1614
1615 #[test]
1616 fn record_count_size_scales_at_integer_boundaries() {
1617 assert_eq!(num_records_size(0), 1);
1618 assert_eq!(num_records_size(255), 1);
1619 assert_eq!(num_records_size(256), 2);
1620 assert_eq!(num_records_size(65535), 2);
1621 assert_eq!(num_records_size(65536), 4);
1622 }
1623
1624 #[test]
1625 fn parse_huge_indirect_record() {
1626 let mut data = Vec::new();
1627 data.extend_from_slice(&0x1234u64.to_le_bytes());
1628 data.extend_from_slice(&99u64.to_le_bytes());
1629 data.extend_from_slice(&7u64.to_le_bytes());
1630 let mut cursor = Cursor::new(&data);
1631
1632 let record = parse_record(&mut cursor, 1, data.len() as u16, 8, 8, None, &[], 0).unwrap();
1633 match record {
1634 BTreeV2Record::HugeIndirectNonFiltered {
1635 address,
1636 length,
1637 object_id,
1638 } => {
1639 assert_eq!(address, 0x1234);
1640 assert_eq!(length, 99);
1641 assert_eq!(object_id, 7);
1642 }
1643 other => panic!("expected huge record, got {:?}", other),
1644 }
1645 }
1646
1647 #[test]
1648 fn parse_shared_heap_record() {
1649 let mut data = Vec::new();
1650 data.push(0);
1651 data.extend_from_slice(&[0, 0, 0]);
1652 data.extend_from_slice(&0xAABB_CCDDu32.to_le_bytes());
1653 data.extend_from_slice(&3u32.to_le_bytes());
1654 data.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
1655 let mut cursor = Cursor::new(&data);
1656
1657 let record = parse_record(&mut cursor, 7, data.len() as u16, 8, 8, None, &[], 0).unwrap();
1658 match record {
1659 BTreeV2Record::SharedMessageHeap {
1660 hash,
1661 reference_count,
1662 heap_id,
1663 } => {
1664 assert_eq!(hash, 0xAABB_CCDD);
1665 assert_eq!(reference_count, 3);
1666 assert_eq!(heap_id, vec![1, 2, 3, 4, 5, 6, 7, 8]);
1667 }
1668 other => panic!("expected shared heap record, got {:?}", other),
1669 }
1670 }
1671
1672 #[test]
1673 fn parse_record_rejects_known_record_that_exceeds_record_size() {
1674 let mut data = Vec::new();
1675 data.extend_from_slice(&0x1234u64.to_le_bytes());
1676 data.extend_from_slice(&99u64.to_le_bytes());
1677 data.extend_from_slice(&7u64.to_le_bytes());
1678 let mut cursor = Cursor::new(&data);
1679
1680 let err = parse_record(&mut cursor, 1, 16, 8, 8, None, &[], 0).unwrap_err();
1681 assert!(
1682 matches!(err, Error::InvalidData(message) if message.contains("consumed 24 bytes but record size is 16"))
1683 );
1684 }
1685
1686 #[test]
1687 fn parse_chunk_record_rejects_size_shorter_than_address() {
1688 let mut data = Vec::new();
1689 data.extend_from_slice(&0x1234u64.to_le_bytes());
1690 let mut cursor = Cursor::new(&data);
1691
1692 let err = parse_record(&mut cursor, 10, 4, 8, 8, None, &[], 0).unwrap_err();
1693 assert!(
1694 matches!(err, Error::InvalidData(message) if message.contains("shorter than its address"))
1695 );
1696 assert_eq!(cursor.position(), 0);
1697 }
1698
1699 #[test]
1700 fn parse_filtered_chunk_record_rejects_size_shorter_than_fixed_fields() {
1701 let mut data = Vec::new();
1702 data.extend_from_slice(&0x1234u64.to_le_bytes());
1703 data.extend_from_slice(&99u64.to_le_bytes());
1704 data.extend_from_slice(&0xAu32.to_le_bytes());
1705 let mut cursor = Cursor::new(&data);
1706
1707 let err = parse_record(&mut cursor, 11, 16, 8, 8, Some(1), &[], 0).unwrap_err();
1710 assert!(
1711 matches!(err, Error::InvalidData(message) if message.contains("shorter than its fixed fields"))
1712 );
1713 assert_eq!(cursor.position(), 0);
1714 }
1715
1716 #[test]
1717 fn parse_chunk_record_scales_offsets_by_chunk_dimensions() {
1718 let mut data = Vec::new();
1719 data.extend_from_slice(&0x1234u64.to_le_bytes());
1720 data.extend_from_slice(&2u64.to_le_bytes());
1721 data.extend_from_slice(&1u64.to_le_bytes());
1722 let mut cursor = Cursor::new(&data);
1723
1724 let record = parse_record(
1725 &mut cursor,
1726 10,
1727 data.len() as u16,
1728 8,
1729 8,
1730 Some(2),
1731 &[5, 7],
1732 0,
1733 )
1734 .unwrap();
1735 match record {
1736 BTreeV2Record::ChunkedNonFiltered { address, offsets } => {
1737 assert_eq!(address, 0x1234);
1738 assert_eq!(offsets, vec![10, 7]);
1739 }
1740 other => panic!("expected non-filtered chunk record, got {:?}", other),
1741 }
1742 }
1743
1744 #[test]
1745 fn parse_filtered_chunk_record_scales_offsets_by_chunk_dimensions() {
1746 let mut data = Vec::new();
1747 data.extend_from_slice(&0x1234u64.to_le_bytes());
1748 data.extend_from_slice(&99u64.to_le_bytes());
1749 data.extend_from_slice(&0xAu32.to_le_bytes());
1750 data.extend_from_slice(&3u64.to_le_bytes());
1751 data.extend_from_slice(&4u64.to_le_bytes());
1752 let mut cursor = Cursor::new(&data);
1753
1754 let record = parse_record(
1755 &mut cursor,
1756 11,
1757 data.len() as u16,
1758 8,
1759 8,
1760 Some(2),
1761 &[5, 7],
1762 0,
1763 )
1764 .unwrap();
1765 match record {
1766 BTreeV2Record::ChunkedFiltered {
1767 address,
1768 chunk_size,
1769 filter_mask,
1770 offsets,
1771 } => {
1772 assert_eq!(address, 0x1234);
1773 assert_eq!(chunk_size, 99);
1774 assert_eq!(filter_mask, 0xA);
1775 assert_eq!(offsets, vec![15, 28]);
1776 }
1777 other => panic!("expected filtered chunk record, got {:?}", other),
1778 }
1779 }
1780
1781 #[test]
1782 fn parse_leaf_with_type5_records() {
1783 let record_size: u16 = 12;
1786 let node_size: u32 = 4096;
1787
1788 let header = BTreeV2Header {
1789 btree_type: 5,
1790 node_size,
1791 record_size,
1792 depth: 0,
1793 split_percent: 75,
1794 merge_percent: 40,
1795 root_node_address: 0, num_records_in_root: 2,
1797 total_records: 2,
1798 };
1799
1800 let mut leaf = Vec::new();
1802 leaf.extend_from_slice(b"BTLF"); leaf.push(0); leaf.push(5); leaf.extend_from_slice(&0xAABBCCDDu32.to_le_bytes());
1808 leaf.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
1809
1810 leaf.extend_from_slice(&0x11223344u32.to_le_bytes());
1812 leaf.extend_from_slice(&[9, 10, 11, 12, 13, 14, 15, 16]);
1813
1814 let checksum = jenkins_lookup3(&leaf);
1817 leaf.extend_from_slice(&checksum.to_le_bytes());
1818 leaf.resize(node_size as usize, 0);
1819
1820 let mut records = Vec::new();
1821 let mut cursor = Cursor::new(&leaf);
1822 parse_leaf_node(
1823 &mut cursor,
1824 &header,
1825 8,
1826 8,
1827 None,
1828 &[],
1829 None,
1830 None,
1831 2,
1832 8, &mut records,
1834 )
1835 .unwrap();
1836
1837 assert_eq!(records.len(), 2);
1838 match &records[0] {
1839 BTreeV2Record::LinkNameHash { hash, heap_id } => {
1840 assert_eq!(*hash, 0xAABBCCDD);
1841 assert_eq!(heap_id, &[1, 2, 3, 4, 5, 6, 7, 8]);
1842 }
1843 _ => panic!("expected LinkNameHash"),
1844 }
1845 match &records[1] {
1846 BTreeV2Record::LinkNameHash { hash, heap_id } => {
1847 assert_eq!(*hash, 0x11223344);
1848 assert_eq!(heap_id, &[9, 10, 11, 12, 13, 14, 15, 16]);
1849 }
1850 _ => panic!("expected LinkNameHash"),
1851 }
1852 }
1853
1854 fn build_type5_record(hash: u32, heap_id: [u8; 8]) -> Vec<u8> {
1855 let mut record = Vec::new();
1856 record.extend_from_slice(&hash.to_le_bytes());
1857 record.extend_from_slice(&heap_id);
1858 record
1859 }
1860
1861 fn build_type5_leaf(records: &[(u32, [u8; 8])], node_size: usize) -> Vec<u8> {
1862 let mut leaf = Vec::new();
1863 leaf.extend_from_slice(b"BTLF");
1864 leaf.push(0);
1865 leaf.push(5);
1866 for &(hash, heap_id) in records {
1867 leaf.extend_from_slice(&build_type5_record(hash, heap_id));
1868 }
1869 let checksum = jenkins_lookup3(&leaf);
1870 leaf.extend_from_slice(&checksum.to_le_bytes());
1871 leaf.resize(node_size, 0);
1872 leaf
1873 }
1874
1875 fn build_type5_internal(
1876 records: &[(u32, [u8; 8])],
1877 children: &[(u64, u8)],
1878 node_size: usize,
1879 ) -> Vec<u8> {
1880 assert_eq!(children.len(), records.len() + 1);
1881
1882 let mut node = Vec::new();
1883 node.extend_from_slice(b"BTIN");
1884 node.push(0);
1885 node.push(5);
1886 for &(hash, heap_id) in records {
1887 node.extend_from_slice(&build_type5_record(hash, heap_id));
1888 }
1889 for &(address, child_records) in children {
1890 node.extend_from_slice(&address.to_le_bytes());
1891 node.push(child_records);
1892 }
1893 let checksum = jenkins_lookup3(&node);
1894 node.extend_from_slice(&checksum.to_le_bytes());
1895 node.resize(node_size, 0);
1896 node
1897 }
1898
1899 fn link_hashes(records: &[BTreeV2Record]) -> Vec<u32> {
1900 records
1901 .iter()
1902 .map(|record| match record {
1903 BTreeV2Record::LinkNameHash { hash, .. } => *hash,
1904 other => panic!("expected LinkNameHash, got {other:?}"),
1905 })
1906 .collect()
1907 }
1908
1909 #[test]
1910 fn collect_depth_one_tree_includes_internal_records_in_order() {
1911 let node_size = 128usize;
1915 let left_addr = node_size as u64;
1916 let right_addr = (node_size * 2) as u64;
1917 let header = BTreeV2Header {
1918 btree_type: 5,
1919 node_size: node_size as u32,
1920 record_size: 12,
1921 depth: 1,
1922 split_percent: 75,
1923 merge_percent: 40,
1924 root_node_address: 0,
1925 num_records_in_root: 1,
1926 total_records: 3,
1927 };
1928
1929 let root = build_type5_internal(
1930 &[(20, [20; 8])],
1931 &[(left_addr, 1), (right_addr, 1)],
1932 node_size,
1933 );
1934 let left_leaf = build_type5_leaf(&[(10, [10; 8])], node_size);
1935 let right_leaf = build_type5_leaf(&[(30, [30; 8])], node_size);
1936
1937 let mut data = vec![0; node_size * 3];
1938 data[0..node_size].copy_from_slice(&root);
1939 data[node_size..node_size * 2].copy_from_slice(&left_leaf);
1940 data[node_size * 2..node_size * 3].copy_from_slice(&right_leaf);
1941
1942 let records = collect_btree_v2_records(&data, &header, 8, 8, None, &[], None).unwrap();
1943 assert_eq!(link_hashes(&records), vec![10, 20, 30]);
1944
1945 let storage = crate::storage::BytesStorage::new(data);
1946 let records =
1947 collect_btree_v2_records_storage(&storage, &header, 8, 8, None, &[], None).unwrap();
1948 assert_eq!(link_hashes(&records), vec![10, 20, 30]);
1949 }
1950
1951 #[test]
1952 fn collect_rejects_depth_above_traversal_limit() {
1953 let header = BTreeV2Header {
1954 btree_type: 5,
1955 node_size: 128,
1956 record_size: 12,
1957 depth: MAX_BTREE_V2_DEPTH + 1,
1958 split_percent: 75,
1959 merge_percent: 40,
1960 root_node_address: 0,
1961 num_records_in_root: 1,
1962 total_records: 1,
1963 };
1964 let data = vec![0; 128];
1965
1966 let err = collect_btree_v2_records(&data, &header, 8, 8, None, &[], None).unwrap_err();
1967 assert!(
1968 matches!(err, Error::InvalidData(message) if message.contains("exceeds traversal limit"))
1969 );
1970 }
1971
1972 #[test]
1973 fn collect_rejects_revisited_leaf_node() {
1974 let node_size = 128usize;
1975 let leaf_addr = node_size as u64;
1976 let header = BTreeV2Header {
1977 btree_type: 5,
1978 node_size: node_size as u32,
1979 record_size: 12,
1980 depth: 1,
1981 split_percent: 75,
1982 merge_percent: 40,
1983 root_node_address: 0,
1984 num_records_in_root: 1,
1985 total_records: 3,
1986 };
1987
1988 let root = build_type5_internal(
1989 &[(20, [20; 8])],
1990 &[(leaf_addr, 1), (leaf_addr, 1)],
1991 node_size,
1992 );
1993 let leaf = build_type5_leaf(&[(10, [10; 8])], node_size);
1994
1995 let mut data = vec![0; node_size * 2];
1996 data[0..node_size].copy_from_slice(&root);
1997 data[node_size..node_size * 2].copy_from_slice(&leaf);
1998
1999 let err = collect_btree_v2_records(&data, &header, 8, 8, None, &[], None).unwrap_err();
2000 assert!(matches!(err, Error::InvalidData(message) if message.contains("revisits node")));
2001
2002 let storage = crate::storage::BytesStorage::new(data);
2003 let err =
2004 collect_btree_v2_records_storage(&storage, &header, 8, 8, None, &[], None).unwrap_err();
2005 assert!(matches!(err, Error::InvalidData(message) if message.contains("revisits node")));
2006 }
2007
2008 #[test]
2009 fn collect_link_name_hash_records_filters_leaf_records() {
2010 let record_size: u16 = 12;
2011 let node_size: u32 = 4096;
2012 let header = BTreeV2Header {
2013 btree_type: 5,
2014 node_size,
2015 record_size,
2016 depth: 0,
2017 split_percent: 75,
2018 merge_percent: 40,
2019 root_node_address: 0,
2020 num_records_in_root: 2,
2021 total_records: 2,
2022 };
2023
2024 let mut leaf = Vec::new();
2025 leaf.extend_from_slice(b"BTLF");
2026 leaf.push(0);
2027 leaf.push(5);
2028 leaf.extend_from_slice(&0x1111_1111u32.to_le_bytes());
2029 leaf.extend_from_slice(&[1, 2, 3, 4, 5, 6, 7, 8]);
2030 leaf.extend_from_slice(&0x2222_2222u32.to_le_bytes());
2031 leaf.extend_from_slice(&[9, 10, 11, 12, 13, 14, 15, 16]);
2032 let checksum = jenkins_lookup3(&leaf);
2033 leaf.extend_from_slice(&checksum.to_le_bytes());
2034 leaf.resize(node_size as usize, 0);
2035
2036 let records =
2037 collect_btree_v2_link_name_hash_records(&leaf, &header, 8, 8, 0x2222_2222).unwrap();
2038 assert_eq!(records.len(), 1);
2039 match &records[0] {
2040 BTreeV2Record::LinkNameHash { hash, heap_id } => {
2041 assert_eq!(*hash, 0x2222_2222);
2042 assert_eq!(heap_id, &[9, 10, 11, 12, 13, 14, 15, 16]);
2043 }
2044 other => panic!("expected LinkNameHash, got {other:?}"),
2045 }
2046 }
2047
2048 #[test]
2049 fn child_link_hash_filter_prunes_disjoint_hash_ranges() {
2050 let records = vec![
2051 BTreeV2Record::LinkNameHash {
2052 hash: 10,
2053 heap_id: vec![],
2054 },
2055 BTreeV2Record::LinkNameHash {
2056 hash: 20,
2057 heap_id: vec![],
2058 },
2059 ];
2060
2061 assert!(child_may_match_link_name_hash(&records, 0, Some(10)));
2062 assert!(!child_may_match_link_name_hash(&records, 0, Some(15)));
2063 assert!(child_may_match_link_name_hash(&records, 1, Some(15)));
2064 assert!(!child_may_match_link_name_hash(&records, 2, Some(15)));
2065 assert!(child_may_match_link_name_hash(&records, 2, Some(20)));
2066 }
2067}