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 cd828725c7 - Show all commits
+128 -35
View File
@@ -7,6 +7,7 @@ use byteorder::{ByteOrder, LittleEndian};
use crate::error::FormatError;
use crate::message_type::MessageType;
use crate::storage::{Storage, len_usize, read_exact_at, read_upto};
/// OHDR signature for v2 object headers.
const OHDR_SIGNATURE: [u8; 4] = *b"OHDR";
@@ -118,32 +119,45 @@ impl ObjectHeader {
offset_size: u8,
length_size: u8,
) -> Result<ObjectHeader, FormatError> {
ensure_len(data, offset, 4)?;
if data[offset..offset + 4] == OHDR_SIGNATURE {
Self::parse_v2(data, offset, offset_size, length_size)
Self::parse_in(&data, offset as u64, offset_size, length_size)
}
/// [`Self::parse`] over any [`Storage`].
///
/// Reads the signature, the prefix (at most [`V2_PREFIX_MAX`] bytes),
/// then each chunk as one bounded read, continuation chunks included.
pub fn parse_in(
file: &dyn Storage,
offset: u64,
offset_size: u8,
length_size: u8,
) -> Result<ObjectHeader, FormatError> {
let sig = read_exact_at(file, offset, 4)?;
if *sig == OHDR_SIGNATURE {
Self::parse_v2(file, offset, offset_size, length_size)
} else {
Self::parse_v1(data, offset, offset_size, length_size)
Self::parse_v1(file, offset, offset_size, length_size)
}
}
fn parse_v1(
data: &[u8],
offset: usize,
file: &dyn Storage,
offset: u64,
offset_size: u8,
length_size: u8,
) -> Result<ObjectHeader, FormatError> {
// version(1) + reserved(1) + num_messages(2) + ref_count(4) + header_size(4) = 12
// then pad to 8-byte alignment from start of header
ensure_len(data, offset, 12)?;
let prefix = read_exact_at(file, offset, 12)?;
let version = data[offset];
let version = prefix[0];
if version != 1 {
return Err(FormatError::InvalidObjectHeaderVersion(version));
}
let num_messages = LittleEndian::read_u16(&data[offset + 2..offset + 4]) as usize;
let reference_count = LittleEndian::read_u32(&data[offset + 4..offset + 8]);
let header_data_size = LittleEndian::read_u32(&data[offset + 8..offset + 12]) as usize;
let num_messages = LittleEndian::read_u16(&prefix[2..4]) as usize;
let reference_count = LittleEndian::read_u32(&prefix[4..8]);
let header_data_size = LittleEndian::read_u32(&prefix[8..12]) as usize;
// libhdf5 (H5O__prefix_deserialize): a header with messages needs room
// for at least one message header, and one without has an empty chunk.
@@ -161,14 +175,13 @@ impl ObjectHeader {
.checked_add(12 + padding)
.ok_or(FormatError::UnexpectedEof {
expected: usize::MAX,
available: data.len(),
available: len_usize(file),
})?;
ensure_len(data, msg_start, header_data_size)?;
// parse_v1_chunk reads the chunk, with the bounds check that was here.
let mut messages = Vec::new();
let chunk0_count = Self::parse_v1_chunk(
data,
file,
msg_start,
header_data_size,
offset_size,
@@ -209,8 +222,8 @@ impl ObjectHeader {
/// "gap", which only version 2 allows).
#[allow(clippy::too_many_arguments)]
fn parse_v1_chunk(
data: &[u8],
offset: usize,
file: &dyn Storage,
offset: u64,
length: usize,
offset_size: u8,
length_size: u8,
@@ -220,9 +233,10 @@ impl ObjectHeader {
if depth_remaining == 0 {
return Err(FormatError::NestingDepthExceeded);
}
ensure_len(data, offset, length)?;
let end = offset + length;
let mut pos = offset;
let chunk = read_exact_at(file, offset, length)?;
let data: &[u8] = &chunk;
let end = length;
let mut pos = 0usize;
let mut count = 0usize;
while pos < end {
@@ -267,8 +281,8 @@ impl ObjectHeader {
let cont_offset = read_offset(body, 0, offset_size)? as usize;
let cont_length = read_offset(body, offset_size as usize, length_size)? as usize;
Self::parse_v1_chunk(
data,
cont_offset,
file,
cont_offset as u64,
cont_length,
offset_size,
length_size,
@@ -282,11 +296,31 @@ impl ObjectHeader {
}
fn parse_v2(
data: &[u8],
offset: usize,
file: &dyn Storage,
offset: u64,
offset_size: u8,
length_size: u8,
) -> Result<ObjectHeader, FormatError> {
// The prefix, read as one window. The window holds the whole prefix
// or ends at the end of the file, so a position past the window is
// past the end of the file: `ensure_len` checks positions relative
// to the header against it and reports them as the whole-file check
// did, with absolute positions and the file's length.
let window = read_upto(file, offset, V2_PREFIX_MAX)?;
let data: &[u8] = &window;
let file_len = len_usize(file);
let base = usize::try_from(offset).unwrap_or(usize::MAX);
let abs = |rel: usize| base.saturating_add(rel);
let ensure_len = |_: &[u8], rel: usize, needed: usize| -> Result<(), FormatError> {
match rel.checked_add(needed) {
Some(end) if end <= data.len() => Ok(()),
_ => Err(FormatError::UnexpectedEof {
expected: abs(rel).saturating_add(needed),
available: file_len,
}),
}
};
let offset = 0usize;
// signature(4) + version(1) + flags(1) = 6
ensure_len(data, offset, 6)?;
@@ -351,15 +385,20 @@ impl ObjectHeader {
}
let chunk0_msg_start = pos;
let chunk0_msg_end = pos
let chunk0_msg_end = abs(pos)
.checked_add(chunk0_size)
.ok_or(FormatError::UnexpectedEof {
expected: usize::MAX,
available: data.len(),
})?;
available: file_len,
})?
- base;
// The whole first chunk, prefix to checksum, in one read (its
// bounds check is the one on the checksum's 4 bytes).
let chunk0 = read_exact_at(file, base as u64, chunk0_msg_end.saturating_add(4))?;
let data: &[u8] = &chunk0;
// Validate checksum: from OHDR signature through all messages (before checksum)
ensure_len(data, chunk0_msg_end, 4)?;
#[cfg(feature = "checksum")]
{
let stored = LittleEndian::read_u32(&data[chunk0_msg_end..chunk0_msg_end + 4]);
@@ -394,8 +433,8 @@ impl ObjectHeader {
}
cont_remaining -= 1;
Self::parse_v2_continuation(
data,
cont_offset,
file,
cont_offset as u64,
cont_length,
has_creation_order,
offset_size,
@@ -495,8 +534,8 @@ impl ObjectHeader {
#[allow(clippy::too_many_arguments)]
fn parse_v2_continuation(
data: &[u8],
offset: usize,
file: &dyn Storage,
offset: u64,
length: usize,
has_creation_order: bool,
offset_size: u8,
@@ -505,7 +544,9 @@ impl ObjectHeader {
continuations: &mut Vec<(usize, usize)>,
) -> Result<(), FormatError> {
// OCHK signature(4) + messages + checksum(4)
ensure_len(data, offset, length)?;
let chunk = read_exact_at(file, offset, length)?;
let data: &[u8] = &chunk;
let offset = 0usize;
if length < 8 {
return Err(FormatError::UnexpectedEof {
expected: 8,
@@ -546,6 +587,10 @@ impl ObjectHeader {
}
}
/// Longest version-2 object header prefix: signature(4) + version(1) +
/// flags(1) + times(16) + attribute phase change(4) + chunk-0 size(8).
const V2_PREFIX_MAX: usize = 34;
/// Size of a version-1 message header: type(2) + size(2) + flags(1) + reserved(3).
const V1_MSG_HEADER_SIZE: usize = 8;
@@ -754,13 +799,13 @@ mod tests {
let mut msg_bytes = Vec::new();
for (mtype, mdata, mflags) in messages {
// v1 message sizes are multiples of 8 (the data is zero-padded).
let padded = mdata.len().div_ceil(8) * 8;
let padded = <[u8]>::len(mdata).div_ceil(8) * 8;
msg_bytes.extend_from_slice(&mtype.to_le_bytes()); // type(2)
msg_bytes.extend_from_slice(&(padded as u16).to_le_bytes()); // size(2)
msg_bytes.push(*mflags); // flags(1)
msg_bytes.extend_from_slice(&[0u8; 3]); // reserved(3)
msg_bytes.extend_from_slice(mdata); // data
msg_bytes.resize(msg_bytes.len() + padded - mdata.len(), 0);
msg_bytes.resize(msg_bytes.len() + padded - <[u8]>::len(mdata), 0);
}
let mut buf = Vec::new();
@@ -1280,4 +1325,52 @@ mod tests {
let err = ObjectHeader::parse(&data, 0, 8, 8).unwrap_err();
assert!(matches!(err, FormatError::UnexpectedEof { .. }));
}
/// Every header, and every truncation of it, parses to the same result
/// (or the same error) through a `read_at`-only storage as from a slice;
/// a header in one chunk takes three reads (signature, prefix, chunk).
#[test]
fn parse_in_matches_slice_parse() {
use crate::storage::CountingStorage;
let mut headers = vec![
build_v1_header(&[], 8, 8),
build_v1_header(&[(0x0001, &[1, 2, 3], 0), (0x0003, &[9; 8], 0)], 8, 8),
build_v2_header(0x00, &[(0x01, &[42], 0)], None),
build_v2_header(0x03, &[(0x01, &[1, 2], 0), (0x03, &[3], 0)], None),
build_v2_header(0x24, &[(0x01, &[1], 0)], Some((1, 2, 3, 4))),
build_v2_header(0x35, &[(0x01, &[1], 0)], Some((5, 6, 7, 8))),
];
// A v2 header with a continuation chunk at 256.
let mut ochk = OCHK_SIGNATURE.to_vec();
ochk.extend_from_slice(&[0x03, 2, 0, 0, 0xDE, 0xAD]);
let sum = crate::checksum::jenkins_lookup3(&ochk);
ochk.extend_from_slice(&sum.to_le_bytes());
let mut cont = 256u64.to_le_bytes().to_vec();
cont.extend_from_slice(&(ochk.len() as u64).to_le_bytes());
let main = build_v2_header(0x00, &[(0x01, &[42], 0), (0x10, &cont, 0)], None);
let mut with_cont = vec![0u8; 256 + ochk.len()];
with_cont[..main.len()].copy_from_slice(&main);
with_cont[256..].copy_from_slice(&ochk);
headers.push(with_cont);
for h in headers {
for at in [0usize, 3] {
for cut in 0..=h.len() {
let mut f = vec![0u8; at];
f.extend_from_slice(&h[..cut]);
if at == 0 && cut == h.len() {
f.resize(f.len() + 64, 0);
}
let want = ObjectHeader::parse(&f, at, 8, 8);
let storage = CountingStorage::new(f.clone());
let got = ObjectHeader::parse_in(&storage, at as u64, 8, 8);
assert_eq!(format!("{got:?}"), format!("{want:?}"), "at {at}, cut {cut}");
}
}
}
let one_chunk = build_v2_header(0x00, &[(0x01, &[42], 0)], None);
let storage = CountingStorage::new(one_chunk);
ObjectHeader::parse_in(&storage, 0, 8, 8).unwrap();
assert_eq!(storage.reads(), 3);
}
}