Skip to main content

rust_hdf5/format/
superblock.rs

1/// Superblock encode/decode for HDF5 files.
2///
3/// The superblock is always at offset 0 (or at a user-hint offset) and
4/// contains the file-level metadata: version, size parameters, and addresses
5/// of the root group and end-of-file.
6///
7/// This module supports:
8/// - v2/v3 superblocks (encode + decode)
9/// - v0/v1 superblocks (decode only, for reading legacy files)
10use crate::format::checksum::checksum_metadata;
11use crate::format::{FormatError, FormatResult};
12
13/// The 8-byte HDF5 file signature that begins every superblock.
14pub const HDF5_SIGNATURE: [u8; 8] = [0x89, 0x48, 0x44, 0x46, 0x0d, 0x0a, 0x1a, 0x0a];
15
16/// `HDF5_SUPERBLOCK_VERSION_DEF`, the version `H5F__super_init` starts from
17/// and the one `HDF5_superblock_ver_bounds` gives `H5F_LIBVER_EARLIEST`.
18pub const SUPERBLOCK_V0: u8 = 0;
19
20/// Superblock version 2.
21pub const SUPERBLOCK_V2: u8 = 2;
22
23/// Superblock version 3 (adds SWMR support).
24pub const SUPERBLOCK_V3: u8 = 3;
25
26/// File consistency flag: file was opened for write access.
27pub const FLAG_WRITE_ACCESS: u8 = 0x01;
28
29/// File consistency flag: file is consistent / was properly closed.
30pub const FLAG_FILE_OK: u8 = 0x02;
31
32/// File consistency flag: file was opened for single-writer/multi-reader.
33pub const FLAG_SWMR_WRITE: u8 = 0x04;
34
35/// Superblock v2/v3 structure.
36///
37/// Layout (O = sizeof_offsets):
38/// ```text
39/// [0..8]              Signature (8 bytes)
40/// [8]                 Version (1 byte)
41/// [9]                 Size of Offsets (1 byte)
42/// [10]                Size of Lengths (1 byte)
43/// [11]                File Consistency Flags (1 byte)
44/// [12..12+O]          Base Address (O bytes)
45/// [12+O..12+2O]       Superblock Extension Address (O bytes)
46/// [12+2O..12+3O]      End of File Address (O bytes)
47/// [12+3O..12+4O]      Root Group Object Header Address (O bytes)
48/// [12+4O..12+4O+4]    Checksum (4 bytes)
49/// ```
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct SuperblockV2V3 {
52    /// Superblock version: 2 or 3.
53    pub version: u8,
54    /// Size of file offsets in bytes (typically 8).
55    pub sizeof_offsets: u8,
56    /// Size of file lengths in bytes (typically 8).
57    pub sizeof_lengths: u8,
58    /// File consistency flags (see `FLAG_*` constants).
59    pub file_consistency_flags: u8,
60    /// Base address of the file: the size of the userblock the superblock
61    /// follows, and the offset every other address in the file is measured
62    /// from. Usually 0.
63    pub base_address: u64,
64    /// Address of the superblock extension object header, or UNDEF.
65    pub superblock_extension_address: u64,
66    /// End-of-file address, measured from the start of the *file* — the one
67    /// field here that includes [`base_address`](Self::base_address).
68    /// `H5F__super_read` takes the allocated end as `end_of_file_address -
69    /// base_address` and calls the file truncated when the real end is below
70    /// it.
71    pub end_of_file_address: u64,
72    /// Address of the root group object header, relative to
73    /// [`base_address`](Self::base_address).
74    pub root_group_object_header_address: u64,
75}
76
77impl SuperblockV2V3 {
78    /// Returns the total encoded size in bytes: 12 + 4*O + 4 (checksum).
79    pub fn encoded_size(&self) -> usize {
80        Self::size_for(self.sizeof_offsets)
81    }
82
83    /// The size a superblock with this offset width encodes to. Versions 2
84    /// and 3 have the same layout, so a writer that must reserve the space
85    /// before it knows which of the two it will emit can ask for it here.
86    pub fn size_for(sizeof_offsets: u8) -> usize {
87        12 + 4 * (sizeof_offsets as usize) + 4
88    }
89
90    /// Encode the superblock to a byte vector, including the trailing checksum.
91    pub fn encode(&self) -> Vec<u8> {
92        let size = self.encoded_size();
93        let mut buf = Vec::with_capacity(size);
94
95        // Signature
96        buf.extend_from_slice(&HDF5_SIGNATURE);
97        // Version
98        buf.push(self.version);
99        // Size of Offsets
100        buf.push(self.sizeof_offsets);
101        // Size of Lengths
102        buf.push(self.sizeof_lengths);
103        // File Consistency Flags
104        buf.push(self.file_consistency_flags);
105
106        // Addresses -- encode as little-endian with sizeof_offsets bytes
107        let o = self.sizeof_offsets as usize;
108        encode_offset(&mut buf, self.base_address, o);
109        encode_offset(&mut buf, self.superblock_extension_address, o);
110        encode_offset(&mut buf, self.end_of_file_address, o);
111        encode_offset(&mut buf, self.root_group_object_header_address, o);
112
113        // Checksum over everything before the checksum field
114        debug_assert_eq!(buf.len(), size - 4);
115        let cksum = checksum_metadata(&buf);
116        buf.extend_from_slice(&cksum.to_le_bytes());
117
118        debug_assert_eq!(buf.len(), size);
119        buf
120    }
121
122    /// Decode a superblock from a byte buffer. Verifies the signature, version,
123    /// and checksum. Returns the parsed superblock.
124    pub fn decode(buf: &[u8]) -> FormatResult<Self> {
125        // Minimum size check: we need at least the fixed 12-byte header to
126        // read sizeof_offsets before computing the full size.
127        if buf.len() < 12 {
128            return Err(FormatError::BufferTooShort {
129                needed: 12,
130                available: buf.len(),
131            });
132        }
133
134        // Signature
135        if buf[0..8] != HDF5_SIGNATURE {
136            return Err(FormatError::InvalidSignature);
137        }
138
139        // Version
140        let version = buf[8];
141        if version != SUPERBLOCK_V2 && version != SUPERBLOCK_V3 {
142            return Err(FormatError::InvalidVersion(version));
143        }
144
145        let sizeof_offsets = buf[9];
146        let sizeof_lengths = buf[10];
147        let file_consistency_flags = buf[11];
148        validate_sizeof(sizeof_offsets, sizeof_lengths)?;
149
150        let o = sizeof_offsets as usize;
151        let total_size = 12 + 4 * o + 4;
152        if buf.len() < total_size {
153            return Err(FormatError::BufferTooShort {
154                needed: total_size,
155                available: buf.len(),
156            });
157        }
158
159        // Verify checksum
160        let data_end = total_size - 4;
161        let stored_cksum = u32::from_le_bytes([
162            buf[data_end],
163            buf[data_end + 1],
164            buf[data_end + 2],
165            buf[data_end + 3],
166        ]);
167        let computed_cksum = checksum_metadata(&buf[..data_end]);
168        if stored_cksum != computed_cksum {
169            return Err(FormatError::ChecksumMismatch {
170                expected: stored_cksum,
171                computed: computed_cksum,
172            });
173        }
174
175        // Decode addresses
176        let mut pos = 12;
177        let base_address = decode_offset(buf, &mut pos, o);
178        let superblock_extension_address = decode_offset(buf, &mut pos, o);
179        let end_of_file_address = decode_offset(buf, &mut pos, o);
180        let root_group_object_header_address = decode_offset(buf, &mut pos, o);
181
182        Ok(SuperblockV2V3 {
183            version,
184            sizeof_offsets,
185            sizeof_lengths,
186            file_consistency_flags,
187            base_address,
188            superblock_extension_address,
189            end_of_file_address,
190            root_group_object_header_address,
191        })
192    }
193}
194
195/// Encode a u64 address as `size` little-endian bytes and append to `buf`.
196fn encode_offset(buf: &mut Vec<u8>, value: u64, size: usize) {
197    let bytes = value.to_le_bytes();
198    buf.extend_from_slice(&bytes[..size]);
199}
200
201/// Reject `sizeof_offsets` / `sizeof_lengths` values this crate cannot
202/// represent. libhdf5 permits 2, 4, 8, 16 and 32; this crate decodes
203/// addresses into a `u64`, so it supports only 2, 4 and 8. Validating here
204/// keeps every downstream `read_addr` / `read_size` helper (which copies
205/// `n` bytes into an 8-byte buffer) from panicking on a hostile file.
206fn validate_sizeof(sizeof_offsets: u8, sizeof_lengths: u8) -> FormatResult<()> {
207    for (name, v) in [
208        ("sizeof_offsets", sizeof_offsets),
209        ("sizeof_lengths", sizeof_lengths),
210    ] {
211        if !matches!(v, 2 | 4 | 8) {
212            return Err(FormatError::InvalidData(format!(
213                "unsupported superblock {name} = {v} (only 2, 4, 8 are supported)"
214            )));
215        }
216    }
217    Ok(())
218}
219
220/// Decode a little-endian address of `size` bytes from `buf` at `*pos`,
221/// advancing `*pos` past the consumed bytes.
222fn decode_offset(buf: &[u8], pos: &mut usize, size: usize) -> u64 {
223    let v = crate::format::bytes::read_le_uint(&buf[*pos..], size);
224    *pos += size;
225    v
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::format::UNDEF_ADDR;
232
233    #[test]
234    fn test_encoded_size() {
235        let sb = SuperblockV2V3 {
236            version: SUPERBLOCK_V3,
237            sizeof_offsets: 8,
238            sizeof_lengths: 8,
239            file_consistency_flags: 0,
240            base_address: 0,
241            superblock_extension_address: UNDEF_ADDR,
242            end_of_file_address: 4096,
243            root_group_object_header_address: 48,
244        };
245        // 12 + 4*8 + 4 = 48
246        assert_eq!(sb.encoded_size(), 48);
247    }
248
249    #[test]
250    fn test_roundtrip_v3_offset8() {
251        let original = SuperblockV2V3 {
252            version: SUPERBLOCK_V3,
253            sizeof_offsets: 8,
254            sizeof_lengths: 8,
255            file_consistency_flags: FLAG_FILE_OK,
256            base_address: 0,
257            superblock_extension_address: UNDEF_ADDR,
258            end_of_file_address: 0x1_0000,
259            root_group_object_header_address: 48,
260        };
261
262        let encoded = original.encode();
263        assert_eq!(encoded.len(), original.encoded_size());
264
265        // Verify signature
266        assert_eq!(&encoded[..8], &HDF5_SIGNATURE);
267
268        let decoded = SuperblockV2V3::decode(&encoded).expect("decode failed");
269        assert_eq!(decoded, original);
270    }
271
272    #[test]
273    fn test_roundtrip_v2_offset4() {
274        let original = SuperblockV2V3 {
275            version: SUPERBLOCK_V2,
276            sizeof_offsets: 4,
277            sizeof_lengths: 4,
278            file_consistency_flags: 0,
279            base_address: 0,
280            superblock_extension_address: 0xFFFF_FFFF,
281            end_of_file_address: 8192,
282            root_group_object_header_address: 28,
283        };
284
285        let encoded = original.encode();
286        // 12 + 4*4 + 4 = 32
287        assert_eq!(encoded.len(), 32);
288
289        let decoded = SuperblockV2V3::decode(&encoded).expect("decode failed");
290        assert_eq!(decoded, original);
291    }
292
293    #[test]
294    fn test_decode_bad_signature() {
295        let mut data = vec![0u8; 48];
296        // Wrong signature
297        data[0] = 0x00;
298        let err = SuperblockV2V3::decode(&data).unwrap_err();
299        assert!(matches!(err, FormatError::InvalidSignature));
300    }
301
302    #[test]
303    fn test_decode_bad_version() {
304        let sb = SuperblockV2V3 {
305            version: SUPERBLOCK_V3,
306            sizeof_offsets: 8,
307            sizeof_lengths: 8,
308            file_consistency_flags: 0,
309            base_address: 0,
310            superblock_extension_address: UNDEF_ADDR,
311            end_of_file_address: 4096,
312            root_group_object_header_address: 48,
313        };
314        let mut encoded = sb.encode();
315        // Corrupt version to 1
316        encoded[8] = 1;
317        let err = SuperblockV2V3::decode(&encoded).unwrap_err();
318        assert!(matches!(err, FormatError::InvalidVersion(1)));
319    }
320
321    #[test]
322    fn test_decode_checksum_mismatch() {
323        let sb = SuperblockV2V3 {
324            version: SUPERBLOCK_V3,
325            sizeof_offsets: 8,
326            sizeof_lengths: 8,
327            file_consistency_flags: 0,
328            base_address: 0,
329            superblock_extension_address: UNDEF_ADDR,
330            end_of_file_address: 4096,
331            root_group_object_header_address: 48,
332        };
333        let mut encoded = sb.encode();
334        // Corrupt a data byte
335        encoded[12] = 0xFF;
336        let err = SuperblockV2V3::decode(&encoded).unwrap_err();
337        assert!(matches!(err, FormatError::ChecksumMismatch { .. }));
338    }
339
340    #[test]
341    fn test_decode_buffer_too_short() {
342        let err = SuperblockV2V3::decode(&[0u8; 4]).unwrap_err();
343        assert!(matches!(err, FormatError::BufferTooShort { .. }));
344    }
345
346    #[test]
347    fn test_flags() {
348        let sb = SuperblockV2V3 {
349            version: SUPERBLOCK_V3,
350            sizeof_offsets: 8,
351            sizeof_lengths: 8,
352            file_consistency_flags: FLAG_WRITE_ACCESS | FLAG_SWMR_WRITE,
353            base_address: 0,
354            superblock_extension_address: UNDEF_ADDR,
355            end_of_file_address: 4096,
356            root_group_object_header_address: 48,
357        };
358        let encoded = sb.encode();
359        let decoded = SuperblockV2V3::decode(&encoded).unwrap();
360        assert_eq!(
361            decoded.file_consistency_flags,
362            FLAG_WRITE_ACCESS | FLAG_SWMR_WRITE
363        );
364    }
365
366    #[test]
367    fn test_roundtrip_with_extra_trailing_data() {
368        // decode should succeed even if the buffer is longer than needed
369        let sb = SuperblockV2V3 {
370            version: SUPERBLOCK_V3,
371            sizeof_offsets: 8,
372            sizeof_lengths: 8,
373            file_consistency_flags: 0,
374            base_address: 0,
375            superblock_extension_address: UNDEF_ADDR,
376            end_of_file_address: 4096,
377            root_group_object_header_address: 48,
378        };
379        let mut encoded = sb.encode();
380        encoded.extend_from_slice(&[0xAA; 100]); // trailing garbage
381        let decoded = SuperblockV2V3::decode(&encoded).unwrap();
382        assert_eq!(decoded, sb);
383    }
384}
385
386// =========================================================================
387// Superblock v0/v1 — decode only (for reading legacy HDF5 files)
388// =========================================================================
389
390/// The 16-byte scratch pad of a symbol table entry, read according to the
391/// entry's cache type (`H5G_cache_t` / `H5G__ent_decode`). Modelling it as a
392/// sum type keeps the soft-link case representable: a `H5G_CACHED_SLINK`
393/// entry has no object header at all, and its whole content is the offset of
394/// the link's value string in the group's local heap.
395#[derive(Debug, Clone, PartialEq, Eq)]
396pub enum SymbolTableCache {
397    /// `H5G_NOTHING_CACHED` — the scratch pad holds nothing.
398    Nothing,
399    /// `H5G_CACHED_STAB` — the child group's B-tree and local heap.
400    SymbolTable { btree_addr: u64, heap_addr: u64 },
401    /// `H5G_CACHED_SLINK` — this entry is a soft link, and the scratch pad
402    /// holds the offset of its value string in the group's local heap.
403    SoftLink { value_offset: u32 },
404}
405
406/// Symbol table entry, as stored in the root group's superblock (v0/v1).
407#[derive(Debug, Clone, PartialEq, Eq)]
408pub struct SymbolTableEntry {
409    /// Offset of the name in the local heap.
410    pub name_offset: u64,
411    /// Address of the object header. Undefined for a soft-link entry, which
412    /// names no object.
413    pub obj_header_addr: u64,
414    /// The scratch pad, decoded per this entry's cache type.
415    pub cache: SymbolTableCache,
416}
417
418impl SymbolTableEntry {
419    /// The cached B-tree and local heap of the child group this entry names,
420    /// or `None` when the scratch pad caches something else (or nothing).
421    pub fn cached_symbol_table(&self) -> Option<(u64, u64)> {
422        match self.cache {
423            SymbolTableCache::SymbolTable {
424                btree_addr,
425                heap_addr,
426            } => Some((btree_addr, heap_addr)),
427            _ => None,
428        }
429    }
430}
431
432/// Superblock v0/v1 structure (decode only).
433///
434/// Layout after 8-byte signature:
435/// ```text
436/// Byte 0: superblock version (0 or 1)
437/// Byte 1: free-space version (0)
438/// Byte 2: root group STE version (0)
439/// Byte 3: reserved (0)
440/// Byte 4: shared header version (0)
441/// Byte 5: sizeof_addr
442/// Byte 6: sizeof_size
443/// Byte 7: reserved (0)
444/// Bytes 8-9: sym_leaf_k (u16 LE)
445/// Bytes 10-11: btree_internal_k (u16 LE)
446/// Bytes 12-15: file_consistency_flags (u32 LE)
447/// [v1 only: bytes 16-17: indexed_storage_k (u16 LE), bytes 18-19: reserved]
448/// Then: base_addr(O), extension_addr(O), eof_addr(O), driver_addr(O)
449/// Then: root group symbol table entry
450/// ```
451#[derive(Debug, Clone, PartialEq, Eq)]
452pub struct SuperblockV0V1 {
453    pub version: u8,
454    pub sizeof_offsets: u8,
455    pub sizeof_lengths: u8,
456    pub file_consistency_flags: u32,
457    pub sym_leaf_k: u16,
458    pub btree_internal_k: u16,
459    pub indexed_storage_k: Option<u16>,
460    /// The userblock size; every other address in the file is measured from
461    /// it, and it is usually 0.
462    pub base_address: u64,
463    pub superblock_extension_address: u64,
464    /// Measured from the start of the *file*, unlike every other address —
465    /// see [`SuperblockV2V3::end_of_file_address`].
466    pub end_of_file_address: u64,
467    pub driver_info_address: u64,
468    pub root_symbol_table_entry: SymbolTableEntry,
469}
470
471impl SuperblockV0V1 {
472    /// The total encoded size in bytes, symbol table entry included.
473    pub fn encoded_size(&self) -> usize {
474        let o = self.sizeof_offsets as usize;
475        let s = self.sizeof_lengths as usize;
476        16 + 8 + if self.version == 1 { 4 } else { 0 } + 4 * o + symbol_table_entry_size(o, s)
477    }
478
479    /// Re-emit this superblock.
480    ///
481    /// The version is written back as it was read: an append to a classic file
482    /// leaves it classic, so a reader that could open the file before the
483    /// append can still open it after. The three sub-format version bytes
484    /// (free space, root symbol table entry, shared header) are 0 in every
485    /// file libhdf5 writes — `HDF5_FREESPACE_VERSION`, `HDF5_OBJECTDIR_VERSION`
486    /// and `HDF5_SHAREDHEADER_VERSION` are compile-time constants, not
487    /// per-file choices — so they are re-emitted as 0 rather than carried.
488    pub fn encode(&self) -> Vec<u8> {
489        let o = self.sizeof_offsets as usize;
490        let s = self.sizeof_lengths as usize;
491        let size = self.encoded_size();
492        let mut buf = Vec::with_capacity(size);
493
494        buf.extend_from_slice(&HDF5_SIGNATURE);
495        buf.push(self.version);
496        buf.push(0); // free-space storage version
497        buf.push(0); // root group symbol table entry version
498        buf.push(0); // reserved
499        buf.push(0); // shared header message format version
500        buf.push(self.sizeof_offsets);
501        buf.push(self.sizeof_lengths);
502        buf.push(0); // reserved
503        buf.extend_from_slice(&self.sym_leaf_k.to_le_bytes());
504        buf.extend_from_slice(&self.btree_internal_k.to_le_bytes());
505        buf.extend_from_slice(&self.file_consistency_flags.to_le_bytes());
506        if self.version == 1 {
507            // Only a version-1 superblock has the field; a version-0 file
508            // leaves the chunked-storage rank at the library default.
509            buf.extend_from_slice(&self.indexed_storage_k.unwrap_or(32).to_le_bytes());
510            buf.extend_from_slice(&[0u8; 2]); // reserved
511        }
512
513        encode_offset(&mut buf, self.base_address, o);
514        encode_offset(&mut buf, self.superblock_extension_address, o);
515        encode_offset(&mut buf, self.end_of_file_address, o);
516        encode_offset(&mut buf, self.driver_info_address, o);
517        encode_symbol_table_entry(&mut buf, &self.root_symbol_table_entry, o, s);
518
519        debug_assert_eq!(buf.len(), size);
520        buf
521    }
522
523    /// Decode a v0/v1 superblock from `buf`. The buffer must start at the
524    /// 8-byte HDF5 signature. Returns the parsed superblock.
525    pub fn decode(buf: &[u8]) -> FormatResult<Self> {
526        // Minimum: 8 (sig) + 8 (fixed header before addresses) = 16
527        if buf.len() < 16 {
528            return Err(FormatError::BufferTooShort {
529                needed: 16,
530                available: buf.len(),
531            });
532        }
533
534        // Signature
535        if buf[0..8] != HDF5_SIGNATURE {
536            return Err(FormatError::InvalidSignature);
537        }
538
539        let version = buf[8];
540        if version != 0 && version != 1 {
541            return Err(FormatError::InvalidVersion(version));
542        }
543
544        // buf[9] = free-space version (must be 0)
545        // buf[10] = root group STE version (must be 0)
546        // buf[11] = reserved
547        // buf[12] = shared header version (must be 0)
548        let sizeof_offsets = buf[13];
549        let sizeof_lengths = buf[14];
550        // buf[15] = reserved
551        validate_sizeof(sizeof_offsets, sizeof_lengths)?;
552
553        let o = sizeof_offsets as usize;
554        let mut pos = 16;
555
556        // Check we have enough for the remaining fixed fields
557        if buf.len() < pos + 4 {
558            return Err(FormatError::BufferTooShort {
559                needed: pos + 4,
560                available: buf.len(),
561            });
562        }
563
564        let sym_leaf_k = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
565        pos += 2;
566        let btree_internal_k = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
567        pos += 2;
568
569        if buf.len() < pos + 4 {
570            return Err(FormatError::BufferTooShort {
571                needed: pos + 4,
572                available: buf.len(),
573            });
574        }
575        let file_consistency_flags =
576            u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
577        pos += 4;
578
579        let indexed_storage_k = if version == 1 {
580            if buf.len() < pos + 4 {
581                return Err(FormatError::BufferTooShort {
582                    needed: pos + 4,
583                    available: buf.len(),
584                });
585            }
586            let k = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
587            pos += 2;
588            // 2 bytes reserved
589            pos += 2;
590            Some(k)
591        } else {
592            None
593        };
594
595        // 4 addresses, each sizeof_offsets bytes
596        let needed = pos + 4 * o;
597        if buf.len() < needed {
598            return Err(FormatError::BufferTooShort {
599                needed,
600                available: buf.len(),
601            });
602        }
603
604        let base_address = decode_offset(buf, &mut pos, o);
605        let superblock_extension_address = decode_offset(buf, &mut pos, o);
606        let end_of_file_address = decode_offset(buf, &mut pos, o);
607        let driver_info_address = decode_offset(buf, &mut pos, o);
608
609        // Root group symbol table entry
610        let ste = decode_symbol_table_entry(buf, &mut pos, o, sizeof_lengths as usize)?;
611
612        Ok(SuperblockV0V1 {
613            version,
614            sizeof_offsets,
615            sizeof_lengths,
616            file_consistency_flags,
617            sym_leaf_k,
618            btree_internal_k,
619            indexed_storage_k,
620            base_address,
621            superblock_extension_address,
622            end_of_file_address,
623            driver_info_address,
624            root_symbol_table_entry: ste,
625        })
626    }
627}
628
629/// A symbol table entry's on-disk size: name offset, object header address,
630/// cache type, reserved word and the 16-byte scratch pad (`H5G_SIZEOF_ENTRY`).
631pub fn symbol_table_entry_size(sizeof_addr: usize, sizeof_size: usize) -> usize {
632    sizeof_size + sizeof_addr + 4 + 4 + 16
633}
634
635/// Encode one symbol table entry, scratch pad included (`H5G__ent_encode`).
636///
637/// The scratch pad is a fixed 16 bytes whatever the cache type puts in it, and
638/// the bytes past what the type uses are zero: `H5G__ent_encode` memsets the
639/// whole pad before writing the cache, so a `H5G_NOTHING_CACHED` entry is 16
640/// zero bytes rather than whatever the entry held before.
641pub fn encode_symbol_table_entry(
642    out: &mut Vec<u8>,
643    entry: &SymbolTableEntry,
644    sizeof_addr: usize,
645    sizeof_size: usize,
646) {
647    let start = out.len();
648    encode_offset(out, entry.name_offset, sizeof_size);
649    encode_offset(out, entry.obj_header_addr, sizeof_addr);
650    let cache_type: u32 = match entry.cache {
651        SymbolTableCache::Nothing => 0,
652        SymbolTableCache::SymbolTable { .. } => 1,
653        SymbolTableCache::SoftLink { .. } => 2,
654    };
655    out.extend_from_slice(&cache_type.to_le_bytes());
656    out.extend_from_slice(&0u32.to_le_bytes()); // reserved
657    let scratch = out.len();
658    match entry.cache {
659        SymbolTableCache::Nothing => {}
660        SymbolTableCache::SymbolTable {
661            btree_addr,
662            heap_addr,
663        } => {
664            encode_offset(out, btree_addr, sizeof_addr);
665            encode_offset(out, heap_addr, sizeof_addr);
666        }
667        SymbolTableCache::SoftLink { value_offset } => {
668            out.extend_from_slice(&value_offset.to_le_bytes());
669        }
670    }
671    out.resize(scratch + 16, 0);
672    debug_assert_eq!(
673        out.len() - start,
674        symbol_table_entry_size(sizeof_addr, sizeof_size)
675    );
676}
677
678/// Decode a symbol table entry from buf at the given position.
679pub fn decode_symbol_table_entry(
680    buf: &[u8],
681    pos: &mut usize,
682    sizeof_addr: usize,
683    sizeof_size: usize,
684) -> FormatResult<SymbolTableEntry> {
685    let needed = *pos + sizeof_size + sizeof_addr + 4 + 4 + 16;
686    if buf.len() < needed {
687        return Err(FormatError::BufferTooShort {
688            needed,
689            available: buf.len(),
690        });
691    }
692
693    let name_offset = decode_offset(buf, pos, sizeof_size);
694    let obj_header_addr = decode_offset(buf, pos, sizeof_addr);
695    let cache_type = u32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
696    *pos += 4;
697    // reserved u32
698    *pos += 4;
699
700    // Scratch pad: 16 bytes, read per the cache type (`H5G__ent_decode`).
701    let scratch_start = *pos;
702    let cache = match cache_type {
703        1 => {
704            let btree_addr = decode_offset(buf, pos, sizeof_addr);
705            let heap_addr = decode_offset(buf, pos, sizeof_addr);
706            SymbolTableCache::SymbolTable {
707                btree_addr,
708                heap_addr,
709            }
710        }
711        2 => {
712            let value_offset =
713                u32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
714            *pos += 4;
715            SymbolTableCache::SoftLink { value_offset }
716        }
717        _ => SymbolTableCache::Nothing,
718    };
719    // The scratch pad is a fixed 16 bytes whatever the cache type consumed.
720    *pos = scratch_start + 16;
721
722    Ok(SymbolTableEntry {
723        name_offset,
724        obj_header_addr,
725        cache,
726    })
727}
728
729/// Detect the superblock version from the first 9+ bytes of a file.
730/// Returns the version byte (0, 1, 2, or 3).
731pub fn detect_superblock_version(buf: &[u8]) -> FormatResult<u8> {
732    if buf.len() < 9 {
733        return Err(FormatError::BufferTooShort {
734            needed: 9,
735            available: buf.len(),
736        });
737    }
738    if buf[0..8] != HDF5_SIGNATURE {
739        return Err(FormatError::InvalidSignature);
740    }
741    Ok(buf[8])
742}
743
744#[cfg(test)]
745mod tests_v0v1 {
746    use super::*;
747    use crate::format::UNDEF_ADDR;
748
749    /// Build a minimal v0 superblock for testing.
750    fn build_v0_superblock(
751        root_obj_header_addr: u64,
752        btree_addr: u64,
753        heap_addr: u64,
754        eof: u64,
755    ) -> Vec<u8> {
756        let sizeof_addr: usize = 8;
757        let sizeof_size: usize = 8;
758        let mut buf = Vec::new();
759
760        // Signature (8 bytes)
761        buf.extend_from_slice(&HDF5_SIGNATURE);
762        // Version 0
763        buf.push(0);
764        // Free-space version
765        buf.push(0);
766        // Root group STE version
767        buf.push(0);
768        // Reserved
769        buf.push(0);
770        // Shared header version
771        buf.push(0);
772        // sizeof_addr
773        buf.push(sizeof_addr as u8);
774        // sizeof_size
775        buf.push(sizeof_size as u8);
776        // Reserved
777        buf.push(0);
778
779        // sym_leaf_k = 4
780        buf.extend_from_slice(&4u16.to_le_bytes());
781        // btree_internal_k = 32
782        buf.extend_from_slice(&32u16.to_le_bytes());
783        // file_consistency_flags = 0
784        buf.extend_from_slice(&0u32.to_le_bytes());
785
786        // base_addr = 0
787        buf.extend_from_slice(&0u64.to_le_bytes()[..sizeof_addr]);
788        // extension_addr = UNDEF
789        buf.extend_from_slice(&UNDEF_ADDR.to_le_bytes()[..sizeof_addr]);
790        // eof_addr
791        buf.extend_from_slice(&eof.to_le_bytes()[..sizeof_addr]);
792        // driver_info_addr = UNDEF
793        buf.extend_from_slice(&UNDEF_ADDR.to_le_bytes()[..sizeof_addr]);
794
795        // Root group symbol table entry:
796        // name_offset (sizeof_size)
797        buf.extend_from_slice(&0u64.to_le_bytes()[..sizeof_size]);
798        // obj_header_addr (sizeof_addr)
799        buf.extend_from_slice(&root_obj_header_addr.to_le_bytes()[..sizeof_addr]);
800        // cache_type = 1 (stab)
801        buf.extend_from_slice(&1u32.to_le_bytes());
802        // reserved
803        buf.extend_from_slice(&0u32.to_le_bytes());
804        // scratch pad: btree_addr + heap_addr
805        buf.extend_from_slice(&btree_addr.to_le_bytes()[..sizeof_addr]);
806        buf.extend_from_slice(&heap_addr.to_le_bytes()[..sizeof_addr]);
807
808        buf
809    }
810
811    /// The first 96 bytes libhdf5 1.14.6 wrote for a default h5py file: an
812    /// append re-emits them, so the decoder and the encoder have to agree with
813    /// the library byte for byte, not merely with each other.
814    #[test]
815    fn a_v0_superblock_re_emits_the_bytes_libhdf5_wrote() {
816        let buf = build_v0_superblock(0x60, 0x88, 0x2a8, 4896);
817        let sb = SuperblockV0V1::decode(&buf).unwrap();
818        assert_eq!(sb.encoded_size(), buf.len());
819        assert_eq!(sb.encode(), buf);
820    }
821
822    /// A version-1 superblock carries the chunked-storage internal "K" the
823    /// version-0 layout has no field for; dropping it on re-emission would
824    /// change the node size every v1 chunk B-tree in the file is measured by.
825    #[test]
826    fn a_v1_superblock_re_emits_its_indexed_storage_k() {
827        let mut buf = build_v0_superblock(0x60, 0x88, 0x2a8, 4896);
828        buf[8] = 1;
829        buf.splice(24..24, [0x40u8, 0x00, 0x00, 0x00]);
830        let sb = SuperblockV0V1::decode(&buf).unwrap();
831        assert_eq!(sb.version, 1);
832        assert_eq!(sb.indexed_storage_k, Some(0x40));
833        assert_eq!(sb.encode(), buf);
834    }
835
836    /// A soft-link entry's scratch pad is a 4-byte heap offset in 16 bytes of
837    /// pad; a nothing-cached entry's is 16 zero bytes.
838    #[test]
839    fn every_symbol_table_cache_shape_round_trips() {
840        for cache in [
841            SymbolTableCache::Nothing,
842            SymbolTableCache::SymbolTable {
843                btree_addr: 0x88,
844                heap_addr: 0x2a8,
845            },
846            SymbolTableCache::SoftLink { value_offset: 24 },
847        ] {
848            let entry = SymbolTableEntry {
849                name_offset: 8,
850                obj_header_addr: 0x320,
851                cache: cache.clone(),
852            };
853            let mut buf = Vec::new();
854            encode_symbol_table_entry(&mut buf, &entry, 8, 8);
855            assert_eq!(buf.len(), symbol_table_entry_size(8, 8));
856            let mut pos = 0;
857            let back = decode_symbol_table_entry(&buf, &mut pos, 8, 8).unwrap();
858            assert_eq!(pos, buf.len());
859            assert_eq!(back, entry);
860        }
861    }
862
863    #[test]
864    fn test_decode_v0() {
865        let buf = build_v0_superblock(0x100, 0x200, 0x300, 0x1000);
866        let sb = SuperblockV0V1::decode(&buf).expect("decode failed");
867        assert_eq!(sb.version, 0);
868        assert_eq!(sb.sizeof_offsets, 8);
869        assert_eq!(sb.sizeof_lengths, 8);
870        assert_eq!(sb.sym_leaf_k, 4);
871        assert_eq!(sb.btree_internal_k, 32);
872        assert_eq!(sb.file_consistency_flags, 0);
873        assert_eq!(sb.indexed_storage_k, None);
874        assert_eq!(sb.base_address, 0);
875        assert_eq!(sb.end_of_file_address, 0x1000);
876        assert_eq!(sb.root_symbol_table_entry.obj_header_addr, 0x100);
877        assert_eq!(
878            sb.root_symbol_table_entry.cached_symbol_table(),
879            Some((0x200, 0x300))
880        );
881    }
882
883    #[test]
884    fn test_decode_v1() {
885        // Build a v1 superblock (includes indexed_storage_k)
886        let sizeof_addr: usize = 8;
887        let sizeof_size: usize = 8;
888        let mut buf = Vec::new();
889        buf.extend_from_slice(&HDF5_SIGNATURE);
890        buf.push(1); // version 1
891        buf.push(0);
892        buf.push(0);
893        buf.push(0);
894        buf.push(0);
895        buf.push(sizeof_addr as u8);
896        buf.push(sizeof_size as u8);
897        buf.push(0);
898        buf.extend_from_slice(&4u16.to_le_bytes());
899        buf.extend_from_slice(&32u16.to_le_bytes());
900        buf.extend_from_slice(&0u32.to_le_bytes());
901        // indexed_storage_k = 16
902        buf.extend_from_slice(&16u16.to_le_bytes());
903        // reserved
904        buf.extend_from_slice(&0u16.to_le_bytes());
905        // addresses
906        buf.extend_from_slice(&0u64.to_le_bytes()[..sizeof_addr]);
907        buf.extend_from_slice(&UNDEF_ADDR.to_le_bytes()[..sizeof_addr]);
908        buf.extend_from_slice(&0x2000u64.to_le_bytes()[..sizeof_addr]);
909        buf.extend_from_slice(&UNDEF_ADDR.to_le_bytes()[..sizeof_addr]);
910        // STE
911        buf.extend_from_slice(&0u64.to_le_bytes()[..sizeof_size]);
912        buf.extend_from_slice(&0x100u64.to_le_bytes()[..sizeof_addr]);
913        buf.extend_from_slice(&1u32.to_le_bytes());
914        buf.extend_from_slice(&0u32.to_le_bytes());
915        buf.extend_from_slice(&0x200u64.to_le_bytes()[..sizeof_addr]);
916        buf.extend_from_slice(&0x300u64.to_le_bytes()[..sizeof_addr]);
917
918        let sb = SuperblockV0V1::decode(&buf).expect("decode failed");
919        assert_eq!(sb.version, 1);
920        assert_eq!(sb.indexed_storage_k, Some(16));
921        assert_eq!(
922            sb.root_symbol_table_entry.cached_symbol_table(),
923            Some((0x200, 0x300))
924        );
925    }
926
927    #[test]
928    fn test_detect_version() {
929        let v0 = build_v0_superblock(0x100, 0x200, 0x300, 0x1000);
930        assert_eq!(detect_superblock_version(&v0).unwrap(), 0);
931
932        let sb_v3 = SuperblockV2V3 {
933            version: SUPERBLOCK_V3,
934            sizeof_offsets: 8,
935            sizeof_lengths: 8,
936            file_consistency_flags: 0,
937            base_address: 0,
938            superblock_extension_address: UNDEF_ADDR,
939            end_of_file_address: 4096,
940            root_group_object_header_address: 48,
941        };
942        let v3 = sb_v3.encode();
943        assert_eq!(detect_superblock_version(&v3).unwrap(), 3);
944    }
945
946    #[test]
947    fn test_bad_sig() {
948        let mut buf = build_v0_superblock(0x100, 0x200, 0x300, 0x1000);
949        buf[0] = 0;
950        assert!(matches!(
951            SuperblockV0V1::decode(&buf).unwrap_err(),
952            FormatError::InvalidSignature
953        ));
954    }
955
956    #[test]
957    fn test_bad_version() {
958        let mut buf = build_v0_superblock(0x100, 0x200, 0x300, 0x1000);
959        buf[8] = 5; // invalid version
960        assert!(matches!(
961            SuperblockV0V1::decode(&buf).unwrap_err(),
962            FormatError::InvalidVersion(5)
963        ));
964    }
965}