Skip to main content

rust_hdf5/format/
local_heap.rs

1//! Local heap decode (for reading legacy HDF5 files).
2//!
3//! The local heap is used by v0/v1 groups to store link names as
4//! null-terminated strings. The heap header lives at a known address and
5//! points to a contiguous data block.
6//!
7//! Header layout:
8//! ```text
9//! "HEAP" (4 bytes)
10//! version: 1 byte (0)
11//! reserved: 3 bytes
12//! data_size: sizeof_size bytes LE
13//! free_list_offset: sizeof_size bytes LE (0xFFFFFFFFFFFFFFFF = none)
14//! data_addr: sizeof_addr bytes LE
15//! ```
16
17use crate::format::bytes::read_le_uint as read_uint;
18use crate::format::{FormatError, FormatResult};
19
20/// The 4-byte local heap signature.
21pub const LOCAL_HEAP_SIGNATURE: [u8; 4] = *b"HEAP";
22
23/// Decoded local heap header.
24#[derive(Debug, Clone, PartialEq, Eq)]
25pub struct LocalHeapHeader {
26    /// Total size of the data segment.
27    pub data_size: u64,
28    /// Offset into the data segment of the first free block, or u64::MAX if none.
29    pub free_list_offset: u64,
30    /// File address of the data segment.
31    pub data_addr: u64,
32}
33
34impl LocalHeapHeader {
35    /// Decode a local heap header from `buf`.
36    ///
37    /// `sizeof_addr` and `sizeof_size` come from the superblock.
38    pub fn decode(buf: &[u8], sizeof_addr: usize, sizeof_size: usize) -> FormatResult<Self> {
39        let min_size = 4 + 1 + 3 + sizeof_size * 2 + sizeof_addr;
40        if buf.len() < min_size {
41            return Err(FormatError::BufferTooShort {
42                needed: min_size,
43                available: buf.len(),
44            });
45        }
46
47        if buf[0..4] != LOCAL_HEAP_SIGNATURE {
48            return Err(FormatError::InvalidSignature);
49        }
50
51        let version = buf[4];
52        if version != 0 {
53            return Err(FormatError::InvalidVersion(version));
54        }
55
56        // buf[5..8] reserved
57        let mut pos = 8;
58
59        let data_size = read_uint(&buf[pos..], sizeof_size);
60        pos += sizeof_size;
61
62        let free_list_offset = read_uint(&buf[pos..], sizeof_size);
63        pos += sizeof_size;
64
65        let data_addr = read_uint(&buf[pos..], sizeof_addr);
66
67        Ok(LocalHeapHeader {
68            data_size,
69            free_list_offset,
70            data_addr,
71        })
72    }
73}
74
75/// `H5HL_FREE_NULL` (H5HLpkg.h:54): the free-list head value that means "no
76/// free block", written in the header's free-list field. It is 1, not an
77/// undefined address, because 1 can never be a legal 8-aligned block offset.
78pub const LOCAL_HEAP_FREE_NULL: u64 = 1;
79
80/// `H5HL_ALIGN` (H5HLprivate.h:29): every heap object, and the data block
81/// itself, is a multiple of eight bytes long.
82pub fn local_heap_align(n: u64) -> u64 {
83    (n + 7) & !7
84}
85
86/// `H5HL_SIZEOF_HDR`: the heap header's own on-disk length, aligned.
87pub fn local_heap_header_size(sizeof_addr: usize, sizeof_size: usize) -> usize {
88    local_heap_align((4 + 1 + 3 + 2 * sizeof_size + sizeof_addr) as u64) as usize
89}
90
91impl LocalHeapHeader {
92    /// Encode the heap header (`H5HL__cache_prefix_serialize`).
93    ///
94    /// A `free_list_offset` of [`LOCAL_HEAP_FREE_NULL`] is the "no free space"
95    /// value; the field is not an address, so it has no undefined form.
96    pub fn encode(&self, sizeof_addr: usize, sizeof_size: usize) -> Vec<u8> {
97        let size = local_heap_header_size(sizeof_addr, sizeof_size);
98        let mut buf = Vec::with_capacity(size);
99        buf.extend_from_slice(&LOCAL_HEAP_SIGNATURE);
100        buf.push(0); // version
101        buf.extend_from_slice(&[0u8; 3]); // reserved
102        buf.extend_from_slice(&self.data_size.to_le_bytes()[..sizeof_size]);
103        buf.extend_from_slice(&self.free_list_offset.to_le_bytes()[..sizeof_size]);
104        buf.extend_from_slice(&self.data_addr.to_le_bytes()[..sizeof_addr]);
105        buf.resize(size, 0); // the alignment tail of H5HL_SIZEOF_HDR
106        buf
107    }
108}
109
110/// A local heap's data segment, built up object by object.
111///
112/// This is the bulk-load half of `H5HL_insert`: every object is placed at the
113/// end of the block and the block grows to fit, which is what that function
114/// does once its free list is empty. The free list itself is not modelled —
115/// the writer rebuilds a group's heap whole rather than patching the one on
116/// disk, so there is never a hole to record.
117#[derive(Debug, Default)]
118pub struct LocalHeapImage {
119    data: Vec<u8>,
120}
121
122impl LocalHeapImage {
123    /// A heap holding just the empty string at offset 0.
124    ///
125    /// Not an optimisation: `H5G__stab_create_components` asserts that offset,
126    /// because a symbol-table B-tree's leftmost key is the empty string and
127    /// every comparison against it reads heap offset 0.
128    pub fn with_empty_string() -> Self {
129        let mut image = Self::default();
130        let offset = image.insert(b"\0");
131        debug_assert_eq!(offset, 0);
132        image
133    }
134
135    /// Append `bytes` (null terminator included) and return its offset.
136    pub fn insert(&mut self, bytes: &[u8]) -> u64 {
137        let offset = self.data.len() as u64;
138        self.data.extend_from_slice(bytes);
139        let padded = local_heap_align(self.data.len() as u64) as usize;
140        self.data.resize(padded, 0);
141        offset
142    }
143
144    /// Append a name as a null-terminated string, returning its offset.
145    pub fn insert_str(&mut self, s: &str) -> u64 {
146        let offset = self.data.len() as u64;
147        self.data.extend_from_slice(s.as_bytes());
148        self.data.push(0);
149        let padded = local_heap_align(self.data.len() as u64) as usize;
150        self.data.resize(padded, 0);
151        offset
152    }
153
154    /// The data segment's bytes, whose length is the heap's `data_size`.
155    pub fn as_bytes(&self) -> &[u8] {
156        &self.data
157    }
158}
159
160/// Look up a null-terminated string in the heap data block by offset.
161///
162/// `heap_data` is the raw bytes of the local heap data segment.
163/// `offset` is the byte offset into that segment where the string starts.
164pub fn local_heap_get_string(heap_data: &[u8], offset: u64) -> FormatResult<String> {
165    let start = offset as usize;
166    if start >= heap_data.len() {
167        return Err(FormatError::InvalidData(format!(
168            "local heap offset {} out of range (heap size {})",
169            offset,
170            heap_data.len()
171        )));
172    }
173
174    // Find the null terminator
175    let end = heap_data[start..]
176        .iter()
177        .position(|&b| b == 0)
178        .map(|p| start + p)
179        .unwrap_or(heap_data.len());
180
181    String::from_utf8(heap_data[start..end].to_vec())
182        .map_err(|e| FormatError::InvalidData(format!("invalid UTF-8 in local heap string: {}", e)))
183}
184
185// ======================================================================= tests
186
187#[cfg(test)]
188mod tests {
189    use super::*;
190
191    fn build_heap_header(
192        data_size: u64,
193        free_list_offset: u64,
194        data_addr: u64,
195        sizeof_addr: usize,
196        sizeof_size: usize,
197    ) -> Vec<u8> {
198        let mut buf = Vec::new();
199        buf.extend_from_slice(&LOCAL_HEAP_SIGNATURE);
200        buf.push(0); // version
201        buf.extend_from_slice(&[0u8; 3]); // reserved
202        buf.extend_from_slice(&data_size.to_le_bytes()[..sizeof_size]);
203        buf.extend_from_slice(&free_list_offset.to_le_bytes()[..sizeof_size]);
204        buf.extend_from_slice(&data_addr.to_le_bytes()[..sizeof_addr]);
205        buf
206    }
207
208    #[test]
209    fn decode_basic() {
210        let buf = build_heap_header(128, u64::MAX, 0x1000, 8, 8);
211        let hdr = LocalHeapHeader::decode(&buf, 8, 8).unwrap();
212        assert_eq!(hdr.data_size, 128);
213        assert_eq!(hdr.free_list_offset, u64::MAX);
214        assert_eq!(hdr.data_addr, 0x1000);
215    }
216
217    #[test]
218    fn decode_4byte() {
219        let buf = build_heap_header(64, 0xFFFFFFFF, 0x800, 4, 4);
220        let hdr = LocalHeapHeader::decode(&buf, 4, 4).unwrap();
221        assert_eq!(hdr.data_size, 64);
222        assert_eq!(hdr.free_list_offset, 0xFFFFFFFF);
223        assert_eq!(hdr.data_addr, 0x800);
224    }
225
226    #[test]
227    fn decode_bad_sig() {
228        let mut buf = build_heap_header(64, 0, 0x800, 8, 8);
229        buf[0] = b'X';
230        assert!(matches!(
231            LocalHeapHeader::decode(&buf, 8, 8).unwrap_err(),
232            FormatError::InvalidSignature
233        ));
234    }
235
236    #[test]
237    fn decode_bad_version() {
238        let mut buf = build_heap_header(64, 0, 0x800, 8, 8);
239        buf[4] = 1;
240        assert!(matches!(
241            LocalHeapHeader::decode(&buf, 8, 8).unwrap_err(),
242            FormatError::InvalidVersion(1)
243        ));
244    }
245
246    #[test]
247    fn decode_too_short() {
248        let buf = [0u8; 4];
249        assert!(matches!(
250            LocalHeapHeader::decode(&buf, 8, 8).unwrap_err(),
251            FormatError::BufferTooShort { .. }
252        ));
253    }
254
255    #[test]
256    fn get_string_basic() {
257        let mut data = Vec::new();
258        data.extend_from_slice(b"\0"); // offset 0: empty
259        data.extend_from_slice(b"hello\0");
260        data.extend_from_slice(b"world\0");
261
262        assert_eq!(local_heap_get_string(&data, 0).unwrap(), "");
263        assert_eq!(local_heap_get_string(&data, 1).unwrap(), "hello");
264        assert_eq!(local_heap_get_string(&data, 7).unwrap(), "world");
265    }
266
267    #[test]
268    fn get_string_out_of_range() {
269        let data = b"hello\0";
270        assert!(matches!(
271            local_heap_get_string(data, 100).unwrap_err(),
272            FormatError::InvalidData(_)
273        ));
274    }
275
276    /// The 32-byte header of the root group's heap in a file h5py wrote with
277    /// no `libver` argument (three datasets `alpha`/`beta`/`gamma`).
278    #[test]
279    fn a_local_heap_header_matches_the_bytes_libhdf5_wrote() {
280        let hdr = LocalHeapHeader {
281            data_size: 88,
282            free_list_offset: 0x20,
283            data_addr: 0x2c8,
284        };
285        let expected = [
286            b'H', b'E', b'A', b'P', 0, 0, 0, 0, // signature, version 0, reserved
287            0x58, 0, 0, 0, 0, 0, 0, 0, // data_size
288            0x20, 0, 0, 0, 0, 0, 0, 0, // free list head
289            0xc8, 0x02, 0, 0, 0, 0, 0, 0, // data segment address
290        ];
291        assert_eq!(hdr.encode(8, 8), expected);
292        assert_eq!(local_heap_header_size(8, 8), 32);
293        assert_eq!(LocalHeapHeader::decode(&expected, 8, 8).unwrap(), hdr);
294    }
295
296    /// The header is 8-aligned, so the 4/4 form pads rather than shrinking to
297    /// its 20 significant bytes.
298    #[test]
299    fn a_four_byte_local_heap_header_pads_to_its_alignment() {
300        assert_eq!(local_heap_header_size(4, 4), 24);
301        let hdr = LocalHeapHeader {
302            data_size: 64,
303            free_list_offset: LOCAL_HEAP_FREE_NULL,
304            data_addr: 0x800,
305        };
306        let buf = hdr.encode(4, 4);
307        assert_eq!(buf.len(), 24);
308        assert_eq!(&buf[20..], &[0, 0, 0, 0]);
309        assert_eq!(LocalHeapHeader::decode(&buf, 4, 4).unwrap(), hdr);
310    }
311
312    /// The same three names, laid into a data segment: the empty string at
313    /// offset 0 and every object padded to eight.
314    #[test]
315    fn a_heap_image_lays_names_out_the_way_libhdf5_does() {
316        let mut heap = LocalHeapImage::with_empty_string();
317        assert_eq!(heap.insert_str("alpha"), 8);
318        assert_eq!(heap.insert_str("beta"), 16);
319        assert_eq!(heap.insert_str("gamma"), 24);
320        assert_eq!(
321            heap.as_bytes(),
322            b"\0\0\0\0\0\0\0\0alpha\0\0\0beta\0\0\0\0gamma\0\0\0"
323        );
324        for (offset, name) in [(0, ""), (8, "alpha"), (16, "beta"), (24, "gamma")] {
325            assert_eq!(
326                local_heap_get_string(heap.as_bytes(), offset).unwrap(),
327                name
328            );
329        }
330    }
331
332    /// A name whose length is already a multiple of eight still gets its
333    /// terminator, so it costs a whole extra eight bytes.
334    #[test]
335    fn a_heap_object_is_padded_after_its_terminator_not_before() {
336        let mut heap = LocalHeapImage::with_empty_string();
337        assert_eq!(heap.insert_str("12345678"), 8);
338        assert_eq!(heap.insert_str("next"), 24);
339        assert_eq!(heap.as_bytes().len(), 32);
340        assert_eq!(
341            local_heap_get_string(heap.as_bytes(), 8).unwrap(),
342            "12345678"
343        );
344    }
345}