clawhdf5: File::open_storage reads any Storage through the full API

File::open_storage(Arc<dyn Storage + Send + Sync>) opens a file served by
any backend: groups, datasets, attributes, read_*, selections, VL data
and virtual datasets (external sources through the new
File::set_vds_resolver) all read through Storage::read_at/read_ranges.
The file's view (user block skipped, bounded by the recorded end of
file) is itself a Storage; File::open and File::from_bytes keep their
mmap and in-memory paths, now as that view's as_contiguous() fast path.
A storage-backed file's metadata cache image is laid over each read it
covers (new CacheImage::entries), as libhdf5 loads it.

The typed readers keep their fast paths over any storage: a contiguous
dataset is read in one piece and converted (read_f64 and friends), and a
contiguous native selection reads only its runs
(data_read::read_selection_native_in). Zero-copy methods answer
ContiguousStorageRequired when the bytes are not in memory, and
File::as_bytes panics there (File::contiguous_bytes is the fallible
form).

Test: tests/storage_equivalence.rs reads every fixture, and with
CLAWHDF5_STORAGE_CORPUS every conformance-corpus file, through File::open
and through open_storage over a read_at-only CountingStorage — tree,
attributes, and every dataset's values several ways — and requires
identical transcripts; it prints the read_at calls and bytes a one-pass
read costs per file.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 16:41:47 -05:00
co-authored by Claude Opus 5.5
parent 3fa5ed1dda
commit 1c3ef98828
7 changed files with 869 additions and 93 deletions
+72
View File
@@ -956,6 +956,78 @@ pub fn read_selection_native<T: NativeElement>(
crate::gather::gather::<T>(raw, dims, elem_size, selection).map(Some)
}
/// [`read_selection_native`] of a contiguous dataset in any [`Storage`],
/// reading only the selected elements' runs (adjacent ones merged, one
/// [`Storage::read_ranges`] call) instead of the whole dataset.
///
/// `Ok(None)` wherever the in-memory fast path does not apply and the
/// caller converts through the byte readers instead: `datatype` is not
/// `T`'s native representation, the layout is not contiguous, or the
/// dataset's bytes cannot be located in the file (no address, storage too
/// small, past the end of file: the cases [`read_raw_data_zerocopy`]
/// fails). Otherwise the result and errors are [`read_selection_native`]'s
/// over those bytes.
pub fn read_selection_native_in<T: NativeElement, S: Storage + ?Sized>(
file_data: &S,
layout: &DataLayout,
dataspace: &Dataspace,
datatype: &Datatype,
selection: &crate::selection::Selection,
) -> Result<Option<Vec<T>>, FormatError> {
if !T::is_native(datatype) {
return Ok(None);
}
let DataLayout::Contiguous {
address: Some(address),
size,
} = layout
else {
return Ok(None);
};
// Where the dataset's bytes are, as `read_raw_data_zerocopy` finds them.
let located = to_usize(dataspace.num_elements())
.ok()
.and_then(|n| n.checked_mul(datatype.type_size() as usize))
.filter(|&len| contiguous_read_len(*size, len).is_ok())
.filter(|&len| {
address
.checked_add(len as u64)
.is_some_and(|end| end <= file_data.len())
});
let Some(len) = located else {
return Ok(None);
};
if let Some(all) = file_data.as_contiguous() {
let start = to_usize(*address)?;
return read_selection_native(
&all[start..start + len],
&dataspace.dimensions,
datatype,
selection,
);
}
let dims = &dataspace.dimensions;
let elem_size = core::mem::size_of::<T>();
let total = dims
.iter()
.try_fold(1u64, |acc, &d| acc.checked_mul(d))
.ok_or_else(|| FormatError::Overflow("dataset shape overflows".into()))?;
let expected = crate::chunked_read::checked_byte_len(total, elem_size)?;
if len != expected {
return Err(FormatError::DataSizeMismatch {
expected,
actual: len,
});
}
let bytes = if let crate::selection::Selection::All = selection {
read_exact_at(file_data, *address, len)?.into_owned()
} else {
crate::partial_read::validate(selection, dims)?;
crate::gather::gather_storage(file_data, *address, len, dims, elem_size, selection)?
};
Ok(Some(native_to_vec(&bytes, bytes.len() / elem_size)))
}
/// The bytes of a slice of [`NativeElement`]s.
#[cfg(feature = "std")]
fn bytes_of_mut<T: NativeElement>(values: &mut [T]) -> &mut [u8] {
@@ -490,6 +490,23 @@ impl CacheImage {
image_block_in(file, self.location)
}
/// Every entry as `(file address, its bytes)`, taken from `block` (the
/// image block, see [`Self::block_in`]), in the order [`Self::apply`]
/// writes them: for a reader that cannot write the image over the
/// file's bytes and lays the entries over each read instead.
pub fn entries<'b>(&self, block: &'b [u8]) -> Result<Vec<(u64, &'b [u8])>, FormatError> {
let short = || FormatError::InvalidCacheImage("image applied to the wrong file");
self.entries
.iter()
.map(|e| {
let src = block
.get(e.image_offset..e.image_offset + e.len)
.ok_or_else(short)?;
Ok((e.address, src))
})
.collect()
}
/// Write every entry over `dst`, the file's bytes from the superblock
/// on (as long as the `data` the image was decoded from), taking the
/// entries from `block` (the image block, see [`Self::block`]). `block`