Skip to main content

rustyhdf5_format/
global_heap.rs

1//! HDF5 Global Heap collection parsing.
2
3#[cfg(not(feature = "std"))]
4use alloc::vec::Vec;
5
6use crate::error::FormatError;
7
8/// Magic signature for global heap collections.
9const GCOL_SIGNATURE: [u8; 4] = [b'G', b'C', b'O', b'L'];
10
11/// A parsed global heap collection.
12#[derive(Debug, Clone)]
13pub struct GlobalHeapCollection {
14    /// Total size of this collection including header.
15    pub collection_size: u64,
16    /// Objects within this collection.
17    pub objects: Vec<GlobalHeapObject>,
18}
19
20/// A single object within a global heap collection.
21#[derive(Debug, Clone)]
22pub struct GlobalHeapObject {
23    /// Object index (1-based; 0 is the free space marker).
24    pub index: u16,
25    /// Reference count.
26    pub reference_count: u16,
27    /// Object data.
28    pub data: Vec<u8>,
29}
30
31fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> {
32    match offset.checked_add(needed) {
33        Some(end) if end <= data.len() => Ok(()),
34        _ => Err(FormatError::UnexpectedEof {
35            expected: offset.saturating_add(needed),
36            available: data.len(),
37        }),
38    }
39}
40
41fn read_length(data: &[u8], offset: usize, length_size: u8) -> Result<u64, FormatError> {
42    let s = length_size as usize;
43    ensure_len(data, offset, s)?;
44    let slice = &data[offset..offset + s];
45    Ok(match length_size {
46        2 => u16::from_le_bytes([slice[0], slice[1]]) as u64,
47        4 => u32::from_le_bytes([slice[0], slice[1], slice[2], slice[3]]) as u64,
48        8 => u64::from_le_bytes([
49            slice[0], slice[1], slice[2], slice[3], slice[4], slice[5], slice[6], slice[7],
50        ]),
51        _ => return Err(FormatError::InvalidLengthSize(length_size)),
52    })
53}
54
55/// Round up to next multiple of 8.
56fn pad8(x: usize) -> usize {
57    (x + 7) & !7
58}
59
60impl GlobalHeapCollection {
61    /// Parse a global heap collection at the given offset in the file data.
62    pub fn parse(
63        file_data: &[u8],
64        offset: usize,
65        length_size: u8,
66    ) -> Result<GlobalHeapCollection, FormatError> {
67        // signature(4) + version(1) + reserved(3) + collection_size(length_size)
68        let header_size = 8 + length_size as usize;
69        ensure_len(file_data, offset, header_size)?;
70
71        if file_data[offset..offset + 4] != GCOL_SIGNATURE {
72            return Err(FormatError::InvalidGlobalHeapSignature);
73        }
74
75        let version = file_data[offset + 4];
76        if version != 1 {
77            return Err(FormatError::InvalidGlobalHeapVersion(version));
78        }
79
80        let collection_size = read_length(file_data, offset + 8, length_size)?;
81        let collection_end = offset + collection_size as usize;
82
83        let mut pos = offset + header_size;
84        let mut objects = Vec::new();
85
86        // Parse objects until we hit index 0 (free space) or run out of space
87        while pos + 2 <= collection_end {
88            ensure_len(file_data, pos, 2)?;
89            let object_index = u16::from_le_bytes([file_data[pos], file_data[pos + 1]]);
90
91            if object_index == 0 {
92                // Free space marker — done
93                break;
94            }
95
96            // object_index(2) + reference_count(2) + reserved(4) + object_size(length_size)
97            let obj_header_size = 8 + length_size as usize;
98            ensure_len(file_data, pos, obj_header_size)?;
99
100            let reference_count =
101                u16::from_le_bytes([file_data[pos + 2], file_data[pos + 3]]);
102            let object_size = read_length(file_data, pos + 8, length_size)? as usize;
103
104            pos += obj_header_size;
105            ensure_len(file_data, pos, object_size)?;
106            let data = file_data[pos..pos + object_size].to_vec();
107
108            objects.push(GlobalHeapObject {
109                index: object_index,
110                reference_count,
111                data,
112            });
113
114            // Advance past data + padding to 8-byte boundary
115            pos += pad8(object_size);
116        }
117
118        Ok(GlobalHeapCollection {
119            collection_size,
120            objects,
121        })
122    }
123
124    /// Get an object by its index.
125    pub fn get_object(&self, index: u16) -> Option<&GlobalHeapObject> {
126        self.objects.iter().find(|o| o.index == index)
127    }
128}
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133
134    /// Build a global heap collection with given objects.
135    fn build_collection(
136        objects: &[(u16, u16, &[u8])], // (index, ref_count, data)
137        length_size: u8,
138    ) -> Vec<u8> {
139        let ls = length_size as usize;
140
141        // Calculate total size
142        let header_size = 8 + ls;
143        let mut obj_size_total = 0usize;
144        for (_, _, data) in objects {
145            let obj_header = 8 + ls;
146            obj_size_total += obj_header + pad8(data.len());
147        }
148        // Free space marker (2 bytes for index 0)
149        obj_size_total += 2;
150        let collection_size = header_size + obj_size_total;
151
152        let mut buf = Vec::new();
153        buf.extend_from_slice(&GCOL_SIGNATURE);
154        buf.push(1); // version
155        buf.extend_from_slice(&[0u8; 3]); // reserved
156
157        // collection_size
158        match length_size {
159            4 => buf.extend_from_slice(&(collection_size as u32).to_le_bytes()),
160            8 => buf.extend_from_slice(&(collection_size as u64).to_le_bytes()),
161            _ => panic!("unsupported length_size"),
162        }
163
164        // Objects
165        for (index, ref_count, data) in objects {
166            buf.extend_from_slice(&index.to_le_bytes());
167            buf.extend_from_slice(&ref_count.to_le_bytes());
168            buf.extend_from_slice(&[0u8; 4]); // reserved
169            match length_size {
170                4 => buf.extend_from_slice(&(data.len() as u32).to_le_bytes()),
171                8 => buf.extend_from_slice(&(data.len() as u64).to_le_bytes()),
172                _ => panic!("unsupported"),
173            }
174            buf.extend_from_slice(data);
175            // Pad to 8 bytes
176            let padded = pad8(data.len());
177            for _ in data.len()..padded {
178                buf.push(0);
179            }
180        }
181
182        // Free space marker
183        buf.extend_from_slice(&0u16.to_le_bytes());
184
185        buf
186    }
187
188    #[test]
189    fn parse_collection_two_objects() {
190        let data = build_collection(
191            &[
192                (1, 1, b"hello"),
193                (2, 1, b"world!!!"),
194            ],
195            8,
196        );
197        let coll = GlobalHeapCollection::parse(&data, 0, 8).unwrap();
198        assert_eq!(coll.objects.len(), 2);
199        assert_eq!(coll.objects[0].index, 1);
200        assert_eq!(coll.objects[0].data, b"hello");
201        assert_eq!(coll.objects[1].index, 2);
202        assert_eq!(coll.objects[1].data, b"world!!!");
203    }
204
205    #[test]
206    fn get_object_by_index() {
207        let data = build_collection(
208            &[
209                (1, 1, b"aaa"),
210                (3, 2, b"bbb"),
211            ],
212            8,
213        );
214        let coll = GlobalHeapCollection::parse(&data, 0, 8).unwrap();
215        let obj = coll.get_object(3).unwrap();
216        assert_eq!(obj.data, b"bbb");
217        assert_eq!(obj.reference_count, 2);
218        assert!(coll.get_object(99).is_none());
219    }
220
221    #[test]
222    fn free_space_terminates_parsing() {
223        // Build collection with free space marker immediately
224        let mut data = Vec::new();
225        data.extend_from_slice(&GCOL_SIGNATURE);
226        data.push(1);
227        data.extend_from_slice(&[0u8; 3]);
228        let size = 8u64 + 8 + 2; // header + length_size + free space marker
229        data.extend_from_slice(&size.to_le_bytes());
230        data.extend_from_slice(&0u16.to_le_bytes()); // free space
231
232        let coll = GlobalHeapCollection::parse(&data, 0, 8).unwrap();
233        assert_eq!(coll.objects.len(), 0);
234    }
235
236    #[test]
237    fn invalid_signature_error() {
238        let mut data = build_collection(&[(1, 1, b"x")], 8);
239        data[0] = b'X'; // corrupt
240        let err = GlobalHeapCollection::parse(&data, 0, 8).unwrap_err();
241        assert_eq!(err, FormatError::InvalidGlobalHeapSignature);
242    }
243
244    #[test]
245    fn invalid_version_error() {
246        let mut data = build_collection(&[(1, 1, b"x")], 8);
247        data[4] = 2; // wrong version
248        let err = GlobalHeapCollection::parse(&data, 0, 8).unwrap_err();
249        assert_eq!(err, FormatError::InvalidGlobalHeapVersion(2));
250    }
251
252    #[test]
253    fn parse_with_4byte_length() {
254        let data = build_collection(&[(1, 1, b"test")], 4);
255        let coll = GlobalHeapCollection::parse(&data, 0, 4).unwrap();
256        assert_eq!(coll.objects.len(), 1);
257        assert_eq!(coll.objects[0].data, b"test");
258    }
259}