rust_hdf5/format/mod.rs
1//! Pure Rust HDF5 on-disk format codec.
2//!
3//! This crate handles encoding and decoding of HDF5 binary structures
4//! (superblock, object headers, messages, chunk indices) without performing
5//! any file I/O. It is used by `hdf5-io` and `hdf5` crates.
6
7pub mod btree_v1;
8pub(crate) mod bytes;
9pub mod checksum;
10pub mod chunk_index;
11pub mod creation_order;
12pub mod dense_attr;
13pub mod dense_link;
14pub mod fractal_heap;
15pub mod fractal_heap_write;
16pub mod free_space;
17pub mod global_heap;
18pub mod local_heap;
19pub mod messages;
20pub mod nbit_scaleoffset;
21pub mod object_header;
22pub mod reference;
23pub mod selection;
24pub mod sohm;
25pub mod sohm_write;
26pub mod storage_kind;
27pub mod superblock;
28pub mod symbol_table;
29pub mod szip;
30
31/// Format context carrying file-level encoding parameters
32#[derive(Debug, Clone, Copy)]
33pub struct FormatContext {
34 pub sizeof_addr: u8,
35 pub sizeof_size: u8,
36}
37
38impl FormatContext {
39 pub fn default_v3() -> Self {
40 Self {
41 sizeof_addr: 8,
42 sizeof_size: 8,
43 }
44 }
45}
46
47/// The low half of libhdf5's `H5Pset_libver_bounds` — the oldest library
48/// release a file must stay readable by.
49///
50/// It is the file-wide switch that picks between on-disk message versions:
51/// libhdf5 keeps one table per message type (`H5O_dtype_ver_bounds`,
52/// `H5O_layout_ver_bounds`, ...) mapping the bound to the version it stamps.
53/// Raising the bound lets the library use newer, tighter encodings; lowering
54/// it keeps older readers able to open the file.
55#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Default)]
56pub enum LibverBound {
57 /// `H5F_LIBVER_EARLIEST`, the default: every message at its oldest
58 /// version that can express what the file holds.
59 #[default]
60 Earliest,
61 /// `H5F_LIBVER_V18`.
62 V18,
63 /// `H5F_LIBVER_V110`.
64 V110,
65 /// `H5F_LIBVER_V112`.
66 V112,
67 /// `H5F_LIBVER_V114`.
68 V114,
69 /// `H5F_LIBVER_V200`, which is `H5F_LIBVER_LATEST` for libhdf5 2.0.
70 V200,
71}
72
73impl LibverBound {
74 /// The datatype message version this bound calls for — libhdf5's
75 /// `H5O_dtype_ver_bounds` (H5T.c), the floor `H5T_set_version` raises a
76 /// datatype to.
77 pub fn dtype_version(self) -> u8 {
78 match self {
79 Self::Earliest => 1,
80 Self::V18 | Self::V110 => 3,
81 Self::V112 | Self::V114 => 4,
82 Self::V200 => 5,
83 }
84 }
85
86 /// The data layout message version this bound calls for — libhdf5's
87 /// `H5O_layout_ver_bounds` (H5Dlayout.c:44).
88 ///
89 /// It is what decides the chunk index, before the dataspace gets a say:
90 /// `H5D__chunk_set_info` reaches the v1.10 indexes — extensible array,
91 /// fixed array, v2 B-tree, single chunk, implicit — only once this
92 /// version is 4 or more (H5Dchunk.c:936). Below that the layout message
93 /// has no index-type field at all and the chunks are indexed by the
94 /// version-1 B-tree, which is why `V18` and `Earliest` share one index
95 /// despite differing in every other message version.
96 pub fn layout_version(self) -> u8 {
97 match self {
98 Self::Earliest => 1,
99 Self::V18 => 3,
100 Self::V110 | Self::V112 | Self::V114 => 4,
101 Self::V200 => 5,
102 }
103 }
104
105 /// The superblock version this bound calls for — libhdf5's
106 /// `HDF5_superblock_ver_bounds` (H5Fsuper.c:68), the floor
107 /// `H5F__super_init` raises the content-derived version to.
108 ///
109 /// Version 0 is the `H5F_LIBVER_EARLIEST` entry
110 /// (`HDF5_SUPERBLOCK_VERSION_DEF`); a writer whose own structures need
111 /// more takes the higher of the two.
112 pub fn superblock_version(self) -> u8 {
113 match self {
114 Self::Earliest => 0,
115 Self::V18 => 2,
116 Self::V110 | Self::V112 | Self::V114 | Self::V200 => 3,
117 }
118 }
119
120 /// The lowest bound whose [`superblock_version`](Self::superblock_version)
121 /// matches an on-disk version byte.
122 ///
123 /// Lossy in one direction: superblock version 3 is shared by four bounds
124 /// (`V110` through `V200`), because raising the low bound past `V18` never
125 /// raises the superblock further — the version alone cannot tell them
126 /// apart, so this reports the lowest, `V110`. A version this crate's own
127 /// writer never emits (1) reads back as `Earliest`, the same legacy
128 /// generation as 0; anything past 3 has no bound to report and falls back
129 /// to the library's newest.
130 pub fn from_superblock_version(version: u8) -> Self {
131 match version {
132 0 | 1 => Self::Earliest,
133 2 => Self::V18,
134 3 => Self::V110,
135 _ => Self::V200,
136 }
137 }
138}
139
140/// Which generation of the on-disk object format one file's objects are
141/// written in.
142///
143/// Not a second [`LibverBound`]: the bound picks the datatype version a
144/// *caller* asked for, while this says which superblock generation the file
145/// already is. The two never combine freely — libhdf5 derives both from the
146/// same low bound, so a version-0/1 superblock always carries version-1 object
147/// headers and the `H5F_LIBVER_EARLIEST` row of every message-version table,
148/// and a version-2/3 superblock always carries version-2 headers and the
149/// `H5F_LIBVER_V18` row. Choosing per message is what would let this writer
150/// emit a combination libhdf5 never writes.
151#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
152pub enum ObjectFormat {
153 /// A version-0/1 superblock file: version-1 object headers, symbol-table
154 /// groups, and the oldest message versions that can express the content.
155 Legacy,
156 /// A version-2/3 superblock file: version-2 object headers and the
157 /// message versions `H5F_LIBVER_V18` calls for.
158 #[default]
159 Modern,
160}
161
162impl ObjectFormat {
163 /// The object header version (`H5O_obj_ver_bounds`, H5Oint.c:125).
164 pub fn object_header_version(self) -> u8 {
165 match self {
166 Self::Legacy => 1,
167 Self::Modern => 2,
168 }
169 }
170
171 /// The floor for a dataspace message's version
172 /// (`H5O_sdspace_ver_bounds`, H5S.c:61). A null dataspace cannot be
173 /// expressed at version 1 and raises itself.
174 pub fn dataspace_version(self) -> u8 {
175 match self {
176 Self::Legacy => 1,
177 Self::Modern => 2,
178 }
179 }
180
181 /// The fill-value message version.
182 ///
183 /// `H5O_fill_ver_bounds` (H5Ofill.c:150) is `H5O_FILL_VERSION_1` at the
184 /// earliest bound, but the bound is only half of it: `H5O__fill_set_version`
185 /// takes `MAX(fill->version, bound)`, and the default creation property list
186 /// starts every fill value at `H5O_FILL_VERSION_2` (H5Pdcpl.c:163). Nothing
187 /// in the public API lowers it, so version 1 is unreachable and a classic
188 /// file's new fill message is version 2.
189 ///
190 /// Version 1 is not the "fill value (old)" message either — that is a
191 /// separate message type (0x04, `MSG_FILL_VALUE_OLD`) with its own
192 /// size-and-bytes encoding, which a classic file carries *alongside* this
193 /// one when the fill value is user-defined.
194 pub fn fill_value_version(self) -> u8 {
195 match self {
196 Self::Legacy => 2,
197 Self::Modern => 3,
198 }
199 }
200
201 /// The attribute message version (`H5O_attr_ver_bounds`, H5Aint.c:95).
202 pub fn attribute_version(self) -> u8 {
203 match self {
204 Self::Legacy => 1,
205 Self::Modern => 3,
206 }
207 }
208
209 /// The filter pipeline message version (`H5O_pline_ver_bounds`,
210 /// H5Opline.c:85).
211 pub fn filter_pipeline_version(self) -> u8 {
212 match self {
213 Self::Legacy => 1,
214 Self::Modern => 2,
215 }
216 }
217}
218
219/// UNDEF address constant
220pub const UNDEF_ADDR: u64 = u64::MAX;
221
222/// Fetches arbitrary file regions for the structure walkers that cannot hold a
223/// file handle themselves (fractal heap, v2 B-tree, dense attribute storage).
224pub trait BlockReader {
225 /// Read up to `len` bytes starting at `offset`. Returning fewer bytes is
226 /// only permitted at end of file; every caller re-checks the length it
227 /// actually needs, so a short read surfaces as `BufferTooShort` rather
228 /// than a misparse.
229 fn read_block(&mut self, offset: u64, len: usize) -> FormatResult<Vec<u8>>;
230}
231
232/// Encode/decode error
233#[derive(Debug)]
234pub enum FormatError {
235 InvalidSignature,
236 InvalidVersion(u8),
237 BufferTooShort { needed: usize, available: usize },
238 ChecksumMismatch { expected: u32, computed: u32 },
239 UnsupportedFeature(String),
240 InvalidData(String),
241}
242
243impl std::fmt::Display for FormatError {
244 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
245 match self {
246 Self::InvalidSignature => write!(f, "invalid HDF5 signature"),
247 Self::InvalidVersion(v) => write!(f, "unsupported version: {}", v),
248 Self::BufferTooShort { needed, available } => {
249 write!(
250 f,
251 "buffer too short: need {} bytes, have {}",
252 needed, available
253 )
254 }
255 Self::ChecksumMismatch { expected, computed } => {
256 write!(
257 f,
258 "checksum mismatch: expected 0x{:08x}, computed 0x{:08x}",
259 expected, computed
260 )
261 }
262 Self::UnsupportedFeature(s) => write!(f, "unsupported feature: {}", s),
263 Self::InvalidData(s) => write!(f, "invalid data: {}", s),
264 }
265 }
266}
267
268impl std::error::Error for FormatError {}
269
270pub type FormatResult<T> = Result<T, FormatError>;