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:
@@ -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`
|
||||
|
||||
Reference in New Issue
Block a user