Merge pull request 'docs(clawhdf5): document DType variants, fix unresolved doc links' (#17) from sdlc-docs/clawhdf5-types-20260514-165210 into main
This commit is contained in:
@@ -0,0 +1,655 @@
|
||||
//! HDF5 Superblock parsing for versions 0, 1, 2, and 3.
|
||||
|
||||
#[cfg(not(feature = "std"))]
|
||||
use alloc::vec::Vec;
|
||||
|
||||
use byteorder::{ByteOrder, LittleEndian};
|
||||
|
||||
use crate::error::FormatError;
|
||||
use crate::signature::HDF5_SIGNATURE;
|
||||
|
||||
/// Parsed HDF5 superblock (all versions).
|
||||
#[derive(Debug, Clone, PartialEq, Eq)]
|
||||
pub struct Superblock {
|
||||
/// Superblock version (0–3).
|
||||
pub version: u8,
|
||||
/// Size of offsets in bytes (2, 4, or 8).
|
||||
pub offset_size: u8,
|
||||
/// Size of lengths in bytes (2, 4, or 8).
|
||||
pub length_size: u8,
|
||||
/// File base address.
|
||||
pub base_address: u64,
|
||||
/// End-of-file address.
|
||||
pub eof_address: u64,
|
||||
/// Root group object header address (v2/v3) or from symbol table entry (v0/v1).
|
||||
pub root_group_address: u64,
|
||||
/// Group leaf node K (v0/v1 only).
|
||||
pub group_leaf_node_k: Option<u16>,
|
||||
/// Group internal node K (v0/v1 only).
|
||||
pub group_internal_node_k: Option<u16>,
|
||||
/// Indexed storage internal node K (v1 only).
|
||||
pub indexed_storage_internal_node_k: Option<u16>,
|
||||
/// Free space address (v0/v1 only).
|
||||
pub free_space_address: Option<u64>,
|
||||
/// Driver info block address (v0/v1 only).
|
||||
pub driver_info_address: Option<u64>,
|
||||
/// File consistency flags.
|
||||
pub consistency_flags: u32,
|
||||
/// Superblock extension address (v2/v3 only).
|
||||
pub superblock_extension_address: Option<u64>,
|
||||
/// CRC32C checksum (v2/v3 only).
|
||||
pub checksum: Option<u32>,
|
||||
}
|
||||
|
||||
/// Read an unsigned integer of `size` bytes (LE) from `data` at `pos`.
|
||||
fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
|
||||
let s = size as usize;
|
||||
if pos.checked_add(s).is_none_or(|end| end > data.len()) {
|
||||
return Err(FormatError::UnexpectedEof {
|
||||
expected: pos.saturating_add(s),
|
||||
available: data.len(),
|
||||
});
|
||||
}
|
||||
let slice = &data[pos..pos + s];
|
||||
Ok(match size {
|
||||
2 => LittleEndian::read_u16(slice) as u64,
|
||||
4 => LittleEndian::read_u32(slice) as u64,
|
||||
8 => LittleEndian::read_u64(slice),
|
||||
_ => unreachable!(), // validated before calling
|
||||
})
|
||||
}
|
||||
|
||||
fn validate_sizes(offset_size: u8, length_size: u8) -> Result<(), FormatError> {
|
||||
if !matches!(offset_size, 2 | 4 | 8) {
|
||||
return Err(FormatError::InvalidOffsetSize(offset_size));
|
||||
}
|
||||
if !matches!(length_size, 2 | 4 | 8) {
|
||||
return Err(FormatError::InvalidLengthSize(length_size));
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn ensure_len(data: &[u8], needed: usize) -> Result<(), FormatError> {
|
||||
if data.len() < needed {
|
||||
Err(FormatError::UnexpectedEof {
|
||||
expected: needed,
|
||||
available: data.len(),
|
||||
})
|
||||
} else {
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// SWMR (Single Writer / Multiple Reader) consistency flag bits.
|
||||
///
|
||||
/// These flags are stored in the `consistency_flags` field of the superblock.
|
||||
/// For v2/v3 superblocks, only the low byte is stored.
|
||||
pub mod swmr_flags {
|
||||
/// Bit 0: File is open for writing.
|
||||
pub const WRITE_ACCESS: u32 = 0x01;
|
||||
/// Bit 2: SWMR write access is active.
|
||||
pub const SWMR_WRITE: u32 = 0x04;
|
||||
}
|
||||
|
||||
impl Superblock {
|
||||
/// Whether the file was opened with write access when the superblock was written.
|
||||
pub fn is_write_access(&self) -> bool {
|
||||
self.consistency_flags & swmr_flags::WRITE_ACCESS != 0
|
||||
}
|
||||
|
||||
/// Whether SWMR write access is active.
|
||||
///
|
||||
/// When this flag is set, readers should periodically re-read the
|
||||
/// superblock's EOF address and refresh metadata to see new data
|
||||
/// written by the single writer.
|
||||
pub fn is_swmr_write(&self) -> bool {
|
||||
self.consistency_flags & swmr_flags::SWMR_WRITE != 0
|
||||
}
|
||||
|
||||
/// Re-read the superblock's EOF address from the file data.
|
||||
///
|
||||
/// In SWMR mode, the writer updates the EOF address as new data is
|
||||
/// appended. Readers should call this periodically to discover new data.
|
||||
/// Returns the updated EOF address, or an error if the superblock
|
||||
/// cannot be re-parsed.
|
||||
pub fn refresh_eof(
|
||||
&mut self,
|
||||
file_data: &[u8],
|
||||
signature_offset: usize,
|
||||
) -> Result<u64, FormatError> {
|
||||
let refreshed = Superblock::parse(file_data, signature_offset)?;
|
||||
self.eof_address = refreshed.eof_address;
|
||||
self.consistency_flags = refreshed.consistency_flags;
|
||||
Ok(self.eof_address)
|
||||
}
|
||||
|
||||
/// Serialize this superblock to bytes.
|
||||
///
|
||||
/// Always writes v2/v3 format. Computes and appends Jenkins lookup3 checksum.
|
||||
pub fn serialize(&self) -> Vec<u8> {
|
||||
let mut buf = Vec::with_capacity(48);
|
||||
buf.extend_from_slice(&HDF5_SIGNATURE);
|
||||
buf.push(self.version);
|
||||
buf.push(self.offset_size);
|
||||
buf.push(self.length_size);
|
||||
buf.push(self.consistency_flags as u8);
|
||||
// base_address
|
||||
Self::write_offset(&mut buf, self.base_address, self.offset_size);
|
||||
// superblock extension address
|
||||
let ext_addr = self.superblock_extension_address.unwrap_or(u64::MAX);
|
||||
Self::write_offset(&mut buf, ext_addr, self.offset_size);
|
||||
// eof_address
|
||||
Self::write_offset(&mut buf, self.eof_address, self.offset_size);
|
||||
// root_group_address
|
||||
Self::write_offset(&mut buf, self.root_group_address, self.offset_size);
|
||||
// checksum
|
||||
let checksum = crate::checksum::jenkins_lookup3(&buf);
|
||||
buf.extend_from_slice(&checksum.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
fn write_offset(buf: &mut Vec<u8>, val: u64, size: u8) {
|
||||
match size {
|
||||
2 => buf.extend_from_slice(&(val as u16).to_le_bytes()),
|
||||
4 => buf.extend_from_slice(&(val as u32).to_le_bytes()),
|
||||
8 => buf.extend_from_slice(&val.to_le_bytes()),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Parse a superblock from `data` starting at `signature_offset`.
|
||||
///
|
||||
/// The signature must be present at the given offset.
|
||||
pub fn parse(data: &[u8], signature_offset: usize) -> Result<Superblock, FormatError> {
|
||||
let d = data
|
||||
.get(signature_offset..)
|
||||
.ok_or(FormatError::UnexpectedEof {
|
||||
expected: signature_offset + 1,
|
||||
available: data.len(),
|
||||
})?;
|
||||
ensure_len(d, 9)?; // signature(8) + version(1)
|
||||
|
||||
// Verify signature
|
||||
if d[..8] != HDF5_SIGNATURE {
|
||||
return Err(FormatError::SignatureNotFound);
|
||||
}
|
||||
|
||||
let version = d[8];
|
||||
match version {
|
||||
0 => Self::parse_v0(d),
|
||||
1 => Self::parse_v1(d),
|
||||
2 | 3 => Self::parse_v2v3(d, version),
|
||||
v => Err(FormatError::UnsupportedVersion(v)),
|
||||
}
|
||||
}
|
||||
|
||||
fn parse_v0(d: &[u8]) -> Result<Superblock, FormatError> {
|
||||
// sig(8) + version(1) + free_space_ver(1) + root_grp_ver(1) + reserved(1)
|
||||
// + shared_hdr_ver(1) + offset_size(1) + length_size(1) + reserved(1)
|
||||
// + group_leaf_k(2) + group_internal_k(2) + consistency_flags(4)
|
||||
// = 24 bytes before variable-sized fields
|
||||
ensure_len(d, 24)?;
|
||||
|
||||
let offset_size = d[13];
|
||||
let length_size = d[14];
|
||||
validate_sizes(offset_size, length_size)?;
|
||||
|
||||
let group_leaf_node_k = LittleEndian::read_u16(&d[16..18]);
|
||||
let group_internal_node_k = LittleEndian::read_u16(&d[18..20]);
|
||||
let consistency_flags = LittleEndian::read_u32(&d[20..24]);
|
||||
|
||||
let os = offset_size as usize;
|
||||
// 4 addresses + root symbol table entry
|
||||
let var_start = 24;
|
||||
let sym_entry_size = os + os + 4 + 4 + 16; // link_name_off, obj_hdr_addr, cache_type, reserved, scratch
|
||||
let total = var_start + 4 * os + sym_entry_size;
|
||||
ensure_len(d, total)?;
|
||||
|
||||
let mut pos = var_start;
|
||||
let base_address = read_offset(d, pos, offset_size)?;
|
||||
pos += os;
|
||||
let free_space_address = read_offset(d, pos, offset_size)?;
|
||||
pos += os;
|
||||
let eof_address = read_offset(d, pos, offset_size)?;
|
||||
pos += os;
|
||||
let driver_info_address = read_offset(d, pos, offset_size)?;
|
||||
pos += os;
|
||||
|
||||
// Root symbol table entry
|
||||
let _link_name_offset = read_offset(d, pos, offset_size)?;
|
||||
pos += os;
|
||||
let object_header_addr = read_offset(d, pos, offset_size)?;
|
||||
|
||||
Ok(Superblock {
|
||||
version: 0,
|
||||
offset_size,
|
||||
length_size,
|
||||
base_address,
|
||||
eof_address,
|
||||
root_group_address: object_header_addr,
|
||||
group_leaf_node_k: Some(group_leaf_node_k),
|
||||
group_internal_node_k: Some(group_internal_node_k),
|
||||
indexed_storage_internal_node_k: None,
|
||||
free_space_address: Some(free_space_address),
|
||||
driver_info_address: Some(driver_info_address),
|
||||
consistency_flags,
|
||||
superblock_extension_address: None,
|
||||
checksum: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_v1(d: &[u8]) -> Result<Superblock, FormatError> {
|
||||
// Same as v0 but adds indexed_storage_internal_node_k(2) + reserved(2) after group_internal_k
|
||||
// sig(8) + version(1) + free_space_ver(1) + root_grp_ver(1) + reserved(1)
|
||||
// + shared_hdr_ver(1) + offset_size(1) + length_size(1) + reserved(1)
|
||||
// + group_leaf_k(2) + group_internal_k(2) + indexed_storage_k(2) + reserved(2)
|
||||
// + consistency_flags(4) = 28
|
||||
ensure_len(d, 28)?;
|
||||
|
||||
let offset_size = d[13];
|
||||
let length_size = d[14];
|
||||
validate_sizes(offset_size, length_size)?;
|
||||
|
||||
let group_leaf_node_k = LittleEndian::read_u16(&d[16..18]);
|
||||
let group_internal_node_k = LittleEndian::read_u16(&d[18..20]);
|
||||
let indexed_storage_internal_node_k = LittleEndian::read_u16(&d[20..22]);
|
||||
// d[22..24] reserved
|
||||
let consistency_flags = LittleEndian::read_u32(&d[24..28]);
|
||||
|
||||
let os = offset_size as usize;
|
||||
let var_start = 28;
|
||||
let sym_entry_size = os + os + 4 + 4 + 16;
|
||||
let total = var_start + 4 * os + sym_entry_size;
|
||||
ensure_len(d, total)?;
|
||||
|
||||
let mut pos = var_start;
|
||||
let base_address = read_offset(d, pos, offset_size)?;
|
||||
pos += os;
|
||||
let free_space_address = read_offset(d, pos, offset_size)?;
|
||||
pos += os;
|
||||
let eof_address = read_offset(d, pos, offset_size)?;
|
||||
pos += os;
|
||||
let driver_info_address = read_offset(d, pos, offset_size)?;
|
||||
pos += os;
|
||||
|
||||
// Root symbol table entry
|
||||
let _link_name_offset = read_offset(d, pos, offset_size)?;
|
||||
pos += os;
|
||||
let object_header_addr = read_offset(d, pos, offset_size)?;
|
||||
|
||||
Ok(Superblock {
|
||||
version: 1,
|
||||
offset_size,
|
||||
length_size,
|
||||
base_address,
|
||||
eof_address,
|
||||
root_group_address: object_header_addr,
|
||||
group_leaf_node_k: Some(group_leaf_node_k),
|
||||
group_internal_node_k: Some(group_internal_node_k),
|
||||
indexed_storage_internal_node_k: Some(indexed_storage_internal_node_k),
|
||||
free_space_address: Some(free_space_address),
|
||||
driver_info_address: Some(driver_info_address),
|
||||
consistency_flags,
|
||||
superblock_extension_address: None,
|
||||
checksum: None,
|
||||
})
|
||||
}
|
||||
|
||||
fn parse_v2v3(d: &[u8], version: u8) -> Result<Superblock, FormatError> {
|
||||
// sig(8) + version(1) + offset_size(1) + length_size(1) + consistency_flags(1) = 12
|
||||
ensure_len(d, 12)?;
|
||||
|
||||
let offset_size = d[9];
|
||||
let length_size = d[10];
|
||||
validate_sizes(offset_size, length_size)?;
|
||||
let consistency_flags = d[11] as u32;
|
||||
|
||||
let os = offset_size as usize;
|
||||
// 4 addresses + checksum(4)
|
||||
let total = 12 + 4 * os + 4;
|
||||
ensure_len(d, total)?;
|
||||
|
||||
let mut pos = 12;
|
||||
let base_address = read_offset(d, pos, offset_size)?;
|
||||
pos += os;
|
||||
let superblock_extension_address = read_offset(d, pos, offset_size)?;
|
||||
pos += os;
|
||||
let eof_address = read_offset(d, pos, offset_size)?;
|
||||
pos += os;
|
||||
let root_group_address = read_offset(d, pos, offset_size)?;
|
||||
pos += os;
|
||||
|
||||
let stored_checksum = LittleEndian::read_u32(&d[pos..pos + 4]);
|
||||
|
||||
// Validate checksum if feature enabled
|
||||
#[cfg(feature = "checksum")]
|
||||
{
|
||||
let computed = crate::checksum::jenkins_lookup3(&d[..pos]);
|
||||
if computed != stored_checksum {
|
||||
return Err(FormatError::ChecksumMismatch {
|
||||
expected: stored_checksum,
|
||||
computed,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Ok(Superblock {
|
||||
version,
|
||||
offset_size,
|
||||
length_size,
|
||||
base_address,
|
||||
eof_address,
|
||||
root_group_address,
|
||||
group_leaf_node_k: None,
|
||||
group_internal_node_k: None,
|
||||
indexed_storage_internal_node_k: None,
|
||||
free_space_address: None,
|
||||
driver_info_address: None,
|
||||
consistency_flags,
|
||||
superblock_extension_address: Some(superblock_extension_address),
|
||||
checksum: Some(stored_checksum),
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
/// Helper to build a v0 superblock byte buffer with 8-byte offsets.
|
||||
fn build_v0_bytes(offset_size: u8) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&HDF5_SIGNATURE); // 0..8
|
||||
buf.push(0); // version = 0
|
||||
buf.push(0); // free_space_version
|
||||
buf.push(0); // root_group_version
|
||||
buf.push(0); // reserved
|
||||
buf.push(0); // shared_header_version
|
||||
buf.push(offset_size); // offset_size
|
||||
buf.push(offset_size); // length_size (same for simplicity)
|
||||
buf.push(0); // reserved
|
||||
buf.extend_from_slice(&4u16.to_le_bytes()); // group_leaf_node_k
|
||||
buf.extend_from_slice(&16u16.to_le_bytes()); // group_internal_node_k
|
||||
buf.extend_from_slice(&0u32.to_le_bytes()); // consistency_flags
|
||||
// base_address
|
||||
write_offset(&mut buf, 0, offset_size);
|
||||
// free_space_address
|
||||
write_offset(&mut buf, 0xFFFFFFFFFFFFFFFF, offset_size);
|
||||
// eof_address
|
||||
write_offset(&mut buf, 4096, offset_size);
|
||||
// driver_info_address
|
||||
write_offset(&mut buf, 0xFFFFFFFFFFFFFFFF, offset_size);
|
||||
// Root symbol table entry
|
||||
write_offset(&mut buf, 0, offset_size); // link_name_offset
|
||||
write_offset(&mut buf, 96, offset_size); // object_header_addr (root group)
|
||||
buf.extend_from_slice(&0u32.to_le_bytes()); // cache_type
|
||||
buf.extend_from_slice(&0u32.to_le_bytes()); // reserved
|
||||
buf.extend_from_slice(&[0u8; 16]); // scratch pad
|
||||
buf
|
||||
}
|
||||
|
||||
fn write_offset(buf: &mut Vec<u8>, val: u64, size: u8) {
|
||||
match size {
|
||||
2 => buf.extend_from_slice(&(val as u16).to_le_bytes()),
|
||||
4 => buf.extend_from_slice(&(val as u32).to_le_bytes()),
|
||||
8 => buf.extend_from_slice(&val.to_le_bytes()),
|
||||
_ => panic!("bad test offset size"),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_v1_bytes(offset_size: u8) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&HDF5_SIGNATURE);
|
||||
buf.push(1); // version
|
||||
buf.push(0); // free_space_version
|
||||
buf.push(0); // root_group_version
|
||||
buf.push(0); // reserved
|
||||
buf.push(0); // shared_header_version
|
||||
buf.push(offset_size);
|
||||
buf.push(offset_size);
|
||||
buf.push(0); // reserved
|
||||
buf.extend_from_slice(&4u16.to_le_bytes()); // group_leaf_node_k
|
||||
buf.extend_from_slice(&16u16.to_le_bytes()); // group_internal_node_k
|
||||
buf.extend_from_slice(&32u16.to_le_bytes()); // indexed_storage_internal_node_k
|
||||
buf.extend_from_slice(&0u16.to_le_bytes()); // reserved
|
||||
buf.extend_from_slice(&0u32.to_le_bytes()); // consistency_flags
|
||||
write_offset(&mut buf, 0, offset_size); // base
|
||||
write_offset(&mut buf, 0xFFFFFFFFFFFFFFFF, offset_size); // free space
|
||||
write_offset(&mut buf, 8192, offset_size); // eof
|
||||
write_offset(&mut buf, 0xFFFFFFFFFFFFFFFF, offset_size); // driver info
|
||||
// Root symbol table entry
|
||||
write_offset(&mut buf, 0, offset_size);
|
||||
write_offset(&mut buf, 200, offset_size); // root group addr
|
||||
buf.extend_from_slice(&0u32.to_le_bytes());
|
||||
buf.extend_from_slice(&0u32.to_le_bytes());
|
||||
buf.extend_from_slice(&[0u8; 16]);
|
||||
buf
|
||||
}
|
||||
|
||||
fn build_v2_bytes(offset_size: u8, version: u8) -> Vec<u8> {
|
||||
let mut buf = Vec::new();
|
||||
buf.extend_from_slice(&HDF5_SIGNATURE);
|
||||
buf.push(version);
|
||||
buf.push(offset_size);
|
||||
buf.push(offset_size); // length_size
|
||||
buf.push(0); // consistency_flags
|
||||
write_offset(&mut buf, 0, offset_size); // base_address
|
||||
write_offset(&mut buf, 0xFFFFFFFFFFFFFFFF, offset_size); // superblock ext
|
||||
write_offset(&mut buf, 2048, offset_size); // eof
|
||||
write_offset(&mut buf, 48, offset_size); // root group obj hdr
|
||||
|
||||
// Compute CRC32C of everything so far
|
||||
let checksum = crate::checksum::jenkins_lookup3(&buf);
|
||||
buf.extend_from_slice(&checksum.to_le_bytes());
|
||||
buf
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_v0_8byte_offsets() {
|
||||
let data = build_v0_bytes(8);
|
||||
let sb = Superblock::parse(&data, 0).unwrap();
|
||||
assert_eq!(sb.version, 0);
|
||||
assert_eq!(sb.offset_size, 8);
|
||||
assert_eq!(sb.base_address, 0);
|
||||
assert_eq!(sb.eof_address, 4096);
|
||||
assert_eq!(sb.root_group_address, 96);
|
||||
assert_eq!(sb.group_leaf_node_k, Some(4));
|
||||
assert_eq!(sb.group_internal_node_k, Some(16));
|
||||
assert_eq!(sb.indexed_storage_internal_node_k, None);
|
||||
assert_eq!(sb.free_space_address, Some(0xFFFFFFFFFFFFFFFF));
|
||||
assert_eq!(sb.driver_info_address, Some(0xFFFFFFFFFFFFFFFF));
|
||||
assert_eq!(sb.checksum, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_v0_4byte_offsets() {
|
||||
let data = build_v0_bytes(4);
|
||||
let sb = Superblock::parse(&data, 0).unwrap();
|
||||
assert_eq!(sb.version, 0);
|
||||
assert_eq!(sb.offset_size, 4);
|
||||
assert_eq!(sb.eof_address, 4096);
|
||||
assert_eq!(sb.root_group_address, 96);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_v1_8byte_offsets() {
|
||||
let data = build_v1_bytes(8);
|
||||
let sb = Superblock::parse(&data, 0).unwrap();
|
||||
assert_eq!(sb.version, 1);
|
||||
assert_eq!(sb.offset_size, 8);
|
||||
assert_eq!(sb.eof_address, 8192);
|
||||
assert_eq!(sb.root_group_address, 200);
|
||||
assert_eq!(sb.indexed_storage_internal_node_k, Some(32));
|
||||
assert_eq!(sb.group_leaf_node_k, Some(4));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_v1_4byte_offsets() {
|
||||
let data = build_v1_bytes(4);
|
||||
let sb = Superblock::parse(&data, 0).unwrap();
|
||||
assert_eq!(sb.version, 1);
|
||||
assert_eq!(sb.offset_size, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_v2_8byte_offsets() {
|
||||
let data = build_v2_bytes(8, 2);
|
||||
let sb = Superblock::parse(&data, 0).unwrap();
|
||||
assert_eq!(sb.version, 2);
|
||||
assert_eq!(sb.offset_size, 8);
|
||||
assert_eq!(sb.eof_address, 2048);
|
||||
assert_eq!(sb.root_group_address, 48);
|
||||
assert!(sb.checksum.is_some());
|
||||
assert_eq!(sb.group_leaf_node_k, None);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_v2_4byte_offsets() {
|
||||
let data = build_v2_bytes(4, 2);
|
||||
let sb = Superblock::parse(&data, 0).unwrap();
|
||||
assert_eq!(sb.version, 2);
|
||||
assert_eq!(sb.offset_size, 4);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_v3() {
|
||||
let data = build_v2_bytes(8, 3);
|
||||
let sb = Superblock::parse(&data, 0).unwrap();
|
||||
assert_eq!(sb.version, 3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn checksum_mismatch_v2() {
|
||||
let mut data = build_v2_bytes(8, 2);
|
||||
// Corrupt the checksum
|
||||
let len = data.len();
|
||||
data[len - 1] ^= 0xFF;
|
||||
let err = Superblock::parse(&data, 0).unwrap_err();
|
||||
matches!(err, FormatError::ChecksumMismatch { .. });
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unsupported_version() {
|
||||
let mut data = vec![0u8; 64];
|
||||
data[..8].copy_from_slice(&HDF5_SIGNATURE);
|
||||
data[8] = 99;
|
||||
assert_eq!(
|
||||
Superblock::parse(&data, 0),
|
||||
Err(FormatError::UnsupportedVersion(99))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_data() {
|
||||
let data = HDF5_SIGNATURE.to_vec(); // Just the signature, no version
|
||||
// Only 8 bytes, need at least 9
|
||||
assert!(matches!(
|
||||
Superblock::parse(&data, 0),
|
||||
Err(FormatError::UnexpectedEof { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn truncated_v0() {
|
||||
let mut data = vec![0u8; 20]; // Too short for v0
|
||||
data[..8].copy_from_slice(&HDF5_SIGNATURE);
|
||||
data[8] = 0; // version 0
|
||||
data[13] = 8; // offset_size
|
||||
data[14] = 8; // length_size
|
||||
assert!(matches!(
|
||||
Superblock::parse(&data, 0),
|
||||
Err(FormatError::UnexpectedEof { .. })
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_offset_size() {
|
||||
let mut data = vec![0u8; 64];
|
||||
data[..8].copy_from_slice(&HDF5_SIGNATURE);
|
||||
data[8] = 0; // version 0
|
||||
data[13] = 3; // invalid offset_size
|
||||
data[14] = 8;
|
||||
assert_eq!(
|
||||
Superblock::parse(&data, 0),
|
||||
Err(FormatError::InvalidOffsetSize(3))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn invalid_length_size() {
|
||||
let mut data = vec![0u8; 64];
|
||||
data[..8].copy_from_slice(&HDF5_SIGNATURE);
|
||||
data[8] = 0;
|
||||
data[13] = 8;
|
||||
data[14] = 5; // invalid length_size
|
||||
assert_eq!(
|
||||
Superblock::parse(&data, 0),
|
||||
Err(FormatError::InvalidLengthSize(5))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn parse_at_nonzero_offset() {
|
||||
let mut data = vec![0u8; 1024];
|
||||
let v0 = build_v0_bytes(8);
|
||||
data[512..512 + v0.len()].copy_from_slice(&v0);
|
||||
let sb = Superblock::parse(&data, 512).unwrap();
|
||||
assert_eq!(sb.version, 0);
|
||||
assert_eq!(sb.root_group_address, 96);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn v2_2byte_offsets() {
|
||||
let data = build_v2_bytes(2, 2);
|
||||
let sb = Superblock::parse(&data, 0).unwrap();
|
||||
assert_eq!(sb.offset_size, 2);
|
||||
assert_eq!(sb.eof_address, 2048);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swmr_flags_default() {
|
||||
let data = build_v2_bytes(8, 3);
|
||||
let sb = Superblock::parse(&data, 0).unwrap();
|
||||
assert!(!sb.is_write_access());
|
||||
assert!(!sb.is_swmr_write());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swmr_write_flag() {
|
||||
let mut data = build_v2_bytes(8, 3);
|
||||
// Set SWMR write flag (bit 2) in consistency_flags byte
|
||||
// In v2/v3, consistency_flags is at offset 11
|
||||
data[11] = 0x04; // SWMR_WRITE
|
||||
// Recompute checksum
|
||||
let cksum_offset = data.len() - 4;
|
||||
let checksum = crate::checksum::jenkins_lookup3(&data[..cksum_offset]);
|
||||
data[cksum_offset..].copy_from_slice(&checksum.to_le_bytes());
|
||||
|
||||
let sb = Superblock::parse(&data, 0).unwrap();
|
||||
assert!(sb.is_swmr_write());
|
||||
assert!(!sb.is_write_access());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn swmr_write_access_flag() {
|
||||
let mut data = build_v2_bytes(8, 3);
|
||||
data[11] = 0x05; // WRITE_ACCESS | SWMR_WRITE
|
||||
let cksum_offset = data.len() - 4;
|
||||
let checksum = crate::checksum::jenkins_lookup3(&data[..cksum_offset]);
|
||||
data[cksum_offset..].copy_from_slice(&checksum.to_le_bytes());
|
||||
|
||||
let sb = Superblock::parse(&data, 0).unwrap();
|
||||
assert!(sb.is_swmr_write());
|
||||
assert!(sb.is_write_access());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn refresh_eof() {
|
||||
let data = build_v2_bytes(8, 3);
|
||||
let mut sb = Superblock::parse(&data, 0).unwrap();
|
||||
let old_eof = sb.eof_address;
|
||||
// Re-reading same data should keep same EOF
|
||||
let new_eof = sb.refresh_eof(&data, 0).unwrap();
|
||||
assert_eq!(new_eof, old_eof);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user