Range reads M0/M1 (indexed lookups, Storage trait), ZFP, in-place editing #17

Merged
osobh merged 52 commits from feat/p3-range-zfp-edit into main 2026-09-26 20:42:22 +00:00
Showing only changes of commit cf2b408a63 - Show all commits
+208 -72
View File
@@ -9,16 +9,19 @@ use alloc::{format, vec, vec::Vec};
use crate::chunk_grid::ChunkGrid;
use crate::chunked_read::ChunkInfo;
use crate::error::FormatError;
use crate::storage::{Storage, Window, len_usize, read_exact_at};
/// Verify the Jenkins lookup3 checksum stored immediately after
/// `data[start..end]`, as every Fixed Array structure carries one.
/// `data[start..end]`, as every Fixed Array structure carries one. `w` is
/// a window of the file and `start`/`end` are relative to it.
///
/// A corrupt chunk index silently yields addresses pointing at the wrong
/// bytes, so a mismatch has to be an error rather than a shrug: without this
/// the damage surfaces as plausible-looking data from the wrong chunk.
#[cfg(feature = "checksum")]
fn verify_checksum(data: &[u8], start: usize, end: usize) -> Result<(), FormatError> {
ensure_len(data, end, 4)?;
fn verify_checksum(w: &Window<'_>, start: usize, end: usize) -> Result<(), FormatError> {
w.ensure(end, 4)?;
let data = &w.bytes;
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]);
if computed != stored {
@@ -31,7 +34,7 @@ fn verify_checksum(data: &[u8], start: usize, end: usize) -> Result<(), FormatEr
}
#[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(())
}
@@ -73,19 +76,6 @@ fn read_length(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
read_offset(data, pos, size)
}
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(data: &[u8], pos: usize, size: u8) -> bool {
let s = size as usize;
if pos + s > data.len() {
@@ -101,13 +91,24 @@ impl FixedArrayHeader {
offset: usize,
offset_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> {
// FAHD signature(4) + version(1) + client_id(1) + element_size(1) +
// max_nelmts_bits(1) + num_elements(length_size) + data_block_addr(offset_size) + checksum(4)
let min_size = 4 + 1 + 1 + 1 + 1 + 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"FAHD" {
return Err(FormatError::ChunkedReadError(
"invalid Fixed Array header signature".into(),
@@ -130,7 +131,7 @@ impl FixedArrayHeader {
pos += length_size as usize;
let data_block_address = read_offset(d, pos, offset_size)?;
pos += offset_size as usize;
verify_checksum(file_data, offset, offset + pos)?;
verify_checksum(&w, 0, pos)?;
Ok(FixedArrayHeader {
client_id,
@@ -156,15 +157,39 @@ pub fn read_fixed_array_chunks(
chunk_dimensions: &[u32],
element_size: u32,
offset_size: u8,
length_size: u8,
) -> Result<Vec<ChunkInfo>, FormatError> {
read_fixed_array_chunks_in(
&file_data,
header,
dataset_dims,
max_dims,
chunk_dimensions,
element_size,
offset_size,
length_size,
)
}
/// [`read_fixed_array_chunks`] over any [`Storage`]: one read of the data
/// block's prefix, one of the whole data block (pages included).
#[allow(clippy::too_many_arguments)]
pub fn read_fixed_array_chunks_in(
file: &dyn Storage,
header: &FixedArrayHeader,
dataset_dims: &[u64],
max_dims: Option<&[u64]>,
chunk_dimensions: &[u32],
element_size: u32,
offset_size: u8,
_length_size: u8,
) -> Result<Vec<ChunkInfo>, FormatError> {
let file_len = len_usize(file);
let db_offset = header.data_block_address as usize;
// Parse data block header: FADB(4) + version(1) + client_id(1) + header_address(offset_size)
let db_header_size = 4 + 1 + 1 + offset_size as usize;
ensure_len(file_data, db_offset, db_header_size)?;
let d = &file_data[db_offset..];
let d = read_exact_at(file, db_offset as u64, db_header_size)?;
if &d[0..4] != b"FADB" {
return Err(FormatError::ChunkedReadError(
"invalid Fixed Array data block signature".into(),
@@ -178,7 +203,7 @@ pub fn read_fixed_array_chunks(
// A chunk index cannot describe more elements than the file has bytes (each
// element occupies at least `offset_size` bytes). Reject a corrupt count
// before it can drive a huge loop or overflow an offset computation.
if num_elements > file_data.len() {
if num_elements > file_len {
return Err(FormatError::ChunkedReadError(
"Fixed Array element count exceeds file size".into(),
));
@@ -208,30 +233,34 @@ pub fn read_fixed_array_chunks(
chunk_dimensions.iter().map(|&d| d as u64).product::<u64>() * element_size as u64;
let mut chunks = Vec::new();
let push_element =
|i: usize, abs: usize, chunks: &mut Vec<ChunkInfo>| -> Result<(), FormatError> {
if let Some((address, chunk_size, filter_mask)) = parse_fa_element(
file_data,
abs,
header.client_id,
offset_size,
header.element_size,
chunk_byte_size,
)? {
// A slot beyond the current extent is ignored, as the
// library does.
let Some(offsets) = grid.offsets(i as u64) else {
return Ok(());
};
chunks.push(ChunkInfo {
chunk_size,
filter_mask,
offsets,
address,
});
}
Ok(())
};
// `rel` is relative to the data block, whose bytes are in `w`.
let push_element = |w: &Window<'_>,
i: usize,
rel: usize,
chunks: &mut Vec<ChunkInfo>|
-> Result<(), FormatError> {
if let Some((address, chunk_size, filter_mask)) = parse_fa_element(
w,
rel,
header.client_id,
offset_size,
header.element_size,
chunk_byte_size,
)? {
// A slot beyond the current extent is ignored, as the
// library does.
let Some(offsets) = grid.offsets(i as u64) else {
return Ok(());
};
chunks.push(ChunkInfo {
chunk_size,
filter_mask,
offsets,
address,
});
}
Ok(())
};
// A data block is paged when it holds more elements than fit in one page.
// `max_nelmts_bits` is an untrusted u8; a shift >= the pointer width would
@@ -246,10 +275,13 @@ pub fn read_fixed_array_chunks(
if !is_paged {
// Non-paged: prefix, then `num_elements` elements packed directly,
// then a checksum over both.
verify_checksum(file_data, db_offset, elem_at(elements_start, num_elements)?)?;
// then a checksum over both. One window holds all of it (or ends at
// the end of the file), so its bounds checks are the whole-file ones.
let end = elem_at(elements_start, num_elements)?;
let w = Window::read(file, db_offset as u64, end.saturating_add(4) - db_offset)?;
verify_checksum(&w, 0, end - db_offset)?;
for i in 0..num_elements {
push_element(i, elem_at(elements_start, i)?, &mut chunks)?;
push_element(&w, i, elem_at(elements_start, i)? - db_offset, &mut chunks)?;
}
return Ok(chunks);
}
@@ -272,22 +304,27 @@ pub fn read_fixed_array_chunks(
.and_then(|x| x.checked_add(4))
.ok_or_else(stride_overflow)?;
if bitmap_start + bitmap_size > file_data.len() {
if bitmap_start + bitmap_size > file_len {
return Err(FormatError::UnexpectedEof {
expected: bitmap_start + bitmap_size,
available: file_data.len(),
available: file_len,
});
}
// The whole data block in one window: every page slot is at most
// `page_stride` bytes, so every position checked below lies inside it
// (or past the end of the file).
let block_len = (pages_start - db_offset).saturating_add(npages.saturating_mul(page_stride));
let w = Window::read(file, db_offset as u64, block_len)?;
// The prefix and page bitmap are covered by their own checksum, and each
// initialised page by one of its own.
verify_checksum(file_data, db_offset, bitmap_start + bitmap_size)?;
verify_checksum(&w, 0, bitmap_start + bitmap_size - db_offset)?;
for p in 0..npages {
let page_first = p * page_nelmts; // < num_elements, cannot overflow
let page_count = core::cmp::min(page_nelmts, num_elements - page_first);
// Check the page-init bit (MSB-first within each byte).
let bit_byte = file_data[bitmap_start + p / 8];
let bit_byte = w.bytes[bitmap_start + p / 8 - db_offset];
let bit_mask = 1u8 << (7 - (p % 8));
if bit_byte & bit_mask == 0 {
continue; // entire page unallocated
@@ -297,21 +334,30 @@ pub fn read_fixed_array_chunks(
.checked_mul(page_stride)
.and_then(|o| pages_start.checked_add(o))
.ok_or_else(stride_overflow)?;
verify_checksum(file_data, page_off, elem_at(page_off, page_count)?)?;
verify_checksum(
&w,
page_off - db_offset,
elem_at(page_off, page_count)? - db_offset,
)?;
for e in 0..page_count {
push_element(page_first + e, elem_at(page_off, e)?, &mut chunks)?;
push_element(
&w,
page_first + e,
elem_at(page_off, e)? - db_offset,
&mut chunks,
)?;
}
}
Ok(chunks)
}
/// Parse a single Fixed Array element at absolute file offset `abs`.
/// Parse a single Fixed Array element at offset `abs` of the window `w`.
///
/// Returns `Some((address, chunk_size, filter_mask))` for an allocated chunk, or
/// `None` if the element is undefined (an unallocated chunk, address all-`0xFF`).
fn parse_fa_element(
file_data: &[u8],
w: &Window<'_>,
abs: usize,
client_id: u8,
offset_size: u8,
@@ -321,12 +367,8 @@ fn parse_fa_element(
let os = offset_size as usize;
if client_id == 0 {
// Non-filtered: element is just the chunk address.
if abs + os > file_data.len() {
return Err(FormatError::UnexpectedEof {
expected: abs + os,
available: file_data.len(),
});
}
w.ensure(abs, os)?;
let file_data: &[u8] = &w.bytes;
if is_undefined(file_data, abs, offset_size) {
return Ok(None);
}
@@ -341,17 +383,14 @@ fn parse_fa_element(
));
}
let chunk_size_bytes = es - os - 4;
if abs + es > file_data.len() {
return Err(FormatError::UnexpectedEof {
expected: abs + es,
available: file_data.len(),
});
}
w.ensure(abs, es)?;
let file_data: &[u8] = &w.bytes;
if is_undefined(file_data, abs, offset_size) {
return Ok(None);
}
let address = read_offset(file_data, abs, offset_size)?;
let chunk_size = read_variable_length(&file_data[abs + os..], chunk_size_bytes)?;
let chunk_size =
read_variable_length(&file_data[abs + os..abs + es - 4], chunk_size_bytes)?;
let fm_off = abs + os + chunk_size_bytes;
let filter_mask = u32::from_le_bytes([
file_data[fm_off],
@@ -813,4 +852,101 @@ mod tests {
.collect();
assert_eq!(got, expect);
}
/// A fixed array (header at 0x100, data block at 0x200) of `n` chunks,
/// filtered or not, paged when `n` exceeds `1 << page_bits`; every
/// page initialised except page 1.
fn build_fixed_array(n: usize, filtered: bool, page_bits: u8) -> Vec<u8> {
let os = 8usize;
let es = if filtered { os + 4 + 4 } else { os };
let (fahd, db) = (0x100usize, 0x200usize);
let mut f = vec![0u8; 0x2000];
f[fahd..fahd + 4].copy_from_slice(b"FAHD");
f[fahd + 5] = u8::from(filtered);
f[fahd + 6] = es as u8;
f[fahd + 7] = page_bits;
f[fahd + 8..fahd + 16].copy_from_slice(&(n as u64).to_le_bytes());
f[fahd + 16..fahd + 24].copy_from_slice(&(db as u64).to_le_bytes());
stamp_checksum(&mut f, fahd, fahd + 24);
f[db..db + 4].copy_from_slice(b"FADB");
f[db + 5] = u8::from(filtered);
f[db + 6..db + 14].copy_from_slice(&(fahd as u64).to_le_bytes());
let elems = db + 6 + os;
let write = |f: &mut Vec<u8>, at: usize, i: usize| {
let addr = if i == 2 {
u64::MAX
} else {
0x1000 + i as u64 * 0x100
};
f[at..at + os].copy_from_slice(&addr.to_le_bytes());
if filtered {
f[at + os..at + os + 4].copy_from_slice(&(100 + i as u32).to_le_bytes());
f[at + os + 4..at + os + 8].copy_from_slice(&(i as u32 & 1).to_le_bytes());
}
};
let page = 1usize << page_bits;
if n <= page {
for i in 0..n {
write(&mut f, elems + i * es, i);
}
stamp_checksum(&mut f, db, elems + n * es);
} else {
let npages = n.div_ceil(page);
let bitmap = npages.div_ceil(8);
for p in 0..npages {
if p != 1 {
f[elems + p / 8] |= 0x80 >> (p % 8);
}
}
stamp_checksum(&mut f, db, elems + bitmap);
let pages_start = elems + bitmap + 4;
for p in (0..npages).filter(|&p| p != 1) {
let at = pages_start + p * (page * es + 4);
let count = page.min(n - p * page);
for e in 0..count {
write(&mut f, at + e * es, p * page + e);
}
stamp_checksum(&mut f, at, at + count * es);
}
}
f
}
/// Non-paged and paged, filtered and unfiltered arrays, cut at every
/// length through the data block and with a damaged byte, read
/// identically through a `read_at`-only storage.
#[test]
fn storage_reads_match_slice_reads() {
use crate::storage::CountingStorage;
for (n, filtered, bits) in [(3, false, 10), (3, true, 10), (11, false, 2), (11, true, 2)] {
let full = build_fixed_array(n, filtered, bits);
let es = if filtered { 16 } else { 8 };
let dims = [n as u64 * 20];
let h = FixedArrayHeader::parse(&full, 0x100, 8, 8).unwrap();
let chunks = read_fixed_array_chunks(&full, &h, &dims, None, &[20], 8, 8, 8).unwrap();
// Chunk 2 is unallocated, and so is page 1 of a paged array.
let expect = if n > 4 { n - 1 - 4 } else { n - 1 };
assert_eq!(chunks.len(), expect);
let mut files = Vec::new();
for cut in (0x100..0x200 + 40 + n * (es + 4) + 16).step_by(3) {
files.push(full[..cut].to_vec());
}
for at in [0x104, 0x210, 0x21a, 0x230] {
let mut damaged = full.clone();
damaged[at] ^= 1;
files.push(damaged);
}
files.push(full);
for f in files {
let storage = CountingStorage::new(f.clone());
let want = FixedArrayHeader::parse(&f, 0x100, 8, 8);
let got = FixedArrayHeader::parse_in(&storage, 0x100, 8, 8);
assert_eq!(format!("{got:?}"), format!("{want:?}"));
let Ok(h) = want else { continue };
let want = read_fixed_array_chunks(&f, &h, &dims, None, &[20], 8, 8, 8);
let got = read_fixed_array_chunks_in(&storage, &h, &dims, None, &[20], 8, 8, 8);
assert_eq!(format!("{got:?}"), format!("{want:?}"), "{} bytes", f.len());
}
}
}
}