Skip to main content

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>;