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]>
173 lines
5.6 KiB
Rust
173 lines
5.6 KiB
Rust
//! HDF5 file signature (magic bytes) detection.
|
|
|
|
use crate::error::FormatError;
|
|
use crate::storage::{Storage, read_exact_at};
|
|
|
|
/// The 8-byte HDF5 magic signature.
|
|
pub const HDF5_SIGNATURE: [u8; 8] = [0x89, b'H', b'D', b'F', b'\r', b'\n', 0x1A, b'\n'];
|
|
|
|
/// Search for the HDF5 signature at valid offsets.
|
|
///
|
|
/// The HDF5 spec says the signature can appear at offset 0, 512, 1024, 2048, 4096, ...
|
|
/// (powers of two starting at 512, plus offset 0).
|
|
///
|
|
/// Returns the byte offset where the signature was found.
|
|
///
|
|
/// A non-zero offset means the file starts with a *user block*, and every
|
|
/// address inside the file is relative to the superblock's position, not to
|
|
/// byte 0 (libhdf5 uses the signature's position as the base address even
|
|
/// when the stored base-address field disagrees). The parsers in this crate
|
|
/// take addresses as indices into `file_data`, so they must be handed the
|
|
/// bytes from the signature on — use [`split_user_block`]. [`Superblock::parse`]
|
|
/// refuses a non-zero offset for this reason.
|
|
///
|
|
/// [`Superblock::parse`]: crate::superblock::Superblock::parse
|
|
pub fn find_signature(data: &[u8]) -> Result<usize, FormatError> {
|
|
// Check offset 0
|
|
if data.len() >= 8 && data[..8] == HDF5_SIGNATURE {
|
|
return Ok(0);
|
|
}
|
|
|
|
// Check powers of 2 starting at 512
|
|
let mut offset = 512;
|
|
while offset + 8 <= data.len() {
|
|
if data[offset..offset + 8] == HDF5_SIGNATURE {
|
|
return Ok(offset);
|
|
}
|
|
offset *= 2;
|
|
}
|
|
|
|
Err(FormatError::SignatureNotFound)
|
|
}
|
|
|
|
/// [`find_signature`] over any [`Storage`]: one 8-byte read per candidate
|
|
/// offset.
|
|
pub fn find_signature_in<S: Storage + ?Sized>(file: &S) -> Result<u64, FormatError> {
|
|
let len = file.len();
|
|
let mut offset = 0u64;
|
|
while offset.checked_add(8).is_some_and(|end| end <= len) {
|
|
if *read_exact_at(file, offset, 8)? == HDF5_SIGNATURE {
|
|
return Ok(offset);
|
|
}
|
|
offset = if offset == 0 { 512 } else { offset * 2 };
|
|
}
|
|
Err(FormatError::SignatureNotFound)
|
|
}
|
|
|
|
/// Split a file into its user block and its HDF5 bytes.
|
|
///
|
|
/// Returns `(user_block, hdf5)`: `user_block` is everything before the
|
|
/// superblock signature (empty for most files) and `hdf5` is the rest, in
|
|
/// which every HDF5 address is a plain index. Pass `hdf5` as `file_data` to
|
|
/// every parser in this crate, and parse the superblock at offset 0 of it.
|
|
pub fn split_user_block(data: &[u8]) -> Result<(&[u8], &[u8]), FormatError> {
|
|
let offset = find_signature(data)?;
|
|
Ok(data.split_at(offset))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn signature_at_offset_0() {
|
|
let mut data = vec![0u8; 64];
|
|
data[..8].copy_from_slice(&HDF5_SIGNATURE);
|
|
assert_eq!(find_signature(&data), Ok(0));
|
|
}
|
|
|
|
#[test]
|
|
fn signature_at_offset_512() {
|
|
let mut data = vec![0u8; 1024];
|
|
data[512..520].copy_from_slice(&HDF5_SIGNATURE);
|
|
assert_eq!(find_signature(&data), Ok(512));
|
|
}
|
|
|
|
#[test]
|
|
fn signature_at_offset_1024() {
|
|
let mut data = vec![0u8; 2048];
|
|
data[1024..1032].copy_from_slice(&HDF5_SIGNATURE);
|
|
assert_eq!(find_signature(&data), Ok(1024));
|
|
}
|
|
|
|
#[test]
|
|
fn signature_at_offset_2048() {
|
|
let mut data = vec![0u8; 4096];
|
|
data[2048..2056].copy_from_slice(&HDF5_SIGNATURE);
|
|
assert_eq!(find_signature(&data), Ok(2048));
|
|
}
|
|
|
|
#[test]
|
|
fn signature_not_found() {
|
|
let data = vec![0u8; 8192];
|
|
assert_eq!(find_signature(&data), Err(FormatError::SignatureNotFound));
|
|
}
|
|
|
|
#[test]
|
|
fn signature_not_found_empty() {
|
|
assert_eq!(find_signature(&[]), Err(FormatError::SignatureNotFound));
|
|
}
|
|
|
|
#[test]
|
|
fn signature_not_found_too_short() {
|
|
assert_eq!(
|
|
find_signature(&[0x89, b'H', b'D']),
|
|
Err(FormatError::SignatureNotFound)
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn signature_at_non_power_of_two_not_found() {
|
|
// Signature at offset 100 should NOT be found
|
|
let mut data = vec![0u8; 1024];
|
|
data[100..108].copy_from_slice(&HDF5_SIGNATURE);
|
|
assert_eq!(find_signature(&data), Err(FormatError::SignatureNotFound));
|
|
}
|
|
|
|
#[test]
|
|
fn split_user_block_rebases_at_the_signature() {
|
|
let mut data = vec![7u8; 1024];
|
|
data[512..520].copy_from_slice(&HDF5_SIGNATURE);
|
|
let (ub, hdf5) = split_user_block(&data).unwrap();
|
|
assert_eq!(ub.len(), 512);
|
|
assert_eq!(hdf5.len(), 512);
|
|
assert_eq!(&hdf5[..8], &HDF5_SIGNATURE);
|
|
|
|
data[..8].copy_from_slice(&HDF5_SIGNATURE);
|
|
let (ub, hdf5) = split_user_block(&data).unwrap();
|
|
assert!(ub.is_empty());
|
|
assert_eq!(hdf5.len(), 1024);
|
|
}
|
|
|
|
#[test]
|
|
fn signature_prefers_earliest() {
|
|
// Signature at both 0 and 512, should return 0
|
|
let mut data = vec![0u8; 1024];
|
|
data[..8].copy_from_slice(&HDF5_SIGNATURE);
|
|
data[512..520].copy_from_slice(&HDF5_SIGNATURE);
|
|
assert_eq!(find_signature(&data), Ok(0));
|
|
}
|
|
|
|
#[test]
|
|
fn find_signature_in_matches_slice_search() {
|
|
use crate::storage::CountingStorage;
|
|
for (len, at) in [
|
|
(0, None),
|
|
(7, None),
|
|
(8, Some(0)),
|
|
(600, Some(512)),
|
|
(5000, Some(4096)),
|
|
(3000, Some(2048)),
|
|
(3000, None),
|
|
] {
|
|
let mut data = vec![0u8; len];
|
|
if let Some(at) = at {
|
|
data[at..at + 8].copy_from_slice(&HDF5_SIGNATURE);
|
|
}
|
|
let want = find_signature(&data).map(|o| o as u64);
|
|
let got = find_signature_in(&CountingStorage::new(data));
|
|
assert_eq!(got, want, "{len} {at:?}");
|
|
}
|
|
}
|
|
}
|