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, UNDEF_ADDR};
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/// Superblock version 2.
17pub const SUPERBLOCK_V2: u8 = 2;
18
19/// Superblock version 3 (adds SWMR support).
20pub const SUPERBLOCK_V3: u8 = 3;
21
22/// File consistency flag: file was opened for write access.
23pub const FLAG_WRITE_ACCESS: u8 = 0x01;
24
25/// File consistency flag: file is consistent / was properly closed.
26pub const FLAG_FILE_OK: u8 = 0x02;
27
28/// File consistency flag: file was opened for single-writer/multi-reader.
29pub const FLAG_SWMR_WRITE: u8 = 0x04;
30
31/// Superblock v2/v3 structure.
32///
33/// Layout (O = sizeof_offsets):
34/// ```text
35/// [0..8]              Signature (8 bytes)
36/// [8]                 Version (1 byte)
37/// [9]                 Size of Offsets (1 byte)
38/// [10]                Size of Lengths (1 byte)
39/// [11]                File Consistency Flags (1 byte)
40/// [12..12+O]          Base Address (O bytes)
41/// [12+O..12+2O]       Superblock Extension Address (O bytes)
42/// [12+2O..12+3O]      End of File Address (O bytes)
43/// [12+3O..12+4O]      Root Group Object Header Address (O bytes)
44/// [12+4O..12+4O+4]    Checksum (4 bytes)
45/// ```
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct SuperblockV2V3 {
48    /// Superblock version: 2 or 3.
49    pub version: u8,
50    /// Size of file offsets in bytes (typically 8).
51    pub sizeof_offsets: u8,
52    /// Size of file lengths in bytes (typically 8).
53    pub sizeof_lengths: u8,
54    /// File consistency flags (see `FLAG_*` constants).
55    pub file_consistency_flags: u8,
56    /// Base address of the file (usually 0).
57    pub base_address: u64,
58    /// Address of the superblock extension object header, or UNDEF.
59    pub superblock_extension_address: u64,
60    /// End-of-file address.
61    pub end_of_file_address: u64,
62    /// Address of the root group object header.
63    pub root_group_object_header_address: u64,
64}
65
66impl SuperblockV2V3 {
67    /// Returns the total encoded size in bytes: 12 + 4*O + 4 (checksum).
68    pub fn encoded_size(&self) -> usize {
69        12 + 4 * (self.sizeof_offsets as usize) + 4
70    }
71
72    /// Encode the superblock to a byte vector, including the trailing checksum.
73    pub fn encode(&self) -> Vec<u8> {
74        let size = self.encoded_size();
75        let mut buf = Vec::with_capacity(size);
76
77        // Signature
78        buf.extend_from_slice(&HDF5_SIGNATURE);
79        // Version
80        buf.push(self.version);
81        // Size of Offsets
82        buf.push(self.sizeof_offsets);
83        // Size of Lengths
84        buf.push(self.sizeof_lengths);
85        // File Consistency Flags
86        buf.push(self.file_consistency_flags);
87
88        // Addresses -- encode as little-endian with sizeof_offsets bytes
89        let o = self.sizeof_offsets as usize;
90        encode_offset(&mut buf, self.base_address, o);
91        encode_offset(&mut buf, self.superblock_extension_address, o);
92        encode_offset(&mut buf, self.end_of_file_address, o);
93        encode_offset(&mut buf, self.root_group_object_header_address, o);
94
95        // Checksum over everything before the checksum field
96        debug_assert_eq!(buf.len(), size - 4);
97        let cksum = checksum_metadata(&buf);
98        buf.extend_from_slice(&cksum.to_le_bytes());
99
100        debug_assert_eq!(buf.len(), size);
101        buf
102    }
103
104    /// Decode a superblock from a byte buffer. Verifies the signature, version,
105    /// and checksum. Returns the parsed superblock.
106    pub fn decode(buf: &[u8]) -> FormatResult<Self> {
107        // Minimum size check: we need at least the fixed 12-byte header to
108        // read sizeof_offsets before computing the full size.
109        if buf.len() < 12 {
110            return Err(FormatError::BufferTooShort {
111                needed: 12,
112                available: buf.len(),
113            });
114        }
115
116        // Signature
117        if buf[0..8] != HDF5_SIGNATURE {
118            return Err(FormatError::InvalidSignature);
119        }
120
121        // Version
122        let version = buf[8];
123        if version != SUPERBLOCK_V2 && version != SUPERBLOCK_V3 {
124            return Err(FormatError::InvalidVersion(version));
125        }
126
127        let sizeof_offsets = buf[9];
128        let sizeof_lengths = buf[10];
129        let file_consistency_flags = buf[11];
130        validate_sizeof(sizeof_offsets, sizeof_lengths)?;
131
132        let o = sizeof_offsets as usize;
133        let total_size = 12 + 4 * o + 4;
134        if buf.len() < total_size {
135            return Err(FormatError::BufferTooShort {
136                needed: total_size,
137                available: buf.len(),
138            });
139        }
140
141        // Verify checksum
142        let data_end = total_size - 4;
143        let stored_cksum = u32::from_le_bytes([
144            buf[data_end],
145            buf[data_end + 1],
146            buf[data_end + 2],
147            buf[data_end + 3],
148        ]);
149        let computed_cksum = checksum_metadata(&buf[..data_end]);
150        if stored_cksum != computed_cksum {
151            return Err(FormatError::ChecksumMismatch {
152                expected: stored_cksum,
153                computed: computed_cksum,
154            });
155        }
156
157        // Decode addresses
158        let mut pos = 12;
159        let base_address = decode_offset(buf, &mut pos, o);
160        let superblock_extension_address = decode_offset(buf, &mut pos, o);
161        let end_of_file_address = decode_offset(buf, &mut pos, o);
162        let root_group_object_header_address = decode_offset(buf, &mut pos, o);
163
164        Ok(SuperblockV2V3 {
165            version,
166            sizeof_offsets,
167            sizeof_lengths,
168            file_consistency_flags,
169            base_address,
170            superblock_extension_address,
171            end_of_file_address,
172            root_group_object_header_address,
173        })
174    }
175}
176
177/// Encode a u64 address as `size` little-endian bytes and append to `buf`.
178fn encode_offset(buf: &mut Vec<u8>, value: u64, size: usize) {
179    let bytes = value.to_le_bytes();
180    buf.extend_from_slice(&bytes[..size]);
181}
182
183/// Reject `sizeof_offsets` / `sizeof_lengths` values this crate cannot
184/// represent. libhdf5 permits 2, 4, 8, 16 and 32; this crate decodes
185/// addresses into a `u64`, so it supports only 2, 4 and 8. Validating here
186/// keeps every downstream `read_addr` / `read_size` helper (which copies
187/// `n` bytes into an 8-byte buffer) from panicking on a hostile file.
188fn validate_sizeof(sizeof_offsets: u8, sizeof_lengths: u8) -> FormatResult<()> {
189    for (name, v) in [
190        ("sizeof_offsets", sizeof_offsets),
191        ("sizeof_lengths", sizeof_lengths),
192    ] {
193        if !matches!(v, 2 | 4 | 8) {
194            return Err(FormatError::InvalidData(format!(
195                "unsupported superblock {name} = {v} (only 2, 4, 8 are supported)"
196            )));
197        }
198    }
199    Ok(())
200}
201
202/// Decode a little-endian address of `size` bytes from `buf` at `*pos`,
203/// advancing `*pos` past the consumed bytes.
204fn decode_offset(buf: &[u8], pos: &mut usize, size: usize) -> u64 {
205    let v = crate::format::bytes::read_le_uint(&buf[*pos..], size);
206    *pos += size;
207    v
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213    use crate::format::UNDEF_ADDR;
214
215    #[test]
216    fn test_encoded_size() {
217        let sb = SuperblockV2V3 {
218            version: SUPERBLOCK_V3,
219            sizeof_offsets: 8,
220            sizeof_lengths: 8,
221            file_consistency_flags: 0,
222            base_address: 0,
223            superblock_extension_address: UNDEF_ADDR,
224            end_of_file_address: 4096,
225            root_group_object_header_address: 48,
226        };
227        // 12 + 4*8 + 4 = 48
228        assert_eq!(sb.encoded_size(), 48);
229    }
230
231    #[test]
232    fn test_roundtrip_v3_offset8() {
233        let original = SuperblockV2V3 {
234            version: SUPERBLOCK_V3,
235            sizeof_offsets: 8,
236            sizeof_lengths: 8,
237            file_consistency_flags: FLAG_FILE_OK,
238            base_address: 0,
239            superblock_extension_address: UNDEF_ADDR,
240            end_of_file_address: 0x1_0000,
241            root_group_object_header_address: 48,
242        };
243
244        let encoded = original.encode();
245        assert_eq!(encoded.len(), original.encoded_size());
246
247        // Verify signature
248        assert_eq!(&encoded[..8], &HDF5_SIGNATURE);
249
250        let decoded = SuperblockV2V3::decode(&encoded).expect("decode failed");
251        assert_eq!(decoded, original);
252    }
253
254    #[test]
255    fn test_roundtrip_v2_offset4() {
256        let original = SuperblockV2V3 {
257            version: SUPERBLOCK_V2,
258            sizeof_offsets: 4,
259            sizeof_lengths: 4,
260            file_consistency_flags: 0,
261            base_address: 0,
262            superblock_extension_address: 0xFFFF_FFFF,
263            end_of_file_address: 8192,
264            root_group_object_header_address: 28,
265        };
266
267        let encoded = original.encode();
268        // 12 + 4*4 + 4 = 32
269        assert_eq!(encoded.len(), 32);
270
271        let decoded = SuperblockV2V3::decode(&encoded).expect("decode failed");
272        assert_eq!(decoded, original);
273    }
274
275    #[test]
276    fn test_decode_bad_signature() {
277        let mut data = vec![0u8; 48];
278        // Wrong signature
279        data[0] = 0x00;
280        let err = SuperblockV2V3::decode(&data).unwrap_err();
281        assert!(matches!(err, FormatError::InvalidSignature));
282    }
283
284    #[test]
285    fn test_decode_bad_version() {
286        let sb = SuperblockV2V3 {
287            version: SUPERBLOCK_V3,
288            sizeof_offsets: 8,
289            sizeof_lengths: 8,
290            file_consistency_flags: 0,
291            base_address: 0,
292            superblock_extension_address: UNDEF_ADDR,
293            end_of_file_address: 4096,
294            root_group_object_header_address: 48,
295        };
296        let mut encoded = sb.encode();
297        // Corrupt version to 1
298        encoded[8] = 1;
299        let err = SuperblockV2V3::decode(&encoded).unwrap_err();
300        assert!(matches!(err, FormatError::InvalidVersion(1)));
301    }
302
303    #[test]
304    fn test_decode_checksum_mismatch() {
305        let sb = SuperblockV2V3 {
306            version: SUPERBLOCK_V3,
307            sizeof_offsets: 8,
308            sizeof_lengths: 8,
309            file_consistency_flags: 0,
310            base_address: 0,
311            superblock_extension_address: UNDEF_ADDR,
312            end_of_file_address: 4096,
313            root_group_object_header_address: 48,
314        };
315        let mut encoded = sb.encode();
316        // Corrupt a data byte
317        encoded[12] = 0xFF;
318        let err = SuperblockV2V3::decode(&encoded).unwrap_err();
319        assert!(matches!(err, FormatError::ChecksumMismatch { .. }));
320    }
321
322    #[test]
323    fn test_decode_buffer_too_short() {
324        let err = SuperblockV2V3::decode(&[0u8; 4]).unwrap_err();
325        assert!(matches!(err, FormatError::BufferTooShort { .. }));
326    }
327
328    #[test]
329    fn test_flags() {
330        let sb = SuperblockV2V3 {
331            version: SUPERBLOCK_V3,
332            sizeof_offsets: 8,
333            sizeof_lengths: 8,
334            file_consistency_flags: FLAG_WRITE_ACCESS | FLAG_SWMR_WRITE,
335            base_address: 0,
336            superblock_extension_address: UNDEF_ADDR,
337            end_of_file_address: 4096,
338            root_group_object_header_address: 48,
339        };
340        let encoded = sb.encode();
341        let decoded = SuperblockV2V3::decode(&encoded).unwrap();
342        assert_eq!(
343            decoded.file_consistency_flags,
344            FLAG_WRITE_ACCESS | FLAG_SWMR_WRITE
345        );
346    }
347
348    #[test]
349    fn test_roundtrip_with_extra_trailing_data() {
350        // decode should succeed even if the buffer is longer than needed
351        let sb = SuperblockV2V3 {
352            version: SUPERBLOCK_V3,
353            sizeof_offsets: 8,
354            sizeof_lengths: 8,
355            file_consistency_flags: 0,
356            base_address: 0,
357            superblock_extension_address: UNDEF_ADDR,
358            end_of_file_address: 4096,
359            root_group_object_header_address: 48,
360        };
361        let mut encoded = sb.encode();
362        encoded.extend_from_slice(&[0xAA; 100]); // trailing garbage
363        let decoded = SuperblockV2V3::decode(&encoded).unwrap();
364        assert_eq!(decoded, sb);
365    }
366}
367
368// =========================================================================
369// Superblock v0/v1 — decode only (for reading legacy HDF5 files)
370// =========================================================================
371
372/// Symbol table entry, as stored in the root group's superblock (v0/v1).
373#[derive(Debug, Clone, PartialEq, Eq)]
374pub struct SymbolTableEntry {
375    /// Offset of the name in the local heap.
376    pub name_offset: u64,
377    /// Address of the object header.
378    pub obj_header_addr: u64,
379    /// Cache type: 0 = nothing cached, 1 = symbol table (group).
380    pub cache_type: u32,
381    /// If cache_type == 1: B-tree address for group children.
382    pub btree_addr: u64,
383    /// If cache_type == 1: local heap address for group names.
384    pub heap_addr: u64,
385}
386
387/// Superblock v0/v1 structure (decode only).
388///
389/// Layout after 8-byte signature:
390/// ```text
391/// Byte 0: superblock version (0 or 1)
392/// Byte 1: free-space version (0)
393/// Byte 2: root group STE version (0)
394/// Byte 3: reserved (0)
395/// Byte 4: shared header version (0)
396/// Byte 5: sizeof_addr
397/// Byte 6: sizeof_size
398/// Byte 7: reserved (0)
399/// Bytes 8-9: sym_leaf_k (u16 LE)
400/// Bytes 10-11: btree_internal_k (u16 LE)
401/// Bytes 12-15: file_consistency_flags (u32 LE)
402/// [v1 only: bytes 16-17: indexed_storage_k (u16 LE), bytes 18-19: reserved]
403/// Then: base_addr(O), extension_addr(O), eof_addr(O), driver_addr(O)
404/// Then: root group symbol table entry
405/// ```
406#[derive(Debug, Clone, PartialEq, Eq)]
407pub struct SuperblockV0V1 {
408    pub version: u8,
409    pub sizeof_offsets: u8,
410    pub sizeof_lengths: u8,
411    pub file_consistency_flags: u32,
412    pub sym_leaf_k: u16,
413    pub btree_internal_k: u16,
414    pub indexed_storage_k: Option<u16>,
415    pub base_address: u64,
416    pub superblock_extension_address: u64,
417    pub end_of_file_address: u64,
418    pub driver_info_address: u64,
419    pub root_symbol_table_entry: SymbolTableEntry,
420}
421
422impl SuperblockV0V1 {
423    /// Decode a v0/v1 superblock from `buf`. The buffer must start at the
424    /// 8-byte HDF5 signature. Returns the parsed superblock.
425    pub fn decode(buf: &[u8]) -> FormatResult<Self> {
426        // Minimum: 8 (sig) + 8 (fixed header before addresses) = 16
427        if buf.len() < 16 {
428            return Err(FormatError::BufferTooShort {
429                needed: 16,
430                available: buf.len(),
431            });
432        }
433
434        // Signature
435        if buf[0..8] != HDF5_SIGNATURE {
436            return Err(FormatError::InvalidSignature);
437        }
438
439        let version = buf[8];
440        if version != 0 && version != 1 {
441            return Err(FormatError::InvalidVersion(version));
442        }
443
444        // buf[9] = free-space version (must be 0)
445        // buf[10] = root group STE version (must be 0)
446        // buf[11] = reserved
447        // buf[12] = shared header version (must be 0)
448        let sizeof_offsets = buf[13];
449        let sizeof_lengths = buf[14];
450        // buf[15] = reserved
451        validate_sizeof(sizeof_offsets, sizeof_lengths)?;
452
453        let o = sizeof_offsets as usize;
454        let mut pos = 16;
455
456        // Check we have enough for the remaining fixed fields
457        if buf.len() < pos + 4 {
458            return Err(FormatError::BufferTooShort {
459                needed: pos + 4,
460                available: buf.len(),
461            });
462        }
463
464        let sym_leaf_k = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
465        pos += 2;
466        let btree_internal_k = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
467        pos += 2;
468
469        if buf.len() < pos + 4 {
470            return Err(FormatError::BufferTooShort {
471                needed: pos + 4,
472                available: buf.len(),
473            });
474        }
475        let file_consistency_flags =
476            u32::from_le_bytes([buf[pos], buf[pos + 1], buf[pos + 2], buf[pos + 3]]);
477        pos += 4;
478
479        let indexed_storage_k = if version == 1 {
480            if buf.len() < pos + 4 {
481                return Err(FormatError::BufferTooShort {
482                    needed: pos + 4,
483                    available: buf.len(),
484                });
485            }
486            let k = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
487            pos += 2;
488            // 2 bytes reserved
489            pos += 2;
490            Some(k)
491        } else {
492            None
493        };
494
495        // 4 addresses, each sizeof_offsets bytes
496        let needed = pos + 4 * o;
497        if buf.len() < needed {
498            return Err(FormatError::BufferTooShort {
499                needed,
500                available: buf.len(),
501            });
502        }
503
504        let base_address = decode_offset(buf, &mut pos, o);
505        let superblock_extension_address = decode_offset(buf, &mut pos, o);
506        let end_of_file_address = decode_offset(buf, &mut pos, o);
507        let driver_info_address = decode_offset(buf, &mut pos, o);
508
509        // Root group symbol table entry
510        let ste = decode_symbol_table_entry(buf, &mut pos, o, sizeof_lengths as usize)?;
511
512        Ok(SuperblockV0V1 {
513            version,
514            sizeof_offsets,
515            sizeof_lengths,
516            file_consistency_flags,
517            sym_leaf_k,
518            btree_internal_k,
519            indexed_storage_k,
520            base_address,
521            superblock_extension_address,
522            end_of_file_address,
523            driver_info_address,
524            root_symbol_table_entry: ste,
525        })
526    }
527}
528
529/// Decode a symbol table entry from buf at the given position.
530pub fn decode_symbol_table_entry(
531    buf: &[u8],
532    pos: &mut usize,
533    sizeof_addr: usize,
534    sizeof_size: usize,
535) -> FormatResult<SymbolTableEntry> {
536    let needed = *pos + sizeof_size + sizeof_addr + 4 + 4 + 16;
537    if buf.len() < needed {
538        return Err(FormatError::BufferTooShort {
539            needed,
540            available: buf.len(),
541        });
542    }
543
544    let name_offset = decode_offset(buf, pos, sizeof_size);
545    let obj_header_addr = decode_offset(buf, pos, sizeof_addr);
546    let cache_type = u32::from_le_bytes([buf[*pos], buf[*pos + 1], buf[*pos + 2], buf[*pos + 3]]);
547    *pos += 4;
548    // reserved u32
549    *pos += 4;
550
551    // Scratch pad: 16 bytes
552    let (btree_addr, heap_addr) = if cache_type == 1 {
553        let btree = decode_offset(buf, pos, sizeof_addr);
554        let heap = decode_offset(buf, pos, sizeof_addr);
555        // Skip remaining scratch pad bytes
556        let used = 2 * sizeof_addr;
557        if used < 16 {
558            *pos += 16 - used;
559        }
560        (btree, heap)
561    } else {
562        *pos += 16;
563        (UNDEF_ADDR, UNDEF_ADDR)
564    };
565
566    Ok(SymbolTableEntry {
567        name_offset,
568        obj_header_addr,
569        cache_type,
570        btree_addr,
571        heap_addr,
572    })
573}
574
575/// Detect the superblock version from the first 9+ bytes of a file.
576/// Returns the version byte (0, 1, 2, or 3).
577pub fn detect_superblock_version(buf: &[u8]) -> FormatResult<u8> {
578    if buf.len() < 9 {
579        return Err(FormatError::BufferTooShort {
580            needed: 9,
581            available: buf.len(),
582        });
583    }
584    if buf[0..8] != HDF5_SIGNATURE {
585        return Err(FormatError::InvalidSignature);
586    }
587    Ok(buf[8])
588}
589
590#[cfg(test)]
591mod tests_v0v1 {
592    use super::*;
593
594    /// Build a minimal v0 superblock for testing.
595    fn build_v0_superblock(
596        root_obj_header_addr: u64,
597        btree_addr: u64,
598        heap_addr: u64,
599        eof: u64,
600    ) -> Vec<u8> {
601        let sizeof_addr: usize = 8;
602        let sizeof_size: usize = 8;
603        let mut buf = Vec::new();
604
605        // Signature (8 bytes)
606        buf.extend_from_slice(&HDF5_SIGNATURE);
607        // Version 0
608        buf.push(0);
609        // Free-space version
610        buf.push(0);
611        // Root group STE version
612        buf.push(0);
613        // Reserved
614        buf.push(0);
615        // Shared header version
616        buf.push(0);
617        // sizeof_addr
618        buf.push(sizeof_addr as u8);
619        // sizeof_size
620        buf.push(sizeof_size as u8);
621        // Reserved
622        buf.push(0);
623
624        // sym_leaf_k = 4
625        buf.extend_from_slice(&4u16.to_le_bytes());
626        // btree_internal_k = 32
627        buf.extend_from_slice(&32u16.to_le_bytes());
628        // file_consistency_flags = 0
629        buf.extend_from_slice(&0u32.to_le_bytes());
630
631        // base_addr = 0
632        buf.extend_from_slice(&0u64.to_le_bytes()[..sizeof_addr]);
633        // extension_addr = UNDEF
634        buf.extend_from_slice(&UNDEF_ADDR.to_le_bytes()[..sizeof_addr]);
635        // eof_addr
636        buf.extend_from_slice(&eof.to_le_bytes()[..sizeof_addr]);
637        // driver_info_addr = UNDEF
638        buf.extend_from_slice(&UNDEF_ADDR.to_le_bytes()[..sizeof_addr]);
639
640        // Root group symbol table entry:
641        // name_offset (sizeof_size)
642        buf.extend_from_slice(&0u64.to_le_bytes()[..sizeof_size]);
643        // obj_header_addr (sizeof_addr)
644        buf.extend_from_slice(&root_obj_header_addr.to_le_bytes()[..sizeof_addr]);
645        // cache_type = 1 (stab)
646        buf.extend_from_slice(&1u32.to_le_bytes());
647        // reserved
648        buf.extend_from_slice(&0u32.to_le_bytes());
649        // scratch pad: btree_addr + heap_addr
650        buf.extend_from_slice(&btree_addr.to_le_bytes()[..sizeof_addr]);
651        buf.extend_from_slice(&heap_addr.to_le_bytes()[..sizeof_addr]);
652
653        buf
654    }
655
656    #[test]
657    fn test_decode_v0() {
658        let buf = build_v0_superblock(0x100, 0x200, 0x300, 0x1000);
659        let sb = SuperblockV0V1::decode(&buf).expect("decode failed");
660        assert_eq!(sb.version, 0);
661        assert_eq!(sb.sizeof_offsets, 8);
662        assert_eq!(sb.sizeof_lengths, 8);
663        assert_eq!(sb.sym_leaf_k, 4);
664        assert_eq!(sb.btree_internal_k, 32);
665        assert_eq!(sb.file_consistency_flags, 0);
666        assert_eq!(sb.indexed_storage_k, None);
667        assert_eq!(sb.base_address, 0);
668        assert_eq!(sb.end_of_file_address, 0x1000);
669        assert_eq!(sb.root_symbol_table_entry.obj_header_addr, 0x100);
670        assert_eq!(sb.root_symbol_table_entry.cache_type, 1);
671        assert_eq!(sb.root_symbol_table_entry.btree_addr, 0x200);
672        assert_eq!(sb.root_symbol_table_entry.heap_addr, 0x300);
673    }
674
675    #[test]
676    fn test_decode_v1() {
677        // Build a v1 superblock (includes indexed_storage_k)
678        let sizeof_addr: usize = 8;
679        let sizeof_size: usize = 8;
680        let mut buf = Vec::new();
681        buf.extend_from_slice(&HDF5_SIGNATURE);
682        buf.push(1); // version 1
683        buf.push(0);
684        buf.push(0);
685        buf.push(0);
686        buf.push(0);
687        buf.push(sizeof_addr as u8);
688        buf.push(sizeof_size as u8);
689        buf.push(0);
690        buf.extend_from_slice(&4u16.to_le_bytes());
691        buf.extend_from_slice(&32u16.to_le_bytes());
692        buf.extend_from_slice(&0u32.to_le_bytes());
693        // indexed_storage_k = 16
694        buf.extend_from_slice(&16u16.to_le_bytes());
695        // reserved
696        buf.extend_from_slice(&0u16.to_le_bytes());
697        // addresses
698        buf.extend_from_slice(&0u64.to_le_bytes()[..sizeof_addr]);
699        buf.extend_from_slice(&UNDEF_ADDR.to_le_bytes()[..sizeof_addr]);
700        buf.extend_from_slice(&0x2000u64.to_le_bytes()[..sizeof_addr]);
701        buf.extend_from_slice(&UNDEF_ADDR.to_le_bytes()[..sizeof_addr]);
702        // STE
703        buf.extend_from_slice(&0u64.to_le_bytes()[..sizeof_size]);
704        buf.extend_from_slice(&0x100u64.to_le_bytes()[..sizeof_addr]);
705        buf.extend_from_slice(&1u32.to_le_bytes());
706        buf.extend_from_slice(&0u32.to_le_bytes());
707        buf.extend_from_slice(&0x200u64.to_le_bytes()[..sizeof_addr]);
708        buf.extend_from_slice(&0x300u64.to_le_bytes()[..sizeof_addr]);
709
710        let sb = SuperblockV0V1::decode(&buf).expect("decode failed");
711        assert_eq!(sb.version, 1);
712        assert_eq!(sb.indexed_storage_k, Some(16));
713        assert_eq!(sb.root_symbol_table_entry.btree_addr, 0x200);
714    }
715
716    #[test]
717    fn test_detect_version() {
718        let v0 = build_v0_superblock(0x100, 0x200, 0x300, 0x1000);
719        assert_eq!(detect_superblock_version(&v0).unwrap(), 0);
720
721        let sb_v3 = SuperblockV2V3 {
722            version: SUPERBLOCK_V3,
723            sizeof_offsets: 8,
724            sizeof_lengths: 8,
725            file_consistency_flags: 0,
726            base_address: 0,
727            superblock_extension_address: UNDEF_ADDR,
728            end_of_file_address: 4096,
729            root_group_object_header_address: 48,
730        };
731        let v3 = sb_v3.encode();
732        assert_eq!(detect_superblock_version(&v3).unwrap(), 3);
733    }
734
735    #[test]
736    fn test_bad_sig() {
737        let mut buf = build_v0_superblock(0x100, 0x200, 0x300, 0x1000);
738        buf[0] = 0;
739        assert!(matches!(
740            SuperblockV0V1::decode(&buf).unwrap_err(),
741            FormatError::InvalidSignature
742        ));
743    }
744
745    #[test]
746    fn test_bad_version() {
747        let mut buf = build_v0_superblock(0x100, 0x200, 0x300, 0x1000);
748        buf[8] = 5; // invalid version
749        assert!(matches!(
750            SuperblockV0V1::decode(&buf).unwrap_err(),
751            FormatError::InvalidVersion(5)
752        ));
753    }
754}