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}