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}