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) <[email protected]>
This commit is contained in:
osobh
2026-09-26 12:44:52 -05:00
co-authored by Claude Opus 5.5
parent 6a4707d791
commit 512a6a753f
+58 -9
View File
@@ -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<u64, FormatError> {
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<u64, FormatError> {
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<Superblock, FormatError> {
if signature_offset != 0 {
return Err(FormatError::UserBlockNotStripped(signature_offset as u64));
Self::parse_in(&data, signature_offset as u64)
}
let d = data
.get(signature_offset..)
.ok_or(FormatError::UnexpectedEof {
expected: signature_offset + 1,
available: data.len(),
})?;
/// [`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<Superblock, FormatError> {
if signature_offset != 0 {
return Err(FormatError::UserBlockNotStripped(signature_offset));
}
// 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);
}
}
}