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#[derive(Debug, Clone, PartialEq, Eq)]
87pub struct FileSpaceInfo {
88    /// The file-space management strategy.
89    pub strategy: FileSpaceStrategy,
90    /// Whether free space is persisted to disk across file close (the manager
91    /// addresses below are written only when this is set).
92    pub persist: bool,
93    /// Smallest free-space section size the managers track.
94    pub threshold: u64,
95    /// File-space page size used for paged allocation.
96    pub page_size: u64,
97    /// Page-end metadata threshold (paged allocation tuning).
98    pub page_end_meta_threshold: u16,
99    /// End-of-allocation address recorded before free-space manager metadata
100    /// was allocated; [`u64::MAX`] when free space is not persisted.
101    pub eoa_pre_fsm: u64,
102    /// Free-space manager header addresses (present only when [`persist`] is
103    /// set); unused slots are [`u64::MAX`]. Followed to their on-disk `FSHD`/
104    /// `FSSE` blocks by [`File::persisted_free_space`](crate::File::persisted_free_space).
105    ///
106    /// [`persist`]: Self::persist
107    pub manager_addrs: Vec<u64>,
108}
109
110impl FileSpaceInfo {
111    /// A non-persisting message recording `strategy` with the given thresholds.
112    /// This is the form the writer emits (no free-space manager blocks).
113    pub(crate) fn non_persistent(
114        strategy: FileSpaceStrategy,
115        threshold: u64,
116        page_size: u64,
117    ) -> Self {
118        FileSpaceInfo {
119            strategy,
120            persist: false,
121            threshold,
122            page_size,
123            page_end_meta_threshold: 0,
124            eoa_pre_fsm: UNDEF,
125            manager_addrs: Vec::new(),
126        }
127    }
128
129    /// A persisting message for a file with no free space yet (the form
130    /// [`FileBuilder`](crate::FileBuilder) emits for `persist = true`): the
131    /// persist flag is set but every manager slot is undefined and no FSM space
132    /// has been allocated, so `eoa_pre_fsm` is [`UNDEF`]. This matches what the C
133    /// library records when persistence is on but nothing is tracked.
134    pub(crate) fn persistent_empty(
135        strategy: FileSpaceStrategy,
136        threshold: u64,
137        page_size: u64,
138    ) -> Self {
139        FileSpaceInfo {
140            strategy,
141            persist: true,
142            threshold,
143            page_size,
144            page_end_meta_threshold: 0,
145            eoa_pre_fsm: UNDEF,
146            manager_addrs: vec![UNDEF; NUM_FILE_FSM_MANAGERS],
147        }
148    }
149
150    /// A persisting message whose first free-space manager is at `manager0_addr`
151    /// (the others undefined), recording `eoa_pre_fsm` — the end-of-allocation
152    /// before the on-disk free-space-manager blocks were appended. This is the
153    /// form [`EditSession`](crate::EditSession) writes when it persists a non-empty
154    /// free list: every tracked region lives in that one manager.
155    pub(crate) fn persistent_single_manager(
156        strategy: FileSpaceStrategy,
157        threshold: u64,
158        page_size: u64,
159        manager0_addr: u64,
160        eoa_pre_fsm: u64,
161    ) -> Self {
162        let mut manager_addrs = vec![UNDEF; NUM_FILE_FSM_MANAGERS];
163        manager_addrs[0] = manager0_addr;
164        FileSpaceInfo {
165            strategy,
166            persist: true,
167            threshold,
168            page_size,
169            page_end_meta_threshold: 0,
170            eoa_pre_fsm,
171            manager_addrs,
172        }
173    }
174
175    /// Serialize the version-1 message body (without the object-header message
176    /// prefix). Manager addresses are written only when [`persist`] is set.
177    ///
178    /// [`persist`]: Self::persist
179    pub(crate) fn serialize(&self) -> Vec<u8> {
180        let mut buf = Vec::with_capacity(29 + self.manager_addrs.len() * 8);
181        buf.push(1); // version
182        buf.push(self.strategy.to_code());
183        buf.push(self.persist as u8);
184        buf.extend_from_slice(&self.threshold.to_le_bytes());
185        buf.extend_from_slice(&self.page_size.to_le_bytes());
186        buf.extend_from_slice(&self.page_end_meta_threshold.to_le_bytes());
187        buf.extend_from_slice(&self.eoa_pre_fsm.to_le_bytes());
188        if self.persist {
189            for &addr in &self.manager_addrs {
190                buf.extend_from_slice(&addr.to_le_bytes());
191            }
192        }
193        buf
194    }
195
196    /// Parse a version-1 message body. `offset_size`/`length_size` come from the
197    /// superblock (both 8 for standard files).
198    pub(crate) fn parse(
199        data: &[u8],
200        offset_size: u8,
201        length_size: u8,
202    ) -> Result<FileSpaceInfo, FormatError> {
203        let os = offset_size as usize;
204        let ls = length_size as usize;
205        // version(1) + strategy(1) + persist(1) + threshold(ls) + page_size(ls)
206        // + page_end(2) + eoa(os)
207        let fixed = 3 + ls + ls + 2 + os;
208        if data.len() < fixed {
209            return Err(FormatError::UnexpectedEof {
210                expected: fixed,
211                available: data.len(),
212            });
213        }
214        let version = data[0];
215        if version != 1 {
216            return Err(FormatError::UnsupportedFileSpaceInfoVersion(version));
217        }
218        let strategy = FileSpaceStrategy::from_code(data[1])?;
219        let persist = data[2] != 0;
220        let mut pos = 3;
221        let threshold = read_uint_le(&data[pos..pos + ls]);
222        pos += ls;
223        let page_size = read_uint_le(&data[pos..pos + ls]);
224        pos += ls;
225        let page_end_meta_threshold = u16::from_le_bytes([data[pos], data[pos + 1]]);
226        pos += 2;
227        let eoa_pre_fsm = read_uint_le(&data[pos..pos + os]);
228        pos += os;
229
230        let mut manager_addrs = Vec::new();
231        if persist {
232            // The remaining bytes are offset-size manager addresses; read as
233            // many as are present rather than assuming a fixed count.
234            while pos + os <= data.len() {
235                manager_addrs.push(read_uint_le(&data[pos..pos + os]));
236                pos += os;
237            }
238        }
239
240        Ok(FileSpaceInfo {
241            strategy,
242            persist,
243            threshold,
244            page_size,
245            page_end_meta_threshold,
246            eoa_pre_fsm,
247            manager_addrs,
248        })
249    }
250}
251
252/// Read a little-endian unsigned integer of 1–8 bytes into a `u64`.
253fn read_uint_le(bytes: &[u8]) -> u64 {
254    let mut v = 0u64;
255    for (i, &b) in bytes.iter().enumerate() {
256        v |= (b as u64) << (8 * i);
257    }
258    v
259}
260
261#[cfg(test)]
262mod tests {
263    use super::*;
264
265    #[test]
266    fn non_persistent_roundtrip_29_bytes() {
267        for strategy in [
268            FileSpaceStrategy::FsmAggr,
269            FileSpaceStrategy::Page,
270            FileSpaceStrategy::Aggr,
271            FileSpaceStrategy::None,
272        ] {
273            let info = FileSpaceInfo::non_persistent(strategy, 1, 4096);
274            let bytes = info.serialize();
275            assert_eq!(bytes.len(), 29, "non-persistent message is 29 bytes");
276            let parsed = FileSpaceInfo::parse(&bytes, 8, 8).unwrap();
277            assert_eq!(parsed, info);
278            assert_eq!(parsed.eoa_pre_fsm, u64::MAX);
279            assert!(parsed.manager_addrs.is_empty());
280        }
281    }
282
283    #[test]
284    fn matches_c_library_none_bytes() {
285        // Exact bytes the reference C library (HDF5 1.14.6) wrote for strategy
286        // NONE, captured via tmp/probe_fsinfo.py.
287        let expected = [
288            0x01u8, 0x03, 0x00, // version=1, strategy=NONE(3), persist=0
289            0x01, 0, 0, 0, 0, 0, 0, 0, // threshold=1
290            0x00, 0x10, 0, 0, 0, 0, 0, 0, // page_size=4096
291            0x00, 0x00, // page end meta threshold
292            0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // eoa = UNDEF
293        ];
294        let info = FileSpaceInfo::non_persistent(FileSpaceStrategy::None, 1, 4096);
295        assert_eq!(info.serialize(), expected);
296    }
297
298    #[test]
299    fn parses_persistent_manager_addresses() {
300        // A persisting message: 29-byte head + three 8-byte manager addresses.
301        let mut bytes = FileSpaceInfo {
302            strategy: FileSpaceStrategy::FsmAggr,
303            persist: true,
304            threshold: 1,
305            page_size: 4096,
306            page_end_meta_threshold: 0,
307            eoa_pre_fsm: 2072,
308            manager_addrs: vec![619, u64::MAX, u64::MAX],
309        }
310        .serialize();
311        assert_eq!(bytes.len(), 29 + 3 * 8);
312        let parsed = FileSpaceInfo::parse(&bytes, 8, 8).unwrap();
313        assert_eq!(parsed.manager_addrs, vec![619, u64::MAX, u64::MAX]);
314        assert_eq!(parsed.eoa_pre_fsm, 2072);
315        assert!(parsed.persist);
316
317        // Truncate the version byte to an unsupported value -> clean error.
318        bytes[0] = 0;
319        assert!(matches!(
320            FileSpaceInfo::parse(&bytes, 8, 8),
321            Err(FormatError::UnsupportedFileSpaceInfoVersion(0))
322        ));
323    }
324
325    #[test]
326    fn rejects_bad_strategy_code() {
327        assert!(matches!(
328            FileSpaceStrategy::from_code(4),
329            Err(FormatError::InvalidFileSpaceStrategy(4))
330        ));
331    }
332}