From 512a6a753fc3d83218c16f0af3859a802e7185c7 Mon Sep 17 00:00:00 2001 From: osobh Date: Sat, 26 Sep 2026 12:44:52 -0500 Subject: [PATCH] format: parse the superblock over Storage Superblock::parse_in(&dyn Storage, offset) reads one bounded window of 128 bytes (the largest superblock is 100) and runs the existing version parsers on it; parse and refresh_eof keep their &[u8] signatures as wrappers. No behaviour change: on a file longer than the window no bounds check can fail, and on a shorter one the window is the whole file. New test: every version and truncation parses to the same result through a read_at-only CountingStorage, in one read. Co-Authored-By: Claude Opus 5.5 (1M context) --- crates/clawhdf5-format/src/superblock.rs | 65 +++++++++++++++++++++--- 1 file changed, 57 insertions(+), 8 deletions(-) diff --git a/crates/clawhdf5-format/src/superblock.rs b/crates/clawhdf5-format/src/superblock.rs index a565321..318c945 100644 --- a/crates/clawhdf5-format/src/superblock.rs +++ b/crates/clawhdf5-format/src/superblock.rs @@ -7,6 +7,11 @@ use byteorder::{ByteOrder, LittleEndian}; use crate::error::FormatError; use crate::signature::HDF5_SIGNATURE; +use crate::storage::{Storage, read_upto}; + +/// Bytes read to parse a superblock: more than the largest one (version 1 +/// with 8-byte offsets and lengths, 100 bytes). +const SUPERBLOCK_READ_LEN: usize = 128; /// Parsed HDF5 superblock (all versions). #[derive(Debug, Clone, PartialEq, Eq)] @@ -161,7 +166,16 @@ impl Superblock { file_data: &[u8], signature_offset: usize, ) -> Result { - let refreshed = Superblock::parse(file_data, signature_offset)?; + self.refresh_eof_in(&file_data, signature_offset as u64) + } + + /// [`Self::refresh_eof`] over any [`Storage`]. + pub fn refresh_eof_in( + &mut self, + file: &dyn Storage, + signature_offset: u64, + ) -> Result { + let refreshed = Superblock::parse_in(file, signature_offset)?; self.eof_address = refreshed.eof_address; self.consistency_flags = refreshed.consistency_flags; Ok(self.eof_address) @@ -219,15 +233,20 @@ impl Superblock { /// [`FormatError::UserBlockNotStripped`] because the addresses in the /// returned superblock would otherwise be applied to the wrong bytes. pub fn parse(data: &[u8], signature_offset: usize) -> Result { + Self::parse_in(&data, signature_offset as u64) + } + + /// [`Self::parse`] over any [`Storage`]: one read of the first + /// [`SUPERBLOCK_READ_LEN`] bytes (fewer when the file is shorter, which + /// is then refused with the same end-of-file errors as a short slice). + pub fn parse_in(file: &dyn Storage, signature_offset: u64) -> Result { if signature_offset != 0 { - return Err(FormatError::UserBlockNotStripped(signature_offset as u64)); + return Err(FormatError::UserBlockNotStripped(signature_offset)); } - let d = data - .get(signature_offset..) - .ok_or(FormatError::UnexpectedEof { - expected: signature_offset + 1, - available: data.len(), - })?; + // Every bounds check below needs at most 100 bytes, so on a longer + // file none of them can fail and the window's length does not show. + let window = read_upto(file, 0, SUPERBLOCK_READ_LEN)?; + let d: &[u8] = &window; ensure_len(d, 9)?; // signature(8) + version(1) // Verify signature @@ -894,4 +913,34 @@ mod tests { assert_eq!(parsed.version, 3); assert_eq!(parsed.page_size, None); } + + /// Through a storage that serves only `read_at`, every version parses + /// to the same superblock, and every truncation to the same error, as + /// from a slice — in one read. + #[test] + fn parse_in_matches_slice_parse() { + use crate::storage::CountingStorage; + let mut files = vec![ + build_v0_bytes(8), + build_v0_bytes(4), + build_v1_bytes(8), + build_v1_bytes(4), + build_v2_bytes(8, 2), + build_v2_bytes(4, 3), + ]; + for f in files.clone() { + let mut long = f.clone(); + long.resize(4096, 0xAB); + files.push(long); + for cut in [0, 5, 9, 13, 20, 30, f.len() - 1] { + files.push(f[..cut.min(f.len())].to_vec()); + } + } + for f in files { + let want = Superblock::parse(&f, 0); + let storage = CountingStorage::new(f.clone()); + assert_eq!(Superblock::parse_in(&storage, 0), want, "{} bytes", f.len()); + assert_eq!(storage.reads(), 1); + } + } }