656 lines
23 KiB
Rust
656 lines
23 KiB
Rust
//! 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);
|
||
}
|
||
}
|