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