Read HDF5 1.6-era files, user blocks, VDS, dense attributes and large groups #13

Merged
osobh merged 28 commits from fix/p1-read-gaps into main 2026-09-26 09:42:10 +00:00
16 changed files with 540 additions and 79 deletions
Showing only changes of commit a6e90f3ee3 - Show all commits
+13
View File
@@ -273,6 +273,19 @@
- CI keeps zlib-ng building and tested; the arm64 job no longer needs cmake. - CI keeps zlib-ng building and tested; the arm64 job no longer needs cmake.
### Correctness ### Correctness
- **Files with a user block** (`h5py.File(..., userblock_size=N)`, `h5jam`;
the superblock at 512, 1024, …) could not be read: every address in the
file is relative to the superblock, but it was applied from byte 0
(`InvalidObjectHeaderVersion` on the root group). `File` (mmap, buffered,
`from_bytes`), `MmapFile`, `LazyFile`, `AsyncHDF5File`, the VOL readers,
the HNSW loader and external VDS sources now view the file from the
superblock on, using the signature's position as the base address as
libhdf5 does; `user_block_size()` reports the user block (h5py's
`userblock_size`), and `as_bytes()` returns the bytes from the superblock
on. **Breaking (format crate):** `Superblock::parse` refuses a non-zero
signature offset with `FormatError::UserBlockNotStripped`, since the
addresses it returns would be applied to the wrong bytes; pass the slice
from `signature::split_user_block` (new) and parse at offset 0.
- `clawhdf5-format` reader: version-1 shared messages (HDF5 1.6-era files, - `clawhdf5-format` reader: version-1 shared messages (HDF5 1.6-era files,
e.g. a dataset using a committed datatype in libhdf5's `tcompound.h5`) e.g. a dataset using a committed datatype in libhdf5's `tcompound.h5`)
read the heap-offset field of the embedded symbol-table entry as the read the heap-offset field of the embedded symbol-table entry as the
+4 -3
View File
@@ -13,7 +13,7 @@ use clawhdf5_format::filter_pipeline::FilterPipeline;
use clawhdf5_format::group_v2::resolve_path_any; use clawhdf5_format::group_v2::resolve_path_any;
use clawhdf5_format::message_type::MessageType; use clawhdf5_format::message_type::MessageType;
use clawhdf5_format::object_header::ObjectHeader; use clawhdf5_format::object_header::ObjectHeader;
use clawhdf5_format::signature::find_signature; use clawhdf5_format::signature::split_user_block;
use clawhdf5_format::superblock::Superblock; use clawhdf5_format::superblock::Superblock;
use clawhdf5_io::FileWriter as IoFileWriter; use clawhdf5_io::FileWriter as IoFileWriter;
@@ -861,8 +861,9 @@ impl HnswIndex {
/// The HDF5 data must contain the `/ann/vectors`, `/ann/graph_layer_*`, /// The HDF5 data must contain the `/ann/vectors`, `/ann/graph_layer_*`,
/// and `/ann/config` datasets as produced by [`to_hdf5_bytes`]. /// and `/ann/config` datasets as produced by [`to_hdf5_bytes`].
pub fn load_from_hdf5(data: &[u8]) -> Result<Self, FormatError> { pub fn load_from_hdf5(data: &[u8]) -> Result<Self, FormatError> {
let sig_offset = find_signature(data)?; // Addresses are relative to the superblock: skip any user block.
let sb = Superblock::parse(data, sig_offset)?; let (_, data) = split_user_block(data)?;
let sb = Superblock::parse(data, 0)?;
// Read config dataset and its attributes // Read config dataset and its attributes
let config_attrs = read_dataset_attrs(data, &sb, "ann/config")?; let config_attrs = read_dataset_attrs(data, &sb, "ann/config")?;
+5 -3
View File
@@ -575,11 +575,13 @@ fn read_named_dataset_raw(
use crate::group_v2::resolve_path_any; use crate::group_v2::resolve_path_any;
use crate::message_type::MessageType; use crate::message_type::MessageType;
use crate::object_header::ObjectHeader; use crate::object_header::ObjectHeader;
use crate::signature::find_signature; use crate::signature::split_user_block;
use crate::superblock::Superblock; use crate::superblock::Superblock;
let sig = find_signature(file_data)?; // An external source file is handed over whole, user block included;
let sb = Superblock::parse(file_data, sig)?; // its addresses are relative to its superblock.
let (_, file_data) = split_user_block(file_data)?;
let sb = Superblock::parse(file_data, 0)?;
let addr = resolve_path_any(file_data, &sb, path)?; let addr = resolve_path_any(file_data, &sb, path)?;
let hdr = ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size)?; let hdr = ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size)?;
+10
View File
@@ -120,6 +120,11 @@ pub enum FormatError {
/// A shared-message reference points at an object header that holds no /// A shared-message reference points at an object header that holds no
/// (unshared) message of the referenced type (raw message type id). /// (unshared) message of the referenced type (raw message type id).
SharedMessageTargetMissing(u16), SharedMessageTargetMissing(u16),
/// A superblock was parsed at a non-zero offset of the buffer (the file
/// has a user block of this many bytes). HDF5 addresses are relative to
/// the superblock, so the buffer must start there: see
/// `signature::split_user_block`.
UserBlockNotStripped(u64),
/// A selection does not fit the dataset it was applied to (wrong rank, or /// A selection does not fit the dataset it was applied to (wrong rank, or
/// it reaches past a dimension's extent). /// it reaches past a dimension's extent).
SelectionOutOfBounds(String), SelectionOutOfBounds(String),
@@ -342,6 +347,11 @@ impl fmt::Display for FormatError {
FormatError::SelectionOutOfBounds(msg) => { FormatError::SelectionOutOfBounds(msg) => {
write!(f, "selection out of bounds: {msg}") write!(f, "selection out of bounds: {msg}")
} }
FormatError::UserBlockNotStripped(n) => write!(
f,
"file has a {n}-byte user block: parse the bytes from the superblock on \
(signature::split_user_block)"
),
FormatError::SharedMessageTargetMissing(t) => write!( FormatError::SharedMessageTargetMissing(t) => write!(
f, f,
"shared message reference points at an object header with no message of type \ "shared message reference points at an object header with no message of type \
+6 -5
View File
@@ -26,12 +26,13 @@
//! use clawhdf5_format::{signature, superblock, object_header, group_v2, //! use clawhdf5_format::{signature, superblock, object_header, group_v2,
//! datatype, dataspace, data_layout, data_read, message_type::MessageType}; //! datatype, dataspace, data_layout, data_read, message_type::MessageType};
//! //!
//! let file_data = std::fs::read("output.h5").unwrap(); //! let bytes = std::fs::read("output.h5").unwrap();
//! let sig = signature::find_signature(&file_data).unwrap(); //! // Addresses are relative to the superblock: skip any user block.
//! let sb = superblock::Superblock::parse(&file_data, sig).unwrap(); //! let (_user_block, file_data) = signature::split_user_block(&bytes).unwrap();
//! let addr = group_v2::resolve_path_any(&file_data, &sb, "data").unwrap(); //! let sb = superblock::Superblock::parse(file_data, 0).unwrap();
//! let addr = group_v2::resolve_path_any(file_data, &sb, "data").unwrap();
//! let hdr = object_header::ObjectHeader::parse( //! let hdr = object_header::ObjectHeader::parse(
//! &file_data, addr as usize, sb.offset_size, sb.length_size).unwrap(); //! file_data, addr as usize, sb.offset_size, sb.length_size).unwrap();
//! ``` //! ```
//! //!
//! # Features //! # Features
+2 -2
View File
@@ -408,8 +408,8 @@ pub fn load_sohm_table(
offset_size: u8, offset_size: u8,
length_size: u8, length_size: u8,
) -> Result<Option<SohmTable>, FormatError> { ) -> Result<Option<SohmTable>, FormatError> {
let sig = crate::signature::find_signature(file_data)?; // `file_data` starts at the superblock (see `signature::split_user_block`).
let sb = crate::superblock::Superblock::parse(file_data, sig)?; let sb = crate::superblock::Superblock::parse(file_data, 0)?;
let Some(ext_addr) = sb let Some(ext_addr) = sb
.superblock_extension_address .superblock_extension_address
.filter(|&a| !is_undefined(a, offset_size)) .filter(|&a| !is_undefined(a, offset_size))
+36
View File
@@ -11,6 +11,16 @@ pub const HDF5_SIGNATURE: [u8; 8] = [0x89, b'H', b'D', b'F', b'\r', b'\n', 0x1A,
/// (powers of two starting at 512, plus offset 0). /// (powers of two starting at 512, plus offset 0).
/// ///
/// Returns the byte offset where the signature was found. /// 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> { pub fn find_signature(data: &[u8]) -> Result<usize, FormatError> {
// Check offset 0 // Check offset 0
if data.len() >= 8 && data[..8] == HDF5_SIGNATURE { if data.len() >= 8 && data[..8] == HDF5_SIGNATURE {
@@ -29,6 +39,17 @@ pub fn find_signature(data: &[u8]) -> Result<usize, FormatError> {
Err(FormatError::SignatureNotFound) 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)] #[cfg(test)]
mod tests { mod tests {
use super::*; use super::*;
@@ -88,6 +109,21 @@ mod tests {
assert_eq!(find_signature(&data), Err(FormatError::SignatureNotFound)); 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] #[test]
fn signature_prefers_earliest() { fn signature_prefers_earliest() {
// Signature at both 0 and 512, should return 0 // Signature at both 0 and 512, should return 0
+21 -2
View File
@@ -174,8 +174,18 @@ impl Superblock {
/// Parse a superblock from `data` starting at `signature_offset`. /// Parse a superblock from `data` starting at `signature_offset`.
/// ///
/// The signature must be present at the given offset. /// The signature must be present at the given offset, and that offset
/// must be 0: every address in an HDF5 file is relative to the
/// superblock, so when a file has a user block (signature at 512, 1024,
/// …) the caller must pass the bytes from the signature on — see
/// [`crate::signature::split_user_block`] — and use that slice as
/// `file_data` everywhere. A non-zero offset is refused with
/// [`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> { pub fn parse(data: &[u8], signature_offset: usize) -> Result<Superblock, FormatError> {
if signature_offset != 0 {
return Err(FormatError::UserBlockNotStripped(signature_offset as u64));
}
let d = data let d = data
.get(signature_offset..) .get(signature_offset..)
.ok_or(FormatError::UnexpectedEof { .ok_or(FormatError::UnexpectedEof {
@@ -676,7 +686,16 @@ mod tests {
let mut data = vec![0u8; 1024]; let mut data = vec![0u8; 1024];
let v0 = build_v0_bytes(8); let v0 = build_v0_bytes(8);
data[512..512 + v0.len()].copy_from_slice(&v0); data[512..512 + v0.len()].copy_from_slice(&v0);
let sb = Superblock::parse(&data, 512).unwrap(); // Addresses are relative to the superblock, so parsing in place
// (where they would be applied to the whole buffer) is refused...
assert_eq!(
Superblock::parse(&data, 512),
Err(FormatError::UserBlockNotStripped(512))
);
// ...and the caller parses the bytes from the signature on.
let (ub, hdf5) = crate::signature::split_user_block(&data).unwrap();
assert_eq!(ub.len(), 512);
let sb = Superblock::parse(hdf5, 0).unwrap();
assert_eq!(sb.version, 0); assert_eq!(sb.version, 0);
assert_eq!(sb.root_group_address, 96); assert_eq!(sb.root_group_address, 96);
} }
+10 -12
View File
@@ -268,28 +268,26 @@ impl AsyncHDF5File {
/// ///
/// Reads the entire file into memory, then parses the superblock. /// Reads the entire file into memory, then parses the superblock.
pub async fn open<R: AsyncHDF5Read>(reader: &R) -> Result<Self, AsyncHDF5Error> { pub async fn open<R: AsyncHDF5Read>(reader: &R) -> Result<Self, AsyncHDF5Error> {
let data = reader.read_all().await?; Self::from_bytes(reader.read_all().await?)
let sig_offset = find_signature(&data)?;
let superblock = Superblock::parse(&data, sig_offset)?;
Ok(Self { data, superblock })
} }
/// Open an HDF5 file asynchronously from a file path. /// Open an HDF5 file asynchronously from a file path.
pub async fn open_path<P: AsRef<Path>>(path: P) -> Result<Self, AsyncHDF5Error> { pub async fn open_path<P: AsRef<Path>>(path: P) -> Result<Self, AsyncHDF5Error> {
let data = tokio::fs::read(path).await?; Self::from_bytes(tokio::fs::read(path).await?)
let sig_offset = find_signature(&data)?;
let superblock = Superblock::parse(&data, sig_offset)?;
Ok(Self { data, superblock })
} }
/// Open an HDF5 file from bytes already in memory. /// Open an HDF5 file from bytes already in memory.
pub fn from_bytes(data: Vec<u8>) -> Result<Self, AsyncHDF5Error> { pub fn from_bytes(mut data: Vec<u8>) -> Result<Self, AsyncHDF5Error> {
let sig_offset = find_signature(&data)?; // HDF5 addresses are relative to the superblock: drop any user block
let superblock = Superblock::parse(&data, sig_offset)?; // so they index `data` directly.
let user_block = find_signature(&data)?;
data.drain(..user_block);
let superblock = Superblock::parse(&data, 0)?;
Ok(Self { data, superblock }) Ok(Self { data, superblock })
} }
/// Access the raw file bytes. /// Access the file bytes from the superblock on (any user block is
/// dropped on open).
pub fn as_bytes(&self) -> &[u8] { pub fn as_bytes(&self) -> &[u8] {
&self.data &self.data
} }
+5 -4
View File
@@ -180,7 +180,7 @@ fn mpi_collective_read(vol: &MpiVol, location: &str, path: &str) -> Result<Vec<u
use clawhdf5_format::{ use clawhdf5_format::{
data_layout::DataLayout, data_read::read_raw_data_full, dataspace::Dataspace, data_layout::DataLayout, data_read::read_raw_data_full, dataspace::Dataspace,
datatype::Datatype, filter_pipeline::FilterPipeline, group_v2::resolve_path_any, datatype::Datatype, filter_pipeline::FilterPipeline, group_v2::resolve_path_any,
message_type::MessageType, object_header::ObjectHeader, signature::find_signature, message_type::MessageType, object_header::ObjectHeader, signature::split_user_block,
superblock::Superblock, superblock::Superblock,
}; };
use mpi::traits::*; use mpi::traits::*;
@@ -192,9 +192,10 @@ fn mpi_collective_read(vol: &MpiVol, location: &str, path: &str) -> Result<Vec<u
let mut len_buf = [0usize; 1]; let mut len_buf = [0usize; 1];
if rank == 0 { if rank == 0 {
let bytes = std::fs::read(location).map_err(VolError::Io)?; let file = std::fs::read(location).map_err(VolError::Io)?;
let sig = find_signature(&bytes).map_err(|e| VolError::DataError(e.to_string()))?; // Addresses are relative to the superblock: skip any user block.
let sb = Superblock::parse(&bytes, sig).map_err(|e| VolError::DataError(e.to_string()))?; let (_, bytes) = split_user_block(&file).map_err(|e| VolError::DataError(e.to_string()))?;
let sb = Superblock::parse(bytes, 0).map_err(|e| VolError::DataError(e.to_string()))?;
let addr = resolve_path_any(&bytes, &sb, path) let addr = resolve_path_any(&bytes, &sb, path)
.map_err(|e| VolError::NotFound(format!("{path}: {e}")))?; .map_err(|e| VolError::NotFound(format!("{path}: {e}")))?;
let oh = ObjectHeader::parse(&bytes, addr as usize, sb.offset_size, sb.length_size) let oh = ObjectHeader::parse(&bytes, addr as usize, sb.offset_size, sb.length_size)
+4 -3
View File
@@ -283,12 +283,13 @@ impl VirtualObjectLayer for NativeVol {
use clawhdf5_format::{ use clawhdf5_format::{
data_layout::DataLayout, data_read::read_raw_data_full, dataspace::Dataspace, data_layout::DataLayout, data_read::read_raw_data_full, dataspace::Dataspace,
datatype::Datatype, filter_pipeline::FilterPipeline, group_v2::resolve_path_any, datatype::Datatype, filter_pipeline::FilterPipeline, group_v2::resolve_path_any,
message_type::MessageType, object_header::ObjectHeader, signature::find_signature, message_type::MessageType, object_header::ObjectHeader, signature::split_user_block,
superblock::Superblock, superblock::Superblock,
}; };
let sig = find_signature(data).map_err(|e| VolError::DataError(e.to_string()))?; // Addresses are relative to the superblock: skip any user block.
let sb = Superblock::parse(data, sig).map_err(|e| VolError::DataError(e.to_string()))?; let (_, data) = split_user_block(data).map_err(|e| VolError::DataError(e.to_string()))?;
let sb = Superblock::parse(data, 0).map_err(|e| VolError::DataError(e.to_string()))?;
let addr = resolve_path_any(data, &sb, path) let addr = resolve_path_any(data, &sb, path)
.map_err(|e| VolError::NotFound(format!("{path}: {e}")))?; .map_err(|e| VolError::NotFound(format!("{path}: {e}")))?;
+28 -14
View File
@@ -42,6 +42,9 @@ use crate::types::{AttrValue, DType, attrs_to_map, classify_datatype};
/// `MemoryReader`, etc. /// `MemoryReader`, etc.
pub struct LazyFile<R: HDF5Read> { pub struct LazyFile<R: HDF5Read> {
reader: R, reader: R,
/// Offset of the superblock in the file (the user-block size); every
/// HDF5 address is relative to it.
base: usize,
superblock: Superblock, superblock: Superblock,
root_header: ObjectHeader, root_header: ObjectHeader,
/// Cache of parsed object headers, keyed by address. /// Cache of parsed object headers, keyed by address.
@@ -73,9 +76,9 @@ impl<R: HDF5Read> LazyFile<R> {
/// ///
/// Parses only the superblock and root group object header. /// Parses only the superblock and root group object header.
pub fn open(reader: R) -> Result<Self, Error> { pub fn open(reader: R) -> Result<Self, Error> {
let data = reader.as_bytes(); let (user_block, data) = signature::split_user_block(reader.as_bytes())?;
let sig_offset = signature::find_signature(data)?; let base = user_block.len();
let superblock = Superblock::parse(data, sig_offset)?; let superblock = Superblock::parse(data, 0)?;
let root_header = ObjectHeader::parse( let root_header = ObjectHeader::parse(
data, data,
superblock.root_group_address as usize, superblock.root_group_address as usize,
@@ -84,15 +87,26 @@ impl<R: HDF5Read> LazyFile<R> {
)?; )?;
Ok(Self { Ok(Self {
reader, reader,
base,
superblock, superblock,
root_header, root_header,
header_cache: RefCell::new(HashMap::new()), header_cache: RefCell::new(HashMap::new()),
}) })
} }
/// Returns the raw file bytes. /// Returns the file's bytes from the superblock on (after any user
/// block), which is the space every HDF5 address in the file indexes.
pub fn as_bytes(&self) -> &[u8] { pub fn as_bytes(&self) -> &[u8] {
self.reader.as_bytes() self.hdf5_bytes()
}
/// Size of the user block before the superblock (0 for most files).
pub fn user_block_size(&self) -> u64 {
self.base as u64
}
fn hdf5_bytes(&self) -> &[u8] {
&self.reader.as_bytes()[self.base..]
} }
/// Returns a reference to the parsed superblock. /// Returns a reference to the parsed superblock.
@@ -110,7 +124,7 @@ impl<R: HDF5Read> LazyFile<R> {
/// Resolve a path and return a `LazyDataset` handle. /// Resolve a path and return a `LazyDataset` handle.
pub fn dataset(&self, path: &str) -> Result<LazyDataset<'_, R>, Error> { pub fn dataset(&self, path: &str) -> Result<LazyDataset<'_, R>, Error> {
let data = self.reader.as_bytes(); let data = self.hdf5_bytes();
let addr = group_v2::resolve_path_any(data, &self.superblock, path)?; let addr = group_v2::resolve_path_any(data, &self.superblock, path)?;
let hdr = self.get_or_parse_header(addr)?; let hdr = self.get_or_parse_header(addr)?;
if !has_message(&hdr, MessageType::DataLayout) { if !has_message(&hdr, MessageType::DataLayout) {
@@ -124,7 +138,7 @@ impl<R: HDF5Read> LazyFile<R> {
/// Resolve a path and return a `LazyGroup` handle. /// Resolve a path and return a `LazyGroup` handle.
pub fn group(&self, path: &str) -> Result<LazyGroup<'_, R>, Error> { pub fn group(&self, path: &str) -> Result<LazyGroup<'_, R>, Error> {
let data = self.reader.as_bytes(); let data = self.hdf5_bytes();
let addr = group_v2::resolve_path_any(data, &self.superblock, path)?; let addr = group_v2::resolve_path_any(data, &self.superblock, path)?;
Ok(LazyGroup { Ok(LazyGroup {
file: self, file: self,
@@ -163,7 +177,7 @@ impl<R: HDF5Read> LazyFile<R> {
} }
// Parse and cache // Parse and cache
let data = self.reader.as_bytes(); let data = self.hdf5_bytes();
let hdr = ObjectHeader::parse( let hdr = ObjectHeader::parse(
data, data,
address as usize, address as usize,
@@ -187,7 +201,7 @@ impl<R: HDF5Read> LazyFile<R> {
impl<R: HDF5Read> std::fmt::Debug for LazyFile<R> { impl<R: HDF5Read> std::fmt::Debug for LazyFile<R> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("LazyFile") f.debug_struct("LazyFile")
.field("size", &self.reader.as_bytes().len()) .field("size", &self.hdf5_bytes().len())
.field("superblock_version", &self.superblock.version) .field("superblock_version", &self.superblock.version)
.field("cached_headers", &self.header_cache.borrow().len()) .field("cached_headers", &self.header_cache.borrow().len())
.finish() .finish()
@@ -234,7 +248,7 @@ impl<'f, R: HDF5Read> LazyGroup<'f, R> {
/// Read all attributes of this group. /// Read all attributes of this group.
pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> { pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
let hdr = self.file.get_or_parse_header(self.address)?; let hdr = self.file.get_or_parse_header(self.address)?;
let data = self.file.reader.as_bytes(); let data = self.file.hdf5_bytes();
let attr_msgs = let attr_msgs =
extract_attributes_full(data, &hdr, self.file.offset_size(), self.file.length_size())?; extract_attributes_full(data, &hdr, self.file.offset_size(), self.file.length_size())?;
Ok(attrs_to_map( Ok(attrs_to_map(
@@ -277,7 +291,7 @@ impl<'f, R: HDF5Read> LazyGroup<'f, R> {
fn children(&self) -> Result<Vec<GroupEntry>, Error> { fn children(&self) -> Result<Vec<GroupEntry>, Error> {
let hdr = self.file.get_or_parse_header(self.address)?; let hdr = self.file.get_or_parse_header(self.address)?;
let data = self.file.reader.as_bytes(); let data = self.file.hdf5_bytes();
let os = self.file.offset_size(); let os = self.file.offset_size();
let ls = self.file.length_size(); let ls = self.file.length_size();
resolve_group_entries(data, &hdr, os, ls).map_err(Error::Format) resolve_group_entries(data, &hdr, os, ls).map_err(Error::Format)
@@ -360,7 +374,7 @@ impl<'f, R: HDF5Read> LazyDataset<'f, R> {
let dl = self.data_layout()?; let dl = self.data_layout()?;
let ds = self.dataspace()?; let ds = self.dataspace()?;
let dt = self.datatype()?; let dt = self.datatype()?;
let slice = data_read::read_raw_data_zerocopy(self.file.reader.as_bytes(), &dl, &ds, &dt)?; let slice = data_read::read_raw_data_zerocopy(self.file.hdf5_bytes(), &dl, &ds, &dt)?;
Ok(slice) Ok(slice)
} }
@@ -401,7 +415,7 @@ impl<'f, R: HDF5Read> LazyDataset<'f, R> {
/// Read all attributes of this dataset. /// Read all attributes of this dataset.
pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> { pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
let data = self.file.reader.as_bytes(); let data = self.file.hdf5_bytes();
let attr_msgs = extract_attributes_full( let attr_msgs = extract_attributes_full(
data, data,
&self.header, &self.header,
@@ -479,7 +493,7 @@ impl<'f, R: HDF5Read> LazyDataset<'f, R> {
let ds = self.dataspace()?; let ds = self.dataspace()?;
let dl = self.data_layout()?; let dl = self.data_layout()?;
let pipeline = self.filter_pipeline()?; let pipeline = self.filter_pipeline()?;
let data = self.file.reader.as_bytes(); let data = self.file.hdf5_bytes();
// Unallocated storage reads as the dataset's fill value. // Unallocated storage reads as the dataset's fill value.
clawhdf5_format::fill_value::read_full_with_fill( clawhdf5_format::fill_value::read_full_with_fill(
&self.header.messages, &self.header.messages,
+35 -15
View File
@@ -34,6 +34,9 @@ use crate::types::{AttrValue, DType, attrs_to_map, classify_datatype};
/// `&[u8]` slice via [`MmapDataset::read_raw_slice`]. /// `&[u8]` slice via [`MmapDataset::read_raw_slice`].
pub struct MmapFile { pub struct MmapFile {
reader: MmapReader, reader: MmapReader,
/// Offset of the superblock in the mapped file (the user-block size);
/// every HDF5 address is relative to it.
base: usize,
superblock: Superblock, superblock: Superblock,
} }
@@ -41,10 +44,25 @@ impl MmapFile {
/// Open an HDF5 file using memory-mapped I/O. /// Open an HDF5 file using memory-mapped I/O.
pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> { pub fn open<P: AsRef<std::path::Path>>(path: P) -> Result<Self, Error> {
let reader = MmapReader::open(path).map_err(Error::Io)?; let reader = MmapReader::open(path).map_err(Error::Io)?;
let data = reader.as_bytes(); let (user_block, data) = signature::split_user_block(reader.as_bytes())?;
let sig_offset = signature::find_signature(data)?; let base = user_block.len();
let superblock = Superblock::parse(data, sig_offset)?; let superblock = Superblock::parse(data, 0)?;
Ok(Self { reader, superblock }) Ok(Self {
reader,
base,
superblock,
})
}
/// The file's bytes from the superblock on — the space HDF5 addresses
/// index into.
fn hdf5_bytes(&self) -> &[u8] {
&self.reader.as_bytes()[self.base..]
}
/// Size of the user block before the superblock (0 for most files).
pub fn user_block_size(&self) -> u64 {
self.base as u64
} }
/// Returns a handle to the root group. /// Returns a handle to the root group.
@@ -57,7 +75,7 @@ impl MmapFile {
/// Resolve a path and return a `MmapDataset` handle. /// Resolve a path and return a `MmapDataset` handle.
pub fn dataset(&self, path: &str) -> Result<MmapDataset<'_>, Error> { pub fn dataset(&self, path: &str) -> Result<MmapDataset<'_>, Error> {
let data = self.reader.as_bytes(); let data = self.hdf5_bytes();
let addr = group_v2::resolve_path_any(data, &self.superblock, path)?; let addr = group_v2::resolve_path_any(data, &self.superblock, path)?;
let hdr = self.parse_header(addr)?; let hdr = self.parse_header(addr)?;
if !has_message(&hdr, MessageType::DataLayout) { if !has_message(&hdr, MessageType::DataLayout) {
@@ -71,7 +89,7 @@ impl MmapFile {
/// Resolve a path and return a `MmapGroup` handle. /// Resolve a path and return a `MmapGroup` handle.
pub fn group(&self, path: &str) -> Result<MmapGroup<'_>, Error> { pub fn group(&self, path: &str) -> Result<MmapGroup<'_>, Error> {
let data = self.reader.as_bytes(); let data = self.hdf5_bytes();
let addr = group_v2::resolve_path_any(data, &self.superblock, path)?; let addr = group_v2::resolve_path_any(data, &self.superblock, path)?;
Ok(MmapGroup { Ok(MmapGroup {
file: self, file: self,
@@ -79,9 +97,11 @@ impl MmapFile {
}) })
} }
/// Returns the raw file bytes (zero-copy from mmap). /// Returns the file's bytes from the superblock on (after any user
/// block), zero-copy from the mmap. Every HDF5 address in the file
/// indexes this slice.
pub fn as_bytes(&self) -> &[u8] { pub fn as_bytes(&self) -> &[u8] {
self.reader.as_bytes() self.hdf5_bytes()
} }
/// Returns a reference to the parsed superblock. /// Returns a reference to the parsed superblock.
@@ -91,7 +111,7 @@ impl MmapFile {
fn parse_header(&self, address: u64) -> Result<ObjectHeader, FormatError> { fn parse_header(&self, address: u64) -> Result<ObjectHeader, FormatError> {
ObjectHeader::parse( ObjectHeader::parse(
self.reader.as_bytes(), self.hdf5_bytes(),
address as usize, address as usize,
self.superblock.offset_size, self.superblock.offset_size,
self.superblock.length_size, self.superblock.length_size,
@@ -155,7 +175,7 @@ impl<'f> MmapGroup<'f> {
/// Read all attributes of this group. /// Read all attributes of this group.
pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> { pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
let data = self.file.reader.as_bytes(); let data = self.file.hdf5_bytes();
let hdr = self.file.parse_header(self.address)?; let hdr = self.file.parse_header(self.address)?;
let attr_msgs = let attr_msgs =
extract_attributes_full(data, &hdr, self.file.offset_size(), self.file.length_size())?; extract_attributes_full(data, &hdr, self.file.offset_size(), self.file.length_size())?;
@@ -198,7 +218,7 @@ impl<'f> MmapGroup<'f> {
} }
fn children(&self) -> Result<Vec<GroupEntry>, Error> { fn children(&self) -> Result<Vec<GroupEntry>, Error> {
let data = self.file.reader.as_bytes(); let data = self.file.hdf5_bytes();
let hdr = self.file.parse_header(self.address)?; let hdr = self.file.parse_header(self.address)?;
let os = self.file.offset_size(); let os = self.file.offset_size();
let ls = self.file.length_size(); let ls = self.file.length_size();
@@ -326,7 +346,7 @@ impl<'f> MmapDataset<'f> {
actual: sz, actual: sz,
})); }));
} }
let data = self.file.reader.as_bytes(); let data = self.file.hdf5_bytes();
let a = addr as usize; let a = addr as usize;
if a + sz > data.len() { if a + sz > data.len() {
return Err(Error::Format(FormatError::UnexpectedEof { return Err(Error::Format(FormatError::UnexpectedEof {
@@ -342,7 +362,7 @@ impl<'f> MmapDataset<'f> {
/// Read all attributes of this dataset. /// Read all attributes of this dataset.
pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> { pub fn attrs(&self) -> Result<HashMap<String, AttrValue>, Error> {
let data = self.file.reader.as_bytes(); let data = self.file.hdf5_bytes();
let attr_msgs = extract_attributes_full( let attr_msgs = extract_attributes_full(
data, data,
&self.header, &self.header,
@@ -423,7 +443,7 @@ impl<'f> MmapDataset<'f> {
// Unallocated storage reads as the dataset's fill value. // Unallocated storage reads as the dataset's fill value.
clawhdf5_format::fill_value::read_full_with_fill( clawhdf5_format::fill_value::read_full_with_fill(
&self.header.messages, &self.header.messages,
self.file.reader.as_bytes(), self.file.hdf5_bytes(),
&dl, &dl,
&ds, &ds,
dt.type_size() as usize, dt.type_size() as usize,
@@ -431,7 +451,7 @@ impl<'f> MmapDataset<'f> {
self.file.length_size(), self.file.length_size(),
|| { || {
Ok(data_read::read_raw_data_full( Ok(data_read::read_raw_data_full(
self.file.reader.as_bytes(), self.file.hdf5_bytes(),
&dl, &dl,
&ds, &ds,
&dt, &dt,
+44 -16
View File
@@ -31,20 +31,43 @@ use crate::types::{AttrValue, DType, attrs_to_map, classify_datatype};
// --------------------------------------------------------------------------- // ---------------------------------------------------------------------------
/// Internal storage: either an owned `Vec<u8>` or a memory-mapped region. /// Internal storage: either an owned `Vec<u8>` or a memory-mapped region.
enum FileData { enum Backing {
Owned(Vec<u8>), Owned(Vec<u8>),
#[cfg(feature = "mmap")] #[cfg(feature = "mmap")]
Mmap(clawhdf5_io::MmapReader), Mmap(clawhdf5_io::MmapReader),
} }
impl FileData { impl Backing {
fn as_bytes(&self) -> &[u8] { fn whole_file(&self) -> &[u8] {
match self { match self {
FileData::Owned(v) => v, Backing::Owned(v) => v,
#[cfg(feature = "mmap")] #[cfg(feature = "mmap")]
FileData::Mmap(r) => r.as_bytes(), Backing::Mmap(r) => r.as_bytes(),
} }
} }
}
/// The file's bytes, viewed from the superblock on. A file may start with a
/// user block (the superblock at 512, 1024, …); every HDF5 address is
/// relative to the superblock, so all parsing goes through [`Self::as_bytes`].
struct FileData {
backing: Backing,
/// Offset of the superblock in the file (the user-block size).
base: usize,
}
impl FileData {
/// Locate the superblock and parse it.
fn new(backing: Backing) -> Result<(Self, Superblock), Error> {
let (user_block, hdf5) = signature::split_user_block(backing.whole_file())?;
let base = user_block.len();
let superblock = Superblock::parse(hdf5, 0)?;
Ok((Self { backing, base }, superblock))
}
fn as_bytes(&self) -> &[u8] {
&self.backing.whole_file()[self.base..]
}
fn len(&self) -> usize { fn len(&self) -> usize {
self.as_bytes().len() self.as_bytes().len()
@@ -81,11 +104,9 @@ impl File {
#[cfg(feature = "mmap")] #[cfg(feature = "mmap")]
{ {
let reader = clawhdf5_io::MmapReader::open(path).map_err(Error::Io)?; let reader = clawhdf5_io::MmapReader::open(path).map_err(Error::Io)?;
let data_ref = reader.as_bytes(); let (data, superblock) = FileData::new(Backing::Mmap(reader))?;
let sig_offset = signature::find_signature(data_ref)?;
let superblock = Superblock::parse(data_ref, sig_offset)?;
Ok(Self { Ok(Self {
data: FileData::Mmap(reader), data,
superblock, superblock,
chunk_cache: ChunkCache::new(), chunk_cache: ChunkCache::new(),
base_dir, base_dir,
@@ -116,10 +137,9 @@ impl File {
/// In-memory files have no directory, so external Virtual Dataset sources /// In-memory files have no directory, so external Virtual Dataset sources
/// cannot be resolved automatically (same-file VDS still works). /// cannot be resolved automatically (same-file VDS still works).
pub fn from_bytes(data: Vec<u8>) -> Result<Self, Error> { pub fn from_bytes(data: Vec<u8>) -> Result<Self, Error> {
let sig_offset = signature::find_signature(&data)?; let (data, superblock) = FileData::new(Backing::Owned(data))?;
let superblock = Superblock::parse(&data, sig_offset)?;
Ok(Self { Ok(Self {
data: FileData::Owned(data), data,
superblock, superblock,
chunk_cache: ChunkCache::new(), chunk_cache: ChunkCache::new(),
base_dir: None, base_dir: None,
@@ -209,11 +229,19 @@ impl File {
Ok(results.into_iter().map(|(_, data)| data).collect()) Ok(results.into_iter().map(|(_, data)| data).collect())
} }
/// Returns the raw file bytes. /// Returns the file's bytes from the superblock on (after any user
/// block). Every HDF5 address in the file indexes this slice, so it is
/// what the `clawhdf5_format` parsers expect as `file_data`.
pub fn as_bytes(&self) -> &[u8] { pub fn as_bytes(&self) -> &[u8] {
self.data.as_bytes() self.data.as_bytes()
} }
/// Size of the user block before the superblock (0 for most files).
/// Matches h5py's `File.userblock_size`.
pub fn user_block_size(&self) -> u64 {
self.data.base as u64
}
/// Returns a reference to the parsed superblock. /// Returns a reference to the parsed superblock.
pub fn superblock(&self) -> &Superblock { pub fn superblock(&self) -> &Superblock {
&self.superblock &self.superblock
@@ -221,10 +249,10 @@ impl File {
/// Returns `true` when the file is backed by memory-mapped I/O. /// Returns `true` when the file is backed by memory-mapped I/O.
pub fn is_mmap(&self) -> bool { pub fn is_mmap(&self) -> bool {
match &self.data { match &self.data.backing {
FileData::Owned(_) => false, Backing::Owned(_) => false,
#[cfg(feature = "mmap")] #[cfg(feature = "mmap")]
FileData::Mmap(_) => true, Backing::Mmap(_) => true,
} }
} }
+314
View File
@@ -0,0 +1,314 @@
//! Files that start with a user block (`h5py.File(..., userblock_size=N)`,
//! `h5jam`): the superblock sits at 512, 1024, ... and every address in the
//! file is relative to it. Each reader (buffered, mmap, `MmapFile`,
//! `LazyFile`) must apply that base, and read the same values h5py does.
//!
//! h5py writes the files; skipped when python3 with h5py is unavailable,
//! unless `CLAWHDF5_REQUIRE_INTEROP=1`.
use std::collections::HashMap;
use std::path::Path;
use std::process::Command;
use clawhdf5::{AttrValue, File, LazyFile, MmapFile};
fn python() -> String {
std::env::var("CLAWHDF5_PYTHON").unwrap_or_else(|_| "python3".to_string())
}
fn interop_required() -> bool {
std::env::var("CLAWHDF5_REQUIRE_INTEROP").is_ok_and(|v| v == "1")
}
fn python_available() -> bool {
Command::new(python())
.args(["-c", "import h5py, numpy"])
.output()
.map(|o| o.status.success())
.unwrap_or(false)
}
macro_rules! skip_if_no_python {
() => {
if !python_available() {
assert!(
!interop_required(),
"CLAWHDF5_REQUIRE_INTEROP=1 but python3 with h5py is not available"
);
eprintln!("SKIP: python3 with h5py not available");
return;
}
};
}
/// Run `script` and return its stdout as `key -> values` (one
/// `key v1 v2 ...` line per key).
fn run_python(script: &str) -> HashMap<String, Vec<String>> {
let output = Command::new(python())
.args(["-c", script])
.output()
.expect("failed to run python");
assert!(
output.status.success(),
"python failed:\n{}",
String::from_utf8_lossy(&output.stderr)
);
String::from_utf8_lossy(&output.stdout)
.lines()
.filter_map(|line| {
let mut words = line.split_whitespace().map(str::to_string);
Some((words.next()?, words.collect()))
})
.collect()
}
fn parse<T: std::str::FromStr>(values: &[String]) -> Vec<T>
where
T::Err: std::fmt::Debug,
{
values.iter().map(|v| v.parse().unwrap()).collect()
}
/// Write a file with a user block of `userblock` bytes holding contiguous,
/// chunked (deflate), compact and committed-type datasets, nested groups,
/// and attributes (compact and, under `latest`, dense). Prints what h5py
/// reads back.
fn write_file(path: &Path, userblock: u32, libver: &str) -> HashMap<String, Vec<String>> {
let script = format!(
r#"
import h5py, numpy as np
path = "{path}"
with h5py.File(path, "w", userblock_size={userblock}, libver={libver}) as f:
f.attrs["title"] = "user block"
f.attrs["answer"] = np.int64(42)
f.create_dataset("contig", data=np.arange(12, dtype="<f8") * 0.5)
f.create_dataset("chunked", data=np.arange(1000, dtype="<i4") * 3 - 7,
chunks=(128,), compression="gzip")
dcpl = h5py.h5p.create(h5py.h5p.DATASET_CREATE)
dcpl.set_layout(h5py.h5d.COMPACT)
space = h5py.h5s.create_simple((5,))
dsid = h5py.h5d.create(f.id, b"compact", h5py.h5t.STD_I64LE, space, dcpl=dcpl)
dsid.write(h5py.h5s.ALL, h5py.h5s.ALL, np.array([5, -4, 3, -2, 1], "<i8"))
f["named_type"] = np.dtype("<f4")
f.create_dataset("committed", data=np.array([1.25, -2.5], "<f4"),
dtype=f["named_type"])
g = f.create_group("a/b")
g.create_dataset("deep", data=np.array([7, 8, 9], "<i8"))
g.attrs["scale"] = 2.5
d = f["contig"]
d.attrs["units"] = "m"
# Enough attributes that `latest` stores them densely (fractal heap).
for i in range(12):
f["a"].attrs["k%02d" % i] = np.int64(i * i)
# And enough links for a dense (fractal-heap) group under `latest`.
many = f.create_group("many")
for i in range(20):
many.create_dataset("d%02d" % i, data=np.array([i], "<i4"))
with h5py.File(path, "r") as f:
print("userblock", f.userblock_size)
print("contig", *f["contig"][()])
print("chunked", *f["chunked"][()])
print("compact", *f["compact"][()])
print("committed", *f["committed"][()])
print("deep", *f["a/b/deep"][()])
print("many", *[int(f["many/d%02d" % i][0]) for i in range(20)])
print("k", *[int(f["a"].attrs["k%02d" % i]) for i in range(12)])
"#,
path = path.display(),
libver = if libver == "default" {
"None".to_string()
} else {
format!("{libver:?}")
},
);
run_python(&script)
}
/// Attribute value rendered for comparison (`AttrValue` has no `PartialEq`).
fn attr(map: &HashMap<String, AttrValue>, key: &str) -> String {
match map.get(key) {
Some(AttrValue::I64(v)) => format!("i64 {v}"),
Some(AttrValue::F64(v)) => format!("f64 {v}"),
Some(AttrValue::String(v)) => format!("str {v}"),
other => format!("{other:?}"),
}
}
fn i64s(v: &[String]) -> Vec<i64> {
parse(v)
}
/// Everything read through the `File` API must match h5py.
fn check_file(file: &File, expected: &HashMap<String, Vec<String>>, label: &str) {
let ub: u64 = expected["userblock"][0].parse().unwrap();
assert_eq!(file.user_block_size(), ub, "{label}: user block size");
assert_eq!(
file.dataset("contig").unwrap().read_f64().unwrap(),
parse::<f64>(&expected["contig"]),
"{label}: contiguous"
);
assert_eq!(
file.dataset("chunked")
.unwrap()
.read_i32()
.unwrap()
.iter()
.map(|&v| v as i64)
.collect::<Vec<_>>(),
i64s(&expected["chunked"]),
"{label}: chunked"
);
assert_eq!(
file.dataset("compact").unwrap().read_i64().unwrap(),
i64s(&expected["compact"]),
"{label}: compact"
);
assert_eq!(
file.dataset("committed").unwrap().read_f32().unwrap(),
parse::<f32>(&expected["committed"]),
"{label}: committed datatype"
);
assert_eq!(
file.dataset("a/b/deep").unwrap().read_i64().unwrap(),
i64s(&expected["deep"]),
"{label}: nested group"
);
let many: Vec<i64> = (0..20)
.map(|i| {
file.dataset(&format!("many/d{i:02}"))
.unwrap()
.read_i32()
.unwrap()[0] as i64
})
.collect();
assert_eq!(many, i64s(&expected["many"]), "{label}: many links");
let root = file.root().attrs().unwrap();
assert_eq!(attr(&root, "title"), "str user block", "{label}");
assert_eq!(attr(&root, "answer"), "i64 42", "{label}");
let a = file.group("a").unwrap().attrs().unwrap();
let k: Vec<i64> = (0..12)
.map(|i| match &a[&format!("k{i:02}")] {
AttrValue::I64(v) => *v,
_ => panic!("{label}: k{i:02} is not an i64"),
})
.collect();
assert_eq!(k, i64s(&expected["k"]), "{label}: attributes");
assert_eq!(
attr(&file.group("a/b").unwrap().attrs().unwrap(), "scale"),
"f64 2.5",
"{label}"
);
assert_eq!(
attr(&file.dataset("contig").unwrap().attrs().unwrap(), "units"),
"str m",
"{label}"
);
}
fn check_all_readers(path: &Path, expected: &HashMap<String, Vec<String>>, label: &str) {
check_file(
&File::open(path).unwrap(),
expected,
&format!("{label} File::open"),
);
check_file(
&File::open_buffered(path).unwrap(),
expected,
&format!("{label} File::open_buffered"),
);
check_file(
&File::from_bytes(std::fs::read(path).unwrap()).unwrap(),
expected,
&format!("{label} File::from_bytes"),
);
let ub: u64 = expected["userblock"][0].parse().unwrap();
let mm = MmapFile::open(path).unwrap();
assert_eq!(mm.user_block_size(), ub, "{label} MmapFile");
assert_eq!(
mm.dataset("contig").unwrap().read_f64().unwrap(),
parse::<f64>(&expected["contig"]),
"{label} MmapFile contiguous"
);
assert_eq!(
mm.dataset("compact").unwrap().read_i64().unwrap(),
i64s(&expected["compact"]),
"{label} MmapFile compact"
);
assert_eq!(
mm.dataset("committed").unwrap().read_f32().unwrap(),
parse::<f32>(&expected["committed"]),
"{label} MmapFile committed"
);
assert_eq!(
mm.dataset("a/b/deep").unwrap().read_i64().unwrap(),
i64s(&expected["deep"]),
"{label} MmapFile nested"
);
assert_eq!(
attr(&mm.root().attrs().unwrap(), "answer"),
"i64 42",
"{label} MmapFile attrs"
);
let lazy = LazyFile::open_mmap(path).unwrap();
assert_eq!(lazy.user_block_size(), ub, "{label} LazyFile");
assert_eq!(
lazy.dataset("contig").unwrap().read_f64().unwrap(),
parse::<f64>(&expected["contig"]),
"{label} LazyFile contiguous"
);
assert_eq!(
lazy.dataset("chunked")
.unwrap()
.read_i32()
.unwrap()
.iter()
.map(|&v| v as i64)
.collect::<Vec<_>>(),
i64s(&expected["chunked"]),
"{label} LazyFile chunked"
);
assert_eq!(
lazy.dataset("committed").unwrap().read_f32().unwrap(),
parse::<f32>(&expected["committed"]),
"{label} LazyFile committed"
);
assert_eq!(
lazy.dataset("a/b/deep").unwrap().read_i64().unwrap(),
i64s(&expected["deep"]),
"{label} LazyFile nested"
);
assert_eq!(
attr(&lazy.root().attrs().unwrap(), "answer"),
"i64 42",
"{label} LazyFile attrs"
);
}
#[test]
fn user_block_files_read_like_h5py() {
skip_if_no_python!();
let dir = tempfile::tempdir().unwrap();
for userblock in [512u32, 4096] {
for libver in ["default", "latest"] {
let label = format!("userblock={userblock} libver={libver}");
let path = dir.path().join(format!("ub_{userblock}_{libver}.h5"));
let expected = write_file(&path, userblock, libver);
assert_eq!(expected["userblock"], [userblock.to_string()], "{label}");
check_all_readers(&path, &expected, &label);
}
}
}
#[test]
fn file_without_user_block_reports_zero() {
skip_if_no_python!();
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("no_ub.h5");
let expected = write_file(&path, 0, "default");
assert_eq!(expected["userblock"], ["0"]);
check_all_readers(&path, &expected, "userblock=0");
}
+3
View File
@@ -73,6 +73,9 @@ the VDS item, which is marked.
- `%b` printf-style source names are not expanded. - `%b` printf-style source names are not expanded.
- Hyperslab selection versions 1 and 2 are refused. - Hyperslab selection versions 1 and 2 are refused.
- **Files with a user block:** the base address is not applied. - **Files with a user block:** the base address is not applied.
**Fixed 2026-09-25:** every reader views the file from the superblock on
(`twithub.h5`, `twithub513.h5`, `h5clear_fsm_persist_user_*.h5`; the
`twithub` files still stop at the user-defined link type below).
- **Old-style shared messages (version 1)** read the wrong address. - **Old-style shared messages (version 1)** read the wrong address.
**Fixed 2026-09-25:** the address follows a length-sized heap offset **Fixed 2026-09-25:** the address follows a length-sized heap offset
(`tcompound.h5`, `tcompound2.h5`; their datasets now stop at the layout (`tcompound.h5`, `tcompound2.h5`; their datasets now stop at the layout