Skip to main content

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}