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