Files
clawhdf5/crates/clawhdf5-format/src/signature.rs
T
osobhandClaude Opus 5.5 a6e90f3ee3 fix(format): apply the base address of files with a user block
A file may start with a user block (h5py userblock_size, h5jam), putting
the superblock at 512, 1024, ...; every address in the file is then
relative to the superblock. The signature search found it, but every
reader passed the whole file to the parsers, so addresses landed
userblock bytes early and the root group failed with
InvalidObjectHeaderVersion (twithub.h5, twithub513.h5,
h5clear_fsm_persist_user_*.h5).

Readers now view the file from the superblock on, taking the signature's
position as the base address as libhdf5 does: File (mmap, buffered,
from_bytes), MmapFile, LazyFile, AsyncHDF5File, the VOL and MPI VOL
readers, the HNSW loader and external VDS source files. File, MmapFile
and LazyFile gain user_block_size(). The new signature::split_user_block
returns the two parts, and Superblock::parse refuses a non-zero offset
(UserBlockNotStripped) so a format-level caller cannot silently apply
superblock-relative addresses to the whole file.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-25 22:07:50 -05:00

136 lines
4.4 KiB
Rust

//! HDF5 file signature (magic bytes) detection.
use crate::error::FormatError;
/// 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)
}
/// 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));
}
}