#[cfg(not(feature = "std"))]
extern crate alloc;
#[cfg(not(feature = "std"))]
use alloc::{format, vec, 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 FixedArrayHeader {
pub client_id: u8,
pub element_size: u8,
pub max_nelmts_bits: u8,
pub num_elements: u64,
pub data_block_address: u64,
}
impl FixedArrayHeader {
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 + 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"FAHD" {
return Err(FormatError::ChunkedReadError(
"invalid Fixed Array header signature".into(),
));
}
let version = d[4];
if version != 0 {
return Err(FormatError::ChunkedReadError(format!(
"unsupported Fixed Array header version: {version}"
)));
}
let client_id = d[5];
let element_size = d[6];
let max_nelmts_bits = d[7];
let mut pos = 8;
let num_elements = read_length(d, pos, length_size)?;
pos += length_size as usize;
let data_block_address = read_offset(d, pos, offset_size)?;
crate::checksum::verify_trailing(&d[..min_size])?;
Ok(FixedArrayHeader {
client_id,
element_size,
max_nelmts_bits,
num_elements,
data_block_address,
})
}
pub fn parse_from_source<S: Source + ?Sized>(
source: &S,
address: u64,
offset_size: u8,
length_size: u8,
) -> Result<Self, FormatError> {
let min_size = 4 + 1 + 1 + 1 + 1 + length_size as usize + offset_size as usize + 4;
let buf = source.read_metadata_at(address, min_size)?;
Self::parse(&buf, 0, offset_size, length_size)
}
}
pub(crate) fn fixed_array_index_spans<S: Source + ?Sized>(
source: &S,
fa_base: u64,
offset_size: u8,
length_size: u8,
) -> Result<Vec<(u64, u64)>, FormatError> {
let header = FixedArrayHeader::parse_from_source(source, fa_base, offset_size, length_size)?;
let os = offset_size as usize;
let fahd_size = (4 + 1 + 1 + 1 + 1 + length_size as usize + os + 4) as u64;
let mut spans = vec![(fa_base, fahd_size)];
if is_undefined_addr(header.data_block_address, offset_size) {
return Ok(spans);
}
let elem_size = if header.client_id == 0 {
os
} else {
header.element_size as usize
};
if header.max_nelmts_bits >= 64 {
return Err(FormatError::ChunkedReadError(
"Fixed Array page exponent out of range".into(),
));
}
let page_size = 1usize << header.max_nelmts_bits;
let num_elements = header.num_elements.to_usize()?;
let db_prefix = 4 + 1 + 1 + os;
let fadb_size: u64 = if num_elements <= page_size {
(db_prefix
+ num_elements
.checked_mul(elem_size)
.ok_or(FormatError::OffsetOverflow {
offset: num_elements as u64,
length: elem_size as u64,
})?
+ 4) as u64
} else {
let npages = num_elements.div_ceil(page_size);
let bitmap_size = npages.div_ceil(8);
let elements_bytes =
num_elements
.checked_mul(elem_size)
.ok_or(FormatError::OffsetOverflow {
offset: num_elements as u64,
length: elem_size as u64,
})?;
(db_prefix + bitmap_size + 4 + elements_bytes + npages * 4) as u64
};
spans.push((header.data_block_address, fadb_size));
Ok(spans)
}
#[allow(clippy::too_many_arguments)]
fn parse_fa_element(
block: &[u8],
elem_pos: usize,
index: usize,
client_id: u8,
chunk_byte_size: u64,
elem_size: usize,
chunk_size_bytes: usize,
offset_size: u8,
grid: &ChunkGrid,
) -> Result<Option<ChunkInfo>, FormatError> {
if elem_size > block.len() || elem_pos > block.len() - elem_size {
return Err(FormatError::UnexpectedEof {
expected: elem_pos.saturating_add(elem_size),
available: block.len(),
});
}
let Some(address) = read_optional_offset(block, elem_pos, offset_size)? else {
return Ok(None);
};
let Some(offsets) = grid.offsets_in_extent(index as u64)? else {
return Ok(None);
};
if client_id == 0 {
Ok(Some(ChunkInfo {
chunk_size: u32_from(chunk_byte_size)?,
filter_mask: 0,
offsets,
address,
}))
} else {
let os = offset_size as usize;
let chunk_size = read_variable_length(&block[elem_pos + os..], chunk_size_bytes)?;
let fm_off = elem_pos + os + chunk_size_bytes;
let filter_mask = u32::from_le_bytes([
block[fm_off],
block[fm_off + 1],
block[fm_off + 2],
block[fm_off + 3],
]);
Ok(Some(ChunkInfo {
chunk_size: u32_from(chunk_size)?,
filter_mask,
offsets,
address,
}))
}
}
#[allow(clippy::too_many_arguments)]
pub fn read_fixed_array_chunks(
file_data: &[u8],
header: &FixedArrayHeader,
grid: &ChunkGrid,
chunk_dimensions: &[u32],
element_size: u32,
offset_size: u8,
_length_size: u8,
) -> Result<Vec<ChunkInfo>, FormatError> {
let db_offset = header.data_block_address.to_usize()?;
let os = offset_size as usize;
let db_header_size = 4 + 1 + 1 + os;
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"FADB" {
return Err(FormatError::ChunkedReadError(
"invalid Fixed Array data block signature".into(),
));
}
let chunk_size_bytes = if header.client_id == 0 {
0
} else {
(header.element_size as usize)
.checked_sub(os + 4)
.ok_or_else(|| {
FormatError::ChunkedReadError("Fixed Array element size too small".into())
})?
};
let elem_size = if header.client_id == 0 {
os
} else {
header.element_size as usize
};
let chunk_byte_size: u64 =
chunk_dimensions.iter().map(|&d| d as u64).product::<u64>() * element_size as u64;
let num_elements = header.num_elements.to_usize()?;
let page_size = (1u64 << header.max_nelmts_bits).to_usize()?;
let is_paged = num_elements > page_size;
let mut chunks = Vec::new();
if !is_paged {
let db_len = num_elements
.checked_mul(elem_size)
.and_then(|elems| elems.checked_add(db_header_size + 4))
.ok_or(FormatError::OffsetOverflow {
offset: num_elements as u64,
length: elem_size as u64,
})?;
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(),
});
}
crate::checksum::verify_trailing(&file_data[db_offset..db_offset + db_len])?;
let mut pos = db_offset + db_header_size;
for index in 0..num_elements {
if let Some(info) = parse_fa_element(
file_data,
pos,
index,
header.client_id,
chunk_byte_size,
elem_size,
chunk_size_bytes,
offset_size,
grid,
)? {
chunks.push(info);
}
pos += elem_size;
}
return Ok(chunks);
}
let npages = num_elements.div_ceil(page_size);
let bitmap_size = npages.div_ceil(8);
let bitmap_pos = db_offset + db_header_size;
let fits = bitmap_pos
.checked_add(bitmap_size)
.and_then(|x| x.checked_add(4))
.is_some_and(|end| end <= file_data.len());
if !fits {
return Err(FormatError::UnexpectedEof {
expected: bitmap_pos.saturating_add(bitmap_size).saturating_add(4),
available: file_data.len(),
});
}
let bitmap = &file_data[bitmap_pos..bitmap_pos + bitmap_size];
crate::checksum::verify_trailing(&file_data[db_offset..bitmap_pos + bitmap_size + 4])?;
let pages_start = bitmap_pos + bitmap_size + 4;
let page_stride = page_size
.checked_mul(elem_size)
.and_then(|bytes| bytes.checked_add(4))
.ok_or(FormatError::OffsetOverflow {
offset: page_size as u64,
length: elem_size as u64,
})?;
for page in 0..npages {
let nelem_in_page = core::cmp::min(page_size, num_elements - page * page_size);
let initialized = (bitmap[page / 8] >> (7 - (page % 8))) & 1 == 1;
if !initialized {
continue;
}
let page_offset = page
.checked_mul(page_stride)
.ok_or(FormatError::OffsetOverflow {
offset: page as u64,
length: page_stride as u64,
})?;
let page_start = pages_start + page_offset;
let page_end = page_start
.checked_add(nelem_in_page * elem_size + 4)
.filter(|&end| end <= file_data.len())
.ok_or(FormatError::UnexpectedEof {
expected: page_start.saturating_add(nelem_in_page * elem_size + 4),
available: file_data.len(),
})?;
crate::checksum::verify_trailing(&file_data[page_start..page_end])?;
for j in 0..nelem_in_page {
let index = page * page_size + j;
let elem_pos = page_start + j * elem_size;
if let Some(info) = parse_fa_element(
file_data,
elem_pos,
index,
header.client_id,
chunk_byte_size,
elem_size,
chunk_size_bytes,
offset_size,
grid,
)? {
chunks.push(info);
}
}
}
Ok(chunks)
}
#[allow(clippy::too_many_arguments)]
pub fn read_fixed_array_chunks_from_source<S: Source + ?Sized>(
source: &S,
header: &FixedArrayHeader,
grid: &ChunkGrid,
chunk_dimensions: &[u32],
element_size: u32,
offset_size: u8,
_length_size: u8,
) -> Result<Vec<ChunkInfo>, FormatError> {
let db_address = header.data_block_address;
let os = offset_size as usize;
let db_header_size = 4 + 1 + 1 + os;
let signature = |block: &[u8]| -> Result<(), FormatError> {
if &block[0..4] != b"FADB" {
return Err(FormatError::ChunkedReadError(
"invalid Fixed Array data block signature".into(),
));
}
Ok(())
};
let chunk_size_bytes = if header.client_id == 0 {
0
} else {
(header.element_size as usize)
.checked_sub(os + 4)
.ok_or_else(|| {
FormatError::ChunkedReadError("Fixed Array element size too small".into())
})?
};
let elem_size = if header.client_id == 0 {
os
} else {
header.element_size as usize
};
let chunk_byte_size: u64 =
chunk_dimensions.iter().map(|&d| d as u64).product::<u64>() * element_size as u64;
let num_elements = header.num_elements.to_usize()?;
let page_size = (1u64 << header.max_nelmts_bits).to_usize()?;
let is_paged = num_elements > page_size;
let mut chunks = Vec::new();
if !is_paged {
let db_len = num_elements
.checked_mul(elem_size)
.and_then(|elems| elems.checked_add(db_header_size + 4))
.ok_or(FormatError::OffsetOverflow {
offset: num_elements as u64,
length: elem_size as u64,
})?;
let region = source.read_metadata_at(db_address, db_len)?;
signature(®ion)?;
crate::checksum::verify_trailing(®ion)?;
for index in 0..num_elements {
if let Some(info) = parse_fa_element(
®ion,
db_header_size + index * elem_size,
index,
header.client_id,
chunk_byte_size,
elem_size,
chunk_size_bytes,
offset_size,
grid,
)? {
chunks.push(info);
}
}
return Ok(chunks);
}
let npages = num_elements.div_ceil(page_size);
let bitmap_size = npages.div_ceil(8);
let bitmap_addr = db_address + db_header_size as u64;
let prefix_and_bitmap =
source.read_metadata_at(db_address, db_header_size + bitmap_size + 4)?;
signature(&prefix_and_bitmap)?;
crate::checksum::verify_trailing(&prefix_and_bitmap)?;
let bitmap = prefix_and_bitmap[db_header_size..db_header_size + bitmap_size].to_vec();
let pages_start_addr = bitmap_addr + bitmap_size as u64 + 4;
let page_stride = page_size
.checked_mul(elem_size)
.and_then(|bytes| bytes.checked_add(4))
.ok_or(FormatError::OffsetOverflow {
offset: page_size as u64,
length: elem_size as u64,
})?;
for page in 0..npages {
let nelem_in_page = core::cmp::min(page_size, num_elements - page * page_size);
let initialized = (bitmap[page / 8] >> (7 - (page % 8))) & 1 == 1;
if !initialized {
continue;
}
let page_offset = page
.checked_mul(page_stride)
.ok_or(FormatError::OffsetOverflow {
offset: page as u64,
length: page_stride as u64,
})?;
let page_addr = pages_start_addr + page_offset as u64;
let region = source.read_metadata_at(
page_addr,
nelem_in_page
.checked_mul(elem_size)
.and_then(|elems| elems.checked_add(4))
.ok_or(FormatError::OffsetOverflow {
offset: nelem_in_page as u64,
length: elem_size as u64,
})?,
)?;
crate::checksum::verify_trailing(®ion)?;
for j in 0..nelem_in_page {
if let Some(info) = parse_fa_element(
®ion,
j * elem_size,
page * page_size + j,
header.client_id,
chunk_byte_size,
elem_size,
chunk_size_bytes,
offset_size,
grid,
)? {
chunks.push(info);
}
}
}
Ok(chunks)
}
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)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::checksum::stamp_trailing as stamp;
const fn fahd_len(os: u8, ls: u8) -> usize {
8 + ls as usize + os as usize + 4
}
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()
}
#[test]
fn read_variable_length_values() {
assert_eq!(read_variable_length(&[0x78, 0x56], 2).unwrap(), 0x5678);
assert_eq!(
read_variable_length(&[0x01, 0x02, 0x03, 0x04], 4).unwrap(),
0x04030201
);
assert_eq!(read_variable_length(&[0xFF], 1).unwrap(), 0xFF);
}
#[test]
fn a_chunk_at_a_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, 3], 4).unwrap();
let fa = crate::chunked_write::build_fixed_array_at(&slots, 8, 8, 8, false, 0);
let grid = ChunkGrid::new(
&[2],
&[4],
Some(&[8]),
crate::chunk_grid::GridOrder::RowMajor,
)
.unwrap();
let header = FixedArrayHeader::parse(&fa, 0, 8, 8).unwrap();
assert_eq!(header.num_elements, 4);
let read = read_fixed_array_chunks(&fa, &header, &grid, &[2], 4, 8, 8).unwrap();
assert_eq!(
read.iter().map(|c| c.address).collect::<Vec<_>>(),
vec![0x1000, 0x2000],
"the slot-3 chunk lies past the dataset's four elements"
);
assert_eq!(read[1].offsets, vec![2]);
}
#[test]
fn parse_fixed_array_header_valid() {
let mut buf = vec![0u8; 256];
buf[0..4].copy_from_slice(b"FAHD");
buf[4] = 0; buf[5] = 1; buf[6] = 16; buf[7] = 10; buf[8..16].copy_from_slice(&5u64.to_le_bytes());
buf[16..24].copy_from_slice(&0x1000u64.to_le_bytes());
stamp(&mut buf, 0, fahd_len(8, 8));
let header = FixedArrayHeader::parse(&buf, 0, 8, 8).unwrap();
assert_eq!(header.client_id, 1);
assert_eq!(header.element_size, 16);
assert_eq!(header.max_nelmts_bits, 10);
assert_eq!(header.num_elements, 5);
assert_eq!(header.data_block_address, 0x1000);
}
#[test]
fn parse_fixed_array_header_invalid_signature() {
let mut buf = vec![0u8; 256];
buf[0..4].copy_from_slice(b"XXXX");
let result = FixedArrayHeader::parse(&buf, 0, 8, 8);
assert!(result.is_err());
}
#[test]
fn parse_fixed_array_header_invalid_version() {
let mut buf = vec![0u8; 256];
buf[0..4].copy_from_slice(b"FAHD");
buf[4] = 1; let result = FixedArrayHeader::parse(&buf, 0, 8, 8);
assert!(result.is_err());
}
#[test]
fn read_non_filtered_chunks() {
let offset_size: u8 = 8;
let length_size: u8 = 8;
let os = offset_size as usize;
let num_chunks = 5u64;
let mut file_data = vec![0u8; 0x3000];
let fahd_offset = 0x100usize;
let db_offset = 0x200usize;
file_data[fahd_offset..fahd_offset + 4].copy_from_slice(b"FAHD");
file_data[fahd_offset + 4] = 0; file_data[fahd_offset + 5] = 0; file_data[fahd_offset + 6] = os as u8; file_data[fahd_offset + 7] = 10; file_data[fahd_offset + 8..fahd_offset + 16].copy_from_slice(&num_chunks.to_le_bytes());
file_data[fahd_offset + 16..fahd_offset + 24]
.copy_from_slice(&(db_offset as u64).to_le_bytes());
stamp(
&mut file_data,
fahd_offset,
fahd_len(offset_size, length_size),
);
file_data[db_offset..db_offset + 4].copy_from_slice(b"FADB");
file_data[db_offset + 4] = 0; file_data[db_offset + 5] = 0; file_data[db_offset + 6..db_offset + 14]
.copy_from_slice(&(fahd_offset as u64).to_le_bytes());
let elem_start = db_offset + 6 + os;
let base_addr = 0x1000u64;
let chunk_byte_size = 20 * 8; for i in 0..5 {
let addr = base_addr + i as u64 * chunk_byte_size as u64;
let pos = elem_start + i * os;
file_data[pos..pos + os].copy_from_slice(&addr.to_le_bytes());
}
stamp(
&mut file_data,
db_offset,
(6 + os) + num_chunks as usize * os + 4,
);
let header =
FixedArrayHeader::parse(&file_data, fahd_offset, offset_size, length_size).unwrap();
let ds_dims = vec![100u64];
let chunk_dims = vec![20u32];
let chunks = read_fixed_array_chunks(
&file_data,
&header,
&dense_grid(&ds_dims, &chunk_dims),
&chunk_dims,
8,
offset_size,
length_size,
)
.unwrap();
assert_eq!(chunks.len(), 5);
for (i, c) in chunks.iter().enumerate() {
assert_eq!(c.address, base_addr + i as u64 * chunk_byte_size as u64);
assert_eq!(c.offsets, vec![i as u64 * 20]);
assert_eq!(c.filter_mask, 0);
assert_eq!(c.chunk_size, chunk_byte_size as u32);
}
#[cfg(feature = "std")]
assert_fa_streams_match(&file_data, fahd_offset, &ds_dims, &chunk_dims, 8, 8, 8);
}
#[cfg(feature = "std")]
fn assert_fa_streams_match(
file_data: &[u8],
header_offset: usize,
ds_dims: &[u64],
chunk_dims: &[u32],
element_size: u32,
offset_size: u8,
length_size: u8,
) {
use crate::source::{BytesSource, ReadSeekSource};
let h =
FixedArrayHeader::parse(file_data, header_offset, offset_size, length_size).unwrap();
let buffered = read_fixed_array_chunks(
file_data,
&h,
&dense_grid(ds_dims, chunk_dims),
chunk_dims,
element_size,
offset_size,
length_size,
)
.unwrap();
let mem = BytesSource::new(file_data);
let hm = FixedArrayHeader::parse_from_source(
&mem,
header_offset as u64,
offset_size,
length_size,
)
.unwrap();
let from_mem = read_fixed_array_chunks_from_source(
&mem,
&hm,
&dense_grid(ds_dims, chunk_dims),
chunk_dims,
element_size,
offset_size,
length_size,
)
.unwrap();
let seek = ReadSeekSource::new(std::io::Cursor::new(file_data.to_vec())).unwrap();
let hs = FixedArrayHeader::parse_from_source(
&seek,
header_offset as u64,
offset_size,
length_size,
)
.unwrap();
let from_seek = read_fixed_array_chunks_from_source(
&seek,
&hs,
&dense_grid(ds_dims, chunk_dims),
chunk_dims,
element_size,
offset_size,
length_size,
)
.unwrap();
assert_eq!(buffered, from_mem, "BytesSource mismatch");
assert_eq!(buffered, from_seek, "ReadSeekSource mismatch");
}
#[test]
fn read_filtered_chunks() {
let offset_size: u8 = 8;
let length_size: u8 = 8;
let os = offset_size as usize;
let num_chunks = 3u64;
let chunk_size_bytes = 4usize;
let elem_size = os + chunk_size_bytes + 4;
let mut file_data = vec![0u8; 0x3000];
let fahd_offset = 0x100usize;
let db_offset = 0x200usize;
file_data[fahd_offset..fahd_offset + 4].copy_from_slice(b"FAHD");
file_data[fahd_offset + 4] = 0;
file_data[fahd_offset + 5] = 1; file_data[fahd_offset + 6] = elem_size as u8;
file_data[fahd_offset + 7] = 10;
file_data[fahd_offset + 8..fahd_offset + 16].copy_from_slice(&num_chunks.to_le_bytes());
file_data[fahd_offset + 16..fahd_offset + 24]
.copy_from_slice(&(db_offset as u64).to_le_bytes());
stamp(
&mut file_data,
fahd_offset,
fahd_len(offset_size, length_size),
);
file_data[db_offset..db_offset + 4].copy_from_slice(b"FADB");
file_data[db_offset + 4] = 0;
file_data[db_offset + 5] = 1;
file_data[db_offset + 6..db_offset + 14]
.copy_from_slice(&(fahd_offset as u64).to_le_bytes());
let elem_start = db_offset + 6 + os;
let test_chunks = [
(0x1000u64, 120u32, 0u32),
(0x2000u64, 115u32, 0u32),
(0x3000u64, 100u32, 0u32),
];
for (i, &(addr, csize, fmask)) in test_chunks.iter().enumerate() {
let pos = elem_start + i * elem_size;
file_data[pos..pos + os].copy_from_slice(&addr.to_le_bytes());
file_data[pos + os..pos + os + 4].copy_from_slice(&csize.to_le_bytes());
file_data[pos + os + 4..pos + os + 8].copy_from_slice(&fmask.to_le_bytes());
}
stamp(
&mut file_data,
db_offset,
(6 + os) + num_chunks as usize * elem_size + 4,
);
let header =
FixedArrayHeader::parse(&file_data, fahd_offset, offset_size, length_size).unwrap();
let ds_dims = vec![60u64];
let chunk_dims = vec![20u32];
let chunks = read_fixed_array_chunks(
&file_data,
&header,
&dense_grid(&ds_dims, &chunk_dims),
&chunk_dims,
8,
offset_size,
length_size,
)
.unwrap();
assert_eq!(chunks.len(), 3);
assert_eq!(chunks[0].address, 0x1000);
assert_eq!(chunks[0].chunk_size, 120);
assert_eq!(chunks[0].filter_mask, 0);
assert_eq!(chunks[0].offsets, vec![0]);
assert_eq!(chunks[1].address, 0x2000);
assert_eq!(chunks[1].chunk_size, 115);
assert_eq!(chunks[2].address, 0x3000);
assert_eq!(chunks[2].chunk_size, 100);
#[cfg(feature = "std")]
assert_fa_streams_match(&file_data, fahd_offset, &ds_dims, &chunk_dims, 8, 8, 8);
}
fn read_paged(bitmap: u8) -> Vec<ChunkInfo> {
let offset_size: u8 = 8;
let length_size: u8 = 8;
let os = offset_size as usize;
let num_chunks = 3u64;
let mut file_data = vec![0u8; 0x400];
let fahd_offset = 0x100usize;
let db_offset = 0x200usize;
file_data[fahd_offset..fahd_offset + 4].copy_from_slice(b"FAHD");
file_data[fahd_offset + 4] = 0; file_data[fahd_offset + 5] = 0; file_data[fahd_offset + 6] = os as u8; file_data[fahd_offset + 7] = 1; file_data[fahd_offset + 8..fahd_offset + 16].copy_from_slice(&num_chunks.to_le_bytes());
file_data[fahd_offset + 16..fahd_offset + 24]
.copy_from_slice(&(db_offset as u64).to_le_bytes());
stamp(
&mut file_data,
fahd_offset,
fahd_len(offset_size, length_size),
);
file_data[db_offset..db_offset + 4].copy_from_slice(b"FADB");
file_data[db_offset + 4] = 0; file_data[db_offset + 5] = 0; file_data[db_offset + 6..db_offset + 14]
.copy_from_slice(&(fahd_offset as u64).to_le_bytes());
file_data[db_offset + 14] = bitmap; stamp(&mut file_data, db_offset, 6 + os + 1 + 4);
let pages_start = db_offset + 14 + 1 + 4;
let stride = 2 * os + 4;
let addrs = [0x1000u64, 0x2000, 0x3000];
for (i, &addr) in addrs.iter().enumerate() {
let page = i / 2;
let j = i % 2;
let pos = pages_start + page * stride + j * os;
file_data[pos..pos + os].copy_from_slice(&addr.to_le_bytes());
}
stamp(&mut file_data, pages_start, 2 * os + 4);
stamp(&mut file_data, pages_start + stride, os + 4);
let header =
FixedArrayHeader::parse(&file_data, fahd_offset, offset_size, length_size).unwrap();
assert_eq!(header.num_elements, 3);
let buffered = read_fixed_array_chunks(
&file_data,
&header,
&dense_grid(&[3], &[1]),
&[1],
8,
offset_size,
length_size,
)
.unwrap();
#[cfg(feature = "std")]
assert_fa_streams_match(
&file_data,
fahd_offset,
&[3],
&[1],
8,
offset_size,
length_size,
);
buffered
}
#[test]
fn read_paged_all_pages_initialized() {
let chunks = read_paged(0b1100_0000);
assert_eq!(chunks.len(), 3);
assert_eq!(chunks[0].address, 0x1000);
assert_eq!(chunks[0].offsets, vec![0]);
assert_eq!(chunks[1].address, 0x2000);
assert_eq!(chunks[1].offsets, vec![1]);
assert_eq!(chunks[2].address, 0x3000);
assert_eq!(chunks[2].offsets, vec![2]);
}
#[test]
fn read_paged_skips_uninitialized_page() {
let chunks = read_paged(0b1000_0000);
assert_eq!(chunks.len(), 2);
assert_eq!(chunks[0].address, 0x1000);
assert_eq!(chunks[1].address, 0x2000);
}
#[cfg(all(feature = "std", feature = "checksum"))]
#[test]
fn a_corrupted_fixed_array_structure_is_refused() {
use crate::chunked_write::{WrittenChunk, build_fixed_array_at};
use crate::source::BytesSource;
let os: u8 = 8;
let ls: u8 = 8;
let base = 0x800u64;
for has_filters in [false, true] {
for &n in &[5u64, 1024, 1025, 3000] {
let chunks: Vec<WrittenChunk> = (0..n)
.map(|i| WrittenChunk {
address: 0x100000 + i * 8,
compressed_size: if has_filters { 8 + (i % 7) } else { 8 },
filter_mask: 0,
})
.collect();
let fa = build_fixed_array_at(
&crate::chunked_write::IndexSlots::dense(&chunks),
8,
os,
ls,
has_filters,
base,
);
let mut file = vec![0u8; base as usize + fa.len()];
file[base as usize..].copy_from_slice(&fa);
let chunk_dims = vec![1u32];
let read_both = |file: &[u8]| -> (Result<Vec<ChunkInfo>, FormatError>, bool) {
let grid = dense_grid(&[n], &chunk_dims);
let buffered =
FixedArrayHeader::parse(file, base as usize, os, ls).and_then(|h| {
read_fixed_array_chunks(file, &h, &grid, &chunk_dims, 8, os, ls)
});
let mem = BytesSource::new(file);
let streamed = FixedArrayHeader::parse_from_source(&mem, base, os, ls)
.and_then(|h| {
read_fixed_array_chunks_from_source(
&mem,
&h,
&grid,
&chunk_dims,
8,
os,
ls,
)
});
(buffered, streamed.is_err())
};
let sound = read_both(&file).0.expect("the sound file must read");
assert_eq!(
sound.len() as u64,
n,
"the fixture must read before it is corrupted (filters={has_filters}, n={n})"
);
let spans =
fixed_array_index_spans(&BytesSource::new(&file), base, os, ls).unwrap();
assert_eq!(spans.len(), 2, "FA index = FAHD + FADB");
let header = FixedArrayHeader::parse(&file, base as usize, os, ls).unwrap();
let elem_size = if has_filters {
header.element_size as usize
} else {
os as usize
};
let page_size = 1usize << header.max_nelmts_bits;
let db_prefix = 4 + 1 + 1 + os as usize;
let mut poke_sites: Vec<(u64, bool)> = spans
.iter()
.enumerate()
.map(|(i, &(at, len))| (at + len - 1, i == 0))
.collect();
if n as usize > page_size {
let (db_at, _) = spans[1];
let bitmap = (n as usize).div_ceil(page_size).div_ceil(8);
poke_sites.push((db_at + (db_prefix + bitmap + 4) as u64 - 1, false));
poke_sites.push((
db_at + (db_prefix + bitmap + 4 + page_size * elem_size + 4) as u64 - 1,
false,
));
}
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 { .. })),
"filters={has_filters}, n={n}: a corrupted checksum at {at:#x} must be \
refused, got {buffered:?}"
);
assert!(
streamed_err,
"filters={has_filters}, n={n}: the streaming backend must refuse what the \
buffered one does, at {at:#x}"
);
if walked {
let walk = fixed_array_index_spans(&BytesSource::new(&file), base, os, ls);
assert!(
matches!(walk, Err(FormatError::ChecksumMismatch { .. })),
"filters={has_filters}, n={n}: the reclaim walk must refuse a corrupt \
header at {at:#x} rather than release spans read out of it, got \
{walk:?}"
);
}
file[at] = original;
}
}
}
}
#[cfg(feature = "std")]
#[test]
fn index_spans_tile_fixed_array_blob() {
use crate::chunked_write::{WrittenChunk, build_fixed_array_at};
let os: u8 = 8;
let ls: u8 = 8;
let base = 0x800u64;
for has_filters in [false, true] {
for &n in &[1u64, 5, 1024, 1025, 3000] {
let chunks: Vec<WrittenChunk> = (0..n)
.map(|i| WrittenChunk {
address: 0x100000 + i * 8,
compressed_size: if has_filters { 8 + (i % 7) } else { 8 },
filter_mask: 0,
})
.collect();
let fa = build_fixed_array_at(
&crate::chunked_write::IndexSlots::dense(&chunks),
8,
os,
ls,
has_filters,
base,
);
let mut file = vec![0u8; base as usize + fa.len()];
file[base as usize..].copy_from_slice(&fa);
let spans =
fixed_array_index_spans(&crate::source::BytesSource::new(&file), base, os, ls)
.unwrap();
assert_eq!(
spans.len(),
2,
"FA index = FAHD + FADB (filters={has_filters}, n={n})"
);
let mut sorted = spans.clone();
sorted.sort_by_key(|&(a, _)| a);
assert_eq!(sorted[0].0, base);
assert_eq!(
sorted[0].0 + sorted[0].1,
sorted[1].0,
"FAHD must abut FADB (filters={has_filters}, n={n})"
);
assert_eq!(
sorted[1].0 + sorted[1].1,
base + fa.len() as u64,
"FA index spans must tile the blob (filters={has_filters}, n={n})"
);
}
}
}
}