Skip to main content

rust_hdf5/format/messages/
superblock_ext.rs

1//! The object-header messages that live in the superblock extension.
2//!
3//! A v2/v3 superblock (and a v0/v1 one, whose second address field is the
4//! extension address in every libhdf5 that writes one) may point at an object
5//! header holding file-level metadata that does not fit in the fixed
6//! superblock: the shared-message table, non-default v1 B-tree "K" values, the
7//! driver-info block, and the file-space strategy. They are grouped in one
8//! module because they are exactly the set of messages that only ever appear
9//! there.
10//!
11//! Upstream: `H5Oshmesg.c`, `H5Obtreek.c`, `H5Odrvinfo.c`, `H5Ofsinfo.c`;
12//! the read side that consumes them is `H5Fsuper.c::H5F__super_read`.
13
14use crate::format::bytes::read_le_uint as read_uint;
15use crate::format::{FormatContext, FormatError, FormatResult};
16
17/// Shared Message Table message (0x000F) — `H5Oshmesg.c`.
18///
19/// Points at the SOHM master table and says how many indexes it holds; the
20/// count is not stored in the table itself, so this message is the only way
21/// to size it.
22#[derive(Debug, Clone, PartialEq, Eq)]
23pub struct SharedMessageTableMessage {
24    /// Message version (0 is the only version defined).
25    pub version: u8,
26    /// File address of the SOHM master table (`SMTB`).
27    pub table_address: u64,
28    /// Number of index headers stored in that table.
29    pub nindexes: u8,
30}
31
32impl SharedMessageTableMessage {
33    /// Decode the message body.
34    pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<Self> {
35        let sa = ctx.sizeof_addr as usize;
36        need(buf, 1 + sa + 1)?;
37        let version = buf[0];
38        if version != 0 {
39            return Err(FormatError::InvalidVersion(version));
40        }
41        Ok(Self {
42            version,
43            table_address: read_uint(&buf[1..], sa),
44            nindexes: buf[1 + sa],
45        })
46    }
47
48    /// Encode the message body (`H5O__shmesg_encode`).
49    pub fn encode(&self, ctx: &FormatContext) -> Vec<u8> {
50        let sa = ctx.sizeof_addr as usize;
51        let mut buf = Vec::with_capacity(1 + sa + 1);
52        buf.push(self.version);
53        buf.extend_from_slice(&self.table_address.to_le_bytes()[..sa]);
54        buf.push(self.nindexes);
55        buf
56    }
57}
58
59/// v1 B-tree "K" values message (0x0013) — `H5Obtreek.c`.
60///
61/// Present only when the file was created with non-default split ranks; its
62/// three values replace the superblock's own (v0/v1) or the library defaults
63/// (v2/v3, whose superblock has no room for them).
64#[derive(Debug, Clone, Copy, PartialEq, Eq)]
65pub struct BtreeKMessage {
66    /// Internal-node 1/2 rank for chunked-storage (type 1) B-trees.
67    pub chunk_internal_k: u16,
68    /// Internal-node 1/2 rank for symbol-table (type 0) B-trees.
69    pub snode_internal_k: u16,
70    /// Symbol-table leaf-node (SNOD) 1/2 rank.
71    pub sym_leaf_k: u16,
72}
73
74impl BtreeKMessage {
75    /// Decode the message body.
76    pub fn decode(buf: &[u8]) -> FormatResult<Self> {
77        need(buf, 1 + 6)?;
78        let version = buf[0];
79        if version != 0 {
80            return Err(FormatError::InvalidVersion(version));
81        }
82        Ok(Self {
83            chunk_internal_k: u16::from_le_bytes([buf[1], buf[2]]),
84            snode_internal_k: u16::from_le_bytes([buf[3], buf[4]]),
85            sym_leaf_k: u16::from_le_bytes([buf[5], buf[6]]),
86        })
87    }
88}
89
90/// Driver info message (0x0014) — `H5Odrvinfo.c`.
91///
92/// Carries the VFD-specific superblock payload (`H5FD_MULTI` / `H5FD_FAMILY`)
93/// that a v0/v1 file keeps in a separate driver-info block. The payload is
94/// handed back verbatim: this crate opens files through a single sec2-like
95/// handle and does not implement the multi/family drivers, so the message is
96/// exposed for inspection rather than acted on.
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct DriverInfoMessage {
99    /// The 8-byte driver identifier, trailing NULs trimmed (e.g. `NCSAmulti`
100    /// is stored as `NCSAmult`).
101    pub name: String,
102    /// Driver-specific payload.
103    pub data: Vec<u8>,
104}
105
106impl DriverInfoMessage {
107    /// Decode the message body.
108    pub fn decode(buf: &[u8]) -> FormatResult<Self> {
109        need(buf, 1 + 8 + 2)?;
110        let version = buf[0];
111        if version != 0 {
112            return Err(FormatError::InvalidVersion(version));
113        }
114        let name_bytes: Vec<u8> = buf[1..9].iter().copied().take_while(|&b| b != 0).collect();
115        let name = String::from_utf8_lossy(&name_bytes).into_owned();
116        let len = u16::from_le_bytes([buf[9], buf[10]]) as usize;
117        if len == 0 {
118            return Err(FormatError::InvalidData(
119                "driver info message declares a zero-length payload".into(),
120            ));
121        }
122        need(buf, 11 + len)?;
123        Ok(Self {
124            name,
125            data: buf[11..11 + len].to_vec(),
126        })
127    }
128}
129
130/// File-space strategy (`H5F_fspace_strategy_t`), as stored in the file-space
131/// info message.
132#[derive(Debug, Clone, Copy, PartialEq, Eq)]
133pub enum FileSpaceStrategy {
134    /// Free-space managers plus aggregators (the library default).
135    FsmAggr,
136    /// Paged aggregation.
137    Page,
138    /// Aggregators only, no persisted free-space manager.
139    Aggr,
140    /// No cross-close reuse.
141    None,
142    /// A strategy byte this crate does not recognize.
143    Unknown(u8),
144}
145
146impl FileSpaceStrategy {
147    fn from_byte(b: u8) -> Self {
148        // `H5F_fspace_strategy_t` in H5Fpublic.h, which is zero-based; the
149        // one-based `H5F_file_space_type_t` below it is the deprecated
150        // version-0 encoding and is mapped separately.
151        match b {
152            0 => Self::FsmAggr,
153            1 => Self::Page,
154            2 => Self::Aggr,
155            3 => Self::None,
156            other => Self::Unknown(other),
157        }
158    }
159
160    /// The `H5F_fspace_strategy_t` value, the inverse of
161    /// [`from_byte`](Self::from_byte).
162    fn to_byte(self) -> u8 {
163        match self {
164            Self::FsmAggr => 0,
165            Self::Page => 1,
166            Self::Aggr => 2,
167            Self::None => 3,
168            Self::Unknown(b) => b,
169        }
170    }
171}
172
173/// Number of page-type free-space managers whose addresses a persisting
174/// version-1 message stores (`H5F_MEM_PAGE_SUPER` .. `H5F_MEM_PAGE_LARGE_OHDR`).
175pub const FS_ADDR_COUNT_V1: usize = 12;
176
177/// Number of memory-type free-space managers a persisting version-0 message
178/// stores (`H5FD_MEM_SUPER` .. `H5FD_MEM_OHDR`). They are the first six of
179/// the twelve slots, which is why the same array serves both encodings.
180const FS_ADDR_COUNT_V0: usize = 6;
181
182/// Maximum file-space page size libhdf5 accepts
183/// (`H5F_FILE_SPACE_PAGE_SIZE_MAX`, H5Fprivate.h:337) — the ceiling
184/// `H5Pset_file_space_page_size` enforces, and what the decoder refuses above.
185pub const PAGE_SIZE_MAX: u64 = 1024 * 1024 * 1024;
186
187/// Smallest file-space page size libhdf5 accepts
188/// (`H5F_FILE_SPACE_PAGE_SIZE_MIN`, H5Fprivate.h:336) — the floor
189/// `H5Pset_file_space_page_size` enforces, and what a paged file's allocation
190/// rules assume.
191pub const PAGE_SIZE_MIN: u64 = 512;
192
193/// `H5F_FILE_SPACE_PAGE_SIZE_DEF` (H5Fprivate.h:335) — the page size a file
194/// gets when nothing says otherwise, which is also the one the version-0
195/// encoding implies.
196pub(crate) const DEFAULT_FILE_SPACE_PAGE_SIZE: u64 = 4096;
197
198/// File-space info message (0x0017) — `H5Ofsinfo.c`.
199///
200/// The version-0 form is the deprecated 1.10.0 encoding. Its four strategy
201/// values are mapped onto the version-1 `strategy`/`persist`/`threshold`
202/// fields on decode (`H5O__fsinfo_decode`), so every consumer of this struct
203/// sees one shape whatever the file carries.
204///
205/// What decode does *not* discard is which encoding the file used:
206/// [`version`](Self::version) keeps it and [`encode`](Self::encode) re-emits
207/// at that version, so appending to a version-0 file leaves it a version-0
208/// file. Upstream instead upgrades — `H5O__fsinfo_encode` has no version-0
209/// branch, and `H5F__super_read` removes a mapped message and writes a
210/// version-1 replacement on read-write open (H5Fsuper.c:843-885) — which is a
211/// format change this crate has no reason to make on the user's behalf, and
212/// which libhdf5 will still make on its own next open.
213#[derive(Debug, Clone, PartialEq, Eq)]
214pub struct FileSpaceInfoMessage {
215    /// The encoding this message uses on disk: 0 for the deprecated 1.10.0
216    /// form, 1 for the current one. Decode records what it read and encode
217    /// writes that same form back; nothing else in the crate branches on it,
218    /// because decode has already normalized the fields below.
219    pub version: u8,
220    /// File-space handling strategy.
221    pub strategy: FileSpaceStrategy,
222    /// Whether free-space manager state is persisted across close/reopen.
223    pub persist: bool,
224    /// Smallest free-space section tracked by a manager.
225    pub threshold: u64,
226    /// File-space page size (paged aggregation).
227    pub page_size: u64,
228    /// Page-end metadata threshold.
229    pub pgend_meta_thres: u16,
230    /// End-of-address before free-space header/section allocation.
231    pub eoa_pre_fsm_fsalloc: u64,
232    /// Addresses of the free-space managers, indexed by
233    /// [`FreeSpaceManager::message_slot`](crate::format::free_space::FreeSpaceManager::message_slot).
234    ///
235    /// Always [`FS_ADDR_COUNT_V1`] entries, undefined where there is no
236    /// manager — including for a non-persisting message, which stores none of
237    /// them, and for a version-0 message, which stores only the first
238    /// `FS_ADDR_COUNT_V0`. Upstream keeps the same full-width array for the
239    /// same reason (`H5O__fsinfo_decode` fills all twelve with `HADDR_UNDEF`
240    /// before reading any): a caller indexes a slot without first asking which
241    /// encoding the slot came from.
242    pub fs_addr: Vec<u64>,
243}
244
245impl FileSpaceInfoMessage {
246    /// Decode the message body.
247    pub fn decode(buf: &[u8], ctx: &FormatContext) -> FormatResult<Self> {
248        let sa = ctx.sizeof_addr as usize;
249        let ss = ctx.sizeof_size as usize;
250        need(buf, 1)?;
251        let version = buf[0];
252        let mut pos = 1;
253
254        if version == 0 {
255            // Deprecated 1.10.0 encoding: strategy byte + threshold, and the
256            // free-space manager addresses only for the "all persist" value.
257            need(buf, pos + 1 + ss)?;
258            let legacy_strategy = buf[pos];
259            pos += 1;
260            let threshold = read_uint(&buf[pos..], ss);
261            pos += ss;
262
263            // H5F_file_space_type_t -> H5F_fspace_strategy_t, per
264            // H5O__fsinfo_decode's version-0 branch.
265            let (strategy, persist, threshold) = match legacy_strategy {
266                1 => (FileSpaceStrategy::FsmAggr, true, threshold),
267                2 => (FileSpaceStrategy::FsmAggr, false, threshold),
268                3 => (FileSpaceStrategy::Aggr, false, 1),
269                4 => (FileSpaceStrategy::None, false, 1),
270                other => {
271                    return Err(FormatError::InvalidData(format!(
272                        "invalid file-space strategy {other} in version-0 file-space info message"
273                    )))
274                }
275            };
276
277            // The six memory-type managers this encoding knows land in the
278            // first six of the twelve slots; the rest stay undefined.
279            let mut fs_addr = vec![crate::format::UNDEF_ADDR; FS_ADDR_COUNT_V1];
280            if persist {
281                need(buf, pos + FS_ADDR_COUNT_V0 * sa)?;
282                for slot in fs_addr.iter_mut().take(FS_ADDR_COUNT_V0) {
283                    *slot = read_uint(&buf[pos..], sa);
284                    pos += sa;
285                }
286            }
287
288            return Ok(Self {
289                version: 0,
290                strategy,
291                persist,
292                threshold,
293                // Values the version-0 encoding does not carry, set to the
294                // library defaults `H5O__fsinfo_decode` gives them. The one
295                // that is not a constant is `eoa_pre_fsm_fsalloc`, which
296                // upstream fills with the file's current end of allocation:
297                // the field means "end of allocation before the managers were
298                // laid out", and a file that never recorded one has no
299                // narrower answer than the whole file.
300                page_size: DEFAULT_FILE_SPACE_PAGE_SIZE,
301                pgend_meta_thres: 0,
302                eoa_pre_fsm_fsalloc: crate::format::UNDEF_ADDR,
303                fs_addr,
304            });
305        }
306
307        if version != 1 {
308            return Err(FormatError::InvalidVersion(version));
309        }
310
311        need(buf, pos + 2 + ss + ss + 2 + sa)?;
312        let strategy = FileSpaceStrategy::from_byte(buf[pos]);
313        pos += 1;
314        let persist = buf[pos] != 0;
315        pos += 1;
316        let threshold = read_uint(&buf[pos..], ss);
317        pos += ss;
318        let page_size = read_uint(&buf[pos..], ss);
319        pos += ss;
320        if page_size == 0 || page_size > PAGE_SIZE_MAX {
321            return Err(FormatError::InvalidData(format!(
322                "invalid file-space page size {page_size}"
323            )));
324        }
325        let pgend_meta_thres = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
326        pos += 2;
327        let eoa_pre_fsm_fsalloc = read_uint(&buf[pos..], sa);
328        pos += sa;
329
330        let mut fs_addr = vec![crate::format::UNDEF_ADDR; FS_ADDR_COUNT_V1];
331        if persist {
332            need(buf, pos + FS_ADDR_COUNT_V1 * sa)?;
333            for slot in fs_addr.iter_mut() {
334                *slot = read_uint(&buf[pos..], sa);
335                pos += sa;
336            }
337        }
338
339        Ok(Self {
340            version: 1,
341            strategy,
342            persist,
343            threshold,
344            page_size,
345            pgend_meta_thres,
346            eoa_pre_fsm_fsalloc,
347            fs_addr,
348        })
349    }
350    /// Encode the message body at the version it was decoded at
351    /// (`H5O__fsinfo_encode`, plus the version-0 layout upstream only reads).
352    ///
353    /// Fails only for a version-0 message carrying something that encoding
354    /// cannot express, which decode cannot produce and only a caller that
355    /// edited the message can reach — see [`encode_v0`](Self::encode_v0).
356    pub fn encode(&self, ctx: &FormatContext) -> FormatResult<Vec<u8>> {
357        if self.version == 0 {
358            return self.encode_v0(ctx);
359        }
360        let sa = ctx.sizeof_addr as usize;
361        let ss = ctx.sizeof_size as usize;
362        let mut buf = Vec::with_capacity(3 + 2 * ss + 2 + sa + FS_ADDR_COUNT_V1 * sa);
363        buf.push(1);
364        buf.push(self.strategy.to_byte());
365        buf.push(self.persist as u8);
366        buf.extend_from_slice(&self.threshold.to_le_bytes()[..ss]);
367        buf.extend_from_slice(&self.page_size.to_le_bytes()[..ss]);
368        buf.extend_from_slice(&self.pgend_meta_thres.to_le_bytes());
369        buf.extend_from_slice(&self.eoa_pre_fsm_fsalloc.to_le_bytes()[..sa]);
370        if self.persist {
371            for addr in &self.fs_addr {
372                buf.extend_from_slice(&addr.to_le_bytes()[..sa]);
373            }
374        }
375        Ok(buf)
376    }
377
378    /// Encode the deprecated 1.10.0 body, the inverse of what
379    /// `H5O__fsinfo_decode`'s version-0 branch reads.
380    ///
381    /// Upstream has no such encoder: it decodes version 0, marks the message
382    /// `mapped`, and rewrites it as version 1 on the next read-write open
383    /// (H5Fsuper.c:866-880). This crate re-emits instead, so appending to a
384    /// version-0 file does not silently change its on-disk format — but that
385    /// is only sound while every field still fits, so the ones the encoding
386    /// cannot hold are checked rather than assumed:
387    ///
388    /// * `strategy`/`persist` must be one of the four `H5F_file_space_type_t`
389    ///   combinations; paged aggregation postdates this encoding.
390    /// * `threshold` is stored only for the two `FSM_AGGR` values; the other
391    ///   two are decoded as 1 and must still be 1.
392    /// * the managers past the sixth must be undefined, there being no slot
393    ///   for them.
394    ///
395    /// `page_size`, `pgend_meta_thres` and `eoa_pre_fsm_fsalloc` are *not*
396    /// checked, because the version-0 encoding never carried them: decode
397    /// synthesizes all three, and the next decode synthesizes them again from
398    /// the same rule. Writing back the first two would be writing back
399    /// constants; the third is re-derived from the file's end of allocation,
400    /// which is the value it would have anyway.
401    pub fn encode_v0(&self, ctx: &FormatContext) -> FormatResult<Vec<u8>> {
402        let sa = ctx.sizeof_addr as usize;
403        let ss = ctx.sizeof_size as usize;
404        // The inverse of the version-0 branch's strategy mapping.
405        let legacy = match (self.strategy, self.persist) {
406            (FileSpaceStrategy::FsmAggr, true) => 1,
407            (FileSpaceStrategy::FsmAggr, false) => 2,
408            (FileSpaceStrategy::Aggr, false) => 3,
409            (FileSpaceStrategy::None, false) => 4,
410            (strategy, persist) => {
411                return Err(FormatError::InvalidData(format!(
412                    "strategy {strategy:?} with persist {persist} has no version-0 \
413                     file-space info encoding"
414                )))
415            }
416        };
417        if legacy > 2 && self.threshold != 1 {
418            return Err(FormatError::InvalidData(format!(
419                "version-0 file-space strategy {legacy} cannot carry threshold {}",
420                self.threshold
421            )));
422        }
423        if self.fs_addr.len() > FS_ADDR_COUNT_V0
424            && self.fs_addr[FS_ADDR_COUNT_V0..]
425                .iter()
426                .any(|&a| a != crate::format::UNDEF_ADDR)
427        {
428            return Err(FormatError::InvalidData(
429                "a version-0 file-space info message has no slot for a page-type \
430                 free-space manager"
431                    .into(),
432            ));
433        }
434
435        let mut buf = Vec::with_capacity(2 + ss + FS_ADDR_COUNT_V0 * sa);
436        buf.push(0);
437        buf.push(legacy);
438        buf.extend_from_slice(&self.threshold.to_le_bytes()[..ss]);
439        if self.persist {
440            let undef = [crate::format::UNDEF_ADDR];
441            for slot in 0..FS_ADDR_COUNT_V0 {
442                let addr = self.fs_addr.get(slot).unwrap_or(&undef[0]);
443                buf.extend_from_slice(&addr.to_le_bytes()[..sa]);
444            }
445        }
446        Ok(buf)
447    }
448}
449
450fn need(buf: &[u8], n: usize) -> FormatResult<()> {
451    if buf.len() < n {
452        Err(FormatError::BufferTooShort {
453            needed: n,
454            available: buf.len(),
455        })
456    } else {
457        Ok(())
458    }
459}
460
461// ======================================================================= tests
462
463#[cfg(test)]
464mod tests {
465    use super::*;
466
467    fn ctx() -> FormatContext {
468        FormatContext::default_v3()
469    }
470
471    #[test]
472    fn shmesg_roundtrip() {
473        let mut buf = vec![0u8];
474        buf.extend_from_slice(&88u64.to_le_bytes());
475        buf.push(3);
476        let m = SharedMessageTableMessage::decode(&buf, &ctx()).unwrap();
477        assert_eq!(m.table_address, 88);
478        assert_eq!(m.nindexes, 3);
479    }
480
481    /// The ten bytes `sohm_list.h5` carries in its superblock extension: the
482    /// table at 88, one index.
483    #[test]
484    fn shmesg_encodes_the_fixture_body() {
485        let m = SharedMessageTableMessage {
486            version: 0,
487            table_address: 88,
488            nindexes: 1,
489        };
490        assert_eq!(
491            m.encode(&ctx()),
492            vec![0, 0x58, 0, 0, 0, 0, 0, 0, 0, 1],
493            "H5O__shmesg_encode order: version, table address, index count"
494        );
495        assert_eq!(
496            SharedMessageTableMessage::decode(&m.encode(&ctx()), &ctx()).unwrap(),
497            m
498        );
499    }
500
501    #[test]
502    fn shmesg_rejects_bad_version() {
503        let mut buf = vec![9u8];
504        buf.extend_from_slice(&88u64.to_le_bytes());
505        buf.push(1);
506        assert!(matches!(
507            SharedMessageTableMessage::decode(&buf, &ctx()).unwrap_err(),
508            FormatError::InvalidVersion(9)
509        ));
510    }
511
512    #[test]
513    fn shmesg_rejects_short_buffer() {
514        assert!(matches!(
515            SharedMessageTableMessage::decode(&[0u8; 4], &ctx()).unwrap_err(),
516            FormatError::BufferTooShort { .. }
517        ));
518    }
519
520    #[test]
521    fn btreek_field_order_matches_upstream() {
522        // version, chunk K, snode K, sym leaf K — H5O__btreek_decode order.
523        let buf = [0u8, 0x40, 0x00, 0x20, 0x00, 0x08, 0x00];
524        let m = BtreeKMessage::decode(&buf).unwrap();
525        assert_eq!(m.chunk_internal_k, 64);
526        assert_eq!(m.snode_internal_k, 32);
527        assert_eq!(m.sym_leaf_k, 8);
528    }
529
530    #[test]
531    fn btreek_rejects_bad_version() {
532        let buf = [1u8, 0, 0, 0, 0, 0, 0];
533        assert!(matches!(
534            BtreeKMessage::decode(&buf).unwrap_err(),
535            FormatError::InvalidVersion(1)
536        ));
537    }
538
539    #[test]
540    fn drvinfo_decodes_name_and_payload() {
541        let mut buf = vec![0u8];
542        buf.extend_from_slice(b"NCSAmult");
543        buf.extend_from_slice(&4u16.to_le_bytes());
544        buf.extend_from_slice(&[1, 2, 3, 4]);
545        let m = DriverInfoMessage::decode(&buf).unwrap();
546        assert_eq!(m.name, "NCSAmult");
547        assert_eq!(m.data, vec![1, 2, 3, 4]);
548    }
549
550    #[test]
551    fn drvinfo_rejects_zero_length() {
552        let mut buf = vec![0u8];
553        buf.extend_from_slice(b"NCSAfami");
554        buf.extend_from_slice(&0u16.to_le_bytes());
555        assert!(matches!(
556            DriverInfoMessage::decode(&buf).unwrap_err(),
557            FormatError::InvalidData(_)
558        ));
559    }
560
561    fn fsinfo_v1(persist: bool) -> Vec<u8> {
562        // version 1, strategy PAGE (H5F_FSPACE_STRATEGY_PAGE == 1).
563        let mut buf = vec![1u8, 1u8, persist as u8];
564        buf.extend_from_slice(&1u64.to_le_bytes()); // threshold
565        buf.extend_from_slice(&4096u64.to_le_bytes()); // page size
566        buf.extend_from_slice(&0u16.to_le_bytes()); // pgend meta threshold
567        buf.extend_from_slice(&0x1000u64.to_le_bytes()); // eoa_pre_fsm_fsalloc
568        if persist {
569            for i in 0..FS_ADDR_COUNT_V1 {
570                buf.extend_from_slice(&((0x2000 + i as u64) * 8).to_le_bytes());
571            }
572        }
573        buf
574    }
575
576    #[test]
577    fn fsinfo_v1_paged_non_persisting() {
578        let m = FileSpaceInfoMessage::decode(&fsinfo_v1(false), &ctx()).unwrap();
579        assert_eq!(m.strategy, FileSpaceStrategy::Page);
580        assert!(!m.persist);
581        assert_eq!(m.page_size, 4096);
582        assert!(m.fs_addr.iter().all(|&a| a == crate::format::UNDEF_ADDR));
583    }
584
585    #[test]
586    fn fsinfo_v1_persisting_reads_twelve_addresses() {
587        let m = FileSpaceInfoMessage::decode(&fsinfo_v1(true), &ctx()).unwrap();
588        assert!(m.persist);
589        assert_eq!(m.fs_addr.len(), FS_ADDR_COUNT_V1);
590        assert_eq!(m.fs_addr[0], 0x2000 * 8);
591        assert_eq!(m.eoa_pre_fsm_fsalloc, 0x1000);
592    }
593
594    #[test]
595    fn fsinfo_v1_rejects_absurd_page_size() {
596        let mut buf = fsinfo_v1(false);
597        buf[11..19].copy_from_slice(&(PAGE_SIZE_MAX + 1).to_le_bytes());
598        assert!(matches!(
599            FileSpaceInfoMessage::decode(&buf, &ctx()).unwrap_err(),
600            FormatError::InvalidData(_)
601        ));
602    }
603
604    #[test]
605    fn fsinfo_v0_all_persist_maps_onto_the_version_one_fields() {
606        let mut buf = vec![0u8, 1u8];
607        buf.extend_from_slice(&7u64.to_le_bytes()); // threshold
608        for i in 0..FS_ADDR_COUNT_V0 {
609            buf.extend_from_slice(&(0x100u64 + i as u64).to_le_bytes());
610        }
611        let m = FileSpaceInfoMessage::decode(&buf, &ctx()).unwrap();
612        assert_eq!(m.version, 0);
613        assert_eq!(m.strategy, FileSpaceStrategy::FsmAggr);
614        assert!(m.persist);
615        assert_eq!(m.threshold, 7);
616        // Twelve slots whatever the encoding: the six the version-0 body
617        // carries, then the page-type ones it has no room for.
618        assert_eq!(m.fs_addr.len(), FS_ADDR_COUNT_V1);
619        assert_eq!(m.fs_addr[5], 0x105);
620        assert!(m.fs_addr[FS_ADDR_COUNT_V0..]
621            .iter()
622            .all(|&a| a == crate::format::UNDEF_ADDR));
623    }
624
625    #[test]
626    fn fsinfo_v0_vfd_maps_to_none() {
627        let mut buf = vec![0u8, 4u8];
628        buf.extend_from_slice(&0u64.to_le_bytes());
629        let m = FileSpaceInfoMessage::decode(&buf, &ctx()).unwrap();
630        assert_eq!(m.strategy, FileSpaceStrategy::None);
631        assert!(!m.persist);
632        assert!(m.fs_addr.iter().all(|&a| a == crate::format::UNDEF_ADDR));
633    }
634
635    /// The four version-0 bodies, hand-built: every `H5F_file_space_type_t`
636    /// value `H5O__fsinfo_decode` accepts. No writer produces these — libhdf5
637    /// has emitted version 1 since 1.10.1 and h5py cannot ask for the older
638    /// form at any `libver` bound — so a byte fixture is the only way to
639    /// exercise the path a 1.10.0-written file takes.
640    fn fsinfo_v0(legacy: u8, threshold: u64) -> Vec<u8> {
641        let mut buf = vec![0u8, legacy];
642        buf.extend_from_slice(&threshold.to_le_bytes());
643        if legacy == 1 {
644            for i in 0..FS_ADDR_COUNT_V0 {
645                buf.extend_from_slice(&(0x400u64 + 0x40 * i as u64).to_le_bytes());
646            }
647        }
648        buf
649    }
650
651    #[test]
652    fn fsinfo_v0_round_trips_as_version_zero() {
653        for (legacy, threshold) in [(1u8, 9u64), (2, 9), (3, 1), (4, 1)] {
654            let bytes = fsinfo_v0(legacy, threshold);
655            let m = FileSpaceInfoMessage::decode(&bytes, &ctx()).unwrap();
656            assert_eq!(m.version, 0, "strategy {legacy}");
657            assert_eq!(
658                m.encode(&ctx()).unwrap(),
659                bytes,
660                "strategy {legacy} did not re-emit the body it was read from"
661            );
662        }
663    }
664
665    #[test]
666    fn a_version_zero_message_re_emits_a_moved_manager() {
667        // What an append does to the message: the managers move, and nothing
668        // else about it changes.
669        let mut m = FileSpaceInfoMessage::decode(&fsinfo_v0(1, 9), &ctx()).unwrap();
670        m.fs_addr[0] = 0x2000;
671        m.fs_addr[2] = 0x3000;
672        m.eoa_pre_fsm_fsalloc = 0x9000;
673        let bytes = m.encode(&ctx()).unwrap();
674        assert_eq!(bytes.len(), fsinfo_v0(1, 9).len());
675        let again = FileSpaceInfoMessage::decode(&bytes, &ctx()).unwrap();
676        assert_eq!(again.fs_addr[0], 0x2000);
677        assert_eq!(again.fs_addr[2], 0x3000);
678        // The one field the older encoding cannot carry: decode synthesizes it
679        // from the file's end of allocation, as `H5O__fsinfo_decode` does.
680        assert_eq!(again.eoa_pre_fsm_fsalloc, crate::format::UNDEF_ADDR);
681        assert_eq!(
682            FileSpaceInfoMessage {
683                eoa_pre_fsm_fsalloc: m.eoa_pre_fsm_fsalloc,
684                ..again
685            },
686            m
687        );
688    }
689
690    #[test]
691    fn fsinfo_v0_refuses_what_it_cannot_encode() {
692        let paged = FileSpaceInfoMessage::decode(&fsinfo_v1(true), &ctx()).unwrap();
693        assert!(matches!(
694            paged.encode_v0(&ctx()).unwrap_err(),
695            FormatError::InvalidData(_)
696        ));
697
698        let mut aggr = FileSpaceInfoMessage::decode(&fsinfo_v0(3, 1), &ctx()).unwrap();
699        aggr.threshold = 64;
700        assert!(matches!(
701            aggr.encode(&ctx()).unwrap_err(),
702            FormatError::InvalidData(_)
703        ));
704
705        let mut paged_manager = FileSpaceInfoMessage::decode(&fsinfo_v0(1, 9), &ctx()).unwrap();
706        paged_manager.fs_addr[FS_ADDR_COUNT_V0] = 0x800;
707        assert!(matches!(
708            paged_manager.encode(&ctx()).unwrap_err(),
709            FormatError::InvalidData(_)
710        ));
711    }
712
713    #[test]
714    fn fsinfo_v1_round_trips() {
715        for persist in [false, true] {
716            let bytes = fsinfo_v1(persist);
717            let m = FileSpaceInfoMessage::decode(&bytes, &ctx()).unwrap();
718            assert_eq!(m.encode(&ctx()).unwrap(), bytes);
719        }
720    }
721
722    #[test]
723    fn fsinfo_rejects_unknown_version() {
724        let buf = vec![9u8; 40];
725        assert!(matches!(
726            FileSpaceInfoMessage::decode(&buf, &ctx()).unwrap_err(),
727            FormatError::InvalidVersion(9)
728        ));
729    }
730}