Files
clawhdf5/crates/clawhdf5-format/src/extensible_array.rs
T

994 lines
34 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 Extensible Array index parsing for chunked datasets (v4 index type 4).
//!
//! Extensible Arrays are used for datasets with exactly one unlimited dimension.
//! Structures: AEHD (header), AEIB (index block), AEDB (data block), AESB (super block).
#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{format, vec, vec::Vec};
use crate::chunked_read::ChunkInfo;
use crate::error::FormatError;
/// Parsed Extensible Array header (AEHD).
#[derive(Debug, Clone)]
pub struct ExtensibleArrayHeader {
/// Client ID: 0 = non-filtered chunks, 1 = filtered chunks.
pub client_id: u8,
/// Size of each array element in bytes.
pub element_size: u8,
/// Max number of elements bits (log2 of the max number of data block elements per page).
pub max_nelmts_bits: u8,
/// Number of elements in the index block.
pub idx_blk_elmts: u8,
/// Minimum number of data block elements.
pub min_dblk_nelmts: u8,
/// Minimum number of elements in a super block.
pub super_blk_min_nelmts: u8,
/// Max number of data block elements bits.
pub max_dblk_nelmts_bits: u8,
/// Total number of elements stored.
pub num_elements: u64,
/// Address of the index block.
pub index_block_address: u64,
}
fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
let s = size as usize;
if pos.checked_add(s).is_none_or(|end| end > data.len()) {
return Err(FormatError::UnexpectedEof {
expected: pos.saturating_add(s),
available: data.len(),
});
}
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 is_undefined_addr(addr: u64, offset_size: u8) -> bool {
match offset_size {
2 => addr == 0xFFFF,
4 => addr == 0xFFFF_FFFF,
8 => addr == 0xFFFF_FFFF_FFFF_FFFF,
_ => false,
}
}
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)
}
fn read_variable_length(data: &[u8], size: usize) -> Result<u64, FormatError> {
if size > 8 || data.len() < size {
return Err(FormatError::ChunkedReadError(
"invalid variable-length size".into(),
));
}
let mut val = 0u64;
for (i, &byte) in data.iter().enumerate().take(size) {
val |= (byte as u64) << (i * 8);
}
Ok(val)
}
impl ExtensibleArrayHeader {
/// Parse an Extensible Array header from file data at the given offset.
pub fn parse(
file_data: &[u8],
offset: usize,
offset_size: u8,
length_size: u8,
) -> Result<Self, FormatError> {
// EAHD: signature(4) + version(1) + client_id(1) + element_size(1) +
// max_nelmts_bits(1) + idx_blk_elmts(1) + min_dblk_nelmts(1) +
// super_blk_min_nelmts(1) + max_dblk_nelmts_bits(1) +
// 6 stats fields (each length_size) + index_block_address(offset_size) + checksum(4)
let min_size =
4 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 6 * length_size as usize + offset_size as usize + 4;
if offset + min_size > file_data.len() {
return Err(FormatError::UnexpectedEof {
expected: offset + min_size,
available: file_data.len(),
});
}
let d = &file_data[offset..];
if &d[0..4] != b"EAHD" {
return Err(FormatError::ChunkedReadError(
"invalid Extensible Array header signature".into(),
));
}
let version = d[4];
if version != 0 {
return Err(FormatError::ChunkedReadError(format!(
"unsupported Extensible Array header version: {version}"
)));
}
let client_id = d[5];
let element_size = d[6];
let max_nelmts_bits = d[7];
let idx_blk_elmts = d[8];
let min_dblk_nelmts = d[9];
let super_blk_min_nelmts = d[10];
let max_dblk_nelmts_bits = d[11];
let mut pos = 12;
// 6 stats fields: [0] unknown, [1] unknown, [2] nsuper_blks_created,
// [3] super_blk_size, [4] nelmts, [5] max_idx_set
// We only need nelmts (field[4]) and skip the rest.
let ls = length_size as usize;
pos += 4 * ls; // skip first 4 stats fields
let num_elements = read_offset(d, pos, length_size)?;
pos += ls; // skip nelmts
pos += ls; // skip max_idx_set (6th stats field)
let index_block_address = read_offset(d, pos, offset_size)?;
Ok(ExtensibleArrayHeader {
client_id,
element_size,
max_nelmts_bits,
idx_blk_elmts,
min_dblk_nelmts,
super_blk_min_nelmts,
max_dblk_nelmts_bits,
num_elements,
index_block_address,
})
}
/// Compute the size of this header in bytes (for write support).
pub fn serialized_size(offset_size: u8, length_size: u8) -> usize {
4 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 6 * length_size as usize + offset_size as usize + 4
}
}
/// Read a single element from the extensible array element data.
/// Returns (chunk_info, bytes_consumed) or None if unallocated.
#[allow(clippy::too_many_arguments)]
fn read_element(
data: &[u8],
pos: usize,
client_id: u8,
element_size: u8,
offset_size: u8,
chunk_byte_size: u64,
linear_index: usize,
num_chunks_per_dim: &[u64],
chunk_dimensions: &[u32],
) -> Result<(Option<ChunkInfo>, usize), FormatError> {
let os = offset_size as usize;
if client_id == 0 {
// Non-filtered: just address
if pos + os > data.len() {
return Err(FormatError::UnexpectedEof {
expected: pos + os,
available: data.len(),
});
}
if is_undefined(data, pos, offset_size) {
return Ok((None, os));
}
let address = read_offset(data, pos, offset_size)?;
let offsets = index_to_chunk_offsets(linear_index, num_chunks_per_dim, chunk_dimensions);
Ok((
Some(ChunkInfo {
chunk_size: chunk_byte_size as u32,
filter_mask: 0,
offsets,
address,
}),
os,
))
} else {
// Filtered: address + compressed_size + filter_mask
let es = element_size as usize;
if es < os + 4 {
return Err(FormatError::ChunkedReadError(
"element_size too small for filtered element".into(),
));
}
let chunk_size_bytes = es - os - 4;
let elem_total = os + chunk_size_bytes + 4;
if pos
.checked_add(elem_total)
.is_none_or(|end| end > data.len())
{
return Err(FormatError::UnexpectedEof {
expected: pos.saturating_add(elem_total),
available: data.len(),
});
}
if is_undefined(data, pos, offset_size) {
return Ok((None, elem_total));
}
let address = read_offset(data, pos, offset_size)?;
let chunk_size = read_variable_length(&data[pos + os..], chunk_size_bytes)?;
let fm_off = pos + os + chunk_size_bytes;
let filter_mask = u32::from_le_bytes([
data[fm_off],
data[fm_off + 1],
data[fm_off + 2],
data[fm_off + 3],
]);
let offsets = index_to_chunk_offsets(linear_index, num_chunks_per_dim, chunk_dimensions);
Ok((
Some(ChunkInfo {
chunk_size: chunk_size as u32,
filter_mask,
offsets,
address,
}),
elem_total,
))
}
}
/// Convert a linear chunk index to N-dimensional chunk offsets in dataset space.
fn index_to_chunk_offsets(
index: usize,
num_chunks_per_dim: &[u64],
chunk_dimensions: &[u32],
) -> Vec<u64> {
let rank = num_chunks_per_dim.len();
let mut offsets = vec![0u64; rank];
let mut remaining = index as u64;
for d in (0..rank).rev() {
let nchunks = num_chunks_per_dim[d];
if nchunks == 0 {
continue;
}
let chunk_idx = remaining % nchunks;
remaining /= nchunks;
offsets[d] = chunk_idx * chunk_dimensions[d] as u64;
}
offsets
}
/// Collect elements from a data block at the given offset.
#[allow(clippy::too_many_arguments)]
fn read_data_block_elements(
file_data: &[u8],
db_offset: usize,
nelmts: usize,
header: &ExtensibleArrayHeader,
offset_size: u8,
chunk_byte_size: u64,
start_index: usize,
num_chunks_per_dim: &[u64],
chunk_dimensions: &[u32],
) -> Result<Vec<ChunkInfo>, FormatError> {
// AEDB: signature(4) + version(1) + client_id(1) + header_address(offset_size)
let db_header_size = 4 + 1 + 1 + offset_size as usize;
if db_offset + db_header_size > file_data.len() {
return Err(FormatError::UnexpectedEof {
expected: db_offset + db_header_size,
available: file_data.len(),
});
}
let d = &file_data[db_offset..];
if &d[0..4] != b"EADB" {
return Err(FormatError::ChunkedReadError(
"invalid Extensible Array data block signature".into(),
));
}
// Skip version(1) + client_id(1) + header_address(offset_size) + block_offset
// Block offset is encoded in ceil(max_nelmts_bits/8) bytes
let blk_off_size = (header.max_nelmts_bits as usize).div_ceil(8);
let mut pos = db_offset + db_header_size + blk_off_size;
// Check if paged
if header.max_nelmts_bits >= usize::BITS as u8 {
return Err(FormatError::Overflow(
"max_nelmts_bits exceeds usize bit width".into(),
));
}
let page_nelmts = 1usize << header.max_nelmts_bits;
let is_paged = nelmts > page_nelmts;
let mut chunks = Vec::new();
if !is_paged {
for i in 0..nelmts {
let (info, consumed) = read_element(
file_data,
pos,
header.client_id,
header.element_size,
offset_size,
chunk_byte_size,
start_index + i,
num_chunks_per_dim,
chunk_dimensions,
)?;
if let Some(ci) = info {
chunks.push(ci);
}
pos += consumed;
}
} else {
// Paged: elements are split into pages of page_nelmts.
// After the data block header comes a page bitmap, then each page
// has page_nelmts elements followed by a 4-byte checksum.
let npages = nelmts.div_ceil(page_nelmts);
// Page bitmap: ceil(npages / 8) bytes
let bitmap_size = npages.div_ceil(8);
// Read bitmap
if pos + bitmap_size > file_data.len() {
return Err(FormatError::UnexpectedEof {
expected: pos + bitmap_size,
available: file_data.len(),
});
}
let bitmap = &file_data[pos..pos + bitmap_size];
pos += bitmap_size;
let elem_bytes = if header.client_id == 0 {
offset_size as usize
} else {
header.element_size as usize
};
let mut global_idx = start_index;
for page_idx in 0..npages {
let byte_idx = page_idx / 8;
let bit_idx = page_idx % 8;
let page_has_data = (bitmap[byte_idx] >> bit_idx) & 1 != 0;
let elems_this_page = if page_idx == npages - 1 {
let remainder = nelmts % page_nelmts;
if remainder == 0 {
page_nelmts
} else {
remainder
}
} else {
page_nelmts
};
if page_has_data {
for i in 0..elems_this_page {
let (info, consumed) = read_element(
file_data,
pos,
header.client_id,
header.element_size,
offset_size,
chunk_byte_size,
global_idx + i,
num_chunks_per_dim,
chunk_dimensions,
)?;
if let Some(ci) = info {
chunks.push(ci);
}
pos += consumed;
}
// Skip page checksum (4 bytes)
pos += 4;
} else {
// Empty page: skip all elements + checksum
pos += elems_this_page * elem_bytes + 4;
}
global_idx += elems_this_page;
}
}
Ok(chunks)
}
/// Read chunk records from an Extensible Array.
///
/// Traverses AEHD -> AEIB -> AEDB/AESB to collect all allocated chunks.
#[allow(clippy::too_many_arguments)]
pub fn read_extensible_array_chunks(
file_data: &[u8],
header: &ExtensibleArrayHeader,
dataset_dims: &[u64],
chunk_dimensions: &[u32],
element_size: u32,
offset_size: u8,
_length_size: u8,
) -> Result<Vec<ChunkInfo>, FormatError> {
let rank = chunk_dimensions.len();
let os = offset_size as usize;
let mut num_chunks_per_dim = Vec::with_capacity(rank);
for d in 0..rank {
let ch_dim = chunk_dimensions[d] as u64;
if ch_dim == 0 {
return Err(FormatError::ChunkedReadError(
"chunk dimension is zero".into(),
));
}
let ds_dim = dataset_dims[d];
num_chunks_per_dim.push(ds_dim.div_ceil(ch_dim));
}
let chunk_byte_size: u64 =
chunk_dimensions.iter().map(|&d| d as u64).product::<u64>() * element_size as u64;
// Parse index block (AEIB)
let ib_offset = header.index_block_address as usize;
let ib_header_size = 4 + 1 + 1 + offset_size as usize; // sig + ver + client + hdr_addr
if ib_offset + ib_header_size > file_data.len() {
return Err(FormatError::UnexpectedEof {
expected: ib_offset + ib_header_size,
available: file_data.len(),
});
}
let ib = &file_data[ib_offset..];
if &ib[0..4] != b"EAIB" {
return Err(FormatError::ChunkedReadError(
"invalid Extensible Array index block signature".into(),
));
}
// Skip version(1) + client_id(1) + header_address(offset_size)
let mut pos = ib_offset + ib_header_size;
let mut chunks = Vec::new();
let mut global_index = 0usize;
let total_elements = header.num_elements as usize;
// 1. Read inline elements in index block
let n_inline = header.idx_blk_elmts as usize;
for i in 0..n_inline {
if global_index + i >= total_elements {
break;
}
let (info, consumed) = read_element(
file_data,
pos,
header.client_id,
header.element_size,
offset_size,
chunk_byte_size,
global_index + i,
&num_chunks_per_dim,
chunk_dimensions,
)?;
if let Some(ci) = info {
chunks.push(ci);
}
pos += consumed;
}
global_index += n_inline.min(total_elements);
// If all elements were inline, we're done
if global_index >= total_elements {
return Ok(chunks);
}
// Compute data block and super block counts
let min_dblk = header.min_dblk_nelmts as usize;
let sblk_min = header.super_blk_min_nelmts as usize;
// The first sblk_min super block levels have their data blocks listed directly
// in the index block. Compute their sizes.
let mut n_direct_dblks = 0usize;
let mut dblk_sizes: Vec<usize> = Vec::new();
{
let mut nelmts = min_dblk;
for sb_level in 0..sblk_min {
if sb_level >= usize::BITS as usize {
return Err(FormatError::Overflow(
"sb_level exceeds usize bit width".into(),
));
}
let ndblks = 1usize << sb_level;
for _ in 0..ndblks {
dblk_sizes.push(nelmts);
n_direct_dblks += 1;
}
if sb_level > 0 {
nelmts *= 2;
}
}
}
// Read direct data block addresses from index block
let mut dblk_addrs: Vec<u64> = Vec::with_capacity(n_direct_dblks);
for _ in 0..n_direct_dblks {
if pos + os > file_data.len() {
break;
}
let addr = read_offset(file_data, pos, offset_size)?;
dblk_addrs.push(addr);
pos += os;
}
// Read elements from direct data blocks
for (i, &addr) in dblk_addrs.iter().enumerate() {
if i >= dblk_sizes.len() {
break;
}
let nelmts = dblk_sizes[i];
if is_undefined_addr(addr, offset_size) {
global_index += nelmts;
continue;
}
let block_chunks = read_data_block_elements(
file_data,
addr as usize,
nelmts,
header,
offset_size,
chunk_byte_size,
global_index,
&num_chunks_per_dim,
chunk_dimensions,
)?;
chunks.extend(block_chunks);
global_index += nelmts;
}
// Remaining elements are in super blocks
let total_in_ib_and_direct: usize = n_inline + dblk_sizes.iter().sum::<usize>();
if total_elements <= total_in_ib_and_direct {
return Ok(chunks);
}
let remaining_elements = total_elements - total_in_ib_and_direct;
// Compute super block layout
let mut sb_addrs: Vec<u64> = Vec::new();
let mut sb_infos: Vec<(usize, usize)> = Vec::new();
{
let mut covered = 0usize;
let mut sb_level = sblk_min;
let mut nelmts_per_dblk = min_dblk;
for lev in 0..sblk_min {
if lev > 0 {
nelmts_per_dblk *= 2;
}
}
while covered < remaining_elements {
if sb_level >= usize::BITS as usize {
return Err(FormatError::Overflow(
"sb_level exceeds usize bit width".into(),
));
}
let ndblks = 1usize << sb_level;
nelmts_per_dblk *= 2;
let total_in_sb = ndblks * nelmts_per_dblk;
sb_infos.push((ndblks, nelmts_per_dblk));
covered += total_in_sb;
sb_level += 1;
}
}
// Read super block addresses from index block
for _ in 0..sb_infos.len() {
if pos + os > file_data.len() {
break;
}
let addr = read_offset(file_data, pos, offset_size)?;
sb_addrs.push(addr);
pos += os;
}
// Process each super block
for (sb_idx, &sb_addr) in sb_addrs.iter().enumerate() {
let (ndblks, nelmts_per_dblk) = sb_infos[sb_idx];
if is_undefined_addr(sb_addr, offset_size) {
global_index += ndblks * nelmts_per_dblk;
continue;
}
let sb_chunks = read_super_block(
file_data,
sb_addr as usize,
ndblks,
nelmts_per_dblk,
header,
offset_size,
chunk_byte_size,
global_index,
&num_chunks_per_dim,
chunk_dimensions,
)?;
chunks.extend(sb_chunks);
global_index += ndblks * nelmts_per_dblk;
}
Ok(chunks)
}
/// Read a super block (AESB) and its data blocks.
#[allow(clippy::too_many_arguments)]
fn read_super_block(
file_data: &[u8],
sb_offset: usize,
ndblks: usize,
nelmts_per_dblk: usize,
header: &ExtensibleArrayHeader,
offset_size: u8,
chunk_byte_size: u64,
start_index: usize,
num_chunks_per_dim: &[u64],
chunk_dimensions: &[u32],
) -> Result<Vec<ChunkInfo>, FormatError> {
let os = offset_size as usize;
// AESB: signature(4) + version(1) + client_id(1) + header_address(offset_size)
let sb_header_size = 4 + 1 + 1 + os;
if sb_offset + sb_header_size > file_data.len() {
return Err(FormatError::UnexpectedEof {
expected: sb_offset + sb_header_size,
available: file_data.len(),
});
}
if &file_data[sb_offset..sb_offset + 4] != b"EASB" {
return Err(FormatError::ChunkedReadError(
"invalid Extensible Array super block signature".into(),
));
}
let mut pos = sb_offset + sb_header_size;
// Read data block addresses
let mut dblk_addrs: Vec<u64> = Vec::with_capacity(ndblks);
for _ in 0..ndblks {
if pos + os > file_data.len() {
return Err(FormatError::UnexpectedEof {
expected: pos + os,
available: file_data.len(),
});
}
let addr = read_offset(file_data, pos, offset_size)?;
dblk_addrs.push(addr);
pos += os;
}
let mut chunks = Vec::new();
let mut global_idx = start_index;
for &addr in &dblk_addrs {
if is_undefined_addr(addr, offset_size) {
global_idx += nelmts_per_dblk;
continue;
}
let block_chunks = read_data_block_elements(
file_data,
addr as usize,
nelmts_per_dblk,
header,
offset_size,
chunk_byte_size,
global_idx,
num_chunks_per_dim,
chunk_dimensions,
)?;
chunks.extend(block_chunks);
global_idx += nelmts_per_dblk;
}
Ok(chunks)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn index_to_offsets_1d() {
let num_chunks = vec![5u64];
let chunk_dims = vec![20u32];
assert_eq!(index_to_chunk_offsets(0, &num_chunks, &chunk_dims), vec![0]);
assert_eq!(
index_to_chunk_offsets(1, &num_chunks, &chunk_dims),
vec![20]
);
assert_eq!(
index_to_chunk_offsets(4, &num_chunks, &chunk_dims),
vec![80]
);
}
#[test]
fn index_to_offsets_2d() {
let num_chunks = vec![3u64, 2];
let chunk_dims = vec![4u32, 3];
assert_eq!(
index_to_chunk_offsets(0, &num_chunks, &chunk_dims),
vec![0, 0]
);
assert_eq!(
index_to_chunk_offsets(1, &num_chunks, &chunk_dims),
vec![0, 3]
);
assert_eq!(
index_to_chunk_offsets(2, &num_chunks, &chunk_dims),
vec![4, 0]
);
}
#[test]
fn parse_header_valid() {
let os: u8 = 8;
let ls: u8 = 8;
let mut buf = vec![0u8; 256];
buf[0..4].copy_from_slice(b"EAHD");
buf[4] = 0; // version
buf[5] = 0; // client_id = non-filtered
buf[6] = 8; // element_size
buf[7] = 10; // max_nelmts_bits
buf[8] = 2; // idx_blk_elmts
buf[9] = 4; // min_dblk_nelmts
buf[10] = 2; // super_blk_min_nelmts
buf[11] = 8; // max_dblk_nelmts_bits
// 6 stats fields (each 8 bytes)
buf[12..20].copy_from_slice(&0u64.to_le_bytes()); // stat[0]
buf[20..28].copy_from_slice(&0u64.to_le_bytes()); // stat[1]
buf[28..36].copy_from_slice(&0u64.to_le_bytes()); // stat[2]
buf[36..44].copy_from_slice(&0u64.to_le_bytes()); // stat[3]
buf[44..52].copy_from_slice(&5u64.to_le_bytes()); // stat[4] = num_elements
buf[52..60].copy_from_slice(&0u64.to_le_bytes()); // stat[5]
buf[60..68].copy_from_slice(&0x1000u64.to_le_bytes()); // index_block_address
let hdr = ExtensibleArrayHeader::parse(&buf, 0, os, ls).unwrap();
assert_eq!(hdr.client_id, 0);
assert_eq!(hdr.element_size, 8);
assert_eq!(hdr.idx_blk_elmts, 2);
assert_eq!(hdr.min_dblk_nelmts, 4);
assert_eq!(hdr.num_elements, 5);
assert_eq!(hdr.index_block_address, 0x1000);
}
#[test]
fn parse_header_invalid_signature() {
let mut buf = vec![0u8; 256];
buf[0..4].copy_from_slice(b"XXXX");
let result = ExtensibleArrayHeader::parse(&buf, 0, 8, 8);
assert!(result.is_err());
}
#[test]
fn parse_header_invalid_version() {
let mut buf = vec![0u8; 256];
buf[0..4].copy_from_slice(b"EAHD");
buf[4] = 1;
let result = ExtensibleArrayHeader::parse(&buf, 0, 8, 8);
assert!(result.is_err());
}
/// Build a synthetic Extensible Array with only inline elements (simplest case).
/// All chunks fit in the index block.
#[test]
fn read_inline_only() {
let os: u8 = 8;
let ls: u8 = 8;
let osv = os as usize;
let num_chunks = 2usize;
let chunk_byte_size = 20u64 * 8; // 20 elements × 8 bytes
let mut file_data = vec![0u8; 0x3000];
// AEHD at offset 0x100
let aehd_offset = 0x100usize;
let aeib_offset = 0x200usize;
// Build AEHD
file_data[aehd_offset..aehd_offset + 4].copy_from_slice(b"EAHD");
file_data[aehd_offset + 4] = 0; // version
file_data[aehd_offset + 5] = 0; // client_id = non-filtered
file_data[aehd_offset + 6] = osv as u8; // element_size
file_data[aehd_offset + 7] = 10; // max_nelmts_bits
file_data[aehd_offset + 8] = num_chunks as u8; // idx_blk_elmts (all inline)
file_data[aehd_offset + 9] = 4; // min_dblk_nelmts
file_data[aehd_offset + 10] = 2; // super_blk_min_nelmts
file_data[aehd_offset + 11] = 8; // max_dblk_nelmts_bits
// 6 stats fields (each 8 bytes), nelmts at stat[4]
file_data[aehd_offset + 44..aehd_offset + 52]
.copy_from_slice(&(num_chunks as u64).to_le_bytes());
file_data[aehd_offset + 60..aehd_offset + 68]
.copy_from_slice(&(aeib_offset as u64).to_le_bytes());
// checksum (4 bytes at +68) — not validated
// Build AEIB at aeib_offset
file_data[aeib_offset..aeib_offset + 4].copy_from_slice(b"EAIB");
file_data[aeib_offset + 4] = 0; // version
file_data[aeib_offset + 5] = 0; // client_id
file_data[aeib_offset + 6..aeib_offset + 14]
.copy_from_slice(&(aehd_offset as u64).to_le_bytes());
// Inline elements
let elem_start = aeib_offset + 6 + osv;
let base_addr = 0x1000u64;
for i in 0..num_chunks {
let addr = base_addr + i as u64 * chunk_byte_size;
let p = elem_start + i * osv;
file_data[p..p + osv].copy_from_slice(&addr.to_le_bytes());
}
let header = ExtensibleArrayHeader::parse(&file_data, aehd_offset, os, ls).unwrap();
let ds_dims = vec![40u64]; // 2 chunks × 20 elements
let chunk_dims = vec![20u32];
let chunks =
read_extensible_array_chunks(&file_data, &header, &ds_dims, &chunk_dims, 8, os, ls)
.unwrap();
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].address, base_addr);
assert_eq!(chunks[0].offsets, vec![0]);
assert_eq!(chunks[0].chunk_size, chunk_byte_size as u32);
assert_eq!(chunks[1].address, base_addr + chunk_byte_size);
assert_eq!(chunks[1].offsets, vec![20]);
}
/// Build a synthetic EA with inline elements + one direct data block.
#[test]
fn read_inline_plus_data_blocks() {
let os: u8 = 8;
let ls: u8 = 8;
let osv = os as usize;
let chunk_byte_size = 10u64 * 8; // 10 elements × 8 bytes
let idx_blk_elmts = 2u8;
let min_dblk_nelmts = 2u8;
let sblk_min = 2u8;
let total_chunks = 4usize; // 2 inline + 2 in data block (1 dblk from sb_level 0)
let mut file_data = vec![0u8; 0x5000];
let aehd_offset = 0x100usize;
let aeib_offset = 0x200usize;
let aedb_offset = 0x300usize;
// EAHD
file_data[aehd_offset..aehd_offset + 4].copy_from_slice(b"EAHD");
file_data[aehd_offset + 4] = 0;
file_data[aehd_offset + 5] = 0; // client_id
file_data[aehd_offset + 6] = osv as u8; // element_size
file_data[aehd_offset + 7] = 10;
file_data[aehd_offset + 8] = idx_blk_elmts;
file_data[aehd_offset + 9] = min_dblk_nelmts;
file_data[aehd_offset + 10] = sblk_min;
file_data[aehd_offset + 11] = 8;
// 6 stats fields (each 8 bytes), nelmts at stat[4] (offset 12 + 4*8 = 44)
file_data[aehd_offset + 44..aehd_offset + 52]
.copy_from_slice(&(total_chunks as u64).to_le_bytes());
// idx_blk_addr at offset 12 + 6*8 = 60
file_data[aehd_offset + 60..aehd_offset + 68]
.copy_from_slice(&(aeib_offset as u64).to_le_bytes());
// AEIB
file_data[aeib_offset..aeib_offset + 4].copy_from_slice(b"EAIB");
file_data[aeib_offset + 4] = 0;
file_data[aeib_offset + 5] = 0;
file_data[aeib_offset + 6..aeib_offset + 14]
.copy_from_slice(&(aehd_offset as u64).to_le_bytes());
let mut pos = aeib_offset + 6 + osv;
// Inline elements (2 chunks)
let base_addr = 0x1000u64;
for i in 0..idx_blk_elmts as usize {
let addr = base_addr + i as u64 * chunk_byte_size;
file_data[pos..pos + osv].copy_from_slice(&addr.to_le_bytes());
pos += osv;
}
// Direct data block addresses: first sb_level=0 has 1 dblk, sb_level=1 has 1 dblk
// Total direct dblks for sblk_min=2: 2^0 + 2^1 = 1 + 2 = 3 (oops)
// Actually: sblk_min levels. level 0: 2^0=1 dblk, level 1: 2^1=2 dblks => 3 dblks
// But we only have 2 remaining elements.
// dblk sizes: level 0: 1 dblk of min_dblk=2; level 1: 2 dblks of 2 each (nelmts doubles at level > 0)
// Wait, re-reading the code: at level 0, nelmts=min_dblk=2, 1 dblk.
// At level 1, 1 dblk, nelmts still 2 (doubles only at level > 0... but the code says
// `if sb_level > 0 { nelmts *= 2 }` after pushing). Let me re-check.
// After push at level 0: nelmts=2. Then if 0>0 false, no double. Push 1 dblk of 2.
// Level 1: ndblks=2. Push 2 dblks of 2. Then 1>0 true, nelmts=4.
// Total: 3 dblks with sizes [2, 2, 2]. Total = 6.
// We only need 2 more elements. So only the first dblk has data.
let n_direct_dblks = 3;
file_data[pos..pos + osv].copy_from_slice(&(aedb_offset as u64).to_le_bytes());
pos += osv;
// 2 more dblk addresses - undefined
for _ in 1..n_direct_dblks {
file_data[pos..pos + osv].copy_from_slice(&u64::MAX.to_le_bytes());
pos += osv;
}
// EADB at aedb_offset (min_dblk_nelmts elements)
file_data[aedb_offset..aedb_offset + 4].copy_from_slice(b"EADB");
file_data[aedb_offset + 4] = 0;
file_data[aedb_offset + 5] = 0;
file_data[aedb_offset + 6..aedb_offset + 14]
.copy_from_slice(&(aehd_offset as u64).to_le_bytes());
// block_offset: ceil(max_nelmts_bits/8) = ceil(10/8) = 2 bytes
// block_offset = 0 for first data block
let blk_off_size = (10usize).div_ceil(8); // max_nelmts_bits=10
let mut dbpos = aedb_offset + 6 + osv + blk_off_size;
for i in 0..min_dblk_nelmts as usize {
let addr = base_addr + (idx_blk_elmts as u64 + i as u64) * chunk_byte_size;
file_data[dbpos..dbpos + osv].copy_from_slice(&addr.to_le_bytes());
dbpos += osv;
}
let header = ExtensibleArrayHeader::parse(&file_data, aehd_offset, os, ls).unwrap();
let ds_dims = vec![40u64];
let chunk_dims = vec![10u32];
let chunks =
read_extensible_array_chunks(&file_data, &header, &ds_dims, &chunk_dims, 8, os, ls)
.unwrap();
assert_eq!(chunks.len(), 4);
for (i, c) in chunks.iter().enumerate() {
assert_eq!(c.address, base_addr + i as u64 * chunk_byte_size);
assert_eq!(c.offsets, vec![i as u64 * 10]);
}
}
/// Test serialized_size computation.
#[test]
fn header_serialized_size() {
// 12 fixed + 6*8 stats + 8 addr + 4 checksum = 72
assert_eq!(ExtensibleArrayHeader::serialized_size(8, 8), 72);
// 12 fixed + 6*4 stats + 4 addr + 4 checksum = 44
assert_eq!(ExtensibleArrayHeader::serialized_size(4, 4), 44);
}
/// Verify read_element for unallocated slots.
#[test]
fn read_element_unallocated() {
let data = vec![0xFFu8; 16];
let num_chunks = vec![5u64];
let chunk_dims = vec![10u32];
let (info, consumed) =
read_element(&data, 0, 0, 8, 8, 80, 0, &num_chunks, &chunk_dims).unwrap();
assert!(info.is_none());
assert_eq!(consumed, 8);
}
/// Verify filtered element reading.
#[test]
fn read_element_filtered() {
let os: u8 = 8;
let chunk_size_bytes = 4usize;
let elem_size = os as usize + chunk_size_bytes + 4;
let mut data = vec![0u8; elem_size + 16];
// Address
data[0..8].copy_from_slice(&0x2000u64.to_le_bytes());
// Compressed size (4 bytes LE)
data[8..12].copy_from_slice(&120u32.to_le_bytes());
// Filter mask
data[12..16].copy_from_slice(&0u32.to_le_bytes());
let num_chunks = vec![5u64];
let chunk_dims = vec![10u32];
let (info, consumed) = read_element(
&data,
0,
1,
elem_size as u8,
os,
80,
2,
&num_chunks,
&chunk_dims,
)
.unwrap();
let ci = info.unwrap();
assert_eq!(ci.address, 0x2000);
assert_eq!(ci.chunk_size, 120);
assert_eq!(ci.filter_mask, 0);
assert_eq!(ci.offsets, vec![20]);
assert_eq!(consumed, elem_size);
}
}