Files
clawhdf5/crates/clawhdf5-format/src/vds.rs
T
osobhandClaude Opus 5.5 3fa5ed1dda format: raw data, VDS and VL data over Storage
Every raw-data path has a generic *_in core, with the &[u8] functions as
thin wrappers: data_read (read_raw_data*, read_raw_data_selection,
read_chunked_native), chunked_read (the v1 B-tree chunk index, list_chunks,
the full, cached, sweep and indexed reads), parallel_read, partial_read,
fill_value (read_full_with_fill, apply_to_unallocated_chunks; and
dataset_fill_value_from_storage is now generic), vds (the virtual file
through Storage, external sources still through the resolver),
vl_data (VlResolver<'a, S = [u8]>, read_vl_strings_in, read_vl_bytes_in),
AttributeMessage::read_vl_strings_in and provenance::verify_dataset_in.

With the whole file in memory nothing changes: chunks and contiguous data
are sliced from it as before. Otherwise a chunked read lists its chunks,
fetches their stored bytes with one Storage::read_ranges call per 64 MiB
batch (chunks the cache already holds are not fetched), then decodes as
today; a selection fetches only the chunks it overlaps, and a contiguous
selection only its runs. Each extent's bounds error is the one the slice
code gave, reported when that extent is reached, so errors keep their
order.

Tests: the equivalence harness now reads every dataset's values (whole,
fill-aware, cached, indexed, three selections, VDS, VL strings and
sequences) through the read_at-only storage and requires the slice
results (all 653 corpus files agree); a misbehaving storage (a failing
Nth read, short reads) only ever yields errors or the right values; and
chunked reads are checked to use one read_ranges call.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
2026-09-26 16:28:01 -05:00

1213 lines
44 KiB
Rust

//! Virtual Dataset (VDS) assembly, following libhdf5's `H5Dvirtual.c`.
//!
//! A virtual dataset stores no data of its own: a list of mappings (kept in
//! the global heap) pairs a selection of the virtual dataspace with a
//! selection of a *source* dataset, in the same file (`"."`) or another one.
//! Reading it means reading each source and scattering the selected source
//! elements into the virtual buffer, pairing the two selections element by
//! element in row-major order. Elements no mapping supplies — unmapped
//! regions, and mappings whose source file or dataset does not exist — read
//! as the virtual dataset's **fill value**, as in libhdf5.
//!
//! Source files other than the virtual file itself are obtained through a
//! caller-supplied [`VdsFileResolver`], since this crate has no filesystem.
#[cfg(not(feature = "std"))]
use alloc::{format, string::String, vec, vec::Vec};
use crate::addr::{checked_addr, to_usize};
use crate::data_layout::{DataLayout, VdsMapping, parse_vds_mappings};
use crate::dataspace::Dataspace;
use crate::datatype::Datatype;
use crate::error::FormatError;
use crate::selection::{SerializedSelection, UNLIMITED};
use crate::storage::Storage;
/// Resolves the name of an external VDS source file, as stored in the
/// mapping, to that file's bytes.
///
/// `Ok(None)` means the file does not exist; its mappings then read as the
/// fill value, as libhdf5 does for a missing source. `Err` refuses the name
/// (e.g. a path the caller will not follow) and fails the read, so that a
/// refused source is never passed off as fill.
pub type VdsFileResolver<'a> = dyn Fn(&str) -> Result<Option<Vec<u8>>, FormatError> + 'a;
/// A fully assembled virtual dataset.
#[derive(Debug, Clone, PartialEq)]
pub struct VirtualData {
/// The virtual dataset's extent.
pub dims: Vec<u64>,
/// Raw element bytes, row-major, in the virtual dataset's datatype.
pub data: Vec<u8>,
/// Number of elements no mapping supplied; they hold the fill value.
pub unmapped: u64,
}
fn vds_err(msg: impl Into<String>) -> FormatError {
FormatError::ChunkedReadError(msg.into())
}
/// Upper bound on the printf-style source datasets probed for one mapping.
const MAX_PRINTF_BLOCKS: u64 = 1 << 20;
/// A source file or dataset name, parsed for printf-style `%b` block-number
/// substitutions (`H5D_virtual_parse_source_name`): `%b` is the block
/// number, `%%` a literal `%`, and any other `%` sequence is invalid.
#[derive(Debug, Clone, PartialEq)]
struct SourceName {
/// Literal text around the substitutions: `segments.len() == subs + 1`.
segments: Vec<String>,
}
impl SourceName {
fn parse(name: &str) -> Result<SourceName, FormatError> {
let mut segments = vec![String::new()];
let mut chars = name.chars();
while let Some(c) = chars.next() {
if c != '%' {
segments.last_mut().expect("never empty").push(c);
continue;
}
match chars.next() {
Some('b') => segments.push(String::new()),
Some('%') => segments.last_mut().expect("never empty").push('%'),
_ => {
return Err(vds_err(format!(
"invalid format specifier in VDS source name {name:?}"
)));
}
}
}
Ok(SourceName { segments })
}
/// Number of `%b` substitutions.
fn subs(&self) -> usize {
self.segments.len() - 1
}
/// The name with every `%b` replaced by `block`.
fn build(&self, block: u64) -> String {
let mut out = String::new();
for (i, seg) in self.segments.iter().enumerate() {
if i > 0 {
out.push_str(&format!("{block}"));
}
out.push_str(seg);
}
out
}
}
/// How a mapping's selections relate, as libhdf5 classifies them.
#[derive(Debug, Clone, Copy, PartialEq)]
enum Kind {
/// Both selections have a fixed size.
Fixed,
/// Both are unlimited (in `vdim` / `sdim`): the mapping grows with the
/// source dataset's extent.
Unlimited { vdim: usize, sdim: usize },
/// The virtual selection repeats a block without limit in `vdim`; block
/// `j` comes from the source named by substituting `j` for `%b`.
Printf { vdim: usize },
}
/// One mapping with its selections decoded.
struct Mapping {
file: SourceName,
dataset: SourceName,
vsel: SerializedSelection,
ssel: SerializedSelection,
kind: Kind,
}
impl Mapping {
fn new(m: VdsMapping) -> Result<Mapping, FormatError> {
let (vsel, _) = SerializedSelection::decode(&m.virtual_selection)?;
let (ssel, _) = SerializedSelection::decode(&m.source_selection)?;
let file = SourceName::parse(&m.source_file)?;
let dataset = SourceName::parse(&m.source_dataset)?;
let subs = file.subs() + dataset.subs();
// The checks of H5D_virtual_check_mapping_pre/_post.
let kind = match (vsel.unlimited_dim(), ssel.unlimited_dim()) {
(Some(vdim), None) => {
if subs == 0 {
return Err(vds_err(
"unlimited virtual selection with a limited source selection \
and no %b in the source names",
));
}
match &vsel {
SerializedSelection::Regular { count, block, .. }
if count[vdim] == UNLIMITED && block[vdim] != UNLIMITED => {}
_ => {
return Err(vds_err(
"printf VDS mapping needs a virtual selection with an unlimited count",
));
}
}
Kind::Printf { vdim }
}
(Some(vdim), Some(sdim)) => {
if non_unlimited_elements(&vsel, vdim) != non_unlimited_elements(&ssel, sdim) {
return Err(vds_err(
"unlimited VDS mapping: virtual and source selections differ \
outside the unlimited dimension",
));
}
Kind::Unlimited { vdim, sdim }
}
(None, Some(_)) => {
return Err(vds_err(
"VDS mapping with an unlimited source selection and a limited \
virtual selection is not supported",
));
}
(None, None) => Kind::Fixed,
};
if subs > 0 && !matches!(kind, Kind::Printf { .. }) {
return Err(vds_err(
"%b in a VDS source name without an unlimited virtual selection",
));
}
Ok(Mapping {
file,
dataset,
vsel,
ssel,
kind,
})
}
}
/// Elements a regular selection selects outside dimension `skip`.
fn non_unlimited_elements(sel: &SerializedSelection, skip: usize) -> Option<u64> {
let SerializedSelection::Regular { count, block, .. } = sel else {
return None;
};
(0..count.len())
.filter(|&d| d != skip)
.try_fold(1u64, |acc, d| {
acc.checked_mul(count[d].checked_mul(block[d])?)
})
}
/// Load and decode the mapping list of a virtual layout.
fn load_mappings<S: Storage + ?Sized>(
file_data: &S,
layout: &DataLayout,
length_size: u8,
) -> Result<Vec<Mapping>, FormatError> {
let DataLayout::Virtual {
global_heap_address,
global_heap_index,
..
} = layout
else {
return Err(vds_err("not a virtual dataset layout"));
};
let Some(addr) = *global_heap_address else {
return Ok(Vec::new());
};
let coll = crate::global_heap::GlobalHeapCollection::parse_in(
file_data,
checked_addr(addr)?,
length_size,
)?;
let index = u16::try_from(*global_heap_index)
.map_err(|_| vds_err("VDS mapping heap index out of range"))?;
let obj = coll
.get_object(index)
.ok_or(FormatError::GlobalHeapObjectNotFound {
collection_address: addr,
index,
})?;
parse_vds_mappings(&obj.data, length_size)?
.into_iter()
.map(Mapping::new)
.collect()
}
/// `H5S__hyper_get_clip_diminfo`: the count and block a regular selection has
/// in its unlimited dimension once clipped to `clip`.
fn clip_diminfo(start: u64, stride: u64, count: u64, block: u64, clip: u64) -> (u64, u64) {
if start >= clip {
if block == UNLIMITED {
(count, 0)
} else {
(0, block)
}
} else if block == UNLIMITED || block == stride {
(1, clip - start)
} else {
((clip - start).div_ceil(stride.max(1)), block)
}
}
/// The unlimited-dimension parameters (start, stride, count, block) of a
/// regular selection.
fn unlim_diminfo(sel: &SerializedSelection, dim: usize) -> Result<[u64; 4], FormatError> {
match sel {
SerializedSelection::Regular {
start,
stride,
count,
block,
} => Ok([start[dim], stride[dim], count[dim], block[dim]]),
_ => Err(vds_err(
"unlimited VDS selection is not a regular hyperslab",
)),
}
}
/// `H5S_hyper_get_clip_extent_match` with `incl_trail = false` (the
/// "last available" view): the extent to clip `clip_sel` (unlimited in
/// `clip_dim`) to so that it holds as many slices as `match_sel` (unlimited
/// in `match_dim`) holds when clipped to `match_clip`.
fn clip_extent_match(
clip_sel: &SerializedSelection,
clip_dim: usize,
match_sel: &SerializedSelection,
match_dim: usize,
match_clip: u64,
) -> Result<u64, FormatError> {
let overflow = || FormatError::Overflow("VDS clip extent overflow".into());
let [mstart, mstride, mcount, mblock] = unlim_diminfo(match_sel, match_dim)?;
let (count, block) = clip_diminfo(mstart, mstride, mcount, mblock, match_clip);
let slices = if block == 0 || count == 0 {
0
} else if count == 1 {
block
} else {
let mut n = block.checked_mul(count).ok_or_else(overflow)?;
let span = mstride
.checked_mul(count - 1)
.and_then(|s| s.checked_add(block))
.ok_or_else(overflow)?;
let room = match_clip - mstart;
if span > room {
n -= span - room;
}
n
};
// H5S__hyper_get_clip_extent_real
let [start, stride, _, block] = unlim_diminfo(clip_sel, clip_dim)?;
if slices == 0 {
return Ok(0);
}
let extent = if block == UNLIMITED || block == stride {
start.checked_add(slices)
} else {
let full = slices / block;
let rem = slices - full * block;
if rem > 0 {
full.checked_mul(stride)
.and_then(|o| start.checked_add(o))
.and_then(|e| e.checked_add(rem))
} else {
(full - 1)
.checked_mul(stride)
.and_then(|o| start.checked_add(o))
.and_then(|e| e.checked_add(block))
}
};
extent.ok_or_else(overflow)
}
/// How each mapping is read, and the resulting extent.
struct Plan {
dims: Vec<u64>,
steps: Vec<Step>,
}
#[derive(Clone, Copy)]
enum Step {
Fixed,
/// Read with the virtual selection clipped to `vclip` and the source
/// selection to the source's extent; `None` if the source is missing.
Unlimited(Option<u64>),
/// Read blocks `0..blocks`.
Printf(u64),
}
/// Work out the extent libhdf5 gives the virtual dataset
/// (`H5D__virtual_set_extent_unlim`, default view `H5D_VDS_LAST_AVAILABLE`
/// with a printf gap of 0) and how much of each unlimited mapping is read.
fn plan<S: Storage + ?Sized>(
mappings: &[Mapping],
stored: &[u64],
sources: &mut Sources<'_, '_, S>,
) -> Result<Plan, FormatError> {
let overflow = || FormatError::Overflow("VDS extent overflow".into());
let rank = stored.len();
let mut new_dims: Vec<Option<u64>> = vec![None; rank];
// Minimum extent needed by the limited parts of every virtual selection
// (H5D_virtual_update_min_dims).
let mut min_dims = vec![0u64; rank];
let mut steps = Vec::with_capacity(mappings.len());
for m in mappings {
let skip = match m.kind {
Kind::Unlimited { vdim, .. } | Kind::Printf { vdim } => Some(vdim),
Kind::Fixed => None,
};
if let Some(ends) = selection_bounds_end(&m.vsel)? {
if ends.len() != rank {
return Err(vds_err("VDS selection rank does not match dataspace rank"));
}
for d in (0..rank).filter(|&d| Some(d) != skip) {
min_dims[d] = min_dims[d].max(ends[d].checked_add(1).ok_or_else(overflow)?);
}
}
let (vdim, clip, step) = match m.kind {
Kind::Fixed => {
steps.push(Step::Fixed);
continue;
}
Kind::Unlimited { vdim, sdim } => {
match sources.dims(&m.file.build(0), &m.dataset.build(0))? {
Some(src_dims) => {
let extent = *src_dims.get(sdim).ok_or_else(|| {
vds_err("VDS source rank does not match its selection")
})?;
let clip = clip_extent_match(&m.vsel, vdim, &m.ssel, sdim, extent)?;
(vdim, clip, Step::Unlimited(Some(clip)))
}
None => (vdim, 0, Step::Unlimited(None)),
}
}
Kind::Printf { vdim } => {
// With a gap of 0 the search stops at the first missing
// source dataset.
let mut found = 0u64;
while sources
.dims(&m.file.build(found), &m.dataset.build(found))?
.is_some()
{
found += 1;
if found > MAX_PRINTF_BLOCKS {
return Err(vds_err("too many printf-style VDS source datasets"));
}
}
let clip = if found == 0 {
0
} else {
// End of block `found - 1` in the unlimited dimension.
let [start, stride, _, block] = unlim_diminfo(&m.vsel, vdim)?;
(found - 1)
.checked_mul(stride)
.and_then(|o| start.checked_add(o))
.and_then(|e| e.checked_add(block))
.ok_or_else(overflow)?
};
(vdim, clip, Step::Printf(found))
}
};
if vdim >= rank {
return Err(vds_err("VDS selection rank does not match dataspace rank"));
}
new_dims[vdim] = Some(new_dims[vdim].map_or(clip, |n| n.max(clip)));
steps.push(step);
}
let dims = (0..rank)
.map(|d| match new_dims[d] {
None => stored[d],
Some(n) => n.max(min_dims[d]),
})
.collect();
Ok(Plan { dims, steps })
}
/// The last selected coordinate in each dimension (`H5S_SELECT_BOUNDS`),
/// ignoring any unlimited dimension; `None` for ALL/NONE and empty selections.
fn selection_bounds_end(sel: &SerializedSelection) -> Result<Option<Vec<u64>>, FormatError> {
let overflow = || FormatError::Overflow("VDS selection bounds overflow".into());
match sel {
SerializedSelection::All | SerializedSelection::None => Ok(None),
SerializedSelection::Regular {
start,
stride,
count,
block,
} => {
if count.contains(&0) || block.contains(&0) {
return Ok(None);
}
let mut ends = Vec::with_capacity(start.len());
for d in 0..start.len() {
if count[d] == UNLIMITED || block[d] == UNLIMITED {
ends.push(0);
continue;
}
let end = (count[d] - 1)
.checked_mul(stride[d])
.and_then(|o| start[d].checked_add(o))
.and_then(|e| e.checked_add(block[d] - 1))
.ok_or_else(overflow)?;
ends.push(end);
}
Ok(Some(ends))
}
SerializedSelection::Blocks { rank, ends, .. } => {
if ends.is_empty() {
return Ok(None);
}
let mut max = vec![0u64; *rank];
for e in ends.chunks_exact(*rank) {
for d in 0..*rank {
max[d] = max[d].max(e[d]);
}
}
Ok(Some(max))
}
}
}
/// The virtual dataset's extent as libhdf5 reports it (`H5Dget_space`).
///
/// For a virtual dataset whose mappings are all of fixed size this is the
/// stored dataspace. With unlimited or printf-style mappings libhdf5
/// recomputes the unlimited dimension from the sources present (the default
/// "last available" view: the largest extent any mapping can fill), which
/// needs the source files, read through `resolver`.
pub fn virtual_dataset_extent(
file_data: &[u8],
layout: &DataLayout,
dataspace: &Dataspace,
_offset_size: u8,
length_size: u8,
resolver: Option<&VdsFileResolver>,
) -> Result<Vec<u64>, FormatError> {
virtual_dataset_extent_in(
file_data,
layout,
dataspace,
_offset_size,
length_size,
resolver,
)
}
/// [`virtual_dataset_extent`] over any [`Storage`].
pub fn virtual_dataset_extent_in<S: Storage + ?Sized>(
file_data: &S,
layout: &DataLayout,
dataspace: &Dataspace,
_offset_size: u8,
length_size: u8,
resolver: Option<&VdsFileResolver>,
) -> Result<Vec<u64>, FormatError> {
let mappings = load_mappings(file_data, layout, length_size)?;
if mappings.iter().all(|m| m.kind == Kind::Fixed) {
return Ok(dataspace.dimensions.clone());
}
let mut sources = Sources::new(file_data, resolver);
Ok(plan(&mappings, &dataspace.dimensions, &mut sources)?.dims)
}
/// Read a whole virtual dataset, at the extent
/// [`virtual_dataset_extent`] reports.
///
/// `fill` is the virtual dataset's fill value (from its fill value message;
/// `None` for the default of zeros); every element no mapping supplies holds
/// it. External source files are read through `resolver`; without one, a
/// mapping to another file is an error.
#[allow(clippy::too_many_arguments)]
pub fn read_virtual_dataset(
file_data: &[u8],
layout: &DataLayout,
dataspace: &Dataspace,
datatype: &Datatype,
fill: Option<&[u8]>,
_offset_size: u8,
length_size: u8,
resolver: Option<&VdsFileResolver>,
) -> Result<VirtualData, FormatError> {
read_virtual_dataset_in(
file_data,
layout,
dataspace,
datatype,
fill,
_offset_size,
length_size,
resolver,
)
}
/// [`read_virtual_dataset`] over any [`Storage`].
#[allow(clippy::too_many_arguments)]
pub fn read_virtual_dataset_in<S: Storage + ?Sized>(
file_data: &S,
layout: &DataLayout,
dataspace: &Dataspace,
datatype: &Datatype,
fill: Option<&[u8]>,
_offset_size: u8,
length_size: u8,
resolver: Option<&VdsFileResolver>,
) -> Result<VirtualData, FormatError> {
let mappings = load_mappings(file_data, layout, length_size)?;
let mut sources = Sources::new(file_data, resolver);
let Plan { dims, steps } = plan(&mappings, &dataspace.dimensions, &mut sources)?;
let elem_size = datatype.type_size() as usize;
let total = dims
.iter()
.try_fold(1u64, |acc, &d| acc.checked_mul(d))
.ok_or_else(|| FormatError::Overflow("virtual dataset extent".into()))?;
let mut data = crate::chunked_read::alloc_output(crate::chunked_read::checked_byte_len(
total, elem_size,
)?)?;
if let Some(fill) = fill.filter(|f| <[u8]>::len(f) == elem_size && f.iter().any(|&b| b != 0)) {
for element in data.chunks_exact_mut(elem_size) {
element.copy_from_slice(fill);
}
}
let mut mapped = vec![false; usize::try_from(total).map_err(|_| vds_err("VDS too large"))?];
for (m, step) in mappings.iter().zip(&steps) {
match (*step, m.kind) {
(Step::Fixed, _) => {
let (file, dset) = (m.file.build(0), m.dataset.build(0));
let Some(src) = sources.dataset(&file, &dset, datatype)? else {
continue; // missing source file or dataset: fill
};
let vidx = selection_indices(&m.vsel, &dims, None)?;
let sidx = selection_indices(&m.ssel, &src.dims, None)?;
scatter(&mut data, &mut mapped, &src.raw, &vidx, &sidx, elem_size)?;
}
(Step::Unlimited(Some(vclip)), Kind::Unlimited { vdim, sdim }) => {
let (file, dset) = (m.file.build(0), m.dataset.build(0));
let Some(src) = sources.dataset(&file, &dset, datatype)? else {
continue;
};
let vidx = selection_indices(&m.vsel, &dims, Some((vdim, vclip)))?;
let extent = *src
.dims
.get(sdim)
.ok_or_else(|| vds_err("VDS source rank does not match its selection"))?;
let sidx = selection_indices(&m.ssel, &src.dims, Some((sdim, extent)))?;
scatter(&mut data, &mut mapped, &src.raw, &vidx, &sidx, elem_size)?;
}
(Step::Unlimited(None), _) => {}
(Step::Printf(blocks), Kind::Printf { vdim }) => {
for j in 0..blocks {
let Some(src) =
sources.dataset(&m.file.build(j), &m.dataset.build(j), datatype)?
else {
continue;
};
let vblock = unlim_block(&m.vsel, vdim, j)?;
let vidx = selection_indices(&vblock, &dims, None)?;
let sidx = selection_indices(&m.ssel, &src.dims, None)?;
scatter(&mut data, &mut mapped, &src.raw, &vidx, &sidx, elem_size)?;
}
}
_ => return Err(vds_err("internal error: VDS plan does not match mapping")),
}
}
let unmapped = mapped.iter().filter(|&&m| !m).count() as u64;
Ok(VirtualData {
dims,
data,
unmapped,
})
}
/// `H5S_hyper_get_unlim_block`: block `j` of a selection whose count is
/// unlimited in `dim`.
fn unlim_block(
sel: &SerializedSelection,
dim: usize,
j: u64,
) -> Result<SerializedSelection, FormatError> {
let SerializedSelection::Regular {
start,
stride,
count,
block,
} = sel
else {
return Err(vds_err("printf VDS selection is not a regular hyperslab"));
};
let mut start = start.clone();
let mut count = count.clone();
start[dim] = j
.checked_mul(stride[dim])
.and_then(|o| start[dim].checked_add(o))
.ok_or_else(|| FormatError::Overflow("VDS block start overflow".into()))?;
count[dim] = 1;
Ok(SerializedSelection::Regular {
start,
stride: stride.clone(),
count,
block: block.clone(),
})
}
/// Copy source element `sidx[i]` to virtual element `vidx[i]` for every `i`.
fn scatter(
out: &mut [u8],
mapped: &mut [bool],
src: &[u8],
vidx: &[u64],
sidx: &[u64],
elem_size: usize,
) -> Result<(), FormatError> {
if vidx.len() != sidx.len() {
return Err(vds_err("virtual/source selection element counts differ"));
}
for (&v, &s) in vidx.iter().zip(sidx) {
let (vo, so) = (to_usize(v)? * elem_size, to_usize(s)? * elem_size);
if vo + elem_size > out.len() || so + elem_size > src.len() {
return Err(vds_err("virtual dataset selection out of bounds"));
}
out[vo..vo + elem_size].copy_from_slice(&src[so..so + elem_size]);
mapped[to_usize(v)?] = true;
}
Ok(())
}
/// Row-major linear indices of the elements `sel` selects in a dataspace of
/// shape `dims`, in the order libhdf5 iterates them (row-major).
///
/// `clip` = `(dim, limit)` drops every coordinate `>= limit` in `dim` — the
/// clipping libhdf5 applies to an unlimited selection
/// (`H5S_hyper_clip_unlim`); an unlimited selection must be clipped.
fn selection_indices(
sel: &SerializedSelection,
dims: &[u64],
clip: Option<(usize, u64)>,
) -> Result<Vec<u64>, FormatError> {
let overflow = || FormatError::Overflow("VDS selection index overflow".into());
let rank = dims.len();
let total = dims
.iter()
.try_fold(1u64, |acc, &d| acc.checked_mul(d))
.ok_or_else(overflow)?;
let mut row_stride = vec![1u64; rank];
for d in (0..rank.saturating_sub(1)).rev() {
row_stride[d] = row_stride[d + 1]
.checked_mul(dims[d + 1])
.ok_or_else(overflow)?;
}
if sel.rank().is_some_and(|r| r != rank) {
return Err(vds_err("VDS selection rank does not match dataspace rank"));
}
let limit = |d: usize| match clip {
Some((cd, l)) if cd == d => l,
_ => u64::MAX,
};
match sel {
SerializedSelection::All => Ok((0..total).collect()),
SerializedSelection::None => Ok(Vec::new()),
SerializedSelection::Regular {
start,
stride,
count,
block,
} => {
// Selected coordinates along each dimension, in order.
let mut per_dim: Vec<Vec<u64>> = Vec::with_capacity(rank);
for d in 0..rank {
let lim = limit(d);
if (count[d] == UNLIMITED || block[d] == UNLIMITED) && lim == u64::MAX {
return Err(vds_err("unlimited VDS selection was not clipped"));
}
let mut coords = Vec::new();
let mut ci = 0u64;
'blocks: while ci < count[d] {
let base = ci
.checked_mul(stride[d])
.and_then(|o| start[d].checked_add(o))
.ok_or_else(overflow)?;
if base >= lim {
break;
}
let mut bi = 0u64;
while bi < block[d] {
let coord = base.checked_add(bi).ok_or_else(overflow)?;
if coord >= lim {
break 'blocks;
}
// Past the extent is malformed; bail before the list
// can grow without bound.
if coord >= dims[d] {
return Err(vds_err("VDS selection exceeds the dataspace extent"));
}
coords.push(coord);
bi += 1;
}
ci += 1;
}
per_dim.push(coords);
}
if per_dim.iter().any(|c| c.is_empty()) {
return Ok(Vec::new());
}
let n = per_dim
.iter()
.try_fold(1usize, |acc, c| acc.checked_mul(c.len()))
.ok_or_else(overflow)?;
let mut out = Vec::with_capacity(n);
let mut idx = vec![0usize; rank];
loop {
let lin: u64 = (0..rank).map(|d| per_dim[d][idx[d]] * row_stride[d]).sum();
out.push(lin);
// Mixed-radix increment, last dimension fastest.
let mut d = rank;
loop {
if d == 0 {
return Ok(out);
}
d -= 1;
idx[d] += 1;
if idx[d] < per_dim[d].len() {
break;
}
idx[d] = 0;
}
}
}
SerializedSelection::Blocks {
rank: _,
starts,
ends,
} => {
// libhdf5 serializes the union as disjoint blocks, so their volumes
// never add up to more than the dataspace.
let mut volume = 0u64;
for (s, e) in starts.chunks_exact(rank).zip(ends.chunks_exact(rank)) {
for d in 0..rank {
if e[d] >= dims[d] {
return Err(vds_err("VDS selection exceeds the dataspace extent"));
}
}
let v = s
.iter()
.zip(e)
.try_fold(1u64, |acc, (&s, &e)| acc.checked_mul(e - s + 1))
.ok_or_else(overflow)?;
volume = volume.checked_add(v).ok_or_else(overflow)?;
if volume > total {
return Err(vds_err("VDS selection blocks overlap"));
}
}
let mut out = Vec::with_capacity(to_usize(volume)?);
for (s, e) in starts.chunks_exact(rank).zip(ends.chunks_exact(rank)) {
let mut cur = s.to_vec();
'block: loop {
if (0..rank).all(|d| cur[d] < limit(d)) {
out.push((0..rank).map(|d| cur[d] * row_stride[d]).sum());
}
for d in (0..rank).rev() {
if cur[d] < e[d] {
cur[d] += 1;
continue 'block;
}
cur[d] = s[d];
}
break;
}
}
out.sort_unstable();
out.dedup();
Ok(out)
}
}
}
/// A source dataset's decoded contents.
struct SourceData {
dims: Vec<u64>,
raw: Vec<u8>,
}
/// Source files and datasets, fetched on demand. The most recently used
/// external file is kept, since consecutive mappings usually share one.
///
/// The virtual dataset's own file (`"."`) is read through its [`Storage`];
/// an external source file is loaded whole, through the resolver.
struct Sources<'a, 'r, S: Storage + ?Sized> {
file_data: &'a S,
resolver: Option<&'r VdsFileResolver<'r>>,
cached_file: Option<(String, Option<Vec<u8>>)>,
}
impl<'a, 'r, S: Storage + ?Sized> Sources<'a, 'r, S> {
fn new(file_data: &'a S, resolver: Option<&'r VdsFileResolver<'r>>) -> Self {
Sources {
file_data,
resolver,
cached_file: None,
}
}
/// The bytes of external source file `name` (not `"."`), or `None` if it
/// does not exist.
fn external(&mut self, name: &str) -> Result<Option<&[u8]>, FormatError> {
if self.cached_file.as_ref().is_none_or(|(n, _)| n != name) {
let resolver = self.resolver.ok_or_else(|| {
vds_err("external-file virtual dataset sources require a file resolver")
})?;
let mut bytes = resolver(name)?;
if let Some(b) = bytes.as_mut() {
load_source_file(b)?;
}
self.cached_file = Some((String::from(name), bytes));
}
// An external file is handed over whole; its addresses are relative
// to its superblock, so skip any user block.
match self.cached_file.as_ref().and_then(|(_, b)| b.as_deref()) {
Some(bytes) => Ok(Some(crate::signature::split_user_block(bytes)?.1)),
None => Ok(None),
}
}
/// The extent of source dataset `path` in file `file`, or `None` when
/// either does not exist.
fn dims(&mut self, file: &str, path: &str) -> Result<Option<Vec<u64>>, FormatError> {
if file == "." {
return Ok(open_source(self.file_data, path)?.map(|s| s.dataspace.dimensions));
}
let Some(bytes) = self.external(file)? else {
return Ok(None);
};
Ok(open_source(bytes, path)?.map(|s| s.dataspace.dimensions))
}
/// Read source dataset `path` from file `file`, or `None` when either
/// does not exist. Its datatype must be the virtual dataset's: libhdf5
/// converts between types here, which is not supported.
fn dataset(
&mut self,
file: &str,
path: &str,
datatype: &Datatype,
) -> Result<Option<SourceData>, FormatError> {
// Variable-length and reference elements are addresses into the file
// that holds them (global-heap IDs, object addresses). Copied out of
// another file they would be decoded against the virtual dataset's
// file and name some other object, so refuse rather than return them.
if file != "." && holds_file_addresses(datatype) {
return Err(vds_err(format!(
"VDS source {path} in {file}: variable-length and reference data \
from another file is not supported"
)));
}
if file == "." {
let Some(src) = open_source(self.file_data, path)? else {
return Ok(None);
};
return read_source(self.file_data, src, path, datatype).map(Some);
}
let Some(bytes) = self.external(file)? else {
return Ok(None);
};
let Some(src) = open_source(bytes, path)? else {
return Ok(None);
};
read_source(bytes, src, path, datatype).map(Some)
}
}
/// Check an external source file's superblock extension as libhdf5 does
/// when it opens the file, and write any metadata cache image over its
/// metadata in place: libhdf5 reads the image's entries instead of the
/// file's own, possibly stale, bytes (`crate::superblock_ext`). A source
/// file whose image cannot be loaded is an error, as other corrupt source
/// files are here.
fn load_source_file(whole: &mut [u8]) -> Result<(), FormatError> {
let base = crate::signature::find_signature(whole)?;
let sb = crate::superblock::Superblock::parse(&whole[base..], 0)?;
// The end of file the superblock records; a truncated source file is
// read as before, up to its length.
let end = sb
.data_end(base as u64, whole.len() as u64)
.map_or(Ok(whole.len()), |e| to_usize(e).map(|e| base + e))?;
crate::superblock_ext::apply_cache_image_in_place(&mut whole[base..end], &sb)
}
/// Whether elements of `dt` contain addresses into their own file:
/// variable-length data (global-heap IDs) or references.
fn holds_file_addresses(dt: &Datatype) -> bool {
match dt {
Datatype::VariableLength { .. } | Datatype::Reference { .. } => true,
Datatype::Compound { members, .. } => {
members.iter().any(|m| holds_file_addresses(&m.datatype))
}
Datatype::Array { base_type, .. } | Datatype::Enumeration { base_type, .. } => {
holds_file_addresses(base_type)
}
_ => false,
}
}
/// An opened source dataset's object header.
struct OpenSource {
offset_size: u8,
length_size: u8,
header: crate::object_header::ObjectHeader,
dataspace: Dataspace,
}
fn source_message<'h>(
src: &'h OpenSource,
path: &str,
t: crate::message_type::MessageType,
) -> Result<&'h crate::object_header::HeaderMessage, FormatError> {
src.header
.messages
.iter()
.find(|m| m.msg_type == t)
.ok_or_else(|| vds_err(format!("VDS source {path} has no {t:?} message")))
}
/// Open source dataset `path` of the file in `file_data`, or `None` if there
/// is no such object (libhdf5 reads a missing source as fill).
fn open_source<S: Storage + ?Sized>(
file_data: &S,
path: &str,
) -> Result<Option<OpenSource>, FormatError> {
use crate::message_type::MessageType;
use crate::shared_message::message_data_with_sohm_in as message_data_with_sohm;
// `file_data` starts at the superblock (see `Sources::external`).
let sb = crate::superblock::Superblock::parse_in(file_data, 0)?;
let (os, ls) = (sb.offset_size, sb.length_size);
let addr = match crate::group_v2::resolve_path_any_in(file_data, &sb, path) {
Ok(a) => a,
Err(FormatError::PathNotFound(_)) => return Ok(None),
Err(e) => return Err(e),
};
let header =
crate::object_header::ObjectHeader::parse_in(file_data, checked_addr(addr)?, os, ls)?;
let mut src = OpenSource {
offset_size: os,
length_size: ls,
header,
dataspace: Dataspace {
space_type: crate::dataspace::DataspaceType::Null,
rank: 0,
dimensions: Vec::new(),
max_dimensions: None,
},
};
let ds_msg = source_message(&src, path, MessageType::Dataspace)?;
src.dataspace = Dataspace::parse(&message_data_with_sohm(file_data, ds_msg, os, ls)?, ls)?;
Ok(Some(src))
}
/// Read an opened source dataset in full (its own fill value applied to
/// unallocated chunks).
fn read_source<S: Storage + ?Sized>(
file_data: &S,
src: OpenSource,
path: &str,
datatype: &Datatype,
) -> Result<SourceData, FormatError> {
use crate::filter_pipeline::FilterPipeline;
use crate::message_type::MessageType;
use crate::shared_message::message_data_with_sohm_in as message_data_with_sohm;
let (os, ls) = (src.offset_size, src.length_size);
let dt_msg = source_message(&src, path, MessageType::Datatype)?;
let (src_type, _) = Datatype::parse(&message_data_with_sohm(file_data, dt_msg, os, ls)?)?;
// libhdf5 converts each source to the virtual dataset's type. Only the
// conversion that is a pure byte swap is done here.
let swap = if &src_type == datatype {
false
} else if differs_only_in_byte_order(&src_type, datatype) {
true
} else {
return Err(vds_err(format!(
"VDS source {path} has a different datatype from the virtual dataset \
(only a byte-order conversion is supported)"
)));
};
let layout = DataLayout::parse(
&source_message(&src, path, MessageType::DataLayout)?.data,
os,
ls,
)?;
// A source that is itself virtual could form a cycle (A -> B -> A) and
// recurse without bound. Nested virtual sources are not supported.
if matches!(layout, DataLayout::Virtual { .. }) {
return Err(vds_err(
"virtual dataset source is itself virtual (unsupported)",
));
}
let pipeline = src
.header
.messages
.iter()
.find(|m| m.msg_type == MessageType::FilterPipeline)
.map(|m| {
message_data_with_sohm(file_data, m, os, ls).and_then(|d| FilterPipeline::parse(&d))
})
.transpose()?;
let raw = crate::fill_value::read_full_with_fill_in(
&src.header.messages,
file_data,
&layout,
&src.dataspace,
src_type.type_size() as usize,
os,
ls,
|| {
crate::data_read::read_raw_data_full_in(
file_data,
&layout,
&src.dataspace,
&src_type,
pipeline.as_ref(),
os,
ls,
)
},
)?;
let mut raw = raw;
if swap {
let size = datatype.type_size() as usize;
for element in raw.chunks_exact_mut(size) {
element.reverse();
}
}
Ok(SourceData {
dims: src.dataspace.dimensions,
raw,
})
}
/// Whether two numeric types are identical apart from their byte order (both
/// little- or big-endian), so converting one to the other is a byte swap.
fn differs_only_in_byte_order(a: &Datatype, b: &Datatype) -> bool {
use crate::datatype::DatatypeByteOrder::{BigEndian, LittleEndian};
let mut a = a.clone();
match &mut a {
Datatype::FixedPoint { byte_order, .. }
| Datatype::FloatingPoint { byte_order, .. }
| Datatype::BitField { byte_order, .. } => {
*byte_order = match byte_order {
LittleEndian => BigEndian,
BigEndian => LittleEndian,
_ => return false,
}
}
_ => return false,
}
&a == b
}
#[cfg(test)]
mod tests {
use super::*;
fn regular(start: u64, stride: u64, count: u64, block: u64) -> SerializedSelection {
SerializedSelection::Regular {
start: vec![start],
stride: vec![stride],
count: vec![count],
block: vec![block],
}
}
#[test]
fn printf_names_follow_libhdf5() {
let n = SourceName::parse("f-%b.h5").unwrap();
assert_eq!(n.subs(), 1);
assert_eq!(n.build(12), "f-12.h5");
let n = SourceName::parse("100%%_%b_%b").unwrap();
assert_eq!(n.build(3), "100%_3_3");
let n = SourceName::parse("plain%%name").unwrap();
assert_eq!((n.subs(), n.build(7)), (0, "plain%name".to_string()));
// Anything else after '%' (or a trailing '%') is invalid.
assert!(SourceName::parse("a%d").is_err());
assert!(SourceName::parse("a%").is_err());
}
#[test]
fn clip_extent_matches_libhdf5_arithmetic() {
// 7 source slices (contiguous unlimited source) into blocks of 3
// every 4: two full blocks and one slice of a third -> extent 9.
let src = regular(0, 1, UNLIMITED, 1);
let v = regular(0, 4, UNLIMITED, 3);
assert_eq!(clip_extent_match(&v, 0, &src, 0, 7).unwrap(), 9);
// Exactly two blocks: the extent ends at the end of the last block.
assert_eq!(clip_extent_match(&v, 0, &src, 0, 6).unwrap(), 7);
// An empty source gives an empty mapping.
assert_eq!(clip_extent_match(&v, 0, &src, 0, 0).unwrap(), 0);
// Unlimited block: the extent is start + slices.
let vb = regular(2, 1, 1, UNLIMITED);
assert_eq!(clip_extent_match(&vb, 0, &src, 0, 5).unwrap(), 7);
// A strided source clipped mid-block counts only the selected slices.
let src2 = regular(1, 4, UNLIMITED, 2); // 1,2, 5,6, 9,10 ...
let dense = regular(0, 1, UNLIMITED, 1);
assert_eq!(clip_extent_match(&dense, 0, &src2, 0, 6).unwrap(), 3);
}
#[test]
fn clipped_selection_drops_the_partial_tail() {
let v = regular(0, 4, UNLIMITED, 3);
assert_eq!(
selection_indices(&v, &[20], Some((0, 9))).unwrap(),
vec![0, 1, 2, 4, 5, 6, 8]
);
// Unclipped unlimited selections cannot be enumerated.
assert!(selection_indices(&v, &[20], None).is_err());
}
#[test]
fn unlimited_mapping_rules() {
let sel = |s: &SerializedSelection| -> Vec<u8> {
// Serialize as version 2 (8-byte regular).
let SerializedSelection::Regular {
start,
stride,
count,
block,
} = s
else {
unreachable!()
};
let mut b = Vec::new();
b.extend_from_slice(&2u32.to_le_bytes());
b.extend_from_slice(&2u32.to_le_bytes());
b.push(1);
b.extend_from_slice(&0u32.to_le_bytes());
b.extend_from_slice(&(start.len() as u32).to_le_bytes());
for d in 0..start.len() {
for v in [start[d], stride[d], count[d], block[d]] {
b.extend_from_slice(&v.to_le_bytes());
}
}
b
};
let mapping = |file: &str, v: &SerializedSelection, s: &SerializedSelection| VdsMapping {
source_file: file.into(),
source_dataset: "d".into(),
source_selection: sel(s),
virtual_selection: sel(v),
};
let unlim = regular(0, 10, UNLIMITED, 10);
let fixed = regular(0, 1, 1, 10);
// Unlimited virtual + limited source needs %b in a name ...
assert!(Mapping::new(mapping("f.h5", &unlim, &fixed)).is_err());
let m = Mapping::new(mapping("f%b.h5", &unlim, &fixed)).unwrap();
assert_eq!(m.kind, Kind::Printf { vdim: 0 });
// ... and %b is refused anywhere else.
assert!(Mapping::new(mapping("f%b.h5", &fixed, &fixed)).is_err());
let src_unlim = regular(0, 1, UNLIMITED, 1);
let m = Mapping::new(mapping("f.h5", &unlim, &src_unlim)).unwrap();
assert_eq!(m.kind, Kind::Unlimited { vdim: 0, sdim: 0 });
// An unlimited source into a limited virtual selection is refused.
assert!(Mapping::new(mapping("f.h5", &fixed, &src_unlim)).is_err());
}
}