Skip to main content

hdf5_pure/
file_space_info.rs

1//! HDF5 File Space Info message (header message type `0x0017`) and the
2//! file-space management strategy it records.
3//!
4//! Introduced in HDF5 1.10, this message lives in the *superblock extension*
5//! (a standalone object header the superblock points at) and records the
6//! choices made through `H5Pset_file_space_strategy` and
7//! `H5Pset_file_space_page_size`: how a file tracks and reuses free space, the
8//! free-space section threshold, and the file-space page size.
9//!
10//! Only the version-1 layout (HDF5 1.10.1+, the only one any current tool
11//! writes) is handled. Byte layout, all little-endian:
12//!
13//! | field                     | size        | notes                          |
14//! |---------------------------|-------------|--------------------------------|
15//! | version                   | 1           | always 1                       |
16//! | strategy                  | 1           | [`FileSpaceStrategy`] code 0–3 |
17//! | persisting free space     | 1           | 0 or 1                         |
18//! | free-space threshold      | length size | smallest tracked section       |
19//! | file-space page size      | length size | paged-allocation page          |
20//! | page end metadata thresh. | 2           |                                |
21//! | EOA before FSM allocation | offset size | `UNDEF` when not persisting     |
22//! | free-space manager addrs  | offset size × N | present only when persisting |
23//!
24//! When free space is not persisted the manager-address array is omitted
25//! entirely (a 29-byte message for the standard 8-byte sizes).
26
27#[cfg(not(feature = "std"))]
28extern crate alloc;
29#[cfg(not(feature = "std"))]
30use alloc::{vec, vec::Vec};
31
32use crate::error::FormatError;
33
34/// An undefined on-disk address (all bits set), HDF5's "no address" sentinel.
35const UNDEF: u64 = u64::MAX;
36
37/// The default free-space section threshold the C library uses.
38pub(crate) const DEFAULT_THRESHOLD: u64 = 1;
39/// The default file-space page size the C library uses.
40pub(crate) const DEFAULT_PAGE_SIZE: u64 = 4096;
41/// Number of free-space-manager address slots a persisting message carries (one
42/// per file memory type); the reference C library writes twelve.
43pub(crate) const NUM_FILE_FSM_MANAGERS: usize = 12;
44
45/// File-space management strategy, mirroring HDF5's `H5F_fspace_strategy_t`
46/// (set with `H5Pset_file_space_strategy`).
47#[derive(Debug, Clone, Copy, PartialEq, Eq)]
48pub enum FileSpaceStrategy {
49    /// Free-space managers, aggregators, and the virtual file driver — the
50    /// HDF5 default. `H5F_FSPACE_STRATEGY_FSM_AGGR`.
51    FsmAggr,
52    /// Paged aggregation backed by free-space managers.
53    /// `H5F_FSPACE_STRATEGY_PAGE`.
54    Page,
55    /// Aggregators and the virtual file driver only, no free-space managers.
56    /// `H5F_FSPACE_STRATEGY_AGGR`.
57    Aggr,
58    /// No free-space tracking; allocation only ever appends.
59    /// `H5F_FSPACE_STRATEGY_NONE`.
60    None,
61}
62
63impl FileSpaceStrategy {
64    /// The on-disk numeric code (0–3).
65    pub(crate) fn to_code(self) -> u8 {
66        match self {
67            FileSpaceStrategy::FsmAggr => 0,
68            FileSpaceStrategy::Page => 1,
69            FileSpaceStrategy::Aggr => 2,
70            FileSpaceStrategy::None => 3,
71        }
72    }
73
74    fn from_code(code: u8) -> Result<Self, FormatError> {
75        match code {
76            0 => Ok(FileSpaceStrategy::FsmAggr),
77            1 => Ok(FileSpaceStrategy::Page),
78            2 => Ok(FileSpaceStrategy::Aggr),
79            3 => Ok(FileSpaceStrategy::None),
80            other => Err(FormatError::InvalidFileSpaceStrategy(other)),
81        }
82    }
83}
84
85/// A parsed (or to-be-written) File Space Info message.
86///
87/// Non-exhaustive: read through [`File::file_space_info`](crate::File::file_space_info),
88/// never constructed by a caller — set a file's strategy with
89/// [`FileBuilder::with_file_space_strategy`](crate::FileBuilder::with_file_space_strategy).
90/// Fields may be added as the format message grows.
91#[derive(Debug, Clone, PartialEq, Eq)]
92#[non_exhaustive]
93pub struct FileSpaceInfo {
94    /// The file-space management strategy.
95    pub strategy: FileSpaceStrategy,
96    /// Whether free space is persisted to disk across file close (the manager
97    /// addresses below are written only when this is set).
98    pub persist: bool,
99    /// Smallest free-space section size the managers track.
100    pub threshold: u64,
101    /// File-space page size used for paged allocation.
102    pub page_size: u64,
103    /// Page-end metadata threshold (paged allocation tuning).
104    pub page_end_meta_threshold: u16,
105    /// End-of-allocation address recorded before free-space manager metadata
106    /// was allocated; [`u64::MAX`] when free space is not persisted.
107    pub eoa_pre_fsm: u64,
108    /// Free-space manager header addresses (present only when [`persist`] is
109    /// set); unused slots are [`u64::MAX`]. Followed to their on-disk `FSHD`/
110    /// `FSSE` blocks by [`File::persisted_free_space`](crate::File::persisted_free_space).
111    ///
112    /// [`persist`]: Self::persist
113    pub manager_addrs: Vec<u64>,
114}
115
116impl FileSpaceInfo {
117    /// A non-persisting message recording `strategy` with the given thresholds.
118    /// This is the form the writer emits (no free-space manager blocks).
119    pub(crate) fn non_persistent(
120        strategy: FileSpaceStrategy,
121        threshold: u64,
122        page_size: u64,
123    ) -> Self {
124        FileSpaceInfo {
125            strategy,
126            persist: false,
127            threshold,
128            page_size,
129            page_end_meta_threshold: 0,
130            eoa_pre_fsm: UNDEF,
131            manager_addrs: Vec::new(),
132        }
133    }
134
135    /// A persisting message for a file with no free space yet (the form
136    /// [`FileBuilder`](crate::FileBuilder) emits for `persist = true`): the
137    /// persist flag is set and every manager slot is undefined because no FSM
138    /// space has been allocated. `eoa_pre_fsm` is left [`UNDEF`] here as a
139    /// placeholder; the writer overwrites it with the real end-of-allocation once
140    /// the layout is known, because libhdf5 requires a persisting file to record a
141    /// defined `eoa_fsm_fsalloc` (an assertion-enabled build aborts on the
142    /// sentinel — issue #178).
143    pub(crate) fn persistent_empty(
144        strategy: FileSpaceStrategy,
145        threshold: u64,
146        page_size: u64,
147    ) -> Self {
148        FileSpaceInfo {
149            strategy,
150            persist: true,
151            threshold,
152            page_size,
153            page_end_meta_threshold: 0,
154            eoa_pre_fsm: UNDEF,
155            manager_addrs: vec![UNDEF; NUM_FILE_FSM_MANAGERS],
156        }
157    }
158
159    /// A persisting message whose first free-space manager is at `manager0_addr`
160    /// (the others undefined), recording `eoa_pre_fsm` — the end-of-allocation
161    /// before the on-disk free-space-manager blocks were appended. This is the
162    /// form [`File::open_rw`](crate::File::open_rw) writes when it persists a non-empty
163    /// free list: every tracked region lives in that one manager.
164    pub(crate) fn persistent_single_manager(
165        strategy: FileSpaceStrategy,
166        threshold: u64,
167        page_size: u64,
168        manager0_addr: u64,
169        eoa_pre_fsm: u64,
170    ) -> Self {
171        let mut manager_addrs = vec![UNDEF; NUM_FILE_FSM_MANAGERS];
172        manager_addrs[0] = manager0_addr;
173        FileSpaceInfo {
174            strategy,
175            persist: true,
176            threshold,
177            page_size,
178            page_end_meta_threshold: 0,
179            eoa_pre_fsm,
180            manager_addrs,
181        }
182    }
183
184    /// A persisting message for a paged file whose free space is tracked by
185    /// per-page-type managers. `slots[k]` is the `FSHD` address of the manager for
186    /// page type `k + 1` (or [`UNDEF`] when that page type tracks no free space);
187    /// `eoa_pre_fsm` is the page-aligned end-of-allocation. This is the form the
188    /// paged writer emits: metadata free space lives in the SUPER manager
189    /// (`slots[0]`), small raw data in DRAW (`slots[2]`), and the trailing
190    /// fragments of large multi-page allocations in the generic-large manager
191    /// (`slots[6]`).
192    pub(crate) fn persistent_managers(
193        strategy: FileSpaceStrategy,
194        threshold: u64,
195        page_size: u64,
196        slots: [u64; NUM_FILE_FSM_MANAGERS],
197        eoa_pre_fsm: u64,
198    ) -> Self {
199        FileSpaceInfo {
200            strategy,
201            persist: true,
202            threshold,
203            page_size,
204            page_end_meta_threshold: 0,
205            eoa_pre_fsm,
206            manager_addrs: slots.to_vec(),
207        }
208    }
209
210    /// Serialize the version-1 message body (without the object-header message
211    /// prefix). Manager addresses are written only when [`persist`] is set.
212    ///
213    /// [`persist`]: Self::persist
214    pub(crate) fn serialize(&self) -> Vec<u8> {
215        let mut buf = Vec::with_capacity(29 + self.manager_addrs.len() * 8);
216        buf.push(1); // version
217        buf.push(self.strategy.to_code());
218        buf.push(self.persist as u8);
219        buf.extend_from_slice(&self.threshold.to_le_bytes());
220        buf.extend_from_slice(&self.page_size.to_le_bytes());
221        buf.extend_from_slice(&self.page_end_meta_threshold.to_le_bytes());
222        buf.extend_from_slice(&self.eoa_pre_fsm.to_le_bytes());
223        if self.persist {
224            for &addr in &self.manager_addrs {
225                buf.extend_from_slice(&addr.to_le_bytes());
226            }
227        }
228        buf
229    }
230
231    /// Parse a version-1 message body. `offset_size`/`length_size` come from the
232    /// superblock (both 8 for standard files).
233    pub(crate) fn parse(
234        data: &[u8],
235        offset_size: u8,
236        length_size: u8,
237    ) -> Result<FileSpaceInfo, FormatError> {
238        let os = offset_size as usize;
239        let ls = length_size as usize;
240        // version(1) + strategy(1) + persist(1) + threshold(ls) + page_size(ls)
241        // + page_end(2) + eoa(os)
242        let fixed = 3 + ls + ls + 2 + os;
243        if data.len() < fixed {
244            return Err(FormatError::UnexpectedEof {
245                expected: fixed,
246                available: data.len(),
247            });
248        }
249        let version = data[0];
250        if version != 1 {
251            return Err(FormatError::UnsupportedFileSpaceInfoVersion(version));
252        }
253        let strategy = FileSpaceStrategy::from_code(data[1])?;
254        let persist = data[2] != 0;
255        let mut pos = 3;
256        let threshold = read_uint_le(&data[pos..pos + ls]);
257        pos += ls;
258        let page_size = read_uint_le(&data[pos..pos + ls]);
259        pos += ls;
260        let page_end_meta_threshold = u16::from_le_bytes([data[pos], data[pos + 1]]);
261        pos += 2;
262        let eoa_pre_fsm = read_uint_le(&data[pos..pos + os]);
263        pos += os;
264
265        let mut manager_addrs = Vec::new();
266        if persist {
267            // The remaining bytes are offset-size manager addresses; read as
268            // many as are present rather than assuming a fixed count.
269            while pos + os <= data.len() {
270                manager_addrs.push(read_uint_le(&data[pos..pos + os]));
271                pos += os;
272            }
273        }
274
275        Ok(FileSpaceInfo {
276            strategy,
277            persist,
278            threshold,
279            page_size,
280            page_end_meta_threshold,
281            eoa_pre_fsm,
282            manager_addrs,
283        })
284    }
285}
286
287/// Read a little-endian unsigned integer of 1–8 bytes into a `u64`.
288fn read_uint_le(bytes: &[u8]) -> u64 {
289    let mut v = 0u64;
290    for (i, &b) in bytes.iter().enumerate() {
291        v |= (b as u64) << (8 * i);
292    }
293    v
294}
295
296#[cfg(test)]
297mod tests {
298    use super::*;
299
300    #[test]
301    fn non_persistent_roundtrip_29_bytes() {
302        for strategy in [
303            FileSpaceStrategy::FsmAggr,
304            FileSpaceStrategy::Page,
305            FileSpaceStrategy::Aggr,
306            FileSpaceStrategy::None,
307        ] {
308            let info = FileSpaceInfo::non_persistent(strategy, 1, 4096);
309            let bytes = info.serialize();
310            assert_eq!(bytes.len(), 29, "non-persistent message is 29 bytes");
311            let parsed = FileSpaceInfo::parse(&bytes, 8, 8).unwrap();
312            assert_eq!(parsed, info);
313            assert_eq!(parsed.eoa_pre_fsm, u64::MAX);
314            assert!(parsed.manager_addrs.is_empty());
315        }
316    }
317
318    #[test]
319    fn matches_c_library_none_bytes() {
320        // Exact bytes the reference C library (HDF5 1.14.6) wrote for strategy
321        // NONE, captured via tmp/probe_fsinfo.py.
322        let expected = [
323            0x01u8, 0x03, 0x00, // version=1, strategy=NONE(3), persist=0
324            0x01, 0, 0, 0, 0, 0, 0, 0, // threshold=1
325            0x00, 0x10, 0, 0, 0, 0, 0, 0, // page_size=4096
326            0x00, 0x00, // page end meta threshold
327            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // eoa = UNDEF
328        ];
329        let info = FileSpaceInfo::non_persistent(FileSpaceStrategy::None, 1, 4096);
330        assert_eq!(info.serialize(), expected);
331    }
332
333    #[test]
334    fn parses_persistent_manager_addresses() {
335        // A persisting message: 29-byte head + three 8-byte manager addresses.
336        let mut bytes = FileSpaceInfo {
337            strategy: FileSpaceStrategy::FsmAggr,
338            persist: true,
339            threshold: 1,
340            page_size: 4096,
341            page_end_meta_threshold: 0,
342            eoa_pre_fsm: 2072,
343            manager_addrs: vec![619, u64::MAX, u64::MAX],
344        }
345        .serialize();
346        assert_eq!(bytes.len(), 29 + 3 * 8);
347        let parsed = FileSpaceInfo::parse(&bytes, 8, 8).unwrap();
348        assert_eq!(parsed.manager_addrs, vec![619, u64::MAX, u64::MAX]);
349        assert_eq!(parsed.eoa_pre_fsm, 2072);
350        assert!(parsed.persist);
351
352        // Truncate the version byte to an unsupported value -> clean error.
353        bytes[0] = 0;
354        assert!(matches!(
355            FileSpaceInfo::parse(&bytes, 8, 8),
356            Err(FormatError::UnsupportedFileSpaceInfoVersion(0))
357        ));
358    }
359
360    #[test]
361    fn persistent_managers_roundtrips_multiple_slots() {
362        // The paged writer's form: SUPER (slot 0), DRAW (slot 2), and the
363        // generic-large manager (slot 6) defined, the rest undefined.
364        let mut slots = [UNDEF; NUM_FILE_FSM_MANAGERS];
365        slots[0] = 841;
366        slots[2] = 18384;
367        slots[6] = 806;
368        let info =
369            FileSpaceInfo::persistent_managers(FileSpaceStrategy::Page, 0, 16384, slots, 65536);
370        assert!(info.persist);
371        assert_eq!(info.eoa_pre_fsm, 65536);
372        let bytes = info.serialize();
373        // 29-byte head + 12 * 8 manager slots.
374        assert_eq!(bytes.len(), 29 + NUM_FILE_FSM_MANAGERS * 8);
375        let parsed = FileSpaceInfo::parse(&bytes, 8, 8).unwrap();
376        assert_eq!(parsed, info);
377        assert_eq!(parsed.manager_addrs[0], 841);
378        assert_eq!(parsed.manager_addrs[2], 18384);
379        assert_eq!(parsed.manager_addrs[6], 806);
380        assert_eq!(parsed.manager_addrs[1], UNDEF);
381    }
382
383    #[test]
384    fn rejects_bad_strategy_code() {
385        assert!(matches!(
386            FileSpaceStrategy::from_code(4),
387            Err(FormatError::InvalidFileSpaceStrategy(4))
388        ));
389    }
390}