format: v2 B-trees, dense groups and group listings over Storage

BTreeV2Header::parse_in, collect_btree_v2_records_in and
find_btree_v2_records_in read one bounded window per node (its size is
known from the parent before the node is read; a count stretched past
node_size is checked against the end of the file first), with the
whole-file bounds errors unchanged. With them, dense attributes, a SOHM
B-tree index and huge fractal-heap objects no longer answer
ContiguousStorageRequired, and group_v1/group_v2 listings, lookups and
path resolution get *_in cores (resolve_group_children_in,
resolve_child_in, resolve_path_any_in, ...). The &[u8] functions are
thin wrappers, as in M1.

The equivalence harness now fails on any ContiguousStorageRequired and
compares v2 B-tree headers, records and descents, group listings, child
lookups and paths; a unit test compares a two-level tree through a
read_at-only storage truncated at every length and with every node byte
flipped.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 16:13:01 -05:00
co-authored by Claude Opus 5.5
parent 8f59b2e1c2
commit 42894bf93b
9 changed files with 544 additions and 297 deletions
+64 -21
View File
@@ -3,12 +3,13 @@
#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
use crate::addr::to_usize;
use crate::btree_v1::collect_symbol_table_nodes;
use crate::addr::checked_addr;
use crate::btree_v1::collect_symbol_table_nodes_in;
use crate::error::FormatError;
use crate::local_heap::LocalHeap;
use crate::message_type::MessageType;
use crate::object_header::ObjectHeader;
use crate::storage::Storage;
use crate::symbol_table::{SymbolTableMessage, SymbolTableNode};
/// A resolved group entry (child name + object header address).
@@ -36,6 +37,16 @@ pub fn resolve_v1_group_entries(
sym_table_msg: &SymbolTableMessage,
offset_size: u8,
length_size: u8,
) -> Result<Vec<GroupEntry>, FormatError> {
resolve_v1_group_entries_in(file_data, sym_table_msg, offset_size, length_size)
}
/// [`resolve_v1_group_entries`] over any [`Storage`].
pub fn resolve_v1_group_entries_in<S: Storage + ?Sized>(
file_data: &S,
sym_table_msg: &SymbolTableMessage,
offset_size: u8,
length_size: u8,
) -> Result<Vec<GroupEntry>, FormatError> {
let entries = v1_group_entries(file_data, sym_table_msg, offset_size, length_size)?;
if entries.iter().any(|e| e.name.is_empty()) {
@@ -46,22 +57,22 @@ pub fn resolve_v1_group_entries(
/// Every entry of a v1 group, empty names included — for looking a name up,
/// which never matches an empty name.
pub(crate) fn v1_group_entries(
file_data: &[u8],
pub(crate) fn v1_group_entries<S: Storage + ?Sized>(
file_data: &S,
sym_table_msg: &SymbolTableMessage,
offset_size: u8,
length_size: u8,
) -> Result<Vec<GroupEntry>, FormatError> {
// Parse local heap
let heap = LocalHeap::parse(
let heap = LocalHeap::parse_in(
file_data,
to_usize(sym_table_msg.local_heap_address)?,
checked_addr(sym_table_msg.local_heap_address)?,
offset_size,
length_size,
)?;
// Collect all SNOD addresses from B-tree
let snod_addrs = collect_symbol_table_nodes(
let snod_addrs = collect_symbol_table_nodes_in(
file_data,
sym_table_msg.btree_address,
offset_size,
@@ -71,15 +82,15 @@ pub(crate) fn v1_group_entries(
let mut entries = Vec::new();
let mut heap_checked = false;
for snod_addr in snod_addrs {
let snod = SymbolTableNode::parse(file_data, to_usize(snod_addr)?, offset_size)?;
let snod = SymbolTableNode::parse_in(file_data, checked_addr(snod_addr)?, offset_size)?;
for entry in &snod.entries {
// Like libhdf5, look at the heap's free list only once a name is
// needed: an empty group with a damaged heap still lists.
if !heap_checked {
heap.validate_free_list(file_data, length_size)?;
heap.validate_free_list_in(file_data, length_size)?;
heap_checked = true;
}
let name = heap.read_string(file_data, entry.link_name_offset)?;
let name = heap.read_string_in(file_data, entry.link_name_offset)?;
entries.push(GroupEntry {
name,
object_header_address: entry.object_header_address,
@@ -103,6 +114,17 @@ pub fn find_v1_soft_link(
name: &str,
offset_size: u8,
length_size: u8,
) -> Result<Option<String>, FormatError> {
find_v1_soft_link_in(file_data, sym_table_msg, name, offset_size, length_size)
}
/// [`find_v1_soft_link`] over any [`Storage`].
pub fn find_v1_soft_link_in<S: Storage + ?Sized>(
file_data: &S,
sym_table_msg: &SymbolTableMessage,
name: &str,
offset_size: u8,
length_size: u8,
) -> Result<Option<String>, FormatError> {
let mut found = None;
for_each_v1_soft_link(
@@ -125,6 +147,16 @@ pub fn v1_soft_links(
sym_table_msg: &SymbolTableMessage,
offset_size: u8,
length_size: u8,
) -> Result<Vec<(String, String)>, FormatError> {
v1_soft_links_in(file_data, sym_table_msg, offset_size, length_size)
}
/// [`v1_soft_links`] over any [`Storage`].
pub fn v1_soft_links_in<S: Storage + ?Sized>(
file_data: &S,
sym_table_msg: &SymbolTableMessage,
offset_size: u8,
length_size: u8,
) -> Result<Vec<(String, String)>, FormatError> {
let mut links = Vec::new();
for_each_v1_soft_link(
@@ -143,21 +175,21 @@ pub fn v1_soft_links(
/// Visit the soft links of a v1 group whose name passes `wanted`, with their
/// target paths, until `visit` returns false.
fn for_each_v1_soft_link(
file_data: &[u8],
fn for_each_v1_soft_link<S: Storage + ?Sized>(
file_data: &S,
sym_table_msg: &SymbolTableMessage,
offset_size: u8,
length_size: u8,
wanted: impl Fn(&str) -> bool,
mut visit: impl FnMut(&str, String) -> bool,
) -> Result<(), FormatError> {
let heap = LocalHeap::parse(
let heap = LocalHeap::parse_in(
file_data,
to_usize(sym_table_msg.local_heap_address)?,
checked_addr(sym_table_msg.local_heap_address)?,
offset_size,
length_size,
)?;
let snod_addrs = collect_symbol_table_nodes(
let snod_addrs = collect_symbol_table_nodes_in(
file_data,
sym_table_msg.btree_address,
offset_size,
@@ -165,16 +197,16 @@ fn for_each_v1_soft_link(
)?;
let mut heap_checked = false;
for snod_addr in snod_addrs {
let snod = SymbolTableNode::parse(file_data, to_usize(snod_addr)?, offset_size)?;
let snod = SymbolTableNode::parse_in(file_data, checked_addr(snod_addr)?, offset_size)?;
for entry in &snod.entries {
if entry.cache_type != CACHE_TYPE_SOFT_LINK {
continue;
}
if !heap_checked {
heap.validate_free_list(file_data, length_size)?;
heap.validate_free_list_in(file_data, length_size)?;
heap_checked = true;
}
let name = heap.read_string(file_data, entry.link_name_offset)?;
let name = heap.read_string_in(file_data, entry.link_name_offset)?;
if !wanted(&name) {
continue;
}
@@ -184,7 +216,7 @@ fn for_each_v1_soft_link(
entry.scratch_pad[2],
entry.scratch_pad[3],
]);
let target = heap.read_string(file_data, u64::from(value_offset))?;
let target = heap.read_string_in(file_data, u64::from(value_offset))?;
if !visit(&name, target) {
return Ok(());
}
@@ -222,6 +254,17 @@ pub fn resolve_path(
path: &str,
offset_size: u8,
length_size: u8,
) -> Result<u64, FormatError> {
resolve_path_in(file_data, root_sym_table, path, offset_size, length_size)
}
/// [`resolve_path`] over any [`Storage`].
pub fn resolve_path_in<S: Storage + ?Sized>(
file_data: &S,
root_sym_table: &SymbolTableMessage,
path: &str,
offset_size: u8,
length_size: u8,
) -> Result<u64, FormatError> {
let components: Vec<&str> = path.split('/').filter(|s| !s.is_empty()).collect();
if components.is_empty() {
@@ -241,9 +284,9 @@ pub fn resolve_path(
return Ok(entry.object_header_address);
}
// Not last — must be a group, parse its object header to get symbol table
let obj_header = ObjectHeader::parse(
let obj_header = ObjectHeader::parse_in(
file_data,
to_usize(entry.object_header_address)?,
checked_addr(entry.object_header_address)?,
offset_size,
length_size,
)?;