fix(format): read unmapped VDS elements as the virtual dataset's fill value
Elements of a virtual dataset that no mapping supplies (unmapped regions, a missing source file, a missing source dataset) read as 0 instead of the fill value libhdf5 returns — silent wrong data for any VDS created with a non-zero fillvalue (read-matrix cases 0471/0472: -1 and 7 read as 0). A missing source dataset was an error; libhdf5 reads it as fill. Move VDS assembly into a new vds module following H5Dvirtual.c: vds::read_virtual_dataset takes the dataset's fill value and a VdsFileResolver that can refuse a name, and reports how many elements were unmapped. Sources are read with their own fill value, and a source whose datatype differs from the virtual dataset's is an error (libhdf5 converts). File passes the dataset's fill value, resolves source names against the virtual file's directory, and refuses names that leave it with an error instead of reading them as fill. read_selection on a VDS goes through the same fill-aware path. The raw-read API (read_raw_data_full*) has no fill value, so it now errors for a VDS with unmapped elements instead of guessing zeros. Tests: vds_interop::vds_unmapped_regions_read_as_fill_value (external, same-file, missing file/dataset, sparse source with its own fill, int fill; earliest and latest format) and vds_source_outside_directory_is_an_error_not_fill, both against h5py; integration_test::v4_virtual_dataset_raw_api_refuses_to_guess_the_fill_value. Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
@@ -83,6 +83,45 @@ fn read_chunked_dataset(file_data: &[u8], dataset_path: &str) -> (Vec<u8>, Datat
|
||||
(raw, datatype, dataspace)
|
||||
}
|
||||
|
||||
/// Helper: read a virtual dataset with `vds::read_virtual_dataset`, giving it
|
||||
/// the dataset's own fill value (same-file sources only).
|
||||
fn read_virtual_fixture(file_data: &[u8], path: &str) -> (Vec<u8>, Datatype) {
|
||||
let sig = find_signature(file_data).unwrap();
|
||||
let sb = Superblock::parse(file_data, sig).unwrap();
|
||||
let addr = resolve_path_any(file_data, &sb, path).unwrap();
|
||||
let hdr =
|
||||
ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size).unwrap();
|
||||
let msg = |t: MessageType| hdr.messages.iter().find(|m| m.msg_type == t).unwrap();
|
||||
let ds = Dataspace::parse(&msg(MessageType::Dataspace).data, sb.length_size).unwrap();
|
||||
let (dt, _) = Datatype::parse(&msg(MessageType::Datatype).data).unwrap();
|
||||
let layout = DataLayout::parse(
|
||||
&msg(MessageType::DataLayout).data,
|
||||
sb.offset_size,
|
||||
sb.length_size,
|
||||
)
|
||||
.unwrap();
|
||||
let fill = clawhdf5_format::fill_value::dataset_fill_value_in(
|
||||
file_data,
|
||||
&hdr.messages,
|
||||
sb.offset_size,
|
||||
sb.length_size,
|
||||
)
|
||||
.unwrap();
|
||||
let v = clawhdf5_format::vds::read_virtual_dataset(
|
||||
file_data,
|
||||
&layout,
|
||||
&ds,
|
||||
&dt,
|
||||
fill.as_deref(),
|
||||
sb.offset_size,
|
||||
sb.length_size,
|
||||
None,
|
||||
)
|
||||
.unwrap();
|
||||
assert_eq!(v.dims, ds.dimensions);
|
||||
(v.data, dt)
|
||||
}
|
||||
|
||||
/// Helper: read any dataset (contiguous or chunked) as f64.
|
||||
fn read_dataset_f64_any(bytes: &[u8], path: &str) -> Vec<f64> {
|
||||
let sig = find_signature(bytes).unwrap();
|
||||
@@ -672,7 +711,7 @@ fn v4_virtual_dataset_same_file_read() {
|
||||
// virt[4:8] <- (unmapped) => fill 0
|
||||
// virt[8:12] <- src_b[0:4] (ALL) => 20,21,22,23
|
||||
let file_data = include_bytes!("fixtures/vds_same_file.h5");
|
||||
let (raw, datatype, _) = read_chunked_dataset(file_data, "virt");
|
||||
let (raw, datatype) = read_virtual_fixture(file_data, "virt");
|
||||
let values = read_as_i32(&raw, &datatype).unwrap();
|
||||
assert_eq!(
|
||||
values,
|
||||
@@ -681,6 +720,38 @@ fn v4_virtual_dataset_same_file_read() {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v4_virtual_dataset_raw_api_refuses_to_guess_the_fill_value() {
|
||||
// The raw read API has no fill value message, so a virtual dataset with an
|
||||
// unmapped region is an error there instead of zeros that may be wrong.
|
||||
let file_data = include_bytes!("fixtures/vds_same_file.h5");
|
||||
let sig = find_signature(file_data).unwrap();
|
||||
let sb = Superblock::parse(file_data, sig).unwrap();
|
||||
let addr = resolve_path_any(file_data, &sb, "virt").unwrap();
|
||||
let hdr =
|
||||
ObjectHeader::parse(file_data, addr as usize, sb.offset_size, sb.length_size).unwrap();
|
||||
let msg = |t: MessageType| hdr.messages.iter().find(|m| m.msg_type == t).unwrap();
|
||||
let ds = Dataspace::parse(&msg(MessageType::Dataspace).data, sb.length_size).unwrap();
|
||||
let (dt, _) = Datatype::parse(&msg(MessageType::Datatype).data).unwrap();
|
||||
let layout = DataLayout::parse(
|
||||
&msg(MessageType::DataLayout).data,
|
||||
sb.offset_size,
|
||||
sb.length_size,
|
||||
)
|
||||
.unwrap();
|
||||
let err = read_raw_data_full(
|
||||
file_data,
|
||||
&layout,
|
||||
&ds,
|
||||
&dt,
|
||||
None,
|
||||
sb.offset_size,
|
||||
sb.length_size,
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.to_string().contains("fill value"), "{err}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v4_virtual_dataset_2d_same_file_read() {
|
||||
// A 4x4 virtual dataset assembled from two 2x2 same-file sources placed as
|
||||
@@ -689,7 +760,7 @@ fn v4_virtual_dataset_2d_same_file_read() {
|
||||
// virt[2:4,2:4] <- src_b = [[5,6],[7,8]]
|
||||
// everything else -> fill 0
|
||||
let file_data = include_bytes!("fixtures/vds_2d_same_file.h5");
|
||||
let (raw, datatype, _) = read_chunked_dataset(file_data, "virt");
|
||||
let (raw, datatype) = read_virtual_fixture(file_data, "virt");
|
||||
let values = read_as_i32(&raw, &datatype).unwrap();
|
||||
assert_eq!(
|
||||
values,
|
||||
|
||||
Reference in New Issue
Block a user