#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{format, vec::Vec};
use crate::bytes::{read_length, read_offset, read_optional_offset};
use crate::chunk_grid::ChunkGrid;
use crate::chunked_read::ChunkInfo;
use crate::convert::{TryToUsize, is_undefined_addr, u32_from};
use crate::error::FormatError;
use crate::source::Source;
#[derive(Debug, Clone)]
pub struct ExtensibleArrayHeader {
pub client_id: u8,
pub element_size: u8,
pub max_nelmts_bits: u8,
pub idx_blk_elmts: u8,
pub min_dblk_nelmts: u8,
pub super_blk_min_nelmts: u8,
pub max_dblk_nelmts_bits: u8,
pub num_elements: u64,
pub index_block_address: u64,
}
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 {
pub fn parse(
file_data: &[u8],
offset: usize,
offset_size: u8,
length_size: u8,
) -> Result<Self, FormatError> {
let min_size =
4 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 1 + 6 * length_size as usize + offset_size as usize + 4;
if min_size > file_data.len() || offset > file_data.len() - min_size {
return Err(FormatError::UnexpectedEof {
expected: offset.saturating_add(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;
let ls = length_size as usize;
pos += 4 * ls; let num_elements = read_length(d, pos, length_size)?; pos += ls;
pos += ls; let index_block_address = read_offset(d, pos, offset_size)?;
crate::checksum::verify_trailing(&d[..min_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,
})
}
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
}
pub fn parse_from_source<S: Source + ?Sized>(
source: &S,
address: u64,
offset_size: u8,
length_size: u8,
) -> Result<Self, FormatError> {
let size = Self::serialized_size(offset_size, length_size);
let buf = source.read_metadata_at(address, size)?;
Self::parse(&buf, 0, offset_size, length_size)
}
}
fn ea_elem_stride(header: &ExtensibleArrayHeader, offset_size: u8) -> usize {
if header.client_id == 0 {
offset_size as usize
} else {
header.element_size as usize
}
}
#[derive(Debug, Clone)]
pub(crate) struct EaGeometry {
pub sblks: Vec<(u64, u64)>,
pub direct_dblk_nelmts: Vec<u64>,
pub nsblk_addrs: usize,
pub first_indirect_sblk: usize,
}
impl EaGeometry {
pub(crate) fn super_block_at(&self, j: usize, page_nelmts: u64) -> SuperBlockGeom {
let (ndblks, dblk_nelmts) = self.sblks[self.first_indirect_sblk + j];
SuperBlockGeom {
ndblks,
blocks: DataBlockGeom {
dblk_nelmts,
page_nelmts,
},
}
}
pub fn from_header(h: &ExtensibleArrayHeader) -> Self {
let min_dblk = h.min_dblk_nelmts as u64;
let sup_blk_min = h.super_blk_min_nelmts as usize;
let log2_min = if min_dblk <= 1 {
0
} else {
min_dblk.trailing_zeros() as u64
};
#[expect(
clippy::cast_possible_truncation,
reason = "max_nelmts_bits is a bit count (<= 64); the super-block count fits usize"
)]
let nsblks = (h.max_nelmts_bits as u64).saturating_sub(log2_min) as usize + 1;
let mut sblks = Vec::with_capacity(nsblks);
let mut ndblks = 1u64;
let mut dblk_nelmts = min_dblk;
for u in 0..nsblks {
sblks.push((ndblks, dblk_nelmts));
if u % 2 == 0 {
dblk_nelmts = dblk_nelmts.saturating_mul(2);
} else {
ndblks = ndblks.saturating_mul(2);
}
}
let mut direct_dblk_nelmts = Vec::new();
for sb in sblks.iter().take(sup_blk_min.min(nsblks)) {
let (nd, dn) = *sb;
for _ in 0..nd {
direct_dblk_nelmts.push(dn);
}
}
let nsblk_addrs = nsblks.saturating_sub(sup_blk_min);
EaGeometry {
sblks,
direct_dblk_nelmts,
nsblk_addrs,
first_indirect_sblk: sup_blk_min,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct DataBlockGeom {
pub(crate) dblk_nelmts: u64,
pub(crate) page_nelmts: u64,
}
impl DataBlockGeom {
pub(crate) const fn is_paged(self) -> bool {
self.dblk_nelmts > self.page_nelmts
}
pub(crate) const fn npages(self) -> u64 {
if self.is_paged() {
self.dblk_nelmts / self.page_nelmts
} else {
0
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct SuperBlockGeom {
pub(crate) ndblks: u64,
pub(crate) blocks: DataBlockGeom,
}
impl SuperBlockGeom {
pub(crate) const fn bitmap_size(self) -> u64 {
self.ndblks * self.blocks.npages().div_ceil(8)
}
}
#[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,
grid: &ChunkGrid,
) -> Result<(Option<ChunkInfo>, usize), FormatError> {
let os = offset_size as usize;
if client_id == 0 {
if os > data.len() || pos > data.len() - os {
return Err(FormatError::UnexpectedEof {
expected: pos.saturating_add(os),
available: data.len(),
});
}
let Some(address) = read_optional_offset(data, pos, offset_size)? else {
return Ok((None, os));
};
let Some(offsets) = grid.offsets_in_extent(linear_index as u64)? else {
return Ok((None, os));
};
Ok((
Some(ChunkInfo {
chunk_size: u32_from(chunk_byte_size)?,
filter_mask: 0,
offsets,
address,
}),
os,
))
} else {
let chunk_size_bytes = (element_size as usize).checked_sub(os + 4).ok_or_else(|| {
FormatError::ChunkedReadError("Extensible Array element size too small".into())
})?;
let elem_total = os + chunk_size_bytes + 4;
if elem_total > data.len() || pos > data.len() - elem_total {
return Err(FormatError::UnexpectedEof {
expected: pos.saturating_add(elem_total),
available: data.len(),
});
}
let Some(address) = read_optional_offset(data, pos, offset_size)? else {
return Ok((None, elem_total));
};
let Some(offsets) = grid.offsets_in_extent(linear_index as u64)? else {
return Ok((None, elem_total));
};
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],
]);
Ok((
Some(ChunkInfo {
chunk_size: u32_from(chunk_size)?,
filter_mask,
offsets,
address,
}),
elem_total,
))
}
}
#[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,
total_elements: usize,
grid: &ChunkGrid,
) -> Result<Vec<ChunkInfo>, FormatError> {
let db_header_size = 4 + 1 + 1 + offset_size as usize;
let blk_off_size = (header.max_nelmts_bits as usize).div_ceil(8);
let db_len = eadb_extent(nelmts, header, offset_size, blk_off_size)?;
if db_len > file_data.len() || db_offset > file_data.len() - db_len {
return Err(FormatError::UnexpectedEof {
expected: db_offset.saturating_add(db_len),
available: file_data.len(),
});
}
let d = &file_data[db_offset..db_offset + db_len];
if &d[0..4] != b"EADB" {
return Err(FormatError::ChunkedReadError(
"invalid Extensible Array data block signature".into(),
));
}
crate::checksum::verify_trailing(d)?;
let mut pos = db_offset + db_header_size + blk_off_size;
let limit = total_elements.saturating_sub(start_index).min(nelmts);
let mut chunks = Vec::new();
for i in 0..limit {
let (info, consumed) = read_element(
file_data,
pos,
header.client_id,
header.element_size,
offset_size,
chunk_byte_size,
start_index + i,
grid,
)?;
if let Some(ci) = info {
chunks.push(ci);
}
pos += consumed;
}
Ok(chunks)
}
fn eadb_extent(
nelmts: usize,
header: &ExtensibleArrayHeader,
offset_size: u8,
blk_off_size: usize,
) -> Result<usize, FormatError> {
let elem_stride = ea_elem_stride(header, offset_size);
nelmts
.checked_mul(elem_stride)
.and_then(|elems| elems.checked_add(4 + 1 + 1 + offset_size as usize + blk_off_size + 4))
.ok_or(FormatError::OffsetOverflow {
offset: nelmts as u64,
length: elem_stride as u64,
})
}
fn page_is_initialized(bitmap: &[u8], page_idx: usize) -> bool {
let byte = page_idx / 8;
let mask = 0x80u8 >> (page_idx % 8);
byte < bitmap.len() && (bitmap[byte] & mask) != 0
}
#[allow(clippy::too_many_arguments)]
fn read_paged_data_block(
file_data: &[u8],
db_offset: usize,
page_nelmts: usize,
npages: usize,
db_local_idx: usize,
page_bitmap: &[u8],
header: &ExtensibleArrayHeader,
offset_size: u8,
chunk_byte_size: u64,
start_index: usize,
total_elements: usize,
grid: &ChunkGrid,
) -> Result<Vec<ChunkInfo>, FormatError> {
let blk_off_size = (header.max_nelmts_bits as usize).div_ceil(8);
let db_header_size = 4 + 1 + 1 + offset_size as usize + blk_off_size + 4;
if db_header_size > file_data.len() || db_offset > file_data.len() - db_header_size {
return Err(FormatError::UnexpectedEof {
expected: db_offset.saturating_add(db_header_size),
available: file_data.len(),
});
}
if &file_data[db_offset..db_offset + 4] != b"EADB" {
return Err(FormatError::ChunkedReadError(
"invalid Extensible Array data block signature".into(),
));
}
crate::checksum::verify_trailing(&file_data[db_offset..db_offset + db_header_size])?;
let mut chunks = Vec::new();
let mut pos = db_offset + db_header_size;
let page_stride = page_nelmts
.checked_mul(ea_elem_stride(header, offset_size))
.and_then(|bytes| bytes.checked_add(4))
.ok_or(FormatError::OffsetOverflow {
offset: page_nelmts as u64,
length: ea_elem_stride(header, offset_size) as u64,
})?;
for page in 0..npages {
let global_page = db_local_idx * npages + page;
if !page_is_initialized(page_bitmap, global_page) {
pos += page_stride;
continue;
}
let page_end = pos
.checked_add(page_stride)
.filter(|&end| end <= file_data.len())
.ok_or(FormatError::UnexpectedEof {
expected: pos.saturating_add(page_stride),
available: file_data.len(),
})?;
crate::checksum::verify_trailing(&file_data[pos..page_end])?;
let page_start = start_index + page * page_nelmts;
let limit = total_elements.saturating_sub(page_start).min(page_nelmts);
for i in 0..limit {
let (info, consumed) = read_element(
file_data,
pos,
header.client_id,
header.element_size,
offset_size,
chunk_byte_size,
page_start + i,
grid,
)?;
if let Some(ci) = info {
chunks.push(ci);
}
pos += consumed;
}
if limit < page_nelmts {
break; }
pos += 4;
}
Ok(chunks)
}
#[allow(clippy::too_many_arguments)]
pub fn read_extensible_array_chunks(
file_data: &[u8],
header: &ExtensibleArrayHeader,
grid: &ChunkGrid,
chunk_dimensions: &[u32],
element_size: u32,
offset_size: u8,
_length_size: u8,
) -> Result<Vec<ChunkInfo>, FormatError> {
let os = offset_size as usize;
let chunk_byte_size: u64 =
chunk_dimensions.iter().map(|&d| d as u64).product::<u64>() * element_size as u64;
let geom = EaGeometry::from_header(header);
let ib_offset = header.index_block_address.to_usize()?;
let ib_header_size = 4 + 1 + 1 + offset_size as usize; let ib_len = crate::chunked_write::aeib_size(
offset_size,
header.idx_blk_elmts as usize,
ea_elem_stride(header, offset_size),
geom.direct_dblk_nelmts.len(),
geom.nsblk_addrs,
);
if ib_len > file_data.len() || ib_offset > file_data.len() - ib_len {
return Err(FormatError::UnexpectedEof {
expected: ib_offset.saturating_add(ib_len),
available: file_data.len(),
});
}
let ib = &file_data[ib_offset..ib_offset + ib_len];
if &ib[0..4] != b"EAIB" {
return Err(FormatError::ChunkedReadError(
"invalid Extensible Array index block signature".into(),
));
}
crate::checksum::verify_trailing(ib)?;
let mut pos = ib_offset + ib_header_size;
let mut chunks = Vec::new();
let mut global_index = 0usize;
let total_elements = header.num_elements.to_usize()?;
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,
grid,
)?;
if let Some(ci) = info {
chunks.push(ci);
}
pos += consumed;
}
global_index += n_inline.min(total_elements);
if global_index >= total_elements {
return Ok(chunks);
}
let mut direct_addrs: Vec<u64> = Vec::with_capacity(geom.direct_dblk_nelmts.len());
for _ in 0..geom.direct_dblk_nelmts.len() {
direct_addrs.push(read_offset(file_data, pos, offset_size)?);
pos += os;
}
for (i, &addr) in direct_addrs.iter().enumerate() {
if global_index >= total_elements {
break;
}
let nelmts = geom.direct_dblk_nelmts[i].to_usize()?;
if !is_undefined_addr(addr, offset_size) {
let block_chunks = read_data_block_elements(
file_data,
addr.to_usize()?,
nelmts,
header,
offset_size,
chunk_byte_size,
global_index,
total_elements,
grid,
)?;
chunks.extend(block_chunks);
}
global_index += nelmts;
}
let mut sblk_addrs: Vec<u64> = Vec::with_capacity(geom.nsblk_addrs);
for _ in 0..geom.nsblk_addrs {
sblk_addrs.push(read_offset(file_data, pos, offset_size)?);
pos += os;
}
for (j, &sb_addr) in sblk_addrs.iter().enumerate() {
if global_index >= total_elements {
break;
}
let sblk_idx = geom.first_indirect_sblk + j;
let (ndblks, dblk_nelmts) = geom.sblks[sblk_idx];
let total_in_sb = (ndblks * dblk_nelmts).to_usize()?;
if !is_undefined_addr(sb_addr, offset_size) {
let sb_chunks = read_super_block(
file_data,
sb_addr.to_usize()?,
ndblks.to_usize()?,
dblk_nelmts.to_usize()?,
header,
offset_size,
chunk_byte_size,
global_index,
total_elements,
grid,
)?;
chunks.extend(sb_chunks);
}
global_index += total_in_sb;
}
Ok(chunks)
}
#[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,
total_elements: usize,
grid: &ChunkGrid,
) -> Result<Vec<ChunkInfo>, FormatError> {
let os = offset_size as usize;
let blk_off_size = (header.max_nelmts_bits as usize).div_ceil(8);
let sb_header_size = 4 + 1 + 1 + os + blk_off_size;
let page_nelmts = 1usize << header.max_dblk_nelmts_bits;
let sb = SuperBlockGeom {
ndblks: ndblks as u64,
blocks: DataBlockGeom {
dblk_nelmts: nelmts_per_dblk as u64,
page_nelmts: page_nelmts as u64,
},
};
let is_paged = sb.blocks.is_paged();
let npages = sb.blocks.npages().to_usize()?;
let bitmap_size = sb.bitmap_size().to_usize()?;
let sb_len = ndblks
.checked_mul(os)
.and_then(|addrs| addrs.checked_add(sb_header_size + bitmap_size + 4))
.ok_or(FormatError::OffsetOverflow {
offset: ndblks as u64,
length: os as u64,
})?;
if sb_len > file_data.len() || sb_offset > file_data.len() - sb_len {
return Err(FormatError::UnexpectedEof {
expected: sb_offset.saturating_add(sb_len),
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(),
));
}
crate::checksum::verify_trailing(&file_data[sb_offset..sb_offset + sb_len])?;
let mut pos = sb_offset + sb_header_size;
let page_bitmap: Vec<u8> = if is_paged {
let bm = file_data[pos..pos + bitmap_size].to_vec();
pos += bitmap_size;
bm
} else {
Vec::new()
};
let mut dblk_addrs: Vec<u64> = Vec::with_capacity(ndblks);
for _ in 0..ndblks {
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 (db_local, &addr) in dblk_addrs.iter().enumerate() {
if !is_undefined_addr(addr, offset_size) {
let block_chunks = if is_paged {
read_paged_data_block(
file_data,
addr.to_usize()?,
page_nelmts,
npages,
db_local,
&page_bitmap,
header,
offset_size,
chunk_byte_size,
global_idx,
total_elements,
grid,
)?
} else {
read_data_block_elements(
file_data,
addr.to_usize()?,
nelmts_per_dblk,
header,
offset_size,
chunk_byte_size,
global_idx,
total_elements,
grid,
)?
};
chunks.extend(block_chunks);
}
global_idx += nelmts_per_dblk;
}
Ok(chunks)
}
#[cfg(feature = "std")]
pub(crate) fn extensible_array_index_spans<S: Source + ?Sized>(
source: &S,
ea_base: u64,
offset_size: u8,
length_size: u8,
) -> Result<Vec<(u64, u64)>, FormatError> {
use crate::chunked_write::{aesb_size, eadb_size};
let header =
ExtensibleArrayHeader::parse_from_source(source, ea_base, offset_size, length_size)?;
let os = offset_size as usize;
let elem_size = ea_elem_stride(&header, offset_size);
if header.max_dblk_nelmts_bits >= 64 {
return Err(FormatError::ChunkedReadError(
"Extensible Array page exponent out of range".into(),
));
}
let page_nelmts = 1u64 << header.max_dblk_nelmts_bits;
let blk_off_size = (header.max_nelmts_bits as usize).div_ceil(8);
let aehd_size = ExtensibleArrayHeader::serialized_size(offset_size, length_size) as u64;
let mut spans = vec![(ea_base, aehd_size)];
if is_undefined_addr(header.index_block_address, offset_size) {
return Ok(spans);
}
let geom = EaGeometry::from_header(&header);
let ndblk_addrs = geom.direct_dblk_nelmts.len();
let nsblk_addrs = geom.nsblk_addrs;
let inline = header.idx_blk_elmts as usize;
let ib_header = 4 + 1 + 1 + os; let aeib_size =
crate::chunked_write::aeib_size(offset_size, inline, elem_size, ndblk_addrs, nsblk_addrs);
let ib_addr = header.index_block_address;
let ib = source.read_metadata_at(ib_addr, aeib_size)?;
if &ib[..4] != b"EAIB" {
return Err(FormatError::ChunkedReadError(
"invalid Extensible Array index block signature".into(),
));
}
crate::checksum::verify_trailing(&ib)?;
spans.push((ib_addr, aeib_size as u64));
let mut pos = ib_header + inline * elem_size;
for &dblk_nelmts in &geom.direct_dblk_nelmts {
let addr = read_offset(&ib, pos, offset_size)?;
pos += os;
if is_undefined_addr(addr, offset_size) {
continue;
}
spans.push((
addr,
eadb_size(
DataBlockGeom {
dblk_nelmts,
page_nelmts,
},
elem_size,
offset_size,
blk_off_size,
),
));
}
for j in 0..nsblk_addrs {
let addr = read_offset(&ib, pos, offset_size)?;
pos += os;
if is_undefined_addr(addr, offset_size) {
continue;
}
let sb = geom.super_block_at(j, page_nelmts);
spans.push((addr, aesb_size(sb, offset_size, blk_off_size)));
easb_data_block_spans(
source,
addr,
sb,
offset_size,
blk_off_size,
elem_size,
&mut spans,
)?;
}
Ok(spans)
}
#[cfg(feature = "std")]
fn easb_data_block_spans<S: Source + ?Sized>(
source: &S,
sb_addr: u64,
sb: SuperBlockGeom,
offset_size: u8,
blk_off_size: usize,
elem_size: usize,
spans: &mut Vec<(u64, u64)>,
) -> Result<(), FormatError> {
use crate::chunked_write::{aesb_size, eadb_size};
let os = offset_size as usize;
let sb_header = 4 + 1 + 1 + os + blk_off_size; let sb_len = aesb_size(sb, offset_size, blk_off_size).to_usize()?;
let block = source.read_metadata_at(sb_addr, sb_len)?;
if &block[..4] != b"EASB" {
return Err(FormatError::ChunkedReadError(
"invalid Extensible Array super block signature".into(),
));
}
crate::checksum::verify_trailing(&block)?;
let mut pos = sb_header + sb.bitmap_size().to_usize()?;
for _ in 0..sb.ndblks {
let addr = read_offset(&block, pos, offset_size)?;
pos += os;
if is_undefined_addr(addr, offset_size) {
continue;
}
spans.push((
addr,
eadb_size(sb.blocks, elem_size, offset_size, blk_off_size),
));
}
Ok(())
}
#[allow(clippy::too_many_arguments)]
pub fn read_extensible_array_chunks_from_source<S: Source + ?Sized>(
source: &S,
header: &ExtensibleArrayHeader,
grid: &ChunkGrid,
chunk_dimensions: &[u32],
element_size: u32,
offset_size: u8,
_length_size: u8,
) -> Result<Vec<ChunkInfo>, FormatError> {
let os = offset_size as usize;
let chunk_byte_size: u64 =
chunk_dimensions.iter().map(|&d| d as u64).product::<u64>() * element_size as u64;
let total_elements = header.num_elements.to_usize()?;
let geom = EaGeometry::from_header(header);
let elem_stride = ea_elem_stride(header, offset_size);
let ib_header_size = 4 + 1 + 1 + os;
let n_inline = header.idx_blk_elmts as usize;
let ndirect = geom.direct_dblk_nelmts.len();
let nsblk = geom.nsblk_addrs;
let inline_bytes = n_inline
.checked_mul(elem_stride)
.ok_or(FormatError::OffsetOverflow {
offset: n_inline as u64,
length: elem_stride as u64,
})?;
let addr_bytes = (ndirect + nsblk)
.checked_mul(os)
.ok_or(FormatError::OffsetOverflow {
offset: (ndirect + nsblk) as u64,
length: os as u64,
})?;
let ib_len = ib_header_size + inline_bytes + addr_bytes + 4;
let ib = source.read_metadata_at(header.index_block_address, ib_len)?;
if &ib[0..4] != b"EAIB" {
return Err(FormatError::ChunkedReadError(
"invalid Extensible Array index block signature".into(),
));
}
crate::checksum::verify_trailing(&ib)?;
let mut pos = ib_header_size;
let mut chunks = Vec::new();
let mut global_index = 0usize;
for i in 0..n_inline {
if global_index + i >= total_elements {
break;
}
let (info, consumed) = read_element(
&ib,
pos,
header.client_id,
header.element_size,
offset_size,
chunk_byte_size,
global_index + i,
grid,
)?;
if let Some(ci) = info {
chunks.push(ci);
}
pos += consumed;
}
global_index += n_inline.min(total_elements);
if global_index >= total_elements {
return Ok(chunks);
}
let mut direct_addrs: Vec<u64> = Vec::with_capacity(ndirect);
for _ in 0..ndirect {
direct_addrs.push(read_offset(&ib, pos, offset_size)?);
pos += os;
}
for (i, &addr) in direct_addrs.iter().enumerate() {
if global_index >= total_elements {
break;
}
let nelmts = geom.direct_dblk_nelmts[i].to_usize()?;
if !is_undefined_addr(addr, offset_size) {
chunks.extend(read_data_block_elements_from_source(
source,
addr,
nelmts,
header,
offset_size,
chunk_byte_size,
global_index,
total_elements,
grid,
)?);
}
global_index += nelmts;
}
let mut sblk_addrs: Vec<u64> = Vec::with_capacity(nsblk);
for _ in 0..nsblk {
sblk_addrs.push(read_offset(&ib, pos, offset_size)?);
pos += os;
}
for (j, &sb_addr) in sblk_addrs.iter().enumerate() {
if global_index >= total_elements {
break;
}
let sblk_idx = geom.first_indirect_sblk + j;
let (ndblks, dblk_nelmts) = geom.sblks[sblk_idx];
let total_in_sb = (ndblks * dblk_nelmts).to_usize()?;
if !is_undefined_addr(sb_addr, offset_size) {
chunks.extend(read_super_block_from_source(
source,
sb_addr,
ndblks.to_usize()?,
dblk_nelmts.to_usize()?,
header,
offset_size,
chunk_byte_size,
global_index,
total_elements,
grid,
)?);
}
global_index += total_in_sb;
}
Ok(chunks)
}
#[allow(clippy::too_many_arguments)]
fn read_data_block_elements_from_source<S: Source + ?Sized>(
source: &S,
db_address: u64,
nelmts: usize,
header: &ExtensibleArrayHeader,
offset_size: u8,
chunk_byte_size: u64,
start_index: usize,
total_elements: usize,
grid: &ChunkGrid,
) -> Result<Vec<ChunkInfo>, FormatError> {
let os = offset_size as usize;
let db_header_size = 4 + 1 + 1 + os;
let blk_off_size = (header.max_nelmts_bits as usize).div_ceil(8);
let limit = total_elements.saturating_sub(start_index).min(nelmts);
let region_len = eadb_extent(nelmts, header, offset_size, blk_off_size)?;
let block = source.read_metadata_at(db_address, region_len)?;
if &block[0..4] != b"EADB" {
return Err(FormatError::ChunkedReadError(
"invalid Extensible Array data block signature".into(),
));
}
crate::checksum::verify_trailing(&block)?;
let mut pos = db_header_size + blk_off_size;
let mut chunks = Vec::new();
for i in 0..limit {
let (info, consumed) = read_element(
&block,
pos,
header.client_id,
header.element_size,
offset_size,
chunk_byte_size,
start_index + i,
grid,
)?;
if let Some(ci) = info {
chunks.push(ci);
}
pos += consumed;
}
Ok(chunks)
}
#[allow(clippy::too_many_arguments)]
fn read_paged_data_block_from_source<S: Source + ?Sized>(
source: &S,
db_address: u64,
page_nelmts: usize,
npages: usize,
db_local_idx: usize,
page_bitmap: &[u8],
header: &ExtensibleArrayHeader,
offset_size: u8,
chunk_byte_size: u64,
start_index: usize,
total_elements: usize,
grid: &ChunkGrid,
) -> Result<Vec<ChunkInfo>, FormatError> {
let blk_off_size = (header.max_nelmts_bits as usize).div_ceil(8);
let db_header_size = 4 + 1 + 1 + offset_size as usize + blk_off_size + 4;
let elem_stride = ea_elem_stride(header, offset_size);
let page_stride = page_nelmts
.checked_mul(elem_stride)
.and_then(|bytes| bytes.checked_add(4))
.ok_or(FormatError::OffsetOverflow {
offset: page_nelmts as u64,
length: elem_stride as u64,
})?;
let mut init_pages = 0usize;
for page in 0..npages {
if page_is_initialized(page_bitmap, db_local_idx * npages + page) {
init_pages = page + 1;
}
}
let pages_bytes = init_pages
.checked_mul(page_stride)
.ok_or(FormatError::OffsetOverflow {
offset: init_pages as u64,
length: page_stride as u64,
})?;
let region_len = db_header_size + pages_bytes;
let block = source.read_metadata_at(db_address, region_len)?;
if block.len() < 4 || &block[0..4] != b"EADB" {
return Err(FormatError::ChunkedReadError(
"invalid Extensible Array data block signature".into(),
));
}
crate::checksum::verify_trailing(&block[..db_header_size])?;
let mut chunks = Vec::new();
let mut pos = db_header_size;
for page in 0..npages {
let global_page = db_local_idx * npages + page;
if !page_is_initialized(page_bitmap, global_page) {
pos += page_stride;
continue;
}
crate::checksum::verify_trailing(&block[pos..pos + page_stride])?;
let page_start = start_index + page * page_nelmts;
let limit = total_elements.saturating_sub(page_start).min(page_nelmts);
for i in 0..limit {
let (info, consumed) = read_element(
&block,
pos,
header.client_id,
header.element_size,
offset_size,
chunk_byte_size,
page_start + i,
grid,
)?;
if let Some(ci) = info {
chunks.push(ci);
}
pos += consumed;
}
if limit < page_nelmts {
break;
}
pos += 4; }
Ok(chunks)
}
#[allow(clippy::too_many_arguments)]
fn read_super_block_from_source<S: Source + ?Sized>(
source: &S,
sb_address: u64,
ndblks: usize,
nelmts_per_dblk: usize,
header: &ExtensibleArrayHeader,
offset_size: u8,
chunk_byte_size: u64,
start_index: usize,
total_elements: usize,
grid: &ChunkGrid,
) -> Result<Vec<ChunkInfo>, FormatError> {
let os = offset_size as usize;
let blk_off_size = (header.max_nelmts_bits as usize).div_ceil(8);
let sb_header_size = 4 + 1 + 1 + os + blk_off_size;
let page_nelmts = 1usize << header.max_dblk_nelmts_bits;
let sb = SuperBlockGeom {
ndblks: ndblks as u64,
blocks: DataBlockGeom {
dblk_nelmts: nelmts_per_dblk as u64,
page_nelmts: page_nelmts as u64,
},
};
let is_paged = sb.blocks.is_paged();
let npages = sb.blocks.npages().to_usize()?;
let bitmap_size = sb.bitmap_size().to_usize()?;
let addr_bytes = ndblks.checked_mul(os).ok_or(FormatError::OffsetOverflow {
offset: ndblks as u64,
length: os as u64,
})?;
let region_len = sb_header_size + bitmap_size + addr_bytes + 4;
let block = source.read_metadata_at(sb_address, region_len)?;
if &block[0..4] != b"EASB" {
return Err(FormatError::ChunkedReadError(
"invalid Extensible Array super block signature".into(),
));
}
crate::checksum::verify_trailing(&block)?;
let mut pos = sb_header_size;
let page_bitmap: Vec<u8> = if is_paged {
let bm = block[pos..pos + bitmap_size].to_vec();
pos += bitmap_size;
bm
} else {
Vec::new()
};
let mut dblk_addrs: Vec<u64> = Vec::with_capacity(ndblks);
for _ in 0..ndblks {
dblk_addrs.push(read_offset(&block, pos, offset_size)?);
pos += os;
}
let mut chunks = Vec::new();
let mut global_idx = start_index;
for (db_local, &addr) in dblk_addrs.iter().enumerate() {
if !is_undefined_addr(addr, offset_size) {
let block_chunks = if is_paged {
read_paged_data_block_from_source(
source,
addr,
page_nelmts,
npages,
db_local,
&page_bitmap,
header,
offset_size,
chunk_byte_size,
global_idx,
total_elements,
grid,
)?
} else {
read_data_block_elements_from_source(
source,
addr,
nelmts_per_dblk,
header,
offset_size,
chunk_byte_size,
global_idx,
total_elements,
grid,
)?
};
chunks.extend(block_chunks);
}
global_idx += nelmts_per_dblk;
}
Ok(chunks)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_data_block_is_paged_only_past_a_full_page() {
let at = |dblk_nelmts| DataBlockGeom {
dblk_nelmts,
page_nelmts: 16,
};
assert!(!at(15).is_paged(), "under a page");
assert!(!at(16).is_paged(), "exactly a page is not paged");
assert!(at(17).is_paged(), "past a page");
assert_eq!(at(16).npages(), 0, "a block that is not paged has no pages");
assert_eq!(at(32).npages(), 2);
}
#[test]
fn the_page_init_bitmap_is_a_byte_per_eight_pages_per_block() {
let sb = |ndblks, dblk_nelmts| SuperBlockGeom {
ndblks,
blocks: DataBlockGeom {
dblk_nelmts,
page_nelmts: 16,
},
};
assert_eq!(sb(4, 16).bitmap_size(), 0);
assert_eq!(sb(2, 8 * 16).bitmap_size(), 2);
assert_eq!(sb(2, 9 * 16).bitmap_size(), 4);
assert_eq!(sb(1, 2 * 16).bitmap_size(), 1);
}
fn dense_grid(dims: &[u64], chunk_dims: &[u32]) -> ChunkGrid {
let cd: Vec<u64> = chunk_dims.iter().map(|&d| u64::from(d)).collect();
ChunkGrid::new(&cd, dims, None, crate::chunk_grid::GridOrder::RowMajor).unwrap()
}
use crate::checksum::stamp_trailing as stamp;
const fn eahd_len(os: u8, ls: u8) -> usize {
12 + 6 * ls as usize + os as usize + 4
}
#[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; buf[5] = 0; buf[6] = 8; buf[7] = 10; buf[8] = 2; buf[9] = 4; buf[10] = 2; buf[11] = 8; buf[12..20].copy_from_slice(&0u64.to_le_bytes()); buf[20..28].copy_from_slice(&0u64.to_le_bytes()); buf[28..36].copy_from_slice(&0u64.to_le_bytes()); buf[36..44].copy_from_slice(&0u64.to_le_bytes()); buf[44..52].copy_from_slice(&5u64.to_le_bytes()); buf[52..60].copy_from_slice(&0u64.to_le_bytes()); buf[60..68].copy_from_slice(&0x1000u64.to_le_bytes()); stamp(&mut buf, 0, eahd_len(os, ls));
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());
}
#[test]
fn a_dimension_of_no_chunks_refuses_rather_than_dividing_by_zero() {
let chunks = [crate::chunked_write::WrittenChunk {
address: 0x1000,
compressed_size: 8,
filter_mask: 0,
}];
let slots = crate::chunked_write::IndexSlots::dense(&chunks);
let ea =
crate::chunked_write::build_extensible_array_at(&slots, 16, 8, 8, false, 0).unwrap();
let header = ExtensibleArrayHeader::parse(&ea, 0, 8, 8).unwrap();
let grid = ChunkGrid::new(
&[2, 2],
&[3, 4],
Some(&[u64::MAX, 0]),
crate::chunk_grid::GridOrder::UnlimitedFirst,
)
.unwrap();
let err = read_extensible_array_chunks(&ea, &header, &grid, &[2, 2], 4, 8, 8).unwrap_err();
assert!(format!("{err}").contains("numbers nothing"), "{err}");
}
#[test]
fn a_chunk_at_an_interior_slot_outside_the_dataset_is_dropped() {
let chunks: Vec<crate::chunked_write::WrittenChunk> = [0x1000u64, 0x2000, 0x3000]
.iter()
.map(|&address| crate::chunked_write::WrittenChunk {
address,
compressed_size: 8,
filter_mask: 0,
})
.collect();
let slots = crate::chunked_write::IndexSlots::new(&chunks, &[0, 1, 2], 3).unwrap();
let ea =
crate::chunked_write::build_extensible_array_at(&slots, 16, 8, 8, false, 0).unwrap();
let grid = ChunkGrid::new(
&[2, 2],
&[3, 3],
Some(&[8, u64::MAX]),
crate::chunk_grid::GridOrder::UnlimitedFirst,
)
.unwrap();
assert_eq!(
grid.offsets_in_extent(2).unwrap(),
None,
"slot 2 is the one"
);
let header = ExtensibleArrayHeader::parse(&ea, 0, 8, 8).unwrap();
let read = read_extensible_array_chunks(&ea, &header, &grid, &[2, 2], 4, 8, 8).unwrap();
assert_eq!(
read.iter().map(|c| c.address).collect::<Vec<_>>(),
vec![0x1000, 0x2000],
"the slot-2 chunk lies past the dataset's three rows"
);
}
#[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;
let mut file_data = vec![0u8; 0x3000];
let aehd_offset = 0x100usize;
let aeib_offset = 0x200usize;
file_data[aehd_offset..aehd_offset + 4].copy_from_slice(b"EAHD");
file_data[aehd_offset + 4] = 0; file_data[aehd_offset + 5] = 0; file_data[aehd_offset + 6] = osv as u8; file_data[aehd_offset + 7] = 10; file_data[aehd_offset + 8] = num_chunks as u8; file_data[aehd_offset + 9] = 4; file_data[aehd_offset + 10] = 2; file_data[aehd_offset + 11] = 8; 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());
stamp(&mut file_data, aehd_offset, eahd_len(os, ls));
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 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());
}
stamp(
&mut file_data,
aeib_offset,
(6 + osv) + 2 * osv + 2 * osv + 7 * osv + 4,
);
let header = ExtensibleArrayHeader::parse(&file_data, aehd_offset, os, ls).unwrap();
let ds_dims = vec![40u64]; let chunk_dims = vec![20u32];
let chunks = read_extensible_array_chunks(
&file_data,
&header,
&dense_grid(&ds_dims, &chunk_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]);
#[cfg(feature = "std")]
assert_ea_streams_match(&file_data, aehd_offset, &ds_dims, &chunk_dims, 8, os, ls);
}
#[cfg(feature = "std")]
fn assert_ea_streams_match(
file_data: &[u8],
aehd_offset: usize,
ds_dims: &[u64],
chunk_dims: &[u32],
element_size: u32,
os: u8,
ls: u8,
) {
use crate::source::{BytesSource, ReadSeekSource};
let h = ExtensibleArrayHeader::parse(file_data, aehd_offset, os, ls).unwrap();
let buffered = read_extensible_array_chunks(
file_data,
&h,
&dense_grid(ds_dims, chunk_dims),
chunk_dims,
element_size,
os,
ls,
)
.unwrap();
let mem = BytesSource::new(file_data);
let hm =
ExtensibleArrayHeader::parse_from_source(&mem, aehd_offset as u64, os, ls).unwrap();
let from_mem = read_extensible_array_chunks_from_source(
&mem,
&hm,
&dense_grid(ds_dims, chunk_dims),
chunk_dims,
element_size,
os,
ls,
)
.unwrap();
let seek = ReadSeekSource::new(std::io::Cursor::new(file_data.to_vec())).unwrap();
let hs =
ExtensibleArrayHeader::parse_from_source(&seek, aehd_offset as u64, os, ls).unwrap();
let from_seek = read_extensible_array_chunks_from_source(
&seek,
&hs,
&dense_grid(ds_dims, chunk_dims),
chunk_dims,
element_size,
os,
ls,
)
.unwrap();
assert_eq!(buffered, from_mem, "BytesSource mismatch");
assert_eq!(buffered, from_seek, "ReadSeekSource mismatch");
}
#[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; let idx_blk_elmts = 2u8;
let min_dblk_nelmts = 2u8;
let sblk_min = 2u8;
let total_chunks = 4usize;
let mut file_data = vec![0u8; 0x5000];
let aehd_offset = 0x100usize;
let aeib_offset = 0x200usize;
let aedb_offset = 0x300usize;
file_data[aehd_offset..aehd_offset + 4].copy_from_slice(b"EAHD");
file_data[aehd_offset + 4] = 0;
file_data[aehd_offset + 5] = 0; file_data[aehd_offset + 6] = osv as u8; 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;
file_data[aehd_offset + 44..aehd_offset + 52]
.copy_from_slice(&(total_chunks as u64).to_le_bytes());
file_data[aehd_offset + 60..aehd_offset + 68]
.copy_from_slice(&(aeib_offset as u64).to_le_bytes());
stamp(&mut file_data, aehd_offset, eahd_len(os, ls));
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;
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;
}
let n_direct_dblks = 2;
file_data[pos..pos + osv].copy_from_slice(&(aedb_offset as u64).to_le_bytes());
pos += osv;
for _ in 1..n_direct_dblks {
file_data[pos..pos + osv].copy_from_slice(&u64::MAX.to_le_bytes());
pos += osv;
}
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());
let blk_off_size = (10usize).div_ceil(8); 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;
}
stamp(
&mut file_data,
aeib_offset,
(6 + osv) + 2 * osv + n_direct_dblks * osv + 8 * osv + 4,
);
stamp(
&mut file_data,
aedb_offset,
(6 + osv) + blk_off_size + min_dblk_nelmts as usize * osv + 4,
);
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,
&dense_grid(&ds_dims, &chunk_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]);
}
#[cfg(feature = "std")]
assert_ea_streams_match(&file_data, aehd_offset, &ds_dims, &chunk_dims, 8, os, ls);
}
#[cfg(feature = "std")]
#[test]
fn streaming_ea_super_blocks_and_paged_match_buffered() {
use crate::chunked_write::{WrittenChunk, build_extensible_array_at};
use crate::source::{BytesSource, ReadSeekSource};
for &n in &[2000u64, 50000, 140000] {
let chunks: Vec<WrittenChunk> = (0..n)
.map(|i| WrittenChunk {
address: 0x10 + i * 8,
compressed_size: 8,
filter_mask: 0,
})
.collect();
let base = 0x1000u64;
let ea = build_extensible_array_at(
&crate::chunked_write::IndexSlots::dense(&chunks),
8,
8,
8,
false,
base,
)
.unwrap();
let mut file = vec![0u8; base as usize + ea.len()];
file[base as usize..].copy_from_slice(&ea);
let ds_dims = vec![n];
let chunk_dims = vec![1u32];
let header = ExtensibleArrayHeader::parse(&file, base as usize, 8, 8).unwrap();
let buffered = read_extensible_array_chunks(
&file,
&header,
&dense_grid(&ds_dims, &chunk_dims),
&chunk_dims,
8,
8,
8,
)
.unwrap();
assert_eq!(buffered.len() as u64, n, "buffered chunk count at n={n}");
let mem = BytesSource::new(&file);
let hm = ExtensibleArrayHeader::parse_from_source(&mem, base, 8, 8).unwrap();
let from_mem = read_extensible_array_chunks_from_source(
&mem,
&hm,
&dense_grid(&ds_dims, &chunk_dims),
&chunk_dims,
8,
8,
8,
)
.unwrap();
let seek = ReadSeekSource::new(std::io::Cursor::new(file)).unwrap();
let hs = ExtensibleArrayHeader::parse_from_source(&seek, base, 8, 8).unwrap();
let from_seek = read_extensible_array_chunks_from_source(
&seek,
&hs,
&dense_grid(&ds_dims, &chunk_dims),
&chunk_dims,
8,
8,
8,
)
.unwrap();
assert_eq!(buffered, from_mem, "BytesSource mismatch at n={n}");
assert_eq!(buffered, from_seek, "ReadSeekSource mismatch at n={n}");
}
}
#[test]
fn eadb_extent_matches_the_writer() {
for &(client_id, element_size) in &[(0u8, 8u8), (1, 20)] {
for &offset_size in &[4u8, 8] {
for &max_nelmts_bits in &[10u8, 16, 32] {
let header = ExtensibleArrayHeader {
client_id,
element_size,
max_nelmts_bits,
idx_blk_elmts: 4,
min_dblk_nelmts: 4,
super_blk_min_nelmts: 2,
max_dblk_nelmts_bits: 10,
num_elements: 0,
index_block_address: 0,
};
let blk_off = (max_nelmts_bits as usize).div_ceil(8);
let stride = ea_elem_stride(&header, offset_size);
let page_nelmts = 1u64 << header.max_dblk_nelmts_bits;
for nelmts in [0u64, 1, 2, 4, 16, 64, 255, 256, 1023, page_nelmts] {
assert_eq!(
eadb_extent(nelmts as usize, &header, offset_size, blk_off).unwrap()
as u64,
crate::chunked_write::eadb_size(
DataBlockGeom {
dblk_nelmts: nelmts,
page_nelmts,
},
stride,
offset_size,
blk_off,
),
"client={client_id} os={offset_size} bits={max_nelmts_bits} \
nelmts={nelmts}"
);
}
}
}
}
}
#[test]
fn header_serialized_size() {
assert_eq!(ExtensibleArrayHeader::serialized_size(8, 8), 72);
assert_eq!(ExtensibleArrayHeader::serialized_size(4, 4), 44);
}
#[test]
fn read_element_unallocated() {
let data = vec![0xFFu8; 16];
let ds_dims = vec![50u64];
let chunk_dims = vec![10u32];
let (info, consumed) =
read_element(&data, 0, 0, 8, 8, 80, 0, &dense_grid(&ds_dims, &chunk_dims)).unwrap();
assert!(info.is_none());
assert_eq!(consumed, 8);
}
#[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];
data[0..8].copy_from_slice(&0x2000u64.to_le_bytes());
data[8..12].copy_from_slice(&120u32.to_le_bytes());
data[12..16].copy_from_slice(&0u32.to_le_bytes());
let ds_dims = vec![50u64];
let chunk_dims = vec![10u32];
let (info, consumed) = read_element(
&data,
0,
1,
elem_size as u8,
os,
80,
2,
&dense_grid(&ds_dims, &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);
}
#[test]
fn filtered_element_smaller_than_its_own_fields_is_refused() {
let os: u8 = 8;
let ds_dims = vec![50u64];
let chunk_dims = vec![10u32];
let data = vec![0u8; 64];
for element_size in 0..(os + 4) {
let err = read_element(
&data,
0,
1,
element_size,
os,
80,
0,
&dense_grid(&ds_dims, &chunk_dims),
)
.expect_err("a filtered element narrower than its own fields must be refused");
assert!(
matches!(err, FormatError::ChunkedReadError(_)),
"element_size {element_size} gave {err:?}, want a ChunkedReadError"
);
}
read_element(
&data,
0,
1,
os + 4 + 1,
os,
80,
0,
&dense_grid(&ds_dims, &chunk_dims),
)
.expect("a width that fits address + 1-byte size + mask must parse");
}
#[cfg(feature = "std")]
#[test]
fn truncated_super_block_addresses_are_refused_by_both_backends() {
use crate::chunked_write::{WrittenChunk, build_extensible_array_at};
use crate::source::BytesSource;
let n = 100u64;
let chunks: Vec<WrittenChunk> = (0..n)
.map(|i| WrittenChunk {
address: 0x10 + i * 8,
compressed_size: 8,
filter_mask: 0,
})
.collect();
let base = 0x1000u64;
let ea = build_extensible_array_at(
&crate::chunked_write::IndexSlots::dense(&chunks),
8,
8,
8,
false,
base,
)
.unwrap();
let mut file = vec![0u8; base as usize + ea.len()];
file[base as usize..].copy_from_slice(&ea);
let ds_dims = vec![n];
let chunk_dims = vec![1u32];
let built = ExtensibleArrayHeader::parse(&file, base as usize, 8, 8).unwrap();
let geom = EaGeometry::from_header(&built);
assert!(
geom.nsblk_addrs > 0,
"fixture must reach the super-block address array"
);
let old_ib = built.index_block_address.to_usize().unwrap();
let ib_len = 4 + 1 + 1 + 8 + built.idx_blk_elmts as usize * 8 + geom.direct_dblk_nelmts.len() * 8 + geom.nsblk_addrs * 8 + 4; let block = file[old_ib..old_ib + ib_len].to_vec();
let new_ib = file.len();
file.extend_from_slice(&block);
let addr_field = base as usize + 12 + 6 * 8;
file[addr_field..addr_field + 8].copy_from_slice(&(new_ib as u64).to_le_bytes());
stamp(&mut file, base as usize, eahd_len(8, 8));
let header = ExtensibleArrayHeader::parse(&file, base as usize, 8, 8).unwrap();
assert_eq!(header.index_block_address as usize, new_ib);
let intact = read_extensible_array_chunks(
&file,
&header,
&dense_grid(&ds_dims, &chunk_dims),
&chunk_dims,
8,
8,
8,
)
.unwrap();
assert_eq!(intact.len() as u64, n, "relocation must preserve the read");
let sblk_start = new_ib
+ 4
+ 1
+ 1
+ 8
+ header.idx_blk_elmts as usize * 8
+ geom.direct_dblk_nelmts.len() * 8;
file.truncate(sblk_start + 4);
let buffered = read_extensible_array_chunks(
&file,
&header,
&dense_grid(&ds_dims, &chunk_dims),
&chunk_dims,
8,
8,
8,
);
match buffered {
Err(FormatError::UnexpectedEof {
expected,
available,
}) => {
assert_eq!(
expected,
new_ib + ib_len,
"the buffered read must fault on the cut index block, not earlier"
);
assert_eq!(available, file.len());
}
other => panic!("buffered read must refuse a truncated address array, got {other:?}"),
}
let mem = BytesSource::new(&file);
let hm = ExtensibleArrayHeader::parse_from_source(&mem, base, 8, 8).unwrap();
let streamed = read_extensible_array_chunks_from_source(
&mem,
&hm,
&dense_grid(&ds_dims, &chunk_dims),
&chunk_dims,
8,
8,
8,
);
assert!(
streamed.is_err(),
"streaming read must refuse the same file, got {streamed:?}"
);
}
#[cfg(all(feature = "std", feature = "checksum"))]
#[test]
fn a_corrupted_extensible_array_structure_is_refused() {
use crate::chunked_write::{WrittenChunk, build_extensible_array_at};
use crate::source::BytesSource;
for &n in &[2000u64, 50000, 140000] {
let chunks: Vec<WrittenChunk> = (0..n)
.map(|i| WrittenChunk {
address: 0x10 + i * 8,
compressed_size: 8,
filter_mask: 0,
})
.collect();
let base = 0x1000u64;
let ea = build_extensible_array_at(
&crate::chunked_write::IndexSlots::dense(&chunks),
8,
8,
8,
false,
base,
)
.unwrap();
let mut file = vec![0u8; base as usize + ea.len()];
file[base as usize..].copy_from_slice(&ea);
let ds_dims = vec![n];
let chunk_dims = vec![1u32];
let read_both = |file: &[u8]| -> (Result<Vec<ChunkInfo>, FormatError>, bool) {
let grid = dense_grid(&ds_dims, &chunk_dims);
let buffered =
ExtensibleArrayHeader::parse(file, base as usize, 8, 8).and_then(|h| {
read_extensible_array_chunks(file, &h, &grid, &chunk_dims, 8, 8, 8)
});
let mem = BytesSource::new(file);
let streamed =
ExtensibleArrayHeader::parse_from_source(&mem, base, 8, 8).and_then(|h| {
read_extensible_array_chunks_from_source(
&mem,
&h,
&grid,
&chunk_dims,
8,
8,
8,
)
});
(buffered, streamed.is_err())
};
assert_eq!(
read_both(&file).0.expect("the sound file must read").len() as u64,
n,
"the fixture must read before it is corrupted, at n={n}"
);
let spans = extensible_array_index_spans(&BytesSource::new(&file), base, 8, 8).unwrap();
assert!(spans.len() > 1, "the sweep must reach past the header");
let header = ExtensibleArrayHeader::parse(&file, base as usize, 8, 8).unwrap();
let stride = ea_elem_stride(&header, 8);
let page_nelmts = 1usize << header.max_dblk_nelmts_bits;
let db_prefix = 4 + 1 + 1 + 8 + (header.max_nelmts_bits as usize).div_ceil(8);
let max_unpaged = db_prefix + page_nelmts * stride + 4;
let mut poke_sites: Vec<(u64, bool)> = Vec::new();
let mut paged = 0;
for &(at, len) in &spans {
let start = at.to_usize().unwrap();
let kind = &file[start..start + 4];
if kind == b"EADB" && len as usize > max_unpaged {
paged += 1;
poke_sites.push((at + db_prefix as u64 + 4 - 1, false));
poke_sites.push((at + (max_unpaged + 4) as u64 - 1, false));
} else {
poke_sites.push((at + len - 1, kind != b"EADB"));
}
}
if n == 140000 {
assert!(paged > 0, "n={n} must reach a paged data block");
}
assert!(
poke_sites.iter().any(|&(_, walked)| walked),
"n={n}: the sweep must reach a structure the reclaim walk reads"
);
for (site, walked) in poke_sites {
let at = site.to_usize().unwrap();
let original = file[at];
file[at] ^= 0x01;
let (buffered, streamed_err) = read_both(&file);
assert!(
matches!(buffered, Err(FormatError::ChecksumMismatch { .. })),
"n={n}: a corrupted checksum at {at:#x} must be refused, got {buffered:?}"
);
assert!(
streamed_err,
"n={n}: the streaming backend must refuse what the buffered one does, at {at:#x}"
);
if walked {
let walk = extensible_array_index_spans(&BytesSource::new(&file), base, 8, 8);
assert!(
matches!(walk, Err(FormatError::ChecksumMismatch { .. })),
"n={n}: the reclaim walk must refuse a corrupt structure at {at:#x} \
rather than release spans read out of it, got {walk:?}"
);
}
file[at] = original;
}
}
}
#[cfg(feature = "std")]
#[test]
fn index_spans_match_builder_layout() {
use crate::chunked_write::{WrittenChunk, build_extensible_array_at};
let os: u8 = 8;
let ls: u8 = 8;
let base = 0x4000u64;
for &n in &[1u64, 4, 20, 100, 244, 300, 2000, 50000, 140000] {
let chunks: Vec<WrittenChunk> = (0..n)
.map(|i| WrittenChunk {
address: 0x100000 + i * 8,
compressed_size: 8,
filter_mask: 0,
})
.collect();
let ea = build_extensible_array_at(
&crate::chunked_write::IndexSlots::dense(&chunks),
8,
os,
ls,
false,
base,
)
.unwrap();
let mut file = vec![0u8; base as usize + ea.len()];
file[base as usize..].copy_from_slice(&ea);
let spans =
extensible_array_index_spans(&crate::source::BytesSource::new(&file), base, os, ls)
.unwrap();
let header = ExtensibleArrayHeader::parse(&file, base as usize, os, ls).unwrap();
let geom = EaGeometry::from_header(&header);
let aehd = ExtensibleArrayHeader::serialized_size(os, ls) as u64;
let aeib = crate::chunked_write::aeib_size(
os,
header.idx_blk_elmts as usize,
os as usize,
geom.direct_dblk_nelmts.len(),
geom.nsblk_addrs,
) as u64;
let stat = |k: usize| {
let off = base as usize + 12 + k * ls as usize;
u64::from_le_bytes(file[off..off + 8].try_into().unwrap())
};
let super_blk_size = stat(1);
let data_blk_size = stat(3);
let expected_total = aehd + aeib + super_blk_size + data_blk_size;
let total: u64 = spans.iter().map(|&(_, l)| l).sum();
assert_eq!(
total, expected_total,
"EA index span total mismatch at n={n}"
);
let mut sorted = spans.clone();
sorted.sort_by_key(|&(a, _)| a);
for w in sorted.windows(2) {
assert!(
w[0].0 + w[0].1 <= w[1].0,
"EA index spans overlap at n={n}: {sorted:?}"
);
}
for &(a, l) in &spans {
assert!(
a >= base && a + l <= base + ea.len() as u64,
"EA index span out of the blob at n={n}: ({a}, {l})"
);
}
}
}
}