use crate::format::superblock::{decode_symbol_table_entry, SymbolTableEntry};
use crate::format::{FormatError, FormatResult};
pub const SNOD_SIGNATURE: [u8; 4] = *b"SNOD";
#[derive(Debug, Clone)]
pub struct SymbolTableNode {
pub entries: Vec<SymbolTableEntry>,
}
impl SymbolTableNode {
pub fn encode(
&self,
node_size: usize,
sizeof_addr: usize,
sizeof_size: usize,
) -> FormatResult<Vec<u8>> {
let entry_size =
crate::format::superblock::symbol_table_entry_size(sizeof_addr, sizeof_size);
let needed = 8 + self.entries.len() * entry_size;
if needed > node_size {
return Err(FormatError::InvalidData(format!(
"symbol table node holds {} entries, {needed} bytes, more than the \
{node_size}-byte record the file's 'sym_leaf_k' allows",
self.entries.len()
)));
}
let Ok(num_symbols) = u16::try_from(self.entries.len()) else {
return Err(FormatError::InvalidData(format!(
"symbol table node holds {} entries, over the 2-byte count field",
self.entries.len()
)));
};
let mut buf = Vec::with_capacity(node_size);
buf.extend_from_slice(&SNOD_SIGNATURE);
buf.push(1); buf.push(0); buf.extend_from_slice(&num_symbols.to_le_bytes());
for entry in &self.entries {
crate::format::superblock::encode_symbol_table_entry(
&mut buf,
entry,
sizeof_addr,
sizeof_size,
);
}
buf.resize(node_size, 0);
Ok(buf)
}
pub fn decode(
buf: &[u8],
sizeof_addr: usize,
sizeof_size: usize,
max_entries: u16,
) -> FormatResult<Self> {
if buf.len() < 8 {
return Err(FormatError::BufferTooShort {
needed: 8,
available: buf.len(),
});
}
if buf[0..4] != SNOD_SIGNATURE {
return Err(FormatError::InvalidSignature);
}
let version = buf[4];
if version != 1 {
return Err(FormatError::InvalidVersion(version));
}
let num_symbols = u16::from_le_bytes([buf[6], buf[7]]) as usize;
if num_symbols > max_entries as usize {
return Err(FormatError::InvalidData(format!(
"symbol table node declares {num_symbols} entries, capacity is {max_entries}"
)));
}
let entry_size = sizeof_size + sizeof_addr + 4 + 4 + 16;
let needed = 8 + num_symbols * entry_size;
if buf.len() < needed {
return Err(FormatError::BufferTooShort {
needed,
available: buf.len(),
});
}
let mut pos = 8;
let mut entries = Vec::with_capacity(num_symbols);
for _ in 0..num_symbols {
let entry = decode_symbol_table_entry(buf, &mut pos, sizeof_addr, sizeof_size)?;
entries.push(entry);
}
Ok(SymbolTableNode { entries })
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::format::superblock::SymbolTableCache;
use crate::format::UNDEF_ADDR;
fn build_snod(
entries: &[(u64, u64, u32, u64, u64)],
sizeof_addr: usize,
sizeof_size: usize,
) -> Vec<u8> {
let mut buf = Vec::new();
buf.extend_from_slice(&SNOD_SIGNATURE);
buf.push(1); buf.push(0); buf.extend_from_slice(&(entries.len() as u16).to_le_bytes());
for &(name_offset, obj_header_addr, cache_type, btree_addr, heap_addr) in entries {
buf.extend_from_slice(&name_offset.to_le_bytes()[..sizeof_size]);
buf.extend_from_slice(&obj_header_addr.to_le_bytes()[..sizeof_addr]);
buf.extend_from_slice(&cache_type.to_le_bytes());
buf.extend_from_slice(&0u32.to_le_bytes());
let mut scratch = Vec::new();
match cache_type {
1 => {
scratch.extend_from_slice(&btree_addr.to_le_bytes()[..sizeof_addr]);
scratch.extend_from_slice(&heap_addr.to_le_bytes()[..sizeof_addr]);
}
2 => scratch.extend_from_slice(&(btree_addr as u32).to_le_bytes()),
_ => {}
}
scratch.resize(16, 0);
buf.extend_from_slice(&scratch);
}
buf
}
#[test]
fn decode_basic() {
let snod = build_snod(
&[
(8, 0x100, 0, UNDEF_ADDR, UNDEF_ADDR), (16, 0x200, 1, 0x300, 0x400), ],
8,
8,
);
let node = SymbolTableNode::decode(&snod, 8, 8, 8).unwrap();
assert_eq!(node.entries.len(), 2);
assert_eq!(node.entries[0].name_offset, 8);
assert_eq!(node.entries[0].obj_header_addr, 0x100);
assert_eq!(node.entries[0].cache, SymbolTableCache::Nothing);
assert_eq!(
node.entries[1].cache,
SymbolTableCache::SymbolTable {
btree_addr: 0x300,
heap_addr: 0x400,
}
);
}
#[test]
fn decode_soft_link_entry() {
let snod = build_snod(&[(16, UNDEF_ADDR, 2, 24, 0)], 8, 8);
let node = SymbolTableNode::decode(
&snod,
8,
8,
crate::format::btree_v1::BTreeV1Config::default().sym_leaf_max_entries(),
)
.unwrap();
assert_eq!(node.entries.len(), 1);
assert_eq!(node.entries[0].name_offset, 16);
assert_eq!(node.entries[0].obj_header_addr, UNDEF_ADDR);
assert_eq!(
node.entries[0].cache,
SymbolTableCache::SoftLink { value_offset: 24 }
);
assert_eq!(node.entries[0].cached_symbol_table(), None);
}
#[test]
fn decode_empty() {
let snod = build_snod(&[], 8, 8);
let node = SymbolTableNode::decode(&snod, 8, 8, 8).unwrap();
assert!(node.entries.is_empty());
}
#[test]
fn decode_bad_sig() {
let mut snod = build_snod(&[], 8, 8);
snod[0] = b'X';
assert!(matches!(
SymbolTableNode::decode(&snod, 8, 8, 8).unwrap_err(),
FormatError::InvalidSignature
));
}
#[test]
fn decode_bad_version() {
let mut snod = build_snod(&[], 8, 8);
snod[4] = 2;
assert!(matches!(
SymbolTableNode::decode(&snod, 8, 8, 8).unwrap_err(),
FormatError::InvalidVersion(2)
));
}
#[test]
fn decode_too_short() {
assert!(matches!(
SymbolTableNode::decode(&[0u8; 4], 8, 8, 8).unwrap_err(),
FormatError::BufferTooShort { .. }
));
}
#[test]
fn an_encoded_snod_matches_the_bytes_libhdf5_wrote() {
let node = SymbolTableNode {
entries: vec![
SymbolTableEntry {
name_offset: 8,
obj_header_addr: 0x320,
cache: SymbolTableCache::Nothing,
},
SymbolTableEntry {
name_offset: 16,
obj_header_addr: 0x578,
cache: SymbolTableCache::Nothing,
},
SymbolTableEntry {
name_offset: 24,
obj_header_addr: 0x688,
cache: SymbolTableCache::Nothing,
},
],
};
let node_size =
crate::format::btree_v1::BTreeV1Config::default().symbol_table_node_size(8, 8);
assert_eq!(node_size, 328);
let encoded = node.encode(node_size, 8, 8).unwrap();
let mut expected = Vec::new();
expected.extend_from_slice(b"SNOD");
expected.extend_from_slice(&[1, 0, 3, 0]); for (name_offset, addr) in [(8u64, 0x320u64), (16, 0x578), (24, 0x688)] {
expected.extend_from_slice(&name_offset.to_le_bytes());
expected.extend_from_slice(&addr.to_le_bytes());
expected.extend_from_slice(&0u32.to_le_bytes()); expected.extend_from_slice(&0u32.to_le_bytes()); expected.extend_from_slice(&[0u8; 16]); }
assert_eq!(expected.len(), 8 + 3 * 40);
assert_eq!(&encoded[..expected.len()], &expected[..]);
assert!(encoded[expected.len()..].iter().all(|&b| b == 0));
assert_eq!(encoded.len(), node_size);
}
#[test]
fn an_encoded_snod_round_trips_every_scratch_pad_shape() {
let node = SymbolTableNode {
entries: vec![
SymbolTableEntry {
name_offset: 8,
obj_header_addr: 0x320,
cache: SymbolTableCache::SymbolTable {
btree_addr: 0x348,
heap_addr: 0x568,
},
},
SymbolTableEntry {
name_offset: 16,
obj_header_addr: UNDEF_ADDR,
cache: SymbolTableCache::SoftLink { value_offset: 24 },
},
],
};
let cfg = crate::format::btree_v1::BTreeV1Config::default();
let node_size = cfg.symbol_table_node_size(8, 8);
let encoded = node.encode(node_size, 8, 8).unwrap();
let decoded = SymbolTableNode::decode(&encoded, 8, 8, cfg.sym_leaf_max_entries()).unwrap();
assert_eq!(decoded.entries.len(), 2);
assert_eq!(decoded.entries[0].cache, node.entries[0].cache);
assert_eq!(decoded.entries[1].cache, node.entries[1].cache);
assert_eq!(decoded.entries[1].obj_header_addr, UNDEF_ADDR);
}
#[test]
fn a_snod_refuses_more_entries_than_its_record_holds() {
let cfg = crate::format::btree_v1::BTreeV1Config::default();
let node = SymbolTableNode {
entries: (0..=cfg.sym_leaf_max_entries())
.map(|i| SymbolTableEntry {
name_offset: u64::from(i) * 8,
obj_header_addr: 0x100,
cache: SymbolTableCache::Nothing,
})
.collect(),
};
assert!(matches!(
node.encode(cfg.symbol_table_node_size(8, 8), 8, 8)
.unwrap_err(),
FormatError::InvalidData(_)
));
}
#[test]
fn decode_4byte() {
let snod = build_snod(&[(4, 0x80, 0, UNDEF_ADDR, UNDEF_ADDR)], 4, 4);
let node = SymbolTableNode::decode(&snod, 4, 4, 8).unwrap();
assert_eq!(node.entries.len(), 1);
assert_eq!(node.entries[0].name_offset, 4);
assert_eq!(node.entries[0].obj_header_addr, 0x80);
}
}