Every `*_in` core and the read helpers take `file: &S` with `S: Storage + ?Sized` instead of `&dyn Storage`, and the `&[u8]` wrappers pass the slice itself, so they compile to a `[u8]` instance: `as_contiguous()` inlines to `Some(self)` and each structure read is the slice code's bounds check again, with no indirect call. `&dyn Storage` still works (`S = dyn Storage`); there is one parser implementation. Also, so the structure reads cost no more than the slice checks did: - ObjectHeader::parse_in reads the prefix once (signature included) instead of the signature and then the prefix: two reads for a one-chunk header instead of three on a range backend; - the symbol-table node and group B-tree (v1) loops walk their entries with chunks_exact over the bytes read, and the node's redundant second bounds check is gone (the entries' read is the check, same error); - a version-1 header's message list is sized from its (capped) count. Same results and errors; the unit and equivalence tests are unchanged. New Criterion bench `clawhdf5/benches/local_metadata_bench.rs` over a 400-group version-1 file written by h5py (new fixture `v1_groups_400.h5`): ObjectHeader::parse, symbol-table nodes, the group B-tree walk and a facade listing, using only APIs that exist atf2ff2c4so it builds there for an A/B. Provisional A/B againstf2ff2c4(busy machine, not for docs): both builds linked into one binary and timed in alternation, 200 rounds; median ratio new/old: facade listing -0.5% to -3.5% (was +14%), ObjectHeader::parse +1% to +2% (was +25%), symbol-table nodes -18%, group B-tree walk -18%, local-heap names and resolve_group_children within +-1.5%. An old-vs-old-copy run shows +-2% from code layout alone. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
269 lines
9.6 KiB
Rust
269 lines
9.6 KiB
Rust
//! HDF5 Symbol Table Message and Symbol Table Node (SNOD) parsing.
|
|
|
|
#[cfg(not(feature = "std"))]
|
|
use alloc::vec::Vec;
|
|
|
|
use crate::error::FormatError;
|
|
use crate::storage::{Storage, read_exact_at};
|
|
|
|
/// Symbol Table message (type 0x0011) found in v1 group object headers.
|
|
#[derive(Debug, Clone, PartialEq)]
|
|
pub struct SymbolTableMessage {
|
|
/// Address of B-tree v1 (type 0) for this group.
|
|
pub btree_address: u64,
|
|
/// Address of the local heap for this group.
|
|
pub local_heap_address: u64,
|
|
}
|
|
|
|
fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
|
|
let s = size as usize;
|
|
if pos.checked_add(s).is_none_or(|end| end > data.len()) {
|
|
return Err(FormatError::UnexpectedEof {
|
|
expected: pos.saturating_add(s),
|
|
available: data.len(),
|
|
});
|
|
}
|
|
let slice = &data[pos..pos + s];
|
|
Ok(match size {
|
|
2 => u16::from_le_bytes([slice[0], slice[1]]) as u64,
|
|
4 => u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]) as u64,
|
|
8 => u64::from_le_bytes([
|
|
slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7],
|
|
]),
|
|
_ => return Err(FormatError::InvalidOffsetSize(size)),
|
|
})
|
|
}
|
|
|
|
impl SymbolTableMessage {
|
|
/// Parse a Symbol Table message from raw message data bytes.
|
|
pub fn parse(data: &[u8], offset_size: u8) -> Result<SymbolTableMessage, FormatError> {
|
|
let os = offset_size as usize;
|
|
if data.len() < os * 2 {
|
|
return Err(FormatError::UnexpectedEof {
|
|
expected: os * 2,
|
|
available: data.len(),
|
|
});
|
|
}
|
|
let btree_address = read_offset(data, 0, offset_size)?;
|
|
let local_heap_address = read_offset(data, os, offset_size)?;
|
|
Ok(SymbolTableMessage {
|
|
btree_address,
|
|
local_heap_address,
|
|
})
|
|
}
|
|
}
|
|
|
|
/// A single entry in a Symbol Table Node (SNOD).
|
|
#[derive(Debug, Clone)]
|
|
pub struct SymbolTableEntry {
|
|
/// Byte offset of the link name in the local heap.
|
|
pub link_name_offset: u64,
|
|
/// Address of the child object's header.
|
|
pub object_header_address: u64,
|
|
/// Cache type: 0=none, 1=group, 2=symbolic link.
|
|
pub cache_type: u32,
|
|
/// 16-byte scratch pad (cached data).
|
|
pub scratch_pad: [u8; 16],
|
|
}
|
|
|
|
/// A parsed Symbol Table Node (SNOD).
|
|
#[derive(Debug, Clone)]
|
|
pub struct SymbolTableNode {
|
|
/// The symbol table entries.
|
|
pub entries: Vec<SymbolTableEntry>,
|
|
}
|
|
|
|
impl SymbolTableNode {
|
|
/// Parse a Symbol Table Node at the given offset in the file data.
|
|
pub fn parse(
|
|
file_data: &[u8],
|
|
offset: usize,
|
|
offset_size: u8,
|
|
) -> Result<SymbolTableNode, FormatError> {
|
|
Self::parse_in(file_data, offset as u64, offset_size)
|
|
}
|
|
|
|
/// [`Self::parse`] over any [`Storage`]: one read of the node's header,
|
|
/// one of its entries.
|
|
pub fn parse_in<S: Storage + ?Sized>(
|
|
file: &S,
|
|
offset: u64,
|
|
offset_size: u8,
|
|
) -> Result<SymbolTableNode, FormatError> {
|
|
// signature(4) + version(1) + reserved(1) + number_of_symbols(2) = 8
|
|
let header = read_exact_at(file, offset, 8)?;
|
|
|
|
if &header[..4] != b"SNOD" {
|
|
return Err(FormatError::InvalidSymbolTableNodeSignature);
|
|
}
|
|
|
|
let version = header[4];
|
|
if version != 1 {
|
|
return Err(FormatError::InvalidSymbolTableNodeVersion(version));
|
|
}
|
|
|
|
let num_symbols = u16::from_le_bytes([header[6], header[7]]) as usize;
|
|
|
|
let os = offset_size as usize;
|
|
// Each entry: link_name_offset(os) + obj_hdr_addr(os) + cache_type(4) + reserved(4) + scratch(16)
|
|
let entry_size = os + os + 4 + 4 + 16;
|
|
// `offset + 8` fits: the header's read checked it. The entries'
|
|
// read is the bounds check (`offset + 8 + entries > file length`,
|
|
// which cannot overflow: at most 65535 entries of 40 bytes).
|
|
let body = read_exact_at(file, offset + 8, num_symbols * entry_size)?;
|
|
let file_data: &[u8] = &body;
|
|
|
|
let mut entries = Vec::with_capacity(num_symbols);
|
|
for entry in file_data.chunks_exact(entry_size) {
|
|
let link_name_offset = read_offset(entry, 0, offset_size)?;
|
|
let object_header_address = read_offset(entry, os, offset_size)?;
|
|
let pos = 2 * os;
|
|
let cache_type =
|
|
u32::from_le_bytes([entry[pos], entry[pos + 1], entry[pos + 2], entry[pos + 3]]);
|
|
// reserved 4 bytes
|
|
let mut scratch_pad = [0u8; 16];
|
|
scratch_pad.copy_from_slice(&entry[pos + 8..pos + 24]);
|
|
|
|
entries.push(SymbolTableEntry {
|
|
link_name_offset,
|
|
object_header_address,
|
|
cache_type,
|
|
scratch_pad,
|
|
});
|
|
}
|
|
|
|
Ok(SymbolTableNode { entries })
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn parse_symbol_table_message_offset8() {
|
|
let mut data = Vec::new();
|
|
data.extend_from_slice(&0x1000u64.to_le_bytes()); // btree
|
|
data.extend_from_slice(&0x2000u64.to_le_bytes()); // heap
|
|
let msg = SymbolTableMessage::parse(&data, 8).unwrap();
|
|
assert_eq!(msg.btree_address, 0x1000);
|
|
assert_eq!(msg.local_heap_address, 0x2000);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_symbol_table_message_offset4() {
|
|
let mut data = Vec::new();
|
|
data.extend_from_slice(&0x800u32.to_le_bytes());
|
|
data.extend_from_slice(&0x900u32.to_le_bytes());
|
|
let msg = SymbolTableMessage::parse(&data, 4).unwrap();
|
|
assert_eq!(msg.btree_address, 0x800);
|
|
assert_eq!(msg.local_heap_address, 0x900);
|
|
}
|
|
|
|
fn build_snod(entries: &[(u64, u64, u32)], offset_size: u8) -> Vec<u8> {
|
|
let mut buf = Vec::new();
|
|
// Pad so SNOD is at offset 0
|
|
buf.extend_from_slice(b"SNOD");
|
|
buf.push(1); // version
|
|
buf.push(0); // reserved
|
|
buf.extend_from_slice(&(entries.len() as u16).to_le_bytes());
|
|
for &(name_off, ohdr_addr, cache_type) in entries {
|
|
match offset_size {
|
|
4 => {
|
|
buf.extend_from_slice(&(name_off as u32).to_le_bytes());
|
|
buf.extend_from_slice(&(ohdr_addr as u32).to_le_bytes());
|
|
}
|
|
8 => {
|
|
buf.extend_from_slice(&name_off.to_le_bytes());
|
|
buf.extend_from_slice(&ohdr_addr.to_le_bytes());
|
|
}
|
|
_ => panic!("test offset_size"),
|
|
}
|
|
buf.extend_from_slice(&cache_type.to_le_bytes());
|
|
buf.extend_from_slice(&0u32.to_le_bytes()); // reserved
|
|
buf.extend_from_slice(&[0u8; 16]); // scratch pad
|
|
}
|
|
buf
|
|
}
|
|
|
|
#[test]
|
|
fn parse_snod_two_entries() {
|
|
let data = build_snod(&[(0, 0x100, 0), (8, 0x200, 1)], 8);
|
|
let snod = SymbolTableNode::parse(&data, 0, 8).unwrap();
|
|
assert_eq!(snod.entries.len(), 2);
|
|
assert_eq!(snod.entries[0].link_name_offset, 0);
|
|
assert_eq!(snod.entries[0].object_header_address, 0x100);
|
|
assert_eq!(snod.entries[0].cache_type, 0);
|
|
assert_eq!(snod.entries[1].link_name_offset, 8);
|
|
assert_eq!(snod.entries[1].object_header_address, 0x200);
|
|
assert_eq!(snod.entries[1].cache_type, 1);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_snod_empty() {
|
|
let data = build_snod(&[], 8);
|
|
let snod = SymbolTableNode::parse(&data, 0, 8).unwrap();
|
|
assert_eq!(snod.entries.len(), 0);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_snod_invalid_signature() {
|
|
let mut data = build_snod(&[], 8);
|
|
data[0] = b'X';
|
|
let err = SymbolTableNode::parse(&data, 0, 8).unwrap_err();
|
|
assert_eq!(err, FormatError::InvalidSymbolTableNodeSignature);
|
|
}
|
|
|
|
#[test]
|
|
fn parse_snod_invalid_version() {
|
|
let mut data = build_snod(&[], 8);
|
|
data[4] = 2; // bad version
|
|
let err = SymbolTableNode::parse(&data, 0, 8).unwrap_err();
|
|
assert_eq!(err, FormatError::InvalidSymbolTableNodeVersion(2));
|
|
}
|
|
|
|
/// A near-`usize::MAX` SNOD offset must error cleanly, not overflow/panic.
|
|
#[test]
|
|
fn parse_snod_rejects_offset_overflow() {
|
|
let data = build_snod(&[], 8);
|
|
let result = SymbolTableNode::parse(&data, usize::MAX - 4, 8);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
/// A huge symbol count combined with a large entries_start must not
|
|
/// overflow the `needed` size computation.
|
|
#[test]
|
|
fn parse_snod_rejects_entries_size_overflow() {
|
|
let mut data = build_snod(&[], 8);
|
|
// num_symbols at offset 6..8 — set to max to blow up entries_start + num_symbols*entry_size
|
|
data[6] = 0xFF;
|
|
data[7] = 0xFF;
|
|
let result = SymbolTableNode::parse(&data, usize::MAX / 2, 8);
|
|
assert!(result.is_err());
|
|
}
|
|
|
|
/// Nodes, cut at every length and at an offset, parse identically
|
|
/// through a `read_at`-only storage.
|
|
#[test]
|
|
fn storage_parse_matches_slice_parse() {
|
|
use crate::storage::CountingStorage;
|
|
for os in [4u8, 8] {
|
|
let node = build_snod(&[(0, 0x100, 0), (8, 0x200, 1), (16, 0x300, 2)], os);
|
|
let mut bad = node.clone();
|
|
bad[4] = 2;
|
|
for full in [node, bad] {
|
|
for at in [0usize, 7] {
|
|
for cut in 0..=full.len() {
|
|
let mut f = vec![0u8; at];
|
|
f.extend_from_slice(&full[..cut]);
|
|
let storage = CountingStorage::new(f.clone());
|
|
let want = SymbolTableNode::parse(&f, at, os);
|
|
let got = SymbolTableNode::parse_in(&storage, at as u64, os);
|
|
assert_eq!(format!("{got:?}"), format!("{want:?}"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|