use crate::format::checksum::checksum_metadata;
use crate::format::{FormatError, FormatResult};
pub const HDF5_SIGNATURE: [u8; 8] = [0x89, 0x48, 0x44, 0x46, 0x0d, 0x0a, 0x1a, 0x0a];
pub const SUPERBLOCK_V0: u8 = 0;
pub const SUPERBLOCK_V2: u8 = 2;
pub const SUPERBLOCK_V3: u8 = 3;
pub const FLAG_WRITE_ACCESS: u8 = 0x01;
pub const FLAG_FILE_OK: u8 = 0x02;
pub const FLAG_SWMR_WRITE: u8 = 0x04;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SuperblockV2V3 {
pub version: u8,
pub sizeof_offsets: u8,
pub sizeof_lengths: u8,
pub file_consistency_flags: u8,
pub base_address: u64,
pub superblock_extension_address: u64,
pub end_of_file_address: u64,
pub root_group_object_header_address: u64,
}
impl SuperblockV2V3 {
pub fn encoded_size(&self) -> usize {
Self::size_for(self.sizeof_offsets)
}
pub fn size_for(sizeof_offsets: u8) -> usize {
12 + 4 * (sizeof_offsets as usize) + 4
}
pub fn encode(&self) -> Vec<u8> {
let size = self.encoded_size();
let mut buf = Vec::with_capacity(size);
buf.extend_from_slice(&HDF5_SIGNATURE);
buf.push(self.version);
buf.push(self.sizeof_offsets);
buf.push(self.sizeof_lengths);
buf.push(self.file_consistency_flags);
let o = self.sizeof_offsets as usize;
encode_offset(&mut buf, self.base_address, o);
encode_offset(&mut buf, self.superblock_extension_address, o);
encode_offset(&mut buf, self.end_of_file_address, o);
encode_offset(&mut buf, self.root_group_object_header_address, o);
debug_assert_eq!(buf.len(), size - 4);
let cksum = checksum_metadata(&buf);
buf.extend_from_slice(&cksum.to_le_bytes());
debug_assert_eq!(buf.len(), size);
buf
}
pub fn decode(buf: &[u8]) -> FormatResult<Self> {
if buf.len() < 12 {
return Err(FormatError::BufferTooShort {
needed: 12,
available: buf.len(),
});
}
if buf[0..8] != HDF5_SIGNATURE {
return Err(FormatError::InvalidSignature);
}
let version = buf[8];
if version != SUPERBLOCK_V2 && version != SUPERBLOCK_V3 {
return Err(FormatError::InvalidVersion(version));
}
let sizeof_offsets = buf[9];
let sizeof_lengths = buf[10];
let file_consistency_flags = buf[11];
validate_sizeof(sizeof_offsets, sizeof_lengths)?;
let o = sizeof_offsets as usize;
let total_size = 12 + 4 * o + 4;
if buf.len() < total_size {
return Err(FormatError::BufferTooShort {
needed: total_size,
available: buf.len(),
});
}
let data_end = total_size - 4;
let stored_cksum = u32::from_le_bytes([
buf[data_end],
buf[data_end + 1],
buf[data_end + 2],
buf[data_end + 3],
]);
let computed_cksum = checksum_metadata(&buf[..data_end]);
if stored_cksum != computed_cksum {
return Err(FormatError::ChecksumMismatch {
expected: stored_cksum,
computed: computed_cksum,
});
}
let mut pos = 12;
let base_address = decode_offset(buf, &mut pos, o);
let superblock_extension_address = decode_offset(buf, &mut pos, o);
let end_of_file_address = decode_offset(buf, &mut pos, o);
let root_group_object_header_address = decode_offset(buf, &mut pos, o);
Ok(SuperblockV2V3 {
version,
sizeof_offsets,
sizeof_lengths,
file_consistency_flags,
base_address,
superblock_extension_address,
end_of_file_address,
root_group_object_header_address,
})
}
}
fn encode_offset(buf: &mut Vec<u8>, value: u64, size: usize) {
let bytes = value.to_le_bytes();
buf.extend_from_slice(&bytes[..size]);
}
fn validate_sizeof(sizeof_offsets: u8, sizeof_lengths: u8) -> FormatResult<()> {
for (name, v) in [
("sizeof_offsets", sizeof_offsets),
("sizeof_lengths", sizeof_lengths),
] {
if !matches!(v, 2 | 4 | 8) {
return Err(FormatError::InvalidData(format!(
"unsupported superblock {name} = {v} (only 2, 4, 8 are supported)"
)));
}
}
Ok(())
}
fn decode_offset(buf: &[u8], pos: &mut usize, size: usize) -> u64 {
let v = crate::format::bytes::read_le_uint(&buf[*pos..], size);
*pos += size;
v
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::UNDEF_ADDR;
#[test]
fn test_encoded_size() {
let sb = SuperblockV2V3 {
version: SUPERBLOCK_V3,
sizeof_offsets: 8,
sizeof_lengths: 8,
file_consistency_flags: 0,
base_address: 0,
superblock_extension_address: UNDEF_ADDR,
end_of_file_address: 4096,
root_group_object_header_address: 48,
};
assert_eq!(sb.encoded_size(), 48);
}
#[test]
fn test_roundtrip_v3_offset8() {
let original = SuperblockV2V3 {
version: SUPERBLOCK_V3,
sizeof_offsets: 8,
sizeof_lengths: 8,
file_consistency_flags: FLAG_FILE_OK,
base_address: 0,
superblock_extension_address: UNDEF_ADDR,
end_of_file_address: 0x1_0000,
root_group_object_header_address: 48,
};
let encoded = original.encode();
assert_eq!(encoded.len(), original.encoded_size());
assert_eq!(&encoded[..8], &HDF5_SIGNATURE);
let decoded = SuperblockV2V3::decode(&encoded).expect("decode failed");
assert_eq!(decoded, original);
}
#[test]
fn test_roundtrip_v2_offset4() {
let original = SuperblockV2V3 {
version: SUPERBLOCK_V2,
sizeof_offsets: 4,
sizeof_lengths: 4,
file_consistency_flags: 0,
base_address: 0,
superblock_extension_address: 0xFFFF_FFFF,
end_of_file_address: 8192,
root_group_object_header_address: 28,
};
let encoded = original.encode();
assert_eq!(encoded.len(), 32);
let decoded = SuperblockV2V3::decode(&encoded).expect("decode failed");
assert_eq!(decoded, original);
}
#[test]
fn test_decode_bad_signature() {
let mut data = vec![0u8; 48];
data[0] = 0x00;
let err = SuperblockV2V3::decode(&data).unwrap_err();
assert!(matches!(err, FormatError::InvalidSignature));
}
#[test]
fn test_decode_bad_version() {
let sb = SuperblockV2V3 {
version: SUPERBLOCK_V3,
sizeof_offsets: 8,
sizeof_lengths: 8,
file_consistency_flags: 0,
base_address: 0,
superblock_extension_address: UNDEF_ADDR,
end_of_file_address: 4096,
root_group_object_header_address: 48,
};
let mut encoded = sb.encode();
encoded[8] = 1;
let err = SuperblockV2V3::decode(&encoded).unwrap_err();
assert!(matches!(err, FormatError::InvalidVersion(1)));
}
#[test]
fn test_decode_checksum_mismatch() {
let sb = SuperblockV2V3 {
version: SUPERBLOCK_V3,
sizeof_offsets: 8,
sizeof_lengths: 8,
file_consistency_flags: 0,
base_address: 0,
superblock_extension_address: UNDEF_ADDR,
end_of_file_address: 4096,
root_group_object_header_address: 48,
};
let mut encoded = sb.encode();
encoded[12] = 0xFF;
let err = SuperblockV2V3::decode(&encoded).unwrap_err();
assert!(matches!(err, FormatError::ChecksumMismatch { .. }));
}
#[test]
fn test_decode_buffer_too_short() {
let err = SuperblockV2V3::decode(&[0u8; 4]).unwrap_err();
assert!(matches!(err, FormatError::BufferTooShort { .. }));
}
#[test]
fn test_flags() {
let sb = SuperblockV2V3 {
version: SUPERBLOCK_V3,
sizeof_offsets: 8,
sizeof_lengths: 8,
file_consistency_flags: FLAG_WRITE_ACCESS | FLAG_SWMR_WRITE,
base_address: 0,
superblock_extension_address: UNDEF_ADDR,
end_of_file_address: 4096,
root_group_object_header_address: 48,
};
let encoded = sb.encode();
let decoded = SuperblockV2V3::decode(&encoded).unwrap();
assert_eq!(
decoded.file_consistency_flags,
FLAG_WRITE_ACCESS | FLAG_SWMR_WRITE
);
}
#[test]
fn test_roundtrip_with_extra_trailing_data() {
let sb = SuperblockV2V3 {
version: SUPERBLOCK_V3,
sizeof_offsets: 8,
sizeof_lengths: 8,
file_consistency_flags: 0,
base_address: 0,
superblock_extension_address: UNDEF_ADDR,
end_of_file_address: 4096,
root_group_object_header_address: 48,
};
let mut encoded = sb.encode();
encoded.extend_from_slice(&[0xAA; 100]); let decoded = SuperblockV2V3::decode(&encoded).unwrap();
assert_eq!(decoded, sb);
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SymbolTableCache {
Nothing,
SymbolTable { btree_addr: u64, heap_addr: u64 },
SoftLink { value_offset: u32 },
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SymbolTableEntry {
pub name_offset: u64,
pub obj_header_addr: u64,
pub cache: SymbolTableCache,
}
impl SymbolTableEntry {
pub fn cached_symbol_table(&self) -> Option<(u64, u64)> {
match self.cache {
SymbolTableCache::SymbolTable {
btree_addr,
heap_addr,
} => Some((btree_addr, heap_addr)),
_ => None,
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SuperblockV0V1 {
pub version: u8,
pub sizeof_offsets: u8,
pub sizeof_lengths: u8,
pub file_consistency_flags: u32,
pub sym_leaf_k: u16,
pub btree_internal_k: u16,
pub indexed_storage_k: Option<u16>,
pub base_address: u64,
pub superblock_extension_address: u64,
pub end_of_file_address: u64,
pub driver_info_address: u64,
pub root_symbol_table_entry: SymbolTableEntry,
}
impl SuperblockV0V1 {
pub fn encoded_size(&self) -> usize {
let o = self.sizeof_offsets as usize;
let s = self.sizeof_lengths as usize;
16 + 8 + if self.version == 1 { 4 } else { 0 } + 4 * o + symbol_table_entry_size(o, s)
}
pub fn encode(&self) -> Vec<u8> {
let o = self.sizeof_offsets as usize;
let s = self.sizeof_lengths as usize;
let size = self.encoded_size();
let mut buf = Vec::with_capacity(size);
buf.extend_from_slice(&HDF5_SIGNATURE);
buf.push(self.version);
buf.push(0); buf.push(0); buf.push(0); buf.push(0); buf.push(self.sizeof_offsets);
buf.push(self.sizeof_lengths);
buf.push(0); buf.extend_from_slice(&self.sym_leaf_k.to_le_bytes());
buf.extend_from_slice(&self.btree_internal_k.to_le_bytes());
buf.extend_from_slice(&self.file_consistency_flags.to_le_bytes());
if self.version == 1 {
buf.extend_from_slice(&self.indexed_storage_k.unwrap_or(32).to_le_bytes());
buf.extend_from_slice(&[0u8; 2]); }
encode_offset(&mut buf, self.base_address, o);
encode_offset(&mut buf, self.superblock_extension_address, o);
encode_offset(&mut buf, self.end_of_file_address, o);
encode_offset(&mut buf, self.driver_info_address, o);
encode_symbol_table_entry(&mut buf, &self.root_symbol_table_entry, o, s);
debug_assert_eq!(buf.len(), size);
buf
}
pub fn decode(buf: &[u8]) -> FormatResult<Self> {
if buf.len() < 16 {
return Err(FormatError::BufferTooShort {
needed: 16,
available: buf.len(),
});
}
if buf[0..8] != HDF5_SIGNATURE {
return Err(FormatError::InvalidSignature);
}
let version = buf[8];
if version != 0 && version != 1 {
return Err(FormatError::InvalidVersion(version));
}
let sizeof_offsets = buf[13];
let sizeof_lengths = buf[14];
validate_sizeof(sizeof_offsets, sizeof_lengths)?;
let o = sizeof_offsets as usize;
let mut pos = 16;
if buf.len() < pos + 4 {
return Err(FormatError::BufferTooShort {
needed: pos + 4,
available: buf.len(),
});
}
let sym_leaf_k = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
pos += 2;
let btree_internal_k = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
pos += 2;
if buf.len() < pos + 4 {
return Err(FormatError::BufferTooShort {
needed: pos + 4,
available: buf.len(),
});
}
let file_consistency_flags =
u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
pos += 4;
let indexed_storage_k = if version == 1 {
if buf.len() < pos + 4 {
return Err(FormatError::BufferTooShort {
needed: pos + 4,
available: buf.len(),
});
}
let k = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
pos += 2;
pos += 2;
Some(k)
} else {
None
};
let needed = pos + 4 * o;
if buf.len() < needed {
return Err(FormatError::BufferTooShort {
needed,
available: buf.len(),
});
}
let base_address = decode_offset(buf, &mut pos, o);
let superblock_extension_address = decode_offset(buf, &mut pos, o);
let end_of_file_address = decode_offset(buf, &mut pos, o);
let driver_info_address = decode_offset(buf, &mut pos, o);
let ste = decode_symbol_table_entry(buf, &mut pos, o, sizeof_lengths as usize)?;
Ok(SuperblockV0V1 {
version,
sizeof_offsets,
sizeof_lengths,
file_consistency_flags,
sym_leaf_k,
btree_internal_k,
indexed_storage_k,
base_address,
superblock_extension_address,
end_of_file_address,
driver_info_address,
root_symbol_table_entry: ste,
})
}
}
pub fn symbol_table_entry_size(sizeof_addr: usize, sizeof_size: usize) -> usize {
sizeof_size + sizeof_addr + 4 + 4 + 16
}
pub fn encode_symbol_table_entry(
out: &mut Vec<u8>,
entry: &SymbolTableEntry,
sizeof_addr: usize,
sizeof_size: usize,
) {
let start = out.len();
encode_offset(out, entry.name_offset, sizeof_size);
encode_offset(out, entry.obj_header_addr, sizeof_addr);
let cache_type: u32 = match entry.cache {
SymbolTableCache::Nothing => 0,
SymbolTableCache::SymbolTable { .. } => 1,
SymbolTableCache::SoftLink { .. } => 2,
};
out.extend_from_slice(&cache_type.to_le_bytes());
out.extend_from_slice(&0u32.to_le_bytes()); let scratch = out.len();
match entry.cache {
SymbolTableCache::Nothing => {}
SymbolTableCache::SymbolTable {
btree_addr,
heap_addr,
} => {
encode_offset(out, btree_addr, sizeof_addr);
encode_offset(out, heap_addr, sizeof_addr);
}
SymbolTableCache::SoftLink { value_offset } => {
out.extend_from_slice(&value_offset.to_le_bytes());
}
}
out.resize(scratch + 16, 0);
debug_assert_eq!(
out.len() - start,
symbol_table_entry_size(sizeof_addr, sizeof_size)
);
}
pub fn decode_symbol_table_entry(
buf: &[u8],
pos: &mut usize,
sizeof_addr: usize,
sizeof_size: usize,
) -> FormatResult<SymbolTableEntry> {
let needed = *pos + sizeof_size + sizeof_addr + 4 + 4 + 16;
if buf.len() < needed {
return Err(FormatError::BufferTooShort {
needed,
available: buf.len(),
});
}
let name_offset = decode_offset(buf, pos, sizeof_size);
let obj_header_addr = decode_offset(buf, pos, sizeof_addr);
let cache_type = u32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
*pos += 4;
*pos += 4;
let scratch_start = *pos;
let cache = match cache_type {
1 => {
let btree_addr = decode_offset(buf, pos, sizeof_addr);
let heap_addr = decode_offset(buf, pos, sizeof_addr);
SymbolTableCache::SymbolTable {
btree_addr,
heap_addr,
}
}
2 => {
let value_offset =
u32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
*pos += 4;
SymbolTableCache::SoftLink { value_offset }
}
_ => SymbolTableCache::Nothing,
};
*pos = scratch_start + 16;
Ok(SymbolTableEntry {
name_offset,
obj_header_addr,
cache,
})
}
pub fn detect_superblock_version(buf: &[u8]) -> FormatResult<u8> {
if buf.len() < 9 {
return Err(FormatError::BufferTooShort {
needed: 9,
available: buf.len(),
});
}
if buf[0..8] != HDF5_SIGNATURE {
return Err(FormatError::InvalidSignature);
}
Ok(buf[8])
}
#[cfg(test)]
mod tests_v0v1 {
use super::*;
use crate::format::UNDEF_ADDR;
fn build_v0_superblock(
root_obj_header_addr: u64,
btree_addr: u64,
heap_addr: u64,
eof: u64,
) -> Vec<u8> {
let sizeof_addr: usize = 8;
let sizeof_size: usize = 8;
let mut buf = Vec::new();
buf.extend_from_slice(&HDF5_SIGNATURE);
buf.push(0);
buf.push(0);
buf.push(0);
buf.push(0);
buf.push(0);
buf.push(sizeof_addr as u8);
buf.push(sizeof_size as u8);
buf.push(0);
buf.extend_from_slice(&4u16.to_le_bytes());
buf.extend_from_slice(&32u16.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(&0u64.to_le_bytes()[..sizeof_addr]);
buf.extend_from_slice(&UNDEF_ADDR.to_le_bytes()[..sizeof_addr]);
buf.extend_from_slice(&eof.to_le_bytes()[..sizeof_addr]);
buf.extend_from_slice(&UNDEF_ADDR.to_le_bytes()[..sizeof_addr]);
buf.extend_from_slice(&0u64.to_le_bytes()[..sizeof_size]);
buf.extend_from_slice(&root_obj_header_addr.to_le_bytes()[..sizeof_addr]);
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(&btree_addr.to_le_bytes()[..sizeof_addr]);
buf.extend_from_slice(&heap_addr.to_le_bytes()[..sizeof_addr]);
buf
}
#[test]
fn a_v0_superblock_re_emits_the_bytes_libhdf5_wrote() {
let buf = build_v0_superblock(0x60, 0x88, 0x2a8, 4896);
let sb = SuperblockV0V1::decode(&buf).unwrap();
assert_eq!(sb.encoded_size(), buf.len());
assert_eq!(sb.encode(), buf);
}
#[test]
fn a_v1_superblock_re_emits_its_indexed_storage_k() {
let mut buf = build_v0_superblock(0x60, 0x88, 0x2a8, 4896);
buf[8] = 1;
buf.splice(24..24, [0x40u8, 0x00, 0x00, 0x00]);
let sb = SuperblockV0V1::decode(&buf).unwrap();
assert_eq!(sb.version, 1);
assert_eq!(sb.indexed_storage_k, Some(0x40));
assert_eq!(sb.encode(), buf);
}
#[test]
fn every_symbol_table_cache_shape_round_trips() {
for cache in [
SymbolTableCache::Nothing,
SymbolTableCache::SymbolTable {
btree_addr: 0x88,
heap_addr: 0x2a8,
},
SymbolTableCache::SoftLink { value_offset: 24 },
] {
let entry = SymbolTableEntry {
name_offset: 8,
obj_header_addr: 0x320,
cache: cache.clone(),
};
let mut buf = Vec::new();
encode_symbol_table_entry(&mut buf, &entry, 8, 8);
assert_eq!(buf.len(), symbol_table_entry_size(8, 8));
let mut pos = 0;
let back = decode_symbol_table_entry(&buf, &mut pos, 8, 8).unwrap();
assert_eq!(pos, buf.len());
assert_eq!(back, entry);
}
}
#[test]
fn test_decode_v0() {
let buf = build_v0_superblock(0x100, 0x200, 0x300, 0x1000);
let sb = SuperblockV0V1::decode(&buf).expect("decode failed");
assert_eq!(sb.version, 0);
assert_eq!(sb.sizeof_offsets, 8);
assert_eq!(sb.sizeof_lengths, 8);
assert_eq!(sb.sym_leaf_k, 4);
assert_eq!(sb.btree_internal_k, 32);
assert_eq!(sb.file_consistency_flags, 0);
assert_eq!(sb.indexed_storage_k, None);
assert_eq!(sb.base_address, 0);
assert_eq!(sb.end_of_file_address, 0x1000);
assert_eq!(sb.root_symbol_table_entry.obj_header_addr, 0x100);
assert_eq!(
sb.root_symbol_table_entry.cached_symbol_table(),
Some((0x200, 0x300))
);
}
#[test]
fn test_decode_v1() {
let sizeof_addr: usize = 8;
let sizeof_size: usize = 8;
let mut buf = Vec::new();
buf.extend_from_slice(&HDF5_SIGNATURE);
buf.push(1); buf.push(0);
buf.push(0);
buf.push(0);
buf.push(0);
buf.push(sizeof_addr as u8);
buf.push(sizeof_size as u8);
buf.push(0);
buf.extend_from_slice(&4u16.to_le_bytes());
buf.extend_from_slice(&32u16.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(&16u16.to_le_bytes());
buf.extend_from_slice(&0u16.to_le_bytes());
buf.extend_from_slice(&0u64.to_le_bytes()[..sizeof_addr]);
buf.extend_from_slice(&UNDEF_ADDR.to_le_bytes()[..sizeof_addr]);
buf.extend_from_slice(&0x2000u64.to_le_bytes()[..sizeof_addr]);
buf.extend_from_slice(&UNDEF_ADDR.to_le_bytes()[..sizeof_addr]);
buf.extend_from_slice(&0u64.to_le_bytes()[..sizeof_size]);
buf.extend_from_slice(&0x100u64.to_le_bytes()[..sizeof_addr]);
buf.extend_from_slice(&1u32.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes());
buf.extend_from_slice(&0x200u64.to_le_bytes()[..sizeof_addr]);
buf.extend_from_slice(&0x300u64.to_le_bytes()[..sizeof_addr]);
let sb = SuperblockV0V1::decode(&buf).expect("decode failed");
assert_eq!(sb.version, 1);
assert_eq!(sb.indexed_storage_k, Some(16));
assert_eq!(
sb.root_symbol_table_entry.cached_symbol_table(),
Some((0x200, 0x300))
);
}
#[test]
fn test_detect_version() {
let v0 = build_v0_superblock(0x100, 0x200, 0x300, 0x1000);
assert_eq!(detect_superblock_version(&v0).unwrap(), 0);
let sb_v3 = SuperblockV2V3 {
version: SUPERBLOCK_V3,
sizeof_offsets: 8,
sizeof_lengths: 8,
file_consistency_flags: 0,
base_address: 0,
superblock_extension_address: UNDEF_ADDR,
end_of_file_address: 4096,
root_group_object_header_address: 48,
};
let v3 = sb_v3.encode();
assert_eq!(detect_superblock_version(&v3).unwrap(), 3);
}
#[test]
fn test_bad_sig() {
let mut buf = build_v0_superblock(0x100, 0x200, 0x300, 0x1000);
buf[0] = 0;
assert!(matches!(
SuperblockV0V1::decode(&buf).unwrap_err(),
FormatError::InvalidSignature
));
}
#[test]
fn test_bad_version() {
let mut buf = build_v0_superblock(0x100, 0x200, 0x300, 0x1000);
buf[8] = 5; assert!(matches!(
SuperblockV0V1::decode(&buf).unwrap_err(),
FormatError::InvalidVersion(5)
));
}
}