feat(format): read unlimited and printf-style VDS mappings like libhdf5

Unlimited VDS mappings were refused, and printf-style source names
("f-%b.h5") were not expanded, so those regions read as fill (read-matrix
case 0470: 29 of 30 values wrong). All 7 virtual datasets in the libhdf5
test set use such mappings.

Implement H5Dvirtual.c's semantics in the vds module:
- %b is the block number, %% a literal %, other specifiers are an error;
  block j of the virtual selection comes from the source named with j,
  probing from 0 to the first missing source (printf gap 0);
- unlimited source/virtual selections are clipped to what the source's
  current extent fills (H5S_hyper_get_clip_extent_match, partial last
  block included);
- the extent is recomputed as H5Dget_space does (view "last available":
  the largest clip, never below what limited mappings need), exposed as
  vds::virtual_dataset_extent and used by Dataset::shape();
- a source in the other byte order is byte-swapped; other conversions stay
  an error.

Tests: vds_interop::vds_printf_source_names,
vds_unlimited_mappings_follow_source_extents (h5py low-level API, earliest
and latest format) and vds_libhdf5_test_files (vds-eiger, 4_vds and
vds-percival-unlim-maxmin from HDF5's tools/test/testfiles/vds, committed
as fixtures) all compare shape and values with h5py; unit tests for the
clip arithmetic, name parsing and mapping rules.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-25 22:10:09 -05:00
co-authored by Claude Opus 5.5
parent e94a52a88b
commit b4a44a2e66
18 changed files with 889 additions and 84 deletions
+680 -82
View File
@@ -45,12 +45,149 @@ 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: String,
dataset: String,
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.
@@ -82,49 +219,269 @@ fn load_mappings(
})?;
parse_vds_mappings(&obj.data, length_size)?
.into_iter()
.map(|m: VdsMapping| {
let (vsel, _) = SerializedSelection::decode(&m.virtual_selection)?;
let (ssel, _) = SerializedSelection::decode(&m.source_selection)?;
Ok(Mapping {
file: m.source_file,
dataset: m.source_dataset,
vsel,
ssel,
})
})
.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(mappings: &[Mapping], stored: &[u64], sources: &mut Sources) -> 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.
/// 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>,
resolver: Option<&VdsFileResolver>,
) -> Result<Vec<u64>, FormatError> {
let mappings = load_mappings(file_data, layout, length_size)?;
check_fixed(&mappings)?;
Ok(dataspace.dimensions.clone())
}
fn check_fixed(mappings: &[Mapping]) -> Result<(), FormatError> {
if mappings
.iter()
.any(|m| m.vsel.unlimited_dim().is_some() || m.ssel.unlimited_dim().is_some())
{
return Err(vds_err(
"unlimited virtual dataset mappings are not supported",
));
if mappings.iter().all(|m| m.kind == Kind::Fixed) {
return Ok(dataspace.dimensions.clone());
}
Ok(())
let mut sources = Sources::new(file_data, resolver);
Ok(plan(&mappings, &dataspace.dimensions, &mut sources)?.dims)
}
/// Read a whole virtual dataset.
/// 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
@@ -137,20 +494,13 @@ pub fn read_virtual_dataset(
dataspace: &Dataspace,
datatype: &Datatype,
fill: Option<&[u8]>,
offset_size: u8,
_offset_size: u8,
length_size: u8,
resolver: Option<&VdsFileResolver>,
) -> Result<VirtualData, FormatError> {
let mappings = load_mappings(file_data, layout, length_size)?;
check_fixed(&mappings)?;
let dims = virtual_dataset_extent(
file_data,
layout,
dataspace,
offset_size,
length_size,
resolver,
)?;
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
@@ -167,14 +517,46 @@ pub fn read_virtual_dataset(
}
let mut mapped = vec![false; usize::try_from(total).map_err(|_| vds_err("VDS too large"))?];
let mut sources = Sources::new(file_data, resolver);
for m in &mappings {
let Some(src) = sources.dataset(&m.file, &m.dataset, 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)?;
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;
@@ -185,6 +567,37 @@ pub fn read_virtual_dataset(
})
}
/// `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],
@@ -395,6 +808,15 @@ impl<'a, 'r> Sources<'a, 'r> {
Ok(self.cached_file.as_ref().and_then(|(_, b)| b.as_deref()))
}
/// 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> {
let Some(bytes) = self.file(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.
@@ -407,20 +829,37 @@ impl<'a, 'r> Sources<'a, 'r> {
let Some(bytes) = self.file(file)? else {
return Ok(None);
};
read_source(bytes, path, datatype)
let Some(src) = open_source(bytes, path)? else {
return Ok(None);
};
read_source(bytes, src, path, datatype).map(Some)
}
}
/// Read source dataset `path` of the file in `file_data` in full (its own
/// fill value applied to unallocated chunks), or `None` if it does not exist.
fn read_source(
file_data: &[u8],
/// 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,
datatype: &Datatype,
) -> Result<Option<SourceData>, FormatError> {
use crate::filter_pipeline::FilterPipeline;
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(file_data: &[u8], path: &str) -> Result<Option<OpenSource>, FormatError> {
use crate::message_type::MessageType;
use crate::object_header::ObjectHeader;
use crate::shared_message::message_data_with_sohm;
let sig = crate::signature::find_signature(file_data)?;
@@ -431,30 +870,55 @@ fn read_source(
Err(FormatError::PathNotFound(_)) => return Ok(None),
Err(e) => return Err(e),
};
let hdr = ObjectHeader::parse(file_data, addr as usize, os, ls)?;
let msg = |t: MessageType| {
hdr.messages
.iter()
.find(|m| m.msg_type == t)
.ok_or_else(|| vds_err(format!("VDS source {path} has no {t:?} message")))
let header = crate::object_header::ObjectHeader::parse(file_data, addr as usize, 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 dataspace = Dataspace::parse(
&message_data_with_sohm(file_data, msg(MessageType::Dataspace)?, os, ls)?,
ls,
)?;
let (src_type, _) = Datatype::parse(&message_data_with_sohm(
file_data,
msg(MessageType::Datatype)?,
os,
ls,
)?)?;
if &src_type != datatype {
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(
file_data: &[u8],
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;
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 \
(type conversion is not supported)"
(only a byte-order conversion is supported)"
)));
}
let layout = DataLayout::parse(&msg(MessageType::DataLayout)?.data, os, ls)?;
};
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 { .. }) {
@@ -462,7 +926,8 @@ fn read_source(
"virtual dataset source is itself virtual (unsupported)",
));
}
let pipeline = hdr
let pipeline = src
.header
.messages
.iter()
.find(|m| m.msg_type == MessageType::FilterPipeline)
@@ -471,10 +936,10 @@ fn read_source(
})
.transpose()?;
let raw = crate::fill_value::read_full_with_fill(
&hdr.messages,
&src.header.messages,
file_data,
&layout,
&dataspace,
&src.dataspace,
src_type.type_size() as usize,
os,
ls,
@@ -482,7 +947,7 @@ fn read_source(
crate::data_read::read_raw_data_full(
file_data,
&layout,
&dataspace,
&src.dataspace,
&src_type,
pipeline.as_ref(),
os,
@@ -490,8 +955,141 @@ fn read_source(
)
},
)?;
Ok(Some(SourceData {
dims: dataspace.dimensions,
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());
}
}