format: read extensible array chunk indexes over Storage

ExtensibleArrayHeader::parse_in reads the header in one read, and
read_extensible_array_chunks_in reads each index block, super block and
data block as a prefix read and then one window of the whole structure
(paged data blocks included); checksums and elements are checked in the
window with bounds errors reported as the whole-file checks did. The
&[u8] functions are wrappers. New test: an array with inline elements
and a data block, cut at every length and damaged in each structure,
reads identically through a read_at-only CountingStorage.

Co-Authored-By: Claude Opus 5.5 (1M context) <[email protected]>
This commit is contained in:
osobh
2026-09-26 13:01:11 -05:00
co-authored by Claude Opus 5.5
parent cf2b408a63
commit ff6d644391
2 changed files with 193 additions and 95 deletions
+183 -95
View File
@@ -12,16 +12,19 @@ use alloc::{format, vec, vec::Vec};
use crate::chunk_grid::ChunkGrid; use crate::chunk_grid::ChunkGrid;
use crate::chunked_read::ChunkInfo; use crate::chunked_read::ChunkInfo;
use crate::error::FormatError; use crate::error::FormatError;
use crate::storage::{Storage, Window, read_exact_at};
/// Verify the Jenkins lookup3 checksum stored immediately after /// Verify the Jenkins lookup3 checksum stored immediately after
/// `data[start..end]`, as every Extensible Array structure carries one. /// `data[start..end]`, as every Extensible Array structure carries one. `w`
/// is a window of the file and `start`/`end` are relative to it.
/// ///
/// A corrupt chunk index yields addresses pointing at the wrong bytes, so a /// A corrupt chunk index yields addresses pointing at the wrong bytes, so a
/// mismatch is an error: otherwise the damage surfaces as plausible data read /// mismatch is an error: otherwise the damage surfaces as plausible data read
/// from the wrong chunk. /// from the wrong chunk.
#[cfg(feature = "checksum")] #[cfg(feature = "checksum")]
fn verify_checksum(data: &[u8], start: usize, end: usize) -> Result<(), FormatError> { fn verify_checksum(w: &Window<'_>, start: usize, end: usize) -> Result<(), FormatError> {
ensure_len(data, end, 4)?; w.ensure(end, 4)?;
let data: &[u8] = &w.bytes;
let stored = u32::from_le_bytes([data[end], data[end + 1], data[end + 2], data[end + 3]]); let stored = u32::from_le_bytes([data[end], data[end + 1], data[end + 2], data[end + 3]]);
let computed = crate::checksum::jenkins_lookup3(&data[start..end]); let computed = crate::checksum::jenkins_lookup3(&data[start..end]);
if computed != stored { if computed != stored {
@@ -34,7 +37,7 @@ fn verify_checksum(data: &[u8], start: usize, end: usize) -> Result<(), FormatEr
} }
#[cfg(not(feature = "checksum"))] #[cfg(not(feature = "checksum"))]
fn verify_checksum(_data: &[u8], _start: usize, _end: usize) -> Result<(), FormatError> { fn verify_checksum(_w: &Window<'_>, _start: usize, _end: usize) -> Result<(), FormatError> {
Ok(()) Ok(())
} }
@@ -80,19 +83,6 @@ fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
}) })
} }
fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> {
if offset
.checked_add(needed)
.is_none_or(|end| end > data.len())
{
return Err(FormatError::UnexpectedEof {
expected: offset.saturating_add(needed),
available: data.len(),
});
}
Ok(())
}
fn is_undefined_addr(addr: u64, offset_size: u8) -> bool { fn is_undefined_addr(addr: u64, offset_size: u8) -> bool {
match offset_size { match offset_size {
2 => addr == 0xFFFF, 2 => addr == 0xFFFF,
@@ -130,6 +120,16 @@ impl ExtensibleArrayHeader {
offset: usize, offset: usize,
offset_size: u8, offset_size: u8,
length_size: u8, length_size: u8,
) -> Result<Self, FormatError> {
Self::parse_in(&file_data, offset as u64, offset_size, length_size)
}
/// [`Self::parse`] over any [`Storage`]: one read of the header.
pub fn parse_in(
file: &dyn Storage,
offset: u64,
offset_size: u8,
length_size: u8,
) -> Result<Self, FormatError> { ) -> Result<Self, FormatError> {
// EAHD: signature(4) + version(1) + client_id(1) + element_size(1) + // EAHD: signature(4) + version(1) + client_id(1) + element_size(1) +
// max_nelmts_bits(1) + idx_blk_elmts(1) + min_dblk_nelmts(1) + // max_nelmts_bits(1) + idx_blk_elmts(1) + min_dblk_nelmts(1) +
@@ -137,9 +137,10 @@ impl ExtensibleArrayHeader {
// 6 stats fields (each length_size) + index_block_address(offset_size) + checksum(4) // 6 stats fields (each length_size) + index_block_address(offset_size) + checksum(4)
let min_size = let min_size =
4 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 6 * length_size as usize + offset_size as usize + 4; 4 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 6 * length_size as usize + offset_size as usize + 4;
ensure_len(file_data, offset, min_size)?; let w = Window::read(file, offset, min_size)?;
w.ensure(0, min_size)?;
let d = &file_data[offset..]; let d: &[u8] = &w.bytes;
if &d[0..4] != b"EAHD" { if &d[0..4] != b"EAHD" {
return Err(FormatError::ChunkedReadError( return Err(FormatError::ChunkedReadError(
"invalid Extensible Array header signature".into(), "invalid Extensible Array header signature".into(),
@@ -172,7 +173,7 @@ impl ExtensibleArrayHeader {
pos += ls; // skip max_idx_set (6th stats field) pos += ls; // skip max_idx_set (6th stats field)
let index_block_address = read_offset(d, pos, offset_size)?; let index_block_address = read_offset(d, pos, offset_size)?;
pos += offset_size as usize; pos += offset_size as usize;
verify_checksum(file_data, offset, offset + pos)?; verify_checksum(&w, 0, pos)?;
Ok(ExtensibleArrayHeader { Ok(ExtensibleArrayHeader {
client_id, client_id,
@@ -193,11 +194,11 @@ impl ExtensibleArrayHeader {
} }
} }
/// Read a single element from the extensible array element data. /// Read a single element at offset `pos` of the window `w`.
/// Returns (chunk_info, bytes_consumed) or None if unallocated. /// Returns (chunk_info, bytes_consumed) or None if unallocated.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn read_element( fn read_element(
data: &[u8], w: &Window<'_>,
pos: usize, pos: usize,
client_id: u8, client_id: u8,
element_size: u8, element_size: u8,
@@ -207,15 +208,11 @@ fn read_element(
grid: &ChunkGrid, grid: &ChunkGrid,
) -> Result<(Option<ChunkInfo>, usize), FormatError> { ) -> Result<(Option<ChunkInfo>, usize), FormatError> {
let os = offset_size as usize; let os = offset_size as usize;
let data: &[u8] = &w.bytes;
if client_id == 0 { if client_id == 0 {
// Non-filtered: just address // Non-filtered: just address
if pos + os > data.len() { w.ensure(pos, os)?;
return Err(FormatError::UnexpectedEof {
expected: pos + os,
available: data.len(),
});
}
if is_undefined(data, pos, offset_size) { if is_undefined(data, pos, offset_size) {
return Ok((None, os)); return Ok((None, os));
} }
@@ -243,15 +240,7 @@ fn read_element(
} }
let chunk_size_bytes = es - os - 4; let chunk_size_bytes = es - os - 4;
let elem_total = os + chunk_size_bytes + 4; let elem_total = os + chunk_size_bytes + 4;
if pos w.ensure(pos, elem_total)?;
.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) { if is_undefined(data, pos, offset_size) {
return Ok((None, elem_total)); return Ok((None, elem_total));
} }
@@ -316,8 +305,8 @@ fn page_nelmts(header: &ExtensibleArrayHeader) -> Option<usize> {
/// stores only its prefix, then one slot per page. /// stores only its prefix, then one slot per page.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn read_data_block_elements( fn read_data_block_elements(
file_data: &[u8], file: &dyn Storage,
db_offset: usize, db_offset: u64,
nelmts: usize, nelmts: usize,
header: &ExtensibleArrayHeader, header: &ExtensibleArrayHeader,
offset_size: u8, offset_size: u8,
@@ -330,21 +319,28 @@ fn read_data_block_elements(
// EADB: signature(4) + version(1) + client_id(1) + header_address(offset_size) // EADB: signature(4) + version(1) + client_id(1) + header_address(offset_size)
// + block offset(arr_off_size) // + block offset(arr_off_size)
let db_header_size = 4 + 1 + 1 + offset_size as usize + arr_off_size(header); let db_header_size = 4 + 1 + 1 + offset_size as usize + arr_off_size(header);
ensure_len(file_data, db_offset, db_header_size)?; let prefix = read_exact_at(file, db_offset, db_header_size)?;
if &file_data[db_offset..db_offset + 4] != b"EADB" { if &prefix[0..4] != b"EADB" {
return Err(FormatError::ChunkedReadError( return Err(FormatError::ChunkedReadError(
"invalid Extensible Array data block signature".into(), "invalid Extensible Array data block signature".into(),
)); ));
} }
let mut pos = db_offset + db_header_size; // Positions below are relative to the data block.
let mut pos = db_header_size;
let page = page_nelmts(header).ok_or_else(|| { let page = page_nelmts(header).ok_or_else(|| {
FormatError::Overflow("Extensible Array page element count overflows usize".into()) FormatError::Overflow("Extensible Array page element count overflows usize".into())
})?; })?;
let elem_bytes = if header.client_id == 0 {
offset_size as usize
} else {
header.element_size as usize
};
let mut chunks = Vec::new(); let mut chunks = Vec::new();
let read_run = |from: usize, let read_run = |w: &Window<'_>,
from: usize,
count: usize, count: usize,
first_index: usize, first_index: usize,
chunks: &mut Vec<ChunkInfo>| chunks: &mut Vec<ChunkInfo>|
@@ -352,7 +348,7 @@ fn read_data_block_elements(
let mut p = from; let mut p = from;
for i in 0..count { for i in 0..count {
let (info, consumed) = read_element( let (info, consumed) = read_element(
file_data, w,
p, p,
header.client_id, header.client_id,
header.element_size, header.element_size,
@@ -370,18 +366,16 @@ fn read_data_block_elements(
}; };
if nelmts <= page { if nelmts <= page {
// Prefix and elements are covered by one checksum. // Prefix and elements are covered by one checksum. One window holds
let elem_bytes = if header.client_id == 0 { // all of it (or ends at the end of the file), so its bounds checks
offset_size as usize // are the whole-file ones.
} else {
header.element_size as usize
};
let end = nelmts let end = nelmts
.checked_mul(elem_bytes) .checked_mul(elem_bytes)
.and_then(|b| pos.checked_add(b)) .and_then(|b| pos.checked_add(b))
.ok_or_else(|| FormatError::Overflow("Extensible Array data block span".into()))?; .ok_or_else(|| FormatError::Overflow("Extensible Array data block span".into()))?;
verify_checksum(file_data, db_offset, end)?; let w = Window::read(file, db_offset, end.saturating_add(4))?;
read_run(pos, nelmts, start_index, &mut chunks)?; verify_checksum(&w, 0, end)?;
read_run(&w, pos, nelmts, start_index, &mut chunks)?;
return Ok(chunks); return Ok(chunks);
} }
@@ -389,18 +383,19 @@ fn read_data_block_elements(
// each holding `page` elements followed by a checksum. Pages whose bit is // each holding `page` elements followed by a checksum. Pages whose bit is
// clear were never written; their slot still occupies the file, so stride // clear were never written; their slot still occupies the file, so stride
// over it rather than reading zeros as addresses. // over it rather than reading zeros as addresses.
verify_checksum(file_data, db_offset, pos)?; let npages = nelmts.div_ceil(page);
// The whole data block in one window: every position checked below lies
// inside it (or past the end of the file).
let block_len = pos
.saturating_add(4)
.saturating_add(npages.saturating_mul(page.saturating_mul(elem_bytes).saturating_add(4)));
let w = Window::read(file, db_offset, block_len)?;
verify_checksum(&w, 0, pos)?;
pos += 4; pos += 4;
let elem_bytes = if header.client_id == 0 {
offset_size as usize
} else {
header.element_size as usize
};
let page_stride = page let page_stride = page
.checked_mul(elem_bytes) .checked_mul(elem_bytes)
.and_then(|b| b.checked_add(4)) .and_then(|b| b.checked_add(4))
.ok_or_else(|| FormatError::Overflow("Extensible Array page stride".into()))?; .ok_or_else(|| FormatError::Overflow("Extensible Array page stride".into()))?;
let npages = nelmts.div_ceil(page);
for p in 0..npages { for p in 0..npages {
// One bit per page across the whole super block, packed contiguously // One bit per page across the whole super block, packed contiguously
// and MSB-first within each byte, as H5VM_bit_get reads it. // and MSB-first within each byte, as H5VM_bit_get reads it.
@@ -412,8 +407,8 @@ fn read_data_block_elements(
let count = core::cmp::min(page, nelmts - p * page); let count = core::cmp::min(page, nelmts - p * page);
// Each page carries its own checksum, over a full page's worth of // Each page carries its own checksum, over a full page's worth of
// slots even when the last one holds fewer live elements. // slots even when the last one holds fewer live elements.
verify_checksum(file_data, pos, pos + page * elem_bytes)?; verify_checksum(&w, pos, pos + page * elem_bytes)?;
read_run(pos, count, start_index + p * page, &mut chunks)?; read_run(&w, pos, count, start_index + p * page, &mut chunks)?;
} }
pos = pos pos = pos
.checked_add(page_stride) .checked_add(page_stride)
@@ -435,6 +430,32 @@ pub fn read_extensible_array_chunks(
chunk_dimensions: &[u32], chunk_dimensions: &[u32],
element_size: u32, element_size: u32,
offset_size: u8, offset_size: u8,
length_size: u8,
) -> Result<Vec<ChunkInfo>, FormatError> {
read_extensible_array_chunks_in(
&file_data,
header,
dataset_dims,
max_dims,
chunk_dimensions,
element_size,
offset_size,
length_size,
)
}
/// [`read_extensible_array_chunks`] over any [`Storage`]: one read of the
/// index block's prefix, one of the whole index block, and the same for
/// every super block and data block it references.
#[allow(clippy::too_many_arguments)]
pub fn read_extensible_array_chunks_in(
file: &dyn Storage,
header: &ExtensibleArrayHeader,
dataset_dims: &[u64],
max_dims: Option<&[u64]>,
chunk_dimensions: &[u32],
element_size: u32,
offset_size: u8,
_length_size: u8, _length_size: u8,
) -> Result<Vec<ChunkInfo>, FormatError> { ) -> Result<Vec<ChunkInfo>, FormatError> {
let os = offset_size as usize; let os = offset_size as usize;
@@ -451,16 +472,17 @@ pub fn read_extensible_array_chunks(
// Parse index block (EAIB): signature(4) + version(1) + client_id(1) // Parse index block (EAIB): signature(4) + version(1) + client_id(1)
// + header address(offset_size), then the inline elements, then the // + header address(offset_size), then the inline elements, then the
// direct data block addresses, then the super block addresses. // direct data block addresses, then the super block addresses.
let ib_offset = header.index_block_address as usize; // Positions below are relative to the index block.
let ib_offset = header.index_block_address;
let ib_header_size = 4 + 1 + 1 + os; let ib_header_size = 4 + 1 + 1 + os;
ensure_len(file_data, ib_offset, ib_header_size)?; let prefix = read_exact_at(file, ib_offset, ib_header_size)?;
if &file_data[ib_offset..ib_offset + 4] != b"EAIB" { if &prefix[0..4] != b"EAIB" {
return Err(FormatError::ChunkedReadError( return Err(FormatError::ChunkedReadError(
"invalid Extensible Array index block signature".into(), "invalid Extensible Array index block signature".into(),
)); ));
} }
let mut pos = ib_offset + ib_header_size; let mut pos = ib_header_size;
let mut chunks = Vec::new(); let mut chunks = Vec::new();
let total_elements = header.num_elements as usize; let total_elements = header.num_elements as usize;
@@ -520,13 +542,16 @@ pub fn read_extensible_array_chunks(
.and_then(|n| n.checked_mul(os).and_then(|b| p.checked_add(b))) .and_then(|n| n.checked_mul(os).and_then(|b| p.checked_add(b)))
}) })
.ok_or_else(|| FormatError::Overflow("Extensible Array index block span".into()))?; .ok_or_else(|| FormatError::Overflow("Extensible Array index block span".into()))?;
verify_checksum(file_data, ib_offset, ib_end)?; // The whole index block in one window: every position read below is
// before `ib_end`.
let w = Window::read(file, ib_offset, ib_end.saturating_add(4))?;
verify_checksum(&w, 0, ib_end)?;
// 1. Elements stored inline in the index block. // 1. Elements stored inline in the index block.
let n_inline = (header.idx_blk_elmts as usize).min(total_elements); let n_inline = (header.idx_blk_elmts as usize).min(total_elements);
for i in 0..n_inline { for i in 0..n_inline {
let (info, consumed) = read_element( let (info, consumed) = read_element(
file_data, &w,
pos, pos,
header.client_id, header.client_id,
header.element_size, header.element_size,
@@ -550,8 +575,8 @@ pub fn read_extensible_array_chunks(
if global_index >= total_elements { if global_index >= total_elements {
return Ok(chunks); return Ok(chunks);
} }
ensure_len(file_data, pos, os)?; w.ensure(pos, os)?;
let addr = read_offset(file_data, pos, offset_size)?; let addr = read_offset(&w.bytes, pos, offset_size)?;
pos += os; pos += os;
if !is_undefined_addr(addr, offset_size) { if !is_undefined_addr(addr, offset_size) {
if dblk_nelmts > page_nelmts(header).unwrap_or(usize::MAX) { if dblk_nelmts > page_nelmts(header).unwrap_or(usize::MAX) {
@@ -562,8 +587,8 @@ pub fn read_extensible_array_chunks(
)); ));
} }
chunks.extend(read_data_block_elements( chunks.extend(read_data_block_elements(
file_data, file,
addr as usize, addr,
dblk_nelmts, dblk_nelmts,
header, header,
offset_size, offset_size,
@@ -583,16 +608,16 @@ pub fn read_extensible_array_chunks(
if global_index >= total_elements { if global_index >= total_elements {
break; break;
} }
ensure_len(file_data, pos, os)?; w.ensure(pos, os)?;
let sb_addr = read_offset(file_data, pos, offset_size)?; let sb_addr = read_offset(&w.bytes, pos, offset_size)?;
pos += os; pos += os;
let (ndblks, dblk_nelmts) = sblk_info(u, dmin).ok_or_else(|| { let (ndblks, dblk_nelmts) = sblk_info(u, dmin).ok_or_else(|| {
FormatError::Overflow("Extensible Array super block layout overflows usize".into()) FormatError::Overflow("Extensible Array super block layout overflows usize".into())
})?; })?;
if !is_undefined_addr(sb_addr, offset_size) { if !is_undefined_addr(sb_addr, offset_size) {
chunks.extend(read_super_block( chunks.extend(read_super_block(
file_data, file,
sb_addr as usize, sb_addr,
ndblks, ndblks,
dblk_nelmts, dblk_nelmts,
header, header,
@@ -618,8 +643,8 @@ pub fn read_extensible_array_chunks(
/// + one address per data block + checksum. /// + one address per data block + checksum.
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
fn read_super_block( fn read_super_block(
file_data: &[u8], file: &dyn Storage,
sb_offset: usize, sb_offset: u64,
ndblks: usize, ndblks: usize,
dblk_nelmts: usize, dblk_nelmts: usize,
header: &ExtensibleArrayHeader, header: &ExtensibleArrayHeader,
@@ -630,9 +655,9 @@ fn read_super_block(
) -> Result<Vec<ChunkInfo>, FormatError> { ) -> Result<Vec<ChunkInfo>, FormatError> {
let os = offset_size as usize; let os = offset_size as usize;
let sb_header_size = 4 + 1 + 1 + os + arr_off_size(header); let sb_header_size = 4 + 1 + 1 + os + arr_off_size(header);
ensure_len(file_data, sb_offset, sb_header_size)?; let prefix = read_exact_at(file, sb_offset, sb_header_size)?;
if &file_data[sb_offset..sb_offset + 4] != b"EASB" { if &prefix[0..4] != b"EASB" {
return Err(FormatError::ChunkedReadError( return Err(FormatError::ChunkedReadError(
"invalid Extensible Array super block signature".into(), "invalid Extensible Array super block signature".into(),
)); ));
@@ -654,9 +679,19 @@ fn read_super_block(
let bitmap_bytes = per_dblk_bitmap let bitmap_bytes = per_dblk_bitmap
.checked_mul(ndblks) .checked_mul(ndblks)
.ok_or_else(|| FormatError::Overflow("Extensible Array page bitmap size".into()))?; .ok_or_else(|| FormatError::Overflow("Extensible Array page bitmap size".into()))?;
let bitmap_start = sb_offset + sb_header_size; // Positions below are relative to the super block, whose bytes (up to
ensure_len(file_data, bitmap_start, bitmap_bytes)?; // its checksum) are all in one window.
let bitmap = &file_data[bitmap_start..bitmap_start + bitmap_bytes]; let bitmap_start = sb_header_size;
let w = Window::read(
file,
sb_offset,
bitmap_start
.saturating_add(bitmap_bytes)
.saturating_add(ndblks.saturating_mul(os))
.saturating_add(4),
)?;
w.ensure(bitmap_start, bitmap_bytes)?;
let bitmap = &w.bytes[bitmap_start..bitmap_start + bitmap_bytes];
let mut pos = bitmap_start + bitmap_bytes; let mut pos = bitmap_start + bitmap_bytes;
let mut chunks = Vec::new(); let mut chunks = Vec::new();
@@ -667,16 +702,16 @@ fn read_super_block(
.checked_mul(os) .checked_mul(os)
.and_then(|b| pos.checked_add(b)) .and_then(|b| pos.checked_add(b))
.ok_or_else(|| FormatError::Overflow("Extensible Array super block span".into()))?; .ok_or_else(|| FormatError::Overflow("Extensible Array super block span".into()))?;
verify_checksum(file_data, sb_offset, sb_end)?; verify_checksum(&w, 0, sb_end)?;
for i in 0..ndblks { for i in 0..ndblks {
ensure_len(file_data, pos, os)?; w.ensure(pos, os)?;
let addr = read_offset(file_data, pos, offset_size)?; let addr = read_offset(&w.bytes, pos, offset_size)?;
pos += os; pos += os;
if !is_undefined_addr(addr, offset_size) { if !is_undefined_addr(addr, offset_size) {
chunks.extend(read_data_block_elements( chunks.extend(read_data_block_elements(
file_data, file,
addr as usize, addr,
dblk_nelmts, dblk_nelmts,
header, header,
offset_size, offset_size,
@@ -887,11 +922,11 @@ mod tests {
assert_eq!(chunks[1].offsets, vec![20]); assert_eq!(chunks[1].offsets, vec![20]);
} }
/// Build a synthetic EA with inline elements + one direct data block. /// A synthetic EA with inline elements + one direct data block: the
#[test] /// file, with the header at 0x100 (8-byte offsets and lengths, 4 chunks
fn read_inline_plus_data_blocks() { /// of 10 elements from 0x1000 on).
fn build_inline_plus_data_blocks() -> Vec<u8> {
let os: u8 = 8; let os: u8 = 8;
let ls: u8 = 8;
let osv = os as usize; let osv = os as usize;
let chunk_byte_size = 10u64 * 8; // 10 elements × 8 bytes let chunk_byte_size = 10u64 * 8; // 10 elements × 8 bytes
let idx_blk_elmts = 2u8; let idx_blk_elmts = 2u8;
@@ -981,8 +1016,17 @@ mod tests {
dbpos += osv; dbpos += osv;
} }
stamp_checksum(&mut file_data, aedb_offset, dbpos); stamp_checksum(&mut file_data, aedb_offset, dbpos);
file_data
}
let header = ExtensibleArrayHeader::parse(&file_data, aehd_offset, os, ls).unwrap(); /// Build a synthetic EA with inline elements + one direct data block.
#[test]
fn read_inline_plus_data_blocks() {
let (os, ls) = (8u8, 8u8);
let chunk_byte_size = 10u64 * 8;
let base_addr = 0x1000u64;
let file_data = build_inline_plus_data_blocks();
let header = ExtensibleArrayHeader::parse(&file_data, 0x100, os, ls).unwrap();
let ds_dims = vec![40u64]; let ds_dims = vec![40u64];
let chunk_dims = vec![10u32]; let chunk_dims = vec![10u32];
let chunks = read_extensible_array_chunks( let chunks = read_extensible_array_chunks(
@@ -1018,7 +1062,8 @@ mod tests {
fn read_element_unallocated() { fn read_element_unallocated() {
let data = vec![0xFFu8; 16]; let data = vec![0xFFu8; 16];
let grid = ChunkGrid::fixed_array(&[50], None, &[10]).unwrap(); let grid = ChunkGrid::fixed_array(&[50], None, &[10]).unwrap();
let (info, consumed) = read_element(&data, 0, 0, 8, 8, 80, 0, &grid).unwrap(); let (info, consumed) =
read_element(&Window::whole(&data), 0, 0, 8, 8, 80, 0, &grid).unwrap();
assert!(info.is_none()); assert!(info.is_none());
assert_eq!(consumed, 8); assert_eq!(consumed, 8);
} }
@@ -1038,8 +1083,17 @@ mod tests {
data[12..16].copy_from_slice(&0u32.to_le_bytes()); data[12..16].copy_from_slice(&0u32.to_le_bytes());
let grid = ChunkGrid::fixed_array(&[50], None, &[10]).unwrap(); let grid = ChunkGrid::fixed_array(&[50], None, &[10]).unwrap();
let (info, consumed) = let (info, consumed) = read_element(
read_element(&data, 0, 1, elem_size as u8, os, 80, 2, &grid).unwrap(); &Window::whole(&data),
0,
1,
elem_size as u8,
os,
80,
2,
&grid,
)
.unwrap();
let ci = info.unwrap(); let ci = info.unwrap();
assert_eq!(ci.address, 0x2000); assert_eq!(ci.address, 0x2000);
assert_eq!(ci.chunk_size, 120); assert_eq!(ci.chunk_size, 120);
@@ -1047,4 +1101,38 @@ mod tests {
assert_eq!(ci.offsets, vec![20]); assert_eq!(ci.offsets, vec![20]);
assert_eq!(consumed, elem_size); assert_eq!(consumed, elem_size);
} }
/// The Storage path reads exactly what the slice path reads: the array
/// whole, cut at every length through its structures, and with a byte
/// damaged in each of them, through a read_at-only CountingStorage.
#[test]
fn storage_reads_match_slice_reads() {
use crate::storage::CountingStorage;
let full = build_inline_plus_data_blocks();
let mut files = Vec::new();
for cut in 0x100..0x340 {
files.push(full[..cut].to_vec());
}
for at in [0x104, 0x150, 0x204, 0x216, 0x230, 0x304, 0x318] {
let mut damaged = full.clone();
damaged[at] ^= 1;
files.push(damaged);
}
files.push(full);
let mut compared = 0;
for f in files {
let storage = CountingStorage::new(f.clone());
let want = ExtensibleArrayHeader::parse(&f, 0x100, 8, 8);
let got = ExtensibleArrayHeader::parse_in(&storage, 0x100, 8, 8);
assert_eq!(format!("{got:?}"), format!("{want:?}"));
let Ok(h) = want else { continue };
for dims in [&[40u64][..], &[25]] {
let want = read_extensible_array_chunks(&f, &h, dims, None, &[10], 8, 8, 8);
let got = read_extensible_array_chunks_in(&storage, &h, dims, None, &[10], 8, 8, 8);
assert_eq!(format!("{got:?}"), format!("{want:?}"), "{} bytes", f.len());
compared += 1;
}
}
assert!(compared > 100);
}
} }
+10
View File
@@ -240,6 +240,16 @@ impl<'a> Window<'a> {
}) })
} }
/// A whole in-memory file as one window (base 0).
#[cfg(test)]
pub fn whole(bytes: &'a [u8]) -> Self {
Window {
bytes: Cow::Borrowed(bytes),
base: 0,
file_len: bytes.len(),
}
}
/// Check that `[rel, rel + needed)` (relative to `base`) is in the file. /// Check that `[rel, rel + needed)` (relative to `base`) is in the file.
pub fn ensure(&self, rel: usize, needed: usize) -> Result<(), FormatError> { pub fn ensure(&self, rel: usize, needed: usize) -> Result<(), FormatError> {
match rel.checked_add(needed) { match rel.checked_add(needed) {