Skip to main content

rust_hdf5/format/
symbol_table.rs

1//! Symbol Table Node (SNOD) decode (for reading legacy HDF5 files).
2//!
3//! In v0/v1 groups, child objects are stored in a B-tree that points to
4//! symbol table nodes (SNODs). Each SNOD contains an array of symbol
5//! table entries, each describing one child object.
6//!
7//! Layout:
8//! ```text
9//! "SNOD" (4 bytes)
10//! version: 1 byte (1)
11//! reserved: 1 byte
12//! num_symbols: u16 LE
13//! entries: num_symbols * symbol_table_entry
14//! ```
15
16use crate::format::superblock::{decode_symbol_table_entry, SymbolTableEntry};
17use crate::format::{FormatError, FormatResult};
18
19/// The 4-byte SNOD signature.
20pub const SNOD_SIGNATURE: [u8; 4] = *b"SNOD";
21
22/// A decoded symbol table node.
23#[derive(Debug, Clone)]
24pub struct SymbolTableNode {
25    /// The symbol table entries in this node.
26    pub entries: Vec<SymbolTableEntry>,
27}
28
29impl SymbolTableNode {
30    /// Encode this node into a block of exactly `node_size` bytes.
31    ///
32    /// A SNOD is a fixed-size record — `H5G_NODE_SIZE(f)`, which
33    /// [`BTreeV1Config::symbol_table_node_size`] computes — whatever fraction
34    /// of it the entries fill, because the B-tree above it allocates every
35    /// leaf the same width. The unused tail is zeroed rather than left as it
36    /// was: `H5G__node_create` calloc's the entry array, so a node libhdf5
37    /// wrote never carries a stale entry past `num_symbols`.
38    ///
39    /// [`BTreeV1Config::symbol_table_node_size`]: crate::format::btree_v1::BTreeV1Config::symbol_table_node_size
40    pub fn encode(
41        &self,
42        node_size: usize,
43        sizeof_addr: usize,
44        sizeof_size: usize,
45    ) -> FormatResult<Vec<u8>> {
46        let entry_size =
47            crate::format::superblock::symbol_table_entry_size(sizeof_addr, sizeof_size);
48        let needed = 8 + self.entries.len() * entry_size;
49        if needed > node_size {
50            return Err(FormatError::InvalidData(format!(
51                "symbol table node holds {} entries, {needed} bytes, more than the \
52                 {node_size}-byte record the file's 'sym_leaf_k' allows",
53                self.entries.len()
54            )));
55        }
56        let Ok(num_symbols) = u16::try_from(self.entries.len()) else {
57            return Err(FormatError::InvalidData(format!(
58                "symbol table node holds {} entries, over the 2-byte count field",
59                self.entries.len()
60            )));
61        };
62
63        let mut buf = Vec::with_capacity(node_size);
64        buf.extend_from_slice(&SNOD_SIGNATURE);
65        buf.push(1); // version
66        buf.push(0); // reserved
67        buf.extend_from_slice(&num_symbols.to_le_bytes());
68        for entry in &self.entries {
69            crate::format::superblock::encode_symbol_table_entry(
70                &mut buf,
71                entry,
72                sizeof_addr,
73                sizeof_size,
74            );
75        }
76        buf.resize(node_size, 0);
77        Ok(buf)
78    }
79
80    /// Decode a symbol table node from `buf`.
81    ///
82    /// `sizeof_addr` and `sizeof_size` come from the superblock, and
83    /// `max_entries` is `2 * sym_leaf_k` — the node's fixed capacity
84    /// (`H5Gpkg.h` `H5G_NODE_SIZE`). A node declaring more than that is
85    /// corrupt, not merely unusual, so it is rejected before its entries are
86    /// read.
87    pub fn decode(
88        buf: &[u8],
89        sizeof_addr: usize,
90        sizeof_size: usize,
91        max_entries: u16,
92    ) -> FormatResult<Self> {
93        if buf.len() < 8 {
94            return Err(FormatError::BufferTooShort {
95                needed: 8,
96                available: buf.len(),
97            });
98        }
99
100        if buf[0..4] != SNOD_SIGNATURE {
101            return Err(FormatError::InvalidSignature);
102        }
103
104        let version = buf[4];
105        if version != 1 {
106            return Err(FormatError::InvalidVersion(version));
107        }
108
109        // buf[5] reserved
110        let num_symbols = u16::from_le_bytes([buf[6], buf[7]]) as usize;
111        if num_symbols > max_entries as usize {
112            return Err(FormatError::InvalidData(format!(
113                "symbol table node declares {num_symbols} entries, capacity is {max_entries}"
114            )));
115        }
116
117        // Each entry: sizeof_size + sizeof_addr + 4 + 4 + 16 bytes
118        let entry_size = sizeof_size + sizeof_addr + 4 + 4 + 16;
119        let needed = 8 + num_symbols * entry_size;
120        if buf.len() < needed {
121            return Err(FormatError::BufferTooShort {
122                needed,
123                available: buf.len(),
124            });
125        }
126
127        let mut pos = 8;
128        let mut entries = Vec::with_capacity(num_symbols);
129
130        for _ in 0..num_symbols {
131            let entry = decode_symbol_table_entry(buf, &mut pos, sizeof_addr, sizeof_size)?;
132            entries.push(entry);
133        }
134
135        Ok(SymbolTableNode { entries })
136    }
137}
138
139// ======================================================================= tests
140
141#[cfg(test)]
142mod tests {
143    use super::*;
144    use crate::format::superblock::SymbolTableCache;
145    use crate::format::UNDEF_ADDR;
146
147    fn build_snod(
148        entries: &[(u64, u64, u32, u64, u64)],
149        sizeof_addr: usize,
150        sizeof_size: usize,
151    ) -> Vec<u8> {
152        let mut buf = Vec::new();
153        buf.extend_from_slice(&SNOD_SIGNATURE);
154        buf.push(1); // version
155        buf.push(0); // reserved
156        buf.extend_from_slice(&(entries.len() as u16).to_le_bytes());
157
158        for &(name_offset, obj_header_addr, cache_type, btree_addr, heap_addr) in entries {
159            // name_offset
160            buf.extend_from_slice(&name_offset.to_le_bytes()[..sizeof_size]);
161            // obj_header_addr
162            buf.extend_from_slice(&obj_header_addr.to_le_bytes()[..sizeof_addr]);
163            // cache_type
164            buf.extend_from_slice(&cache_type.to_le_bytes());
165            // reserved
166            buf.extend_from_slice(&0u32.to_le_bytes());
167            // scratch pad (16 bytes)
168            let mut scratch = Vec::new();
169            match cache_type {
170                1 => {
171                    scratch.extend_from_slice(&btree_addr.to_le_bytes()[..sizeof_addr]);
172                    scratch.extend_from_slice(&heap_addr.to_le_bytes()[..sizeof_addr]);
173                }
174                // A soft-link entry caches the 4-byte heap offset of its
175                // value string; `btree_addr` carries it in this builder.
176                2 => scratch.extend_from_slice(&(btree_addr as u32).to_le_bytes()),
177                _ => {}
178            }
179            scratch.resize(16, 0);
180            buf.extend_from_slice(&scratch);
181        }
182
183        buf
184    }
185
186    #[test]
187    fn decode_basic() {
188        let snod = build_snod(
189            &[
190                (8, 0x100, 0, UNDEF_ADDR, UNDEF_ADDR), // dataset
191                (16, 0x200, 1, 0x300, 0x400),          // group
192            ],
193            8,
194            8,
195        );
196        let node = SymbolTableNode::decode(&snod, 8, 8, 8).unwrap();
197        assert_eq!(node.entries.len(), 2);
198        assert_eq!(node.entries[0].name_offset, 8);
199        assert_eq!(node.entries[0].obj_header_addr, 0x100);
200        assert_eq!(node.entries[0].cache, SymbolTableCache::Nothing);
201        assert_eq!(
202            node.entries[1].cache,
203            SymbolTableCache::SymbolTable {
204                btree_addr: 0x300,
205                heap_addr: 0x400,
206            }
207        );
208    }
209
210    /// A `H5G_CACHED_SLINK` entry names no object; its scratch pad holds the
211    /// 4-byte offset of the link's value in the group's local heap. Reading
212    /// it as a cached B-tree/heap pair (the old shape) lost the offset and
213    /// left an entry whose object header address is undefined — which is how
214    /// a soft link in a v0/v1 group vanished from the listing.
215    #[test]
216    fn decode_soft_link_entry() {
217        let snod = build_snod(&[(16, UNDEF_ADDR, 2, 24, 0)], 8, 8);
218        let node = SymbolTableNode::decode(
219            &snod,
220            8,
221            8,
222            crate::format::btree_v1::BTreeV1Config::default().sym_leaf_max_entries(),
223        )
224        .unwrap();
225        assert_eq!(node.entries.len(), 1);
226        assert_eq!(node.entries[0].name_offset, 16);
227        assert_eq!(node.entries[0].obj_header_addr, UNDEF_ADDR);
228        assert_eq!(
229            node.entries[0].cache,
230            SymbolTableCache::SoftLink { value_offset: 24 }
231        );
232        assert_eq!(node.entries[0].cached_symbol_table(), None);
233    }
234
235    #[test]
236    fn decode_empty() {
237        let snod = build_snod(&[], 8, 8);
238        let node = SymbolTableNode::decode(&snod, 8, 8, 8).unwrap();
239        assert!(node.entries.is_empty());
240    }
241
242    #[test]
243    fn decode_bad_sig() {
244        let mut snod = build_snod(&[], 8, 8);
245        snod[0] = b'X';
246        assert!(matches!(
247            SymbolTableNode::decode(&snod, 8, 8, 8).unwrap_err(),
248            FormatError::InvalidSignature
249        ));
250    }
251
252    #[test]
253    fn decode_bad_version() {
254        let mut snod = build_snod(&[], 8, 8);
255        snod[4] = 2;
256        assert!(matches!(
257            SymbolTableNode::decode(&snod, 8, 8, 8).unwrap_err(),
258            FormatError::InvalidVersion(2)
259        ));
260    }
261
262    #[test]
263    fn decode_too_short() {
264        assert!(matches!(
265            SymbolTableNode::decode(&[0u8; 4], 8, 8, 8).unwrap_err(),
266            FormatError::BufferTooShort { .. }
267        ));
268    }
269
270    /// The root group's SNOD in a file h5py wrote with no `libver` argument,
271    /// byte for byte: three plain hard links, and 208 zero bytes of unused
272    /// capacity behind them.
273    #[test]
274    fn an_encoded_snod_matches_the_bytes_libhdf5_wrote() {
275        let node = SymbolTableNode {
276            entries: vec![
277                SymbolTableEntry {
278                    name_offset: 8,
279                    obj_header_addr: 0x320,
280                    cache: SymbolTableCache::Nothing,
281                },
282                SymbolTableEntry {
283                    name_offset: 16,
284                    obj_header_addr: 0x578,
285                    cache: SymbolTableCache::Nothing,
286                },
287                SymbolTableEntry {
288                    name_offset: 24,
289                    obj_header_addr: 0x688,
290                    cache: SymbolTableCache::Nothing,
291                },
292            ],
293        };
294        let node_size =
295            crate::format::btree_v1::BTreeV1Config::default().symbol_table_node_size(8, 8);
296        assert_eq!(node_size, 328);
297        let encoded = node.encode(node_size, 8, 8).unwrap();
298        let mut expected = Vec::new();
299        expected.extend_from_slice(b"SNOD");
300        expected.extend_from_slice(&[1, 0, 3, 0]); // version 1, reserved, 3 symbols
301        for (name_offset, addr) in [(8u64, 0x320u64), (16, 0x578), (24, 0x688)] {
302            expected.extend_from_slice(&name_offset.to_le_bytes());
303            expected.extend_from_slice(&addr.to_le_bytes());
304            expected.extend_from_slice(&0u32.to_le_bytes()); // cache type: nothing
305            expected.extend_from_slice(&0u32.to_le_bytes()); // reserved
306            expected.extend_from_slice(&[0u8; 16]); // scratch pad
307        }
308        assert_eq!(expected.len(), 8 + 3 * 40);
309        assert_eq!(&encoded[..expected.len()], &expected[..]);
310        assert!(encoded[expected.len()..].iter().all(|&b| b == 0));
311        assert_eq!(encoded.len(), node_size);
312    }
313
314    /// The cached-symbol-table and soft-link scratch pads survive the round
315    /// trip: a group child keeps its B-tree/heap pair, a soft link keeps the
316    /// heap offset of its value.
317    #[test]
318    fn an_encoded_snod_round_trips_every_scratch_pad_shape() {
319        let node = SymbolTableNode {
320            entries: vec![
321                SymbolTableEntry {
322                    name_offset: 8,
323                    obj_header_addr: 0x320,
324                    cache: SymbolTableCache::SymbolTable {
325                        btree_addr: 0x348,
326                        heap_addr: 0x568,
327                    },
328                },
329                SymbolTableEntry {
330                    name_offset: 16,
331                    obj_header_addr: UNDEF_ADDR,
332                    cache: SymbolTableCache::SoftLink { value_offset: 24 },
333                },
334            ],
335        };
336        let cfg = crate::format::btree_v1::BTreeV1Config::default();
337        let node_size = cfg.symbol_table_node_size(8, 8);
338        let encoded = node.encode(node_size, 8, 8).unwrap();
339        let decoded = SymbolTableNode::decode(&encoded, 8, 8, cfg.sym_leaf_max_entries()).unwrap();
340        assert_eq!(decoded.entries.len(), 2);
341        assert_eq!(decoded.entries[0].cache, node.entries[0].cache);
342        assert_eq!(decoded.entries[1].cache, node.entries[1].cache);
343        assert_eq!(decoded.entries[1].obj_header_addr, UNDEF_ADDR);
344    }
345
346    /// A SNOD is a fixed-width record, so overfilling it is a layout error the
347    /// encoder must refuse rather than a buffer it can grow.
348    #[test]
349    fn a_snod_refuses_more_entries_than_its_record_holds() {
350        let cfg = crate::format::btree_v1::BTreeV1Config::default();
351        let node = SymbolTableNode {
352            entries: (0..=cfg.sym_leaf_max_entries())
353                .map(|i| SymbolTableEntry {
354                    name_offset: u64::from(i) * 8,
355                    obj_header_addr: 0x100,
356                    cache: SymbolTableCache::Nothing,
357                })
358                .collect(),
359        };
360        assert!(matches!(
361            node.encode(cfg.symbol_table_node_size(8, 8), 8, 8)
362                .unwrap_err(),
363            FormatError::InvalidData(_)
364        ));
365    }
366
367    #[test]
368    fn decode_4byte() {
369        let snod = build_snod(&[(4, 0x80, 0, UNDEF_ADDR, UNDEF_ADDR)], 4, 4);
370        let node = SymbolTableNode::decode(&snod, 4, 4, 8).unwrap();
371        assert_eq!(node.entries.len(), 1);
372        assert_eq!(node.entries[0].name_offset, 4);
373        assert_eq!(node.entries[0].obj_header_addr, 0x80);
374    }
375}