diff --git a/crates/clawhdf5-format/src/object_header.rs b/crates/clawhdf5-format/src/object_header.rs index 1fc8696..d0f36db 100644 --- a/crates/clawhdf5-format/src/object_header.rs +++ b/crates/clawhdf5-format/src/object_header.rs @@ -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 { - 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 { + 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 { // 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 { + // 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); + } }