hdf5_pure/address.rs
1//! The superblock base address, as a type distinct from the addresses it shifts.
2//!
3//! # Why this exists
4//!
5//! The format specification says of the superblock's base address that "unless
6//! otherwise noted, all other file addresses are relative to this base address".
7//! A userblock makes that base non-zero — 512 bytes for every `.mat` file this
8//! crate writes — so an HDF5 file holds addresses in *two* frames at once:
9//!
10//! * a **stored** address, as written in metadata, relative to the base;
11//! * an **absolute** byte position in the file, which is what a
12//! [`Source`](crate::source::Source) reads at.
13//!
14//! Both are `u64`, both are usually equal (a plain file has a base of zero), and
15//! the compiler has nothing to say about mixing them. The result is a defect
16//! class rather than a defect: a reader that forgets the base lands inside the
17//! userblock, and every test written against a base-0 file passes anyway.
18//! `crates/crosscheck/tests/userblock_base_address.rs` was written after that happened
19//! three separate times — dense attributes, object header continuations, and
20//! dense link storage — and it covers the ground its four rows name, which is
21//! not the same as covering the ground.
22//!
23//! [`BaseAddress`] does not make the two frames distinct types; addresses stay
24//! `u64` throughout the crate. What it makes distinct is the **base itself**,
25//! which is the operand every one of those defects got wrong. That buys three
26//! things a bare `u64` did not give:
27//!
28//! * A base cannot be passed where an address is expected, or an address where a
29//! base is expected. Both directions were previously a silent argument swap in
30//! functions that take `(address, ..., base_address)` — the shape of nearly
31//! every parser in this crate.
32//! * Both conversions are **checked, and named**: 46 call sites of
33//! [`BaseAddress::absolute`] and 18 of [`BaseAddress::relative`], plus their
34//! test fixtures. Most replaced a hand-written `checked_add(base)` chain, but
35//! **seven unchecked additions and eighteen unchecked subtractions** were in
36//! that set, each of which panics in a debug build and wraps in a release one.
37//! Two were the `stored_addr + base` in [`group_v2`](crate::group_v2)'s two
38//! path resolvers, now the single [`ChildLookup::of`](crate::group_v2::ChildLookup)
39//! that both share; `a_link_target_that_overflows_the_base_address_is_refused`
40//! covers it. The rest are in the write engine, where an absolute address goes
41//! back to stored form on the way into a link message or a superblock. Three
42//! additions of a base survive unconverted, all inside `debug_assert_eq!` in
43//! [`file_writer`](crate::file_writer), whose bodies a release build does not
44//! compile.
45//! * "This code does not need the base" becomes [`BaseAddress::ZERO`]. Five call
46//! sites in the crate's code paths make that claim, so a `grep` finds all five
47//! — where a literal `0` argument was indistinguishable from the other zeroes
48//! on the line. (Another 34 are test fixtures satisfying a signature with a
49//! base they do not care about.)
50//!
51//! # What it deliberately does not do
52//!
53//! The read path resolves the two frames two different ways, and this type is
54//! neutral between them. Object headers and group entries add the base to each
55//! address as they parse it ([`absolute`](BaseAddress::absolute)); raw data,
56//! chunk indices, and dense attribute storage instead *frame the file* at the
57//! base — [`frame`](crate::source::frame) for a buffer,
58//! [`BaseOffsetSource`](crate::source::BaseOffsetSource) for a stream — and read
59//! stored addresses against that shifted view directly. Both are correct, and
60//! which one a given parser is owed is a fact about the parser, not about the
61//! base. A newtype for stored-versus-absolute *addresses* would encode that too;
62//! it would also have to propagate through every chunk address, free-space
63//! section, and index element in the crate, which is a far larger change than
64//! this one and is not attempted here.
65
66use crate::error::FormatError;
67
68/// The byte offset at which a file's HDF5 image begins, as reported by
69/// [`Superblock::base_address`](crate::Superblock::base_address).
70///
71/// Zero for a plain file, and the userblock size for a file that has one — 512
72/// bytes for every `.mat` file this crate writes. Every address stored in a
73/// file's metadata is relative to it, so an absolute position in the file is
74/// the stored address plus this. [`get`](Self::get) is the number.
75///
76/// It is a type of its own rather than a `u64` because the base and the
77/// addresses it shifts are both file offsets that mean different things, and
78/// mixing them is a defect this crate hit three separate times: a reader that
79/// forgets the base lands inside the userblock, and every test written against
80/// a file without one passes anyway.
81#[derive(Debug, Clone, Copy, PartialEq, Eq)]
82pub struct BaseAddress(u64);
83
84impl BaseAddress {
85 /// The base address of a file with no userblock, where a stored address and
86 /// an absolute file offset are the same number.
87 ///
88 /// Written out at every call site that passes no base, so that "this parser
89 /// is already reading a base-framed view" and "this file cannot have a
90 /// userblock" are claims a reader can find and check, rather than a `0`
91 /// among the other arguments.
92 pub(crate) const ZERO: Self = Self(0);
93
94 /// Take a base address read from a superblock.
95 pub(crate) const fn new(base: u64) -> Self {
96 Self(base)
97 }
98
99 /// The base as a plain integer: zero for a plain file, the userblock size
100 /// for a file that has one.
101 pub const fn get(self) -> u64 {
102 self.0
103 }
104
105 /// Whether this file's stored addresses are already absolute.
106 ///
107 /// The two framing helpers are the identity in this case, and several paths
108 /// take a cheaper route through it (a streaming read need not wrap its
109 /// source at all); a `base == 0` test spelled by hand reads as a magic
110 /// number where this reads as the question being asked.
111 pub(crate) const fn is_zero(self) -> bool {
112 self.0 == 0
113 }
114
115 /// The absolute file position of `stored`, an address as written in metadata.
116 ///
117 /// [`FormatError::OffsetOverflow`] if the sum exceeds `u64`, which a
118 /// malformed file can arrange: both operands are file-derived.
119 pub(crate) fn absolute(self, stored: u64) -> Result<u64, FormatError> {
120 stored
121 .checked_add(self.0)
122 .ok_or(FormatError::OffsetOverflow {
123 offset: stored,
124 length: self.0,
125 })
126 }
127
128 /// The stored (base-relative) form of the absolute file position `at`, which
129 /// is what metadata naming that position must hold.
130 ///
131 /// [`FormatError::AddressBelowBase`] if `at` is below the base, i.e. inside
132 /// the userblock, where no HDF5 structure can live. Every call site this
133 /// replaced subtracted without checking, so such an input wrapped to a
134 /// near-`u64::MAX` address in a release build and panicked in a debug one.
135 pub(crate) fn relative(self, at: u64) -> Result<u64, FormatError> {
136 at.checked_sub(self.0).ok_or(FormatError::AddressBelowBase {
137 address: at,
138 base: self.0,
139 })
140 }
141}
142
143#[cfg(test)]
144mod tests {
145 use super::*;
146
147 /// The point of the type: a round trip through both conversions is the
148 /// identity, for a base that shifts and a base that does not.
149 #[test]
150 fn the_two_conversions_invert_each_other() {
151 for base in [BaseAddress::ZERO, BaseAddress::new(512)] {
152 for stored in [0u64, 1, 4096, u32::MAX as u64] {
153 let at = base.absolute(stored).unwrap();
154 assert_eq!(base.relative(at).unwrap(), stored, "base {base:?}");
155 }
156 }
157 }
158
159 /// A base of zero leaves an address alone in both directions — which is what
160 /// makes a forgotten base invisible on every plain file, and why the type
161 /// exists.
162 #[test]
163 fn a_zero_base_is_the_identity() {
164 assert!(BaseAddress::ZERO.is_zero());
165 assert_eq!(BaseAddress::ZERO.absolute(1234).unwrap(), 1234);
166 assert_eq!(BaseAddress::ZERO.relative(1234).unwrap(), 1234);
167 }
168
169 /// A userblock shifts by exactly its size, in both directions.
170 #[test]
171 fn a_userblock_shifts_by_its_size() {
172 let base = BaseAddress::new(512);
173 assert!(!base.is_zero());
174 assert_eq!(base.absolute(96).unwrap(), 608);
175 assert_eq!(base.relative(608).unwrap(), 96);
176 }
177
178 /// Both operands are file-derived, so a malformed file can overflow the sum.
179 /// It is reported rather than wrapped into a valid-looking address.
180 #[test]
181 fn an_overflowing_sum_is_reported_not_wrapped() {
182 let base = BaseAddress::new(512);
183 assert_eq!(
184 base.absolute(u64::MAX),
185 Err(FormatError::OffsetOverflow {
186 offset: u64::MAX,
187 length: 512,
188 })
189 );
190 }
191
192 /// An address inside the userblock has no stored form: no HDF5 structure
193 /// lives below the base. The two subtractions this replaced would have
194 /// wrapped to a near-`u64::MAX` address instead.
195 #[test]
196 fn an_address_below_the_base_is_refused_rather_than_wrapped() {
197 let base = BaseAddress::new(512);
198 assert_eq!(
199 base.relative(511),
200 Err(FormatError::AddressBelowBase {
201 address: 511,
202 base: 512,
203 })
204 );
205 // The boundary itself is the image's first byte, and does have one.
206 assert_eq!(base.relative(512).unwrap(), 0);
207 }
208}