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:
@@ -7,6 +7,11 @@ use byteorder::{ByteOrder, LittleEndian};
|
|||||||
|
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
use crate::signature::HDF5_SIGNATURE;
|
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).
|
/// Parsed HDF5 superblock (all versions).
|
||||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||||
@@ -161,7 +166,16 @@ impl Superblock {
|
|||||||
file_data: &[u8],
|
file_data: &[u8],
|
||||||
signature_offset: usize,
|
signature_offset: usize,
|
||||||
) -> Result<u64, FormatError> {
|
) -> 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.eof_address = refreshed.eof_address;
|
||||||
self.consistency_flags = refreshed.consistency_flags;
|
self.consistency_flags = refreshed.consistency_flags;
|
||||||
Ok(self.eof_address)
|
Ok(self.eof_address)
|
||||||
@@ -219,15 +233,20 @@ impl Superblock {
|
|||||||
/// [`FormatError::UserBlockNotStripped`] because the addresses in the
|
/// [`FormatError::UserBlockNotStripped`] because the addresses in the
|
||||||
/// returned superblock would otherwise be applied to the wrong bytes.
|
/// returned superblock would otherwise be applied to the wrong bytes.
|
||||||
pub fn parse(data: &[u8], signature_offset: usize) -> Result<Superblock, FormatError> {
|
pub fn parse(data: &[u8], signature_offset: usize) -> Result<Superblock, FormatError> {
|
||||||
|
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<Superblock, FormatError> {
|
||||||
if signature_offset != 0 {
|
if signature_offset != 0 {
|
||||||
return Err(FormatError::UserBlockNotStripped(signature_offset as u64));
|
return Err(FormatError::UserBlockNotStripped(signature_offset));
|
||||||
}
|
}
|
||||||
let d = data
|
// Every bounds check below needs at most 100 bytes, so on a longer
|
||||||
.get(signature_offset..)
|
// file none of them can fail and the window's length does not show.
|
||||||
.ok_or(FormatError::UnexpectedEof {
|
let window = read_upto(file, 0, SUPERBLOCK_READ_LEN)?;
|
||||||
expected: signature_offset + 1,
|
let d: &[u8] = &window;
|
||||||
available: data.len(),
|
|
||||||
})?;
|
|
||||||
ensure_len(d, 9)?; // signature(8) + version(1)
|
ensure_len(d, 9)?; // signature(8) + version(1)
|
||||||
|
|
||||||
// Verify signature
|
// Verify signature
|
||||||
@@ -894,4 +913,34 @@ mod tests {
|
|||||||
assert_eq!(parsed.version, 3);
|
assert_eq!(parsed.version, 3);
|
||||||
assert_eq!(parsed.page_size, None);
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user