Files
clawhdf5/crates/clawhdf5-format/src/data_layout.rs
T
Omar Sobh 55959b4920
CI / test (push) Failing after 15s
ci: wire up CI, fix no_std build, fix stale package names in scripts
- Add .gitea/workflows/ci.yml running scripts/ci-test.sh (fmt, clippy,
  test, no_std check) on push/PR to main.
- Fix stale rustyhdf5-py/rustyhdf5-format package names in
  ci-test.sh/check-nostd.sh, which had been silently no-op'ing those
  checks (cargo warns but doesn't fail on an unknown --exclude/-p
  target).
- With those checks actually running, fix the real issues they surface:
  - clippy: useless_conversion in chunked_write.rs, byte_char_slices in
    global_heap.rs/object_header.rs.
  - cargo fmt: apply formatting across the workspace (whitespace only).
  - no_std (thumbv7em-none-eabihf) build errors in clawhdf5-format:
    core::sync::atomic::AtomicU64 doesn't exist on that target (no
    native 64-bit atomics) — switch profiling.rs's counters to
    portable-atomic, which falls back to a CAS-based emulation there
    and is a no-op wrapper elsewhere. Add missing alloc imports for
    Box (filters.rs), Vec (filters_szip.rs), and format! (dict_encoding.rs)
    on no_std paths. Replace f64::powi (std/libm-only) with a small
    local exponentiation-by-squaring helper in the scale-offset filter.
2026-08-05 10:50:13 -07:00

866 lines
33 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! HDF5 Data Layout message parsing (message type 0x0008).
#[cfg(not(feature = "std"))]
use alloc::{string::String, vec::Vec};
#[cfg(feature = "std")]
use std::string::String;
use crate::error::FormatError;
/// A single VDS (Virtual Dataset) source mapping.
///
/// Maps a region of the virtual dataset to a region of a source dataset
/// in a (possibly external) HDF5 file.
#[derive(Debug, Clone, PartialEq)]
pub struct VdsMapping {
/// Source file name (may be "." for the same file).
pub source_file: String,
/// Source dataset path within the source file.
pub source_dataset: String,
/// Serialized source selection bytes (dataspace selection).
pub source_selection: Vec<u8>,
/// Serialized virtual selection bytes (dataspace selection).
pub virtual_selection: Vec<u8>,
}
/// Parsed HDF5 data layout message.
#[derive(Debug, Clone, PartialEq)]
pub enum DataLayout {
/// Compact: data stored inline in the message.
Compact {
/// The inline raw data bytes.
data: Vec<u8>,
},
/// Contiguous: data stored at a single address in the file.
Contiguous {
/// File address of the data, or `None` if undefined (all 0xFF).
address: Option<u64>,
/// Size of the data in bytes.
size: u64,
},
/// Chunked: data stored in chunks via a B-tree.
Chunked {
/// Chunk dimension sizes.
chunk_dimensions: Vec<u32>,
/// B-tree address, or `None` if undefined.
btree_address: Option<u64>,
/// Layout version (3 or 4).
version: u8,
/// Chunk index type (v4 only).
chunk_index_type: Option<u8>,
/// Filtered size for v4 single chunk with filters.
single_chunk_filtered_size: Option<u64>,
/// Filter mask for v4 single chunk with filters.
single_chunk_filter_mask: Option<u32>,
},
/// Virtual dataset layout (v4 only).
Virtual {
/// Layout version.
version: u8,
/// Global heap address where VDS mappings are stored.
global_heap_address: Option<u64>,
/// Index of the object in the global heap collection.
global_heap_index: u32,
/// Parsed VDS source mappings (populated after global heap lookup).
mappings: Vec<VdsMapping>,
},
}
/// Parse VDS mappings from global-heap object data.
///
/// The global-heap block holding a VDS mapping list is laid out as
/// (reverse-engineered and validated against HDF5 2.0):
///
/// ```text
/// version(1) · nused(length_size, LE) · entry[nused] · checksum(4)
/// ```
///
/// Each entry is:
/// - source file name — a null-terminated string in **block version 0**; in
/// **block version 1** a same-file reference is encoded as a single `0x04`
/// marker byte (the source file is the virtual file itself) in place of the
/// name;
/// - source dataset name (null-terminated string);
/// - source selection (serialized `H5S` dataspace selection — self-describing
/// in length);
/// - virtual selection (serialized `H5S` dataspace selection).
///
/// The selections are decoded with [`crate::selection::Selection`] purely to
/// learn their byte length so the entry list can be walked; the raw selection
/// bytes are retained on each [`VdsMapping`] for the reader to interpret.
pub fn parse_vds_mappings(
heap_data: &[u8],
length_size: u8,
) -> Result<Vec<VdsMapping>, FormatError> {
use crate::selection::Selection;
let ls = length_size as usize;
if heap_data.len() < 1 + ls {
return Ok(Vec::new());
}
let version = heap_data[0];
let mut pos = 1;
let nused = read_length(heap_data, pos, length_size)?;
pos += ls;
// `nused` is untrusted; don't pre-allocate from it. Each entry consumes at
// least a few bytes, so the loop is naturally bounded by the heap data and
// a bogus `nused` simply errors out on the first short read.
let mut mappings = Vec::new();
// Reads one self-describing selection at `pos`, returning its raw bytes and
// advancing past it — bounds-checked so a corrupt selection can't overrun.
let read_selection = |heap_data: &[u8], pos: &mut usize| -> Result<Vec<u8>, FormatError> {
let rest = heap_data.get(*pos..).ok_or(FormatError::UnexpectedEof {
expected: *pos,
available: heap_data.len(),
})?;
let (_, len) = Selection::decode_serialized(rest)?;
let bytes = rest
.get(..len)
.ok_or(FormatError::UnexpectedEof {
expected: pos.saturating_add(len),
available: heap_data.len(),
})?
.to_vec();
*pos += len;
Ok(bytes)
};
for _ in 0..nused {
// Source file name (with the version-1 same-file marker handled).
let source_file = if version >= 1 && heap_data.get(pos) == Some(&0x04) {
pos += 1;
String::from(".")
} else {
read_null_terminated_string(heap_data, &mut pos)?
};
// Source dataset name.
let source_dataset = read_null_terminated_string(heap_data, &mut pos)?;
// Source selection, then virtual selection (both self-describing length).
let source_selection = read_selection(heap_data, &mut pos)?;
let virtual_selection = read_selection(heap_data, &mut pos)?;
mappings.push(VdsMapping {
source_file,
source_dataset,
source_selection,
virtual_selection,
});
}
Ok(mappings)
}
/// Read a null-terminated UTF-8 string from data starting at `pos`.
fn read_null_terminated_string(data: &[u8], pos: &mut usize) -> Result<String, FormatError> {
let start = *pos;
while *pos < data.len() && data[*pos] != 0 {
*pos += 1;
}
if *pos >= data.len() {
return Err(FormatError::UnexpectedEof {
expected: start + 1,
available: data.len(),
});
}
let s = String::from_utf8_lossy(&data[start..*pos]).into_owned();
*pos += 1; // skip null terminator
Ok(s)
}
fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> {
match offset.checked_add(needed) {
Some(end) if end <= data.len() => Ok(()),
_ => Err(FormatError::UnexpectedEof {
expected: offset.saturating_add(needed),
available: data.len(),
}),
}
}
fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
let s = size as usize;
ensure_len(data, pos, s)?;
let slice = &data[pos..pos + s];
Ok(match size {
2 => u16::from_le_bytes([slice[0], slice[1]]) as u64,
4 => u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]) as u64,
8 => u64::from_le_bytes([
slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7],
]),
_ => {
return Err(FormatError::InvalidOffsetSize(size));
}
})
}
fn read_length(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
read_offset(data, pos, size)
}
/// Check if all bytes in a slice are 0xFF (undefined address).
fn is_undefined(data: &[u8], pos: usize, size: u8) -> bool {
let s = size as usize;
if pos + s > data.len() {
return false;
}
data[pos..pos + s].iter().all(|&b| b == 0xFF)
}
impl DataLayout {
/// For a Virtual layout, resolve VDS mappings from the global heap.
///
/// Reads the global heap collection at the stored address and parses the
/// VDS mapping entries from the referenced object. After calling this
/// method, the `mappings` field will be populated.
///
/// No-op for non-Virtual layouts.
pub fn resolve_vds_mappings(
&mut self,
file_data: &[u8],
length_size: u8,
) -> Result<(), FormatError> {
if let DataLayout::Virtual {
global_heap_address,
global_heap_index,
mappings,
..
} = self
&& let Some(addr) = *global_heap_address
{
let coll = crate::global_heap::GlobalHeapCollection::parse(
file_data,
addr as usize,
length_size,
)?;
let obj = coll.get_object(*global_heap_index as u16).ok_or(
FormatError::GlobalHeapObjectNotFound {
collection_address: addr,
index: *global_heap_index as u16,
},
)?;
*mappings = parse_vds_mappings(&obj.data, length_size)?;
}
Ok(())
}
/// Parse a data layout message from raw message bytes.
///
/// `offset_size` and `length_size` come from the superblock.
pub fn parse(data: &[u8], offset_size: u8, length_size: u8) -> Result<DataLayout, FormatError> {
ensure_len(data, 0, 2)?;
let version = data[0];
let layout_class = data[1];
match version {
3 => Self::parse_v3(data, layout_class, offset_size, length_size),
// v5 (emitted by HDF5 1.14+/2.0 with `libver=latest`) uses the same
// message structure as v4 — only the version number was bumped.
4 | 5 => Self::parse_v4(data, layout_class, offset_size, length_size),
_ => Err(FormatError::InvalidLayoutVersion(version)),
}
}
fn parse_v3(
data: &[u8],
layout_class: u8,
offset_size: u8,
length_size: u8,
) -> Result<DataLayout, FormatError> {
let pos = 2;
match layout_class {
0 => {
// Compact
ensure_len(data, pos, 2)?;
let data_size = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
ensure_len(data, pos + 2, data_size)?;
let raw = data[pos + 2..pos + 2 + data_size].to_vec();
Ok(DataLayout::Compact { data: raw })
}
1 => {
// Contiguous
let os = offset_size as usize;
let ls = length_size as usize;
ensure_len(data, pos, os + ls)?;
let address = if is_undefined(data, pos, offset_size) {
None
} else {
Some(read_offset(data, pos, offset_size)?)
};
let size = read_length(data, pos + os, length_size)?;
Ok(DataLayout::Contiguous { address, size })
}
2 => {
// Chunked
ensure_len(data, pos, 1)?;
let dimensionality = data[pos] as usize;
let mut p = pos + 1;
// btree address first
let os = offset_size as usize;
ensure_len(data, p, os)?;
let btree_address = if is_undefined(data, p, offset_size) {
None
} else {
Some(read_offset(data, p, offset_size)?)
};
p += os;
// chunk dim sizes: dimensionality × 4 bytes each
ensure_len(data, p, dimensionality * 4)?;
let mut chunk_dimensions = Vec::with_capacity(dimensionality);
for _ in 0..dimensionality {
let dim = u32::from_le_bytes([data[p], data[p + 1], data[p + 2], data[p + 3]]);
chunk_dimensions.push(dim);
p += 4;
}
Ok(DataLayout::Chunked {
chunk_dimensions,
btree_address,
version: 3,
chunk_index_type: None,
single_chunk_filtered_size: None,
single_chunk_filter_mask: None,
})
}
_ => Err(FormatError::InvalidLayoutClass(layout_class)),
}
}
fn parse_v4(
data: &[u8],
layout_class: u8,
offset_size: u8,
length_size: u8,
) -> Result<DataLayout, FormatError> {
let pos = 2;
match layout_class {
0 => {
// Compact — same as v3
ensure_len(data, pos, 2)?;
let data_size = u16::from_le_bytes([data[pos], data[pos + 1]]) as usize;
ensure_len(data, pos + 2, data_size)?;
let raw = data[pos + 2..pos + 2 + data_size].to_vec();
Ok(DataLayout::Compact { data: raw })
}
1 => {
// Contiguous — same as v3
let os = offset_size as usize;
let ls = length_size as usize;
ensure_len(data, pos, os + ls)?;
let address = if is_undefined(data, pos, offset_size) {
None
} else {
Some(read_offset(data, pos, offset_size)?)
};
let size = read_length(data, pos + os, length_size)?;
Ok(DataLayout::Contiguous { address, size })
}
2 => {
// Chunked v4
ensure_len(data, pos, 3)?;
let flags = data[pos];
let dimensionality = data[pos + 1] as usize;
let dim_size_encoded_length = data[pos + 2] as usize;
let mut p = pos + 3;
// dimension sizes
ensure_len(data, p, dimensionality * dim_size_encoded_length)?;
let mut chunk_dimensions = Vec::with_capacity(dimensionality);
for _ in 0..dimensionality {
let val = match dim_size_encoded_length {
1 => data[p] as u32,
2 => u16::from_le_bytes([data[p], data[p + 1]]) as u32,
4 => u32::from_le_bytes([data[p], data[p + 1], data[p + 2], data[p + 3]]),
8 => {
// V4 chunked encodes dimension sizes as 8 bytes, but
// our ChunkedStorageV4 stores them as u32. We read only
// the low 4 bytes (little-endian). This silently
// truncates dimensions > 4 GiB, which are not expected
// in practice (HDF5 chunk dimensions are always small).
// If the high bytes are non-zero, the file is malformed
// or uses dimensions we cannot represent.
let high = u32::from_le_bytes([
data[p + 4],
data[p + 5],
data[p + 6],
data[p + 7],
]);
if high != 0 {
return Err(FormatError::UnexpectedEof {
expected: p + 8,
available: data.len(),
});
}
u32::from_le_bytes([data[p], data[p + 1], data[p + 2], data[p + 3]])
}
_ => {
return Err(FormatError::UnexpectedEof {
expected: p + dim_size_encoded_length,
available: data.len(),
});
}
};
chunk_dimensions.push(val);
p += dim_size_encoded_length;
}
// chunk index type
ensure_len(data, p, 1)?;
let chunk_index_type = data[p];
p += 1;
// Parse index-specific fields
let mut single_chunk_filtered_size = None;
let mut single_chunk_filter_mask = None;
let btree_address = match chunk_index_type {
1 => {
// Single chunk
// H5O_LAYOUT_CHUNK_SINGLE_INDEX_WITH_FILTER = 0x02
let filters_present = flags & 0x02 != 0;
if filters_present {
// filtered_size(length_size) + filter_mask(4) + address(offset_size)
let ls = length_size as usize;
let os = offset_size as usize;
ensure_len(data, p, ls + 4 + os)?;
single_chunk_filtered_size = Some(read_length(data, p, length_size)?);
p += ls;
single_chunk_filter_mask = Some(u32::from_le_bytes([
data[p],
data[p + 1],
data[p + 2],
data[p + 3],
]));
p += 4;
if is_undefined(data, p, offset_size) {
None
} else {
Some(read_offset(data, p, offset_size)?)
}
} else {
// just address(offset_size)
ensure_len(data, p, offset_size as usize)?;
if is_undefined(data, p, offset_size) {
None
} else {
Some(read_offset(data, p, offset_size)?)
}
}
}
2 => {
// Implicit: just address
ensure_len(data, p, offset_size as usize)?;
if is_undefined(data, p, offset_size) {
None
} else {
Some(read_offset(data, p, offset_size)?)
}
}
3 => {
// Fixed Array: max_dblk_page_nelmts_bits(1) + address(offset_size)
ensure_len(data, p, 1 + offset_size as usize)?;
p += 1; // skip max_dblk_page_nelmts_bits
if is_undefined(data, p, offset_size) {
None
} else {
Some(read_offset(data, p, offset_size)?)
}
}
4 => {
// Extensible Array: 5 creation params + address(offset_size)
ensure_len(data, p, 5 + offset_size as usize)?;
p += 5; // skip EA creation parameters
if is_undefined(data, p, offset_size) {
None
} else {
Some(read_offset(data, p, offset_size)?)
}
}
5 => {
// B-tree v2: node_size(4) + split_percent(1) + merge_percent(1) + address
ensure_len(data, p, 6 + offset_size as usize)?;
p += 6;
if is_undefined(data, p, offset_size) {
None
} else {
Some(read_offset(data, p, offset_size)?)
}
}
_ => {
// Unknown index type: try just address
ensure_len(data, p, offset_size as usize)?;
if is_undefined(data, p, offset_size) {
None
} else {
Some(read_offset(data, p, offset_size)?)
}
}
};
Ok(DataLayout::Chunked {
chunk_dimensions,
btree_address,
version: 4,
chunk_index_type: Some(chunk_index_type),
single_chunk_filtered_size,
single_chunk_filter_mask,
})
}
3 => {
// Virtual: global_heap_address(offset_size) + global_heap_index(4)
let os = offset_size as usize;
ensure_len(data, pos, os + 4)?;
let global_heap_address = if is_undefined(data, pos, offset_size) {
None
} else {
Some(read_offset(data, pos, offset_size)?)
};
let idx_pos = pos + os;
let global_heap_index = u32::from_le_bytes([
data[idx_pos],
data[idx_pos + 1],
data[idx_pos + 2],
data[idx_pos + 3],
]);
Ok(DataLayout::Virtual {
version: 4,
global_heap_address,
global_heap_index,
mappings: Vec::new(),
})
}
_ => Err(FormatError::InvalidLayoutClass(layout_class)),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn v3_compact() {
let mut buf = vec![3u8, 0]; // version=3, class=0 (compact)
buf.extend_from_slice(&5u16.to_le_bytes()); // data_size=5
buf.extend_from_slice(&[0xAA, 0xBB, 0xCC, 0xDD, 0xEE]); // data
let layout = DataLayout::parse(&buf, 8, 8).unwrap();
assert_eq!(
layout,
DataLayout::Compact {
data: vec![0xAA, 0xBB, 0xCC, 0xDD, 0xEE]
}
);
}
#[test]
fn v3_contiguous() {
let mut buf = vec![3u8, 1]; // version=3, class=1 (contiguous)
buf.extend_from_slice(&0x1000u64.to_le_bytes()); // address
buf.extend_from_slice(&256u64.to_le_bytes()); // size
let layout = DataLayout::parse(&buf, 8, 8).unwrap();
assert_eq!(
layout,
DataLayout::Contiguous {
address: Some(0x1000),
size: 256,
}
);
}
#[test]
fn v3_contiguous_undefined_address() {
let mut buf = vec![3u8, 1];
buf.extend_from_slice(&[0xFF; 8]); // undefined address
buf.extend_from_slice(&0u64.to_le_bytes()); // size
let layout = DataLayout::parse(&buf, 8, 8).unwrap();
assert_eq!(
layout,
DataLayout::Contiguous {
address: None,
size: 0,
}
);
}
#[test]
fn v3_chunked() {
let mut buf = vec![3u8, 2]; // version=3, class=2 (chunked)
buf.push(3); // dimensionality=3 (rank+1)
buf.extend_from_slice(&0x2000u64.to_le_bytes()); // btree address
// 3 chunk dim sizes × 4 bytes
buf.extend_from_slice(&100u32.to_le_bytes());
buf.extend_from_slice(&200u32.to_le_bytes());
buf.extend_from_slice(&8u32.to_le_bytes()); // last = element size
let layout = DataLayout::parse(&buf, 8, 8).unwrap();
assert_eq!(
layout,
DataLayout::Chunked {
chunk_dimensions: vec![100, 200, 8],
btree_address: Some(0x2000),
version: 3,
chunk_index_type: None,
single_chunk_filtered_size: None,
single_chunk_filter_mask: None,
}
);
}
#[test]
fn v4_compact() {
let mut buf = vec![4u8, 0]; // version=4, class=0
buf.extend_from_slice(&3u16.to_le_bytes());
buf.extend_from_slice(&[1, 2, 3]);
let layout = DataLayout::parse(&buf, 8, 8).unwrap();
assert_eq!(
layout,
DataLayout::Compact {
data: vec![1, 2, 3]
}
);
}
#[test]
fn v4_contiguous() {
let mut buf = vec![4u8, 1];
buf.extend_from_slice(&0x5000u64.to_le_bytes());
buf.extend_from_slice(&512u64.to_le_bytes());
let layout = DataLayout::parse(&buf, 8, 8).unwrap();
assert_eq!(
layout,
DataLayout::Contiguous {
address: Some(0x5000),
size: 512,
}
);
}
#[test]
fn v5_chunked_from_hdf5_2_0() {
// Real data layout message from h5py 3.16 / HDF5 2.0 (`libver=latest`)
// for a gzip-compressed 1-D chunked dataset. Version 5 uses the same
// structure as v4 (here: chunked, Fixed Array index). Regression guard
// for reading modern-format chunked datasets.
let bytes: [u8; 17] = [
0x05, 0x02, 0x00, 0x02, 0x01, 0x0a, 0x08, 0x03, 0x0a, 0xef, 0x05, 0x00, 0x00, 0x00,
0x00, 0x00, 0x00,
];
let layout = DataLayout::parse(&bytes, 8, 8).unwrap();
match layout {
DataLayout::Chunked {
chunk_dimensions,
chunk_index_type,
..
} => {
assert_eq!(chunk_dimensions, vec![10, 8]);
assert_eq!(chunk_index_type, Some(3)); // Fixed Array
}
other => panic!("expected Chunked, got {other:?}"),
}
}
#[test]
fn v4_chunked_single_chunk_no_filters() {
let mut buf = vec![4u8, 2]; // version=4, class=2
buf.push(0); // flags (no filters)
buf.push(2); // dimensionality=2
buf.push(4); // dim_size_encoded_length=4
buf.extend_from_slice(&64u32.to_le_bytes()); // dim 0
buf.extend_from_slice(&32u32.to_le_bytes()); // dim 1
buf.push(1); // chunk_index_type=1 (single chunk)
buf.extend_from_slice(&0x3000u64.to_le_bytes()); // chunk address
let layout = DataLayout::parse(&buf, 8, 8).unwrap();
assert_eq!(
layout,
DataLayout::Chunked {
chunk_dimensions: vec![64, 32],
btree_address: Some(0x3000),
version: 4,
chunk_index_type: Some(1),
single_chunk_filtered_size: None,
single_chunk_filter_mask: None,
}
);
}
#[test]
fn v4_chunked_single_chunk_with_filters() {
let mut buf = vec![4u8, 2]; // version=4, class=2
buf.push(0x02); // flags bit 1 = single chunk with filter
buf.push(1); // dimensionality=1
buf.push(4); // dim_size_encoded_length=4
buf.extend_from_slice(&128u32.to_le_bytes()); // dim 0
buf.push(1); // chunk_index_type=1 (single chunk)
// filters present: filtered_size(8) + filter_mask(4) + address(8)
buf.extend_from_slice(&1024u64.to_le_bytes()); // filtered size
buf.extend_from_slice(&0u32.to_le_bytes()); // filter mask
buf.extend_from_slice(&0x4000u64.to_le_bytes()); // address
let layout = DataLayout::parse(&buf, 8, 8).unwrap();
assert_eq!(
layout,
DataLayout::Chunked {
chunk_dimensions: vec![128],
btree_address: Some(0x4000),
version: 4,
chunk_index_type: Some(1),
single_chunk_filtered_size: Some(1024),
single_chunk_filter_mask: Some(0),
}
);
}
#[test]
fn invalid_version() {
// v3-v5 are supported; v6 is not a real layout message version.
let buf = vec![6u8, 0, 0, 0];
let err = DataLayout::parse(&buf, 8, 8).unwrap_err();
assert_eq!(err, FormatError::InvalidLayoutVersion(6));
}
#[test]
fn invalid_class_v3() {
let buf = vec![3u8, 5];
let err = DataLayout::parse(&buf, 8, 8).unwrap_err();
assert_eq!(err, FormatError::InvalidLayoutClass(5));
}
#[test]
fn invalid_class_v4() {
let buf = vec![4u8, 7];
let err = DataLayout::parse(&buf, 8, 8).unwrap_err();
assert_eq!(err, FormatError::InvalidLayoutClass(7));
}
#[test]
fn v3_contiguous_4byte_offsets() {
let mut buf = vec![3u8, 1];
buf.extend_from_slice(&0x800u32.to_le_bytes());
buf.extend_from_slice(&24u32.to_le_bytes());
let layout = DataLayout::parse(&buf, 4, 4).unwrap();
assert_eq!(
layout,
DataLayout::Contiguous {
address: Some(0x800),
size: 24,
}
);
}
#[test]
fn v4_virtual() {
let mut buf = vec![4u8, 3]; // version=4, class=3 (virtual)
buf.extend_from_slice(&0x5000u64.to_le_bytes()); // global heap address
buf.extend_from_slice(&1u32.to_le_bytes()); // global heap index
let layout = DataLayout::parse(&buf, 8, 8).unwrap();
assert_eq!(
layout,
DataLayout::Virtual {
version: 4,
global_heap_address: Some(0x5000),
global_heap_index: 1,
mappings: Vec::new(),
}
);
}
#[test]
fn v4_virtual_undefined_address() {
let mut buf = vec![4u8, 3];
buf.extend_from_slice(&[0xFF; 8]); // undefined address
buf.extend_from_slice(&0u32.to_le_bytes());
let layout = DataLayout::parse(&buf, 8, 8).unwrap();
assert_eq!(
layout,
DataLayout::Virtual {
version: 4,
global_heap_address: None,
global_heap_index: 0,
mappings: Vec::new(),
}
);
}
#[test]
fn parse_vds_mappings_same_file_v1() {
// The exact global-heap block written by HDF5 2.0 for a same-file VDS
// with two sources: src_a -> virtual[0:4], src_b -> virtual[4:8].
let blob = [
0x01u8, // block version 1
0x02, 0, 0, 0, 0, 0, 0, 0, // nused = 2 (length_size = 8)
// entry 0
0x04, // same-file marker (replaces file name)
0x73, 0x72, 0x63, 0x5f, 0x61, 0x00, // "src_a\0"
0x03, 0, 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // source sel = ALL
0x02, 0, 0, 0, 0x03, 0, 0, 0, 0x01, 0x02, 0x01, 0, 0, 0, // virtual sel: HYPER v3
0x00, 0x00, 0x01, 0x00, 0x01, 0x00, 0x04, 0x00, // start0 stride1 count1 block4
// entry 1
0x04, 0x73, 0x72, 0x63, 0x5f, 0x62, 0x00, // "src_b\0"
0x03, 0, 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // source sel = ALL
0x02, 0, 0, 0, 0x03, 0, 0, 0, 0x01, 0x02, 0x01, 0, 0, 0, // virtual sel: HYPER v3
0x04, 0x00, 0x01, 0x00, 0x01, 0x00, 0x04, 0x00, // start4 stride1 count1 block4
0x68, 0xf0, 0x3e, 0xe4, // checksum (ignored)
];
let mappings = parse_vds_mappings(&blob, 8).unwrap();
assert_eq!(mappings.len(), 2);
assert_eq!(mappings[0].source_file, ".");
assert_eq!(mappings[0].source_dataset, "src_a");
assert_eq!(mappings[1].source_file, ".");
assert_eq!(mappings[1].source_dataset, "src_b");
// Virtual selections decode to [0:4] and [4:8].
use crate::selection::Selection;
let (v0, _) = Selection::decode_serialized(&mappings[0].virtual_selection).unwrap();
let (v1, _) = Selection::decode_serialized(&mappings[1].virtual_selection).unwrap();
assert_eq!(v0.iter_linear_1d(8).unwrap(), vec![0, 1, 2, 3]);
assert_eq!(v1.iter_linear_1d(8).unwrap(), vec![4, 5, 6, 7]);
}
#[test]
fn parse_vds_mappings_external_v0() {
// Block version 0 with an explicit (external) source file name.
let blob = [
0x00u8, // block version 0
0x01, 0, 0, 0, 0, 0, 0, 0, // nused = 1
0x73, 0x72, 0x63, 0x5f, 0x65, 0x78, 0x74, 0x2e, 0x68, 0x35,
0x00, // "src_ext.h5\0"
0x64, 0x61, 0x74, 0x61, 0x00, // "data\0"
0x03, 0, 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // source sel = ALL
0x03, 0, 0, 0, 0x01, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, // virtual sel = ALL
];
let mappings = parse_vds_mappings(&blob, 8).unwrap();
assert_eq!(mappings.len(), 1);
assert_eq!(mappings[0].source_file, "src_ext.h5");
assert_eq!(mappings[0].source_dataset, "data");
}
#[test]
fn parse_vds_mappings_huge_nused_does_not_oom_or_panic() {
// nused = u64::MAX with no entry data: must error, not pre-allocate or
// overrun.
let mut blob = vec![0x01u8];
blob.extend_from_slice(&u64::MAX.to_le_bytes());
assert!(parse_vds_mappings(&blob, 8).is_err());
}
#[test]
fn parse_vds_mappings_truncated_selection_does_not_overrun() {
// One entry whose source selection (ALL) is truncated to 8 of 16 bytes.
let blob = [
0x01u8, // version 1
0x01, 0, 0, 0, 0, 0, 0, 0, // nused = 1
0x04, // same-file marker
0x78, 0x00, // "x\0"
0x03, 0, 0, 0, 0x01, 0, 0, 0, // ALL header, truncated (8 of 16 bytes)
];
assert!(parse_vds_mappings(&blob, 8).is_err());
}
#[test]
fn parse_vds_mappings_empty_is_ok_empty() {
assert!(parse_vds_mappings(&[], 8).unwrap().is_empty());
// Header present, nused = 0.
let blob = [0x01u8, 0, 0, 0, 0, 0, 0, 0, 0];
assert!(parse_vds_mappings(&blob, 8).unwrap().is_empty());
}
}