Range reads M0/M1 (indexed lookups, Storage trait), ZFP, in-place editing #17
@@ -7,6 +7,7 @@ use byteorder::{ByteOrder, LittleEndian};
|
|||||||
|
|
||||||
use crate::error::FormatError;
|
use crate::error::FormatError;
|
||||||
use crate::message_type::MessageType;
|
use crate::message_type::MessageType;
|
||||||
|
use crate::storage::{Storage, len_usize, read_exact_at, read_upto};
|
||||||
|
|
||||||
/// OHDR signature for v2 object headers.
|
/// OHDR signature for v2 object headers.
|
||||||
const OHDR_SIGNATURE: [u8; 4] = *b"OHDR";
|
const OHDR_SIGNATURE: [u8; 4] = *b"OHDR";
|
||||||
@@ -118,32 +119,45 @@ impl ObjectHeader {
|
|||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<ObjectHeader, FormatError> {
|
) -> Result<ObjectHeader, FormatError> {
|
||||||
ensure_len(data, offset, 4)?;
|
Self::parse_in(&data, offset as u64, offset_size, length_size)
|
||||||
if data[offset..offset + 4] == OHDR_SIGNATURE {
|
}
|
||||||
Self::parse_v2(data, offset, 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 {
|
} else {
|
||||||
Self::parse_v1(data, offset, offset_size, length_size)
|
Self::parse_v1(file, offset, offset_size, length_size)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn parse_v1(
|
fn parse_v1(
|
||||||
data: &[u8],
|
file: &dyn Storage,
|
||||||
offset: usize,
|
offset: u64,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<ObjectHeader, FormatError> {
|
) -> Result<ObjectHeader, FormatError> {
|
||||||
// version(1) + reserved(1) + num_messages(2) + ref_count(4) + header_size(4) = 12
|
// version(1) + reserved(1) + num_messages(2) + ref_count(4) + header_size(4) = 12
|
||||||
// then pad to 8-byte alignment from start of header
|
// 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 {
|
if version != 1 {
|
||||||
return Err(FormatError::InvalidObjectHeaderVersion(version));
|
return Err(FormatError::InvalidObjectHeaderVersion(version));
|
||||||
}
|
}
|
||||||
|
|
||||||
let num_messages = LittleEndian::read_u16(&data[offset + 2..offset + 4]) as usize;
|
let num_messages = LittleEndian::read_u16(&prefix[2..4]) as usize;
|
||||||
let reference_count = LittleEndian::read_u32(&data[offset + 4..offset + 8]);
|
let reference_count = LittleEndian::read_u32(&prefix[4..8]);
|
||||||
let header_data_size = LittleEndian::read_u32(&data[offset + 8..offset + 12]) as usize;
|
let header_data_size = LittleEndian::read_u32(&prefix[8..12]) as usize;
|
||||||
|
|
||||||
// libhdf5 (H5O__prefix_deserialize): a header with messages needs room
|
// libhdf5 (H5O__prefix_deserialize): a header with messages needs room
|
||||||
// for at least one message header, and one without has an empty chunk.
|
// for at least one message header, and one without has an empty chunk.
|
||||||
@@ -161,14 +175,13 @@ impl ObjectHeader {
|
|||||||
.checked_add(12 + padding)
|
.checked_add(12 + padding)
|
||||||
.ok_or(FormatError::UnexpectedEof {
|
.ok_or(FormatError::UnexpectedEof {
|
||||||
expected: usize::MAX,
|
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 mut messages = Vec::new();
|
||||||
let chunk0_count = Self::parse_v1_chunk(
|
let chunk0_count = Self::parse_v1_chunk(
|
||||||
data,
|
file,
|
||||||
msg_start,
|
msg_start,
|
||||||
header_data_size,
|
header_data_size,
|
||||||
offset_size,
|
offset_size,
|
||||||
@@ -209,8 +222,8 @@ impl ObjectHeader {
|
|||||||
/// "gap", which only version 2 allows).
|
/// "gap", which only version 2 allows).
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn parse_v1_chunk(
|
fn parse_v1_chunk(
|
||||||
data: &[u8],
|
file: &dyn Storage,
|
||||||
offset: usize,
|
offset: u64,
|
||||||
length: usize,
|
length: usize,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
@@ -220,9 +233,10 @@ impl ObjectHeader {
|
|||||||
if depth_remaining == 0 {
|
if depth_remaining == 0 {
|
||||||
return Err(FormatError::NestingDepthExceeded);
|
return Err(FormatError::NestingDepthExceeded);
|
||||||
}
|
}
|
||||||
ensure_len(data, offset, length)?;
|
let chunk = read_exact_at(file, offset, length)?;
|
||||||
let end = offset + length;
|
let data: &[u8] = &chunk;
|
||||||
let mut pos = offset;
|
let end = length;
|
||||||
|
let mut pos = 0usize;
|
||||||
let mut count = 0usize;
|
let mut count = 0usize;
|
||||||
|
|
||||||
while pos < end {
|
while pos < end {
|
||||||
@@ -267,8 +281,8 @@ impl ObjectHeader {
|
|||||||
let cont_offset = read_offset(body, 0, offset_size)? as usize;
|
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;
|
let cont_length = read_offset(body, offset_size as usize, length_size)? as usize;
|
||||||
Self::parse_v1_chunk(
|
Self::parse_v1_chunk(
|
||||||
data,
|
file,
|
||||||
cont_offset,
|
cont_offset as u64,
|
||||||
cont_length,
|
cont_length,
|
||||||
offset_size,
|
offset_size,
|
||||||
length_size,
|
length_size,
|
||||||
@@ -282,11 +296,31 @@ impl ObjectHeader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
fn parse_v2(
|
fn parse_v2(
|
||||||
data: &[u8],
|
file: &dyn Storage,
|
||||||
offset: usize,
|
offset: u64,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
length_size: u8,
|
length_size: u8,
|
||||||
) -> Result<ObjectHeader, FormatError> {
|
) -> 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
|
// signature(4) + version(1) + flags(1) = 6
|
||||||
ensure_len(data, offset, 6)?;
|
ensure_len(data, offset, 6)?;
|
||||||
|
|
||||||
@@ -351,15 +385,20 @@ impl ObjectHeader {
|
|||||||
}
|
}
|
||||||
|
|
||||||
let chunk0_msg_start = pos;
|
let chunk0_msg_start = pos;
|
||||||
let chunk0_msg_end = pos
|
let chunk0_msg_end = abs(pos)
|
||||||
.checked_add(chunk0_size)
|
.checked_add(chunk0_size)
|
||||||
.ok_or(FormatError::UnexpectedEof {
|
.ok_or(FormatError::UnexpectedEof {
|
||||||
expected: usize::MAX,
|
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)
|
// Validate checksum: from OHDR signature through all messages (before checksum)
|
||||||
ensure_len(data, chunk0_msg_end, 4)?;
|
|
||||||
#[cfg(feature = "checksum")]
|
#[cfg(feature = "checksum")]
|
||||||
{
|
{
|
||||||
let stored = LittleEndian::read_u32(&data[chunk0_msg_end..chunk0_msg_end + 4]);
|
let stored = LittleEndian::read_u32(&data[chunk0_msg_end..chunk0_msg_end + 4]);
|
||||||
@@ -394,8 +433,8 @@ impl ObjectHeader {
|
|||||||
}
|
}
|
||||||
cont_remaining -= 1;
|
cont_remaining -= 1;
|
||||||
Self::parse_v2_continuation(
|
Self::parse_v2_continuation(
|
||||||
data,
|
file,
|
||||||
cont_offset,
|
cont_offset as u64,
|
||||||
cont_length,
|
cont_length,
|
||||||
has_creation_order,
|
has_creation_order,
|
||||||
offset_size,
|
offset_size,
|
||||||
@@ -495,8 +534,8 @@ impl ObjectHeader {
|
|||||||
|
|
||||||
#[allow(clippy::too_many_arguments)]
|
#[allow(clippy::too_many_arguments)]
|
||||||
fn parse_v2_continuation(
|
fn parse_v2_continuation(
|
||||||
data: &[u8],
|
file: &dyn Storage,
|
||||||
offset: usize,
|
offset: u64,
|
||||||
length: usize,
|
length: usize,
|
||||||
has_creation_order: bool,
|
has_creation_order: bool,
|
||||||
offset_size: u8,
|
offset_size: u8,
|
||||||
@@ -505,7 +544,9 @@ impl ObjectHeader {
|
|||||||
continuations: &mut Vec<(usize, usize)>,
|
continuations: &mut Vec<(usize, usize)>,
|
||||||
) -> Result<(), FormatError> {
|
) -> Result<(), FormatError> {
|
||||||
// OCHK signature(4) + messages + checksum(4)
|
// 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 {
|
if length < 8 {
|
||||||
return Err(FormatError::UnexpectedEof {
|
return Err(FormatError::UnexpectedEof {
|
||||||
expected: 8,
|
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).
|
/// Size of a version-1 message header: type(2) + size(2) + flags(1) + reserved(3).
|
||||||
const V1_MSG_HEADER_SIZE: usize = 8;
|
const V1_MSG_HEADER_SIZE: usize = 8;
|
||||||
|
|
||||||
@@ -754,13 +799,13 @@ mod tests {
|
|||||||
let mut msg_bytes = Vec::new();
|
let mut msg_bytes = Vec::new();
|
||||||
for (mtype, mdata, mflags) in messages {
|
for (mtype, mdata, mflags) in messages {
|
||||||
// v1 message sizes are multiples of 8 (the data is zero-padded).
|
// 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(&mtype.to_le_bytes()); // type(2)
|
||||||
msg_bytes.extend_from_slice(&(padded as u16).to_le_bytes()); // size(2)
|
msg_bytes.extend_from_slice(&(padded as u16).to_le_bytes()); // size(2)
|
||||||
msg_bytes.push(*mflags); // flags(1)
|
msg_bytes.push(*mflags); // flags(1)
|
||||||
msg_bytes.extend_from_slice(&[0u8; 3]); // reserved(3)
|
msg_bytes.extend_from_slice(&[0u8; 3]); // reserved(3)
|
||||||
msg_bytes.extend_from_slice(mdata); // data
|
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();
|
let mut buf = Vec::new();
|
||||||
@@ -1280,4 +1325,52 @@ mod tests {
|
|||||||
let err = ObjectHeader::parse(&data, 0, 8, 8).unwrap_err();
|
let err = ObjectHeader::parse(&data, 0, 8, 8).unwrap_err();
|
||||||
assert!(matches!(err, FormatError::UnexpectedEof { .. }));
|
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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user