feat: assemble 1-D same-file Virtual Datasets (VDS)

A virtual layout previously returned UnsupportedVersion. Implement reading
for the common 1-D, same-file case, reverse-engineered and validated against
HDF5 2.0.

- Rewrite parse_vds_mappings to the real global-heap block format
  (version(1) · nused(length_size) · entries · checksum(4)), where each
  entry is source-file(null) · source-dataset(null) · source-selection ·
  virtual-selection. Block version 1 encodes a same-file source as a single
  0x04 marker in place of the file name; version 0 stores an explicit file
  name. The selections are H5S-serialized and self-describing in length, so
  they are decoded to find entry boundaries. The previous parser used a
  guessed layout that did not match real files.

- Extend Selection with decode_serialized() (H5S_select_serialize: ALL,
  NONE, and version-3 regular hyperslabs) and iter_linear_1d().

- Add read_virtual_data: resolve the mapping block from the global heap,
  read each same-file source dataset, and scatter its selected elements into
  the virtual buffer; unmapped regions stay at the zero fill value.
  External-file sources and N-D selections return a clean unsupported error.

Tests: real-file integration test (vds_same_file.h5: partial source slice +
fill gap), selection decoder unit tests built from the fixture bytes, and
same-file/external mapping-parser unit tests.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
This commit is contained in:
osobh
2026-06-03 20:01:51 +00:00
co-authored by Claude Opus 4.8
parent a24fcb8be4
commit 908af40282
6 changed files with 523 additions and 86 deletions
+240
View File
@@ -19,6 +19,8 @@ use alloc::{vec, vec::Vec};
use core::ops::Range;
use crate::error::FormatError;
/// A selection describing which elements of a dataset to access.
#[derive(Debug, Clone, PartialEq)]
pub enum Selection {
@@ -220,6 +222,169 @@ impl Selection {
}
}
}
/// Decode a selection from its on-disk **`H5S_select_serialize`** form.
///
/// Returns the selection and the number of bytes consumed (selections are
/// self-describing in length, so the count lets a caller walk a packed list
/// of selections — as the Virtual Dataset global-heap block does).
///
/// Only the forms needed for VDS assembly are decoded: `ALL`, `NONE`, and
/// **regular** hyperslabs serialized at **version 3** (the encoding HDF5
/// 1.10+/2.0 emit). Point selections, irregular hyperslabs, and older
/// hyperslab versions return an error rather than mis-decoding.
pub fn decode_serialized(data: &[u8]) -> Result<(Selection, usize), FormatError> {
if data.len() < 8 {
return Err(FormatError::UnexpectedEof {
expected: 8,
available: data.len(),
});
}
let sel_type = u32::from_le_bytes([data[0], data[1], data[2], data[3]]);
let version = u32::from_le_bytes([data[4], data[5], data[6], data[7]]);
match sel_type {
// ALL / NONE: type(4) + version(4) + reserved(4) + length(4) = 16 bytes.
3 => Ok((Selection::All, 16)),
0 => Ok((Selection::None, 16)),
2 => decode_hyperslab_serialized(data, version),
1 => Err(FormatError::ChunkedReadError(
"VDS point selections are not supported".into(),
)),
_ => Err(FormatError::ChunkedReadError(
"unknown dataspace selection type".into(),
)),
}
}
/// Enumerate the selected element indices of a **1-D** dataspace of the
/// given `extent`, in selection order.
///
/// Returns an error for selections of rank != 1 (N-dimensional VDS
/// assembly is not supported in this build).
pub fn iter_linear_1d(&self, extent: u64) -> Result<Vec<u64>, FormatError> {
match self {
Selection::All => Ok((0..extent).collect()),
Selection::None => Ok(Vec::new()),
Selection::Hyperslab {
start,
stride,
count,
block,
} => {
if start.len() != 1 {
return Err(FormatError::ChunkedReadError(
"only 1-D VDS hyperslab selections are supported".into(),
));
}
let (s, st, c, b) = (start[0], stride[0], count[0], block[0]);
let mut out = Vec::new();
for ci in 0..c {
let base = s + ci * st;
for bi in 0..b {
let idx = base + bi;
if idx >= extent {
return Err(FormatError::ChunkedReadError(
"VDS hyperslab selection exceeds dataspace extent".into(),
));
}
out.push(idx);
}
}
Ok(out)
}
Selection::Points(pts) => {
let mut out = Vec::with_capacity(pts.len());
for p in pts {
if p.len() != 1 {
return Err(FormatError::ChunkedReadError(
"only 1-D VDS point selections are supported".into(),
));
}
if p[0] >= extent {
return Err(FormatError::ChunkedReadError(
"VDS point selection exceeds dataspace extent".into(),
));
}
out.push(p[0]);
}
Ok(out)
}
}
}
}
/// Decode an `H5S_SEL_HYPER` selection in its serialized form. Only version-3
/// **regular** hyperslabs are supported.
fn decode_hyperslab_serialized(
data: &[u8],
version: u32,
) -> Result<(Selection, usize), FormatError> {
if version != 3 {
return Err(FormatError::ChunkedReadError(
"only version-3 hyperslab selections are supported".into(),
));
}
// type(4) ver(4) flags(1) enc_size(1) rank(4) [start,stride,count,block]*rank
if data.len() < 14 {
return Err(FormatError::UnexpectedEof {
expected: 14,
available: data.len(),
});
}
let flags = data[8];
let enc_size = data[9] as usize;
// Bit 0 set => regular hyperslab. Irregular hyperslabs list explicit blocks.
if flags & 0x01 == 0 {
return Err(FormatError::ChunkedReadError(
"irregular VDS hyperslab selections are not supported".into(),
));
}
if enc_size != 2 && enc_size != 4 && enc_size != 8 {
return Err(FormatError::ChunkedReadError(
"unsupported hyperslab coordinate encoding size".into(),
));
}
let rank = u32::from_le_bytes([data[10], data[11], data[12], data[13]]) as usize;
let mut pos = 14;
let read_coord = |data: &[u8], pos: usize| -> Result<u64, FormatError> {
if pos + enc_size > data.len() {
return Err(FormatError::UnexpectedEof {
expected: pos + enc_size,
available: data.len(),
});
}
let mut v = 0u64;
for (i, &b) in data[pos..pos + enc_size].iter().enumerate() {
v |= (b as u64) << (i * 8);
}
Ok(v)
};
let (mut start, mut stride, mut count, mut block) = (
Vec::with_capacity(rank),
Vec::with_capacity(rank),
Vec::with_capacity(rank),
Vec::with_capacity(rank),
);
for _ in 0..rank {
start.push(read_coord(data, pos)?);
pos += enc_size;
stride.push(read_coord(data, pos)?);
pos += enc_size;
count.push(read_coord(data, pos)?);
pos += enc_size;
block.push(read_coord(data, pos)?);
pos += enc_size;
}
Ok((
Selection::Hyperslab {
start,
stride,
count,
block,
},
pos,
))
}
// ---------------------------------------------------------------------------
@@ -313,4 +478,79 @@ mod tests {
// Chunk [9..10] should not intersect (only row 9, but selection ends at row 8)
assert!(!sel.intersects_chunk(&[9], &[1]));
}
#[test]
fn decode_all_selection_16_bytes() {
let bytes = [3u8, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0];
let (sel, consumed) = Selection::decode_serialized(&bytes).unwrap();
assert_eq!(sel, Selection::All);
assert_eq!(consumed, 16);
assert_eq!(sel.iter_linear_1d(4).unwrap(), vec![0, 1, 2, 3]);
}
#[test]
fn decode_regular_hyperslab_matches_vds_fixture() {
// Exact virtual selection for src_a in the VDS fixture:
// start=0 stride=1 count=1 block=4, version 3, enc_size 2, rank 1.
let bytes = [
0x02, 0, 0, 0, // type = HYPER
0x03, 0, 0, 0, // version 3
0x01, // flags = regular
0x02, // enc_size = 2
0x01, 0, 0, 0, // rank = 1
0x00, 0x00, // start
0x01, 0x00, // stride
0x01, 0x00, // count
0x04, 0x00, // block
];
let (sel, consumed) = Selection::decode_serialized(&bytes).unwrap();
assert_eq!(consumed, 22);
assert_eq!(
sel,
Selection::Hyperslab {
start: vec![0],
stride: vec![1],
count: vec![1],
block: vec![4],
}
);
assert_eq!(sel.iter_linear_1d(8).unwrap(), vec![0, 1, 2, 3]);
}
#[test]
fn decode_hyperslab_start4() {
let bytes = [
0x02, 0, 0, 0, 0x03, 0, 0, 0, 0x01, 0x02, 0x01, 0, 0, 0, //
0x04, 0x00, 0x01, 0x00, 0x01, 0x00, 0x04, 0x00,
];
let (sel, _) = Selection::decode_serialized(&bytes).unwrap();
assert_eq!(sel.iter_linear_1d(8).unwrap(), vec![4, 5, 6, 7]);
}
#[test]
fn decode_strided_hyperslab_iter() {
// start=1 stride=3 count=2 block=2 => 1,2, 4,5
let bytes = [
0x02, 0, 0, 0, 0x03, 0, 0, 0, 0x01, 0x02, 0x01, 0, 0, 0, //
0x01, 0x00, 0x03, 0x00, 0x02, 0x00, 0x02, 0x00,
];
let (sel, _) = Selection::decode_serialized(&bytes).unwrap();
assert_eq!(sel.iter_linear_1d(8).unwrap(), vec![1, 2, 4, 5]);
}
#[test]
fn decode_nd_hyperslab_iter_rejected() {
let bytes = [
0x02, 0, 0, 0, 0x03, 0, 0, 0, 0x01, 0x02, 0x02, 0, 0, 0, // rank 2
0, 0, 1, 0, 1, 0, 2, 0, 0, 0, 1, 0, 1, 0, 2, 0,
];
let (sel, _) = Selection::decode_serialized(&bytes).unwrap();
assert!(sel.iter_linear_1d(16).is_err());
}
#[test]
fn decode_irregular_hyperslab_rejected() {
let bytes = [0x02u8, 0, 0, 0, 0x03, 0, 0, 0, 0x00, 0x02, 0x01, 0, 0, 0];
assert!(Selection::decode_serialized(&bytes).is_err());
}
}