hdf5-pure-rust 0.2.0

Pure Rust implementation of the HDF5 file format. No C dependencies.
Documentation
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
use std::io::{Read, Seek};

use crate::error::{Error, Result};
use crate::format::checksum::checksum_metadata;
use crate::io::reader::HdfReader;

/// Magic number for v2 object headers: "OHDR"
const OHDR_MAGIC: [u8; 4] = [b'O', b'H', b'D', b'R'];

/// Magic number for v2 continuation chunks: "OCHK"
const OCHK_MAGIC: [u8; 4] = [b'O', b'C', b'H', b'K'];

// Object header flags (v2)
pub const HDR_CHUNK0_SIZE_MASK: u8 = 0x03;
pub const HDR_ATTR_CRT_ORDER_TRACKED: u8 = 0x04;
pub const HDR_ATTR_CRT_ORDER_INDEXED: u8 = 0x08;
pub const HDR_ATTR_STORE_PHASE_CHANGE: u8 = 0x10;
pub const HDR_STORE_TIMES: u8 = 0x20;

// Message type IDs
pub const MSG_NIL: u16 = 0x0000;
pub const MSG_DATASPACE: u16 = 0x0001;
pub const MSG_LINK_INFO: u16 = 0x0002;
pub const MSG_DATATYPE: u16 = 0x0003;
pub const MSG_FILL_VALUE_OLD: u16 = 0x0004;
pub const MSG_FILL_VALUE: u16 = 0x0005;
pub const MSG_LINK: u16 = 0x0006;
pub const MSG_EXTERNAL_FILE_LIST: u16 = 0x0007;
pub const MSG_LAYOUT: u16 = 0x0008;
pub const MSG_BOGUS: u16 = 0x0009;
pub const MSG_GROUP_INFO: u16 = 0x000A;
pub const MSG_FILTER_PIPELINE: u16 = 0x000B;
pub const MSG_ATTRIBUTE: u16 = 0x000C;
pub const MSG_OBJ_COMMENT: u16 = 0x000D;
pub const MSG_OBJ_MOD_TIME_OLD: u16 = 0x000E;
pub const MSG_SHARED_MSG_TABLE: u16 = 0x000F;
pub const MSG_HEADER_CONTINUATION: u16 = 0x0010;
pub const MSG_SYMBOL_TABLE: u16 = 0x0011;
pub const MSG_OBJ_MOD_TIME: u16 = 0x0012;
pub const MSG_BTREE_K: u16 = 0x0013;
pub const MSG_DRIVER_INFO: u16 = 0x0014;
pub const MSG_ATTR_INFO: u16 = 0x0015;
pub const MSG_OBJ_REF_COUNT: u16 = 0x0016;
pub const MSG_FILE_SPACE_INFO: u16 = 0x0017;

/// A raw message from an object header.
#[derive(Debug, Clone)]
pub struct RawMessage {
    /// Message type ID.
    pub msg_type: u16,
    /// Message flags.
    pub flags: u8,
    /// Creation order index (v2 only, if tracked).
    pub creation_index: Option<u16>,
    /// Raw message data bytes.
    pub data: Vec<u8>,
}

/// Parsed object header.
#[derive(Debug, Clone)]
pub struct ObjectHeader {
    /// Header version (1 or 2).
    pub version: u8,
    /// Header flags (v2 only).
    pub flags: u8,
    /// Reference count.
    pub refcount: u32,
    /// Access time (v2, if HDR_STORE_TIMES).
    pub atime: Option<u32>,
    /// Modification time (v2, if HDR_STORE_TIMES).
    pub mtime: Option<u32>,
    /// Change time (v2, if HDR_STORE_TIMES).
    pub ctime: Option<u32>,
    /// Birth time (v2, if HDR_STORE_TIMES).
    pub btime: Option<u32>,
    /// Max compact attributes (v2, if HDR_ATTR_STORE_PHASE_CHANGE).
    pub max_compact_attrs: Option<u16>,
    /// Min dense attributes (v2, if HDR_ATTR_STORE_PHASE_CHANGE).
    pub min_dense_attrs: Option<u16>,
    /// All messages parsed from this header (including continuation chunks).
    pub messages: Vec<RawMessage>,
}

impl ObjectHeader {
    /// Read an object header at the given file address.
    pub fn read_at<R: Read + Seek>(reader: &mut HdfReader<R>, addr: u64) -> Result<Self> {
        reader.seek(addr)?;

        // Peek at the first byte to determine version.
        // V2 headers start with "OHDR" magic; v1 starts with version byte (1).
        let first_bytes = reader.read_bytes(4)?;

        if first_bytes == OHDR_MAGIC {
            Self::read_v2(reader, addr)
        } else {
            // Seek back and re-read as v1. The first byte is the version.
            reader.seek(addr)?;
            Self::read_v1(reader)
        }
    }

    /// Read a v1 object header.
    fn read_v1<R: Read + Seek>(reader: &mut HdfReader<R>) -> Result<Self> {
        let version = reader.read_u8()?;
        if version != 1 {
            return Err(Error::InvalidFormat(format!(
                "expected object header v1, got {version}"
            )));
        }

        // Reserved byte
        reader.skip(1)?;

        let num_messages = reader.read_u16()?;
        let refcount = reader.read_u32()?;
        let chunk_data_size = reader.read_u32()? as u64;

        // Reserved/padding to 8-byte boundary (v1 header is 12 bytes after version,
        // total prefix = 1+1+2+4+4 = 12, need to align to 8: 12 is already aligned to 4,
        // but the v1 header macro says H5O_ALIGN_OLD(12) = align to 8 = 16, so 4 padding bytes)
        reader.skip(4)?;

        // Now read messages from chunk data
        let chunk_start = reader.position()?;
        let chunk_end = chunk_start + chunk_data_size;

        let mut messages = Vec::new();
        let mut continuations = Vec::new();

        Self::read_v1_messages(reader, chunk_end, num_messages, &mut messages, &mut continuations)?;

        // Process continuation chunks
        for (cont_addr, cont_len) in continuations {
            Self::read_v1_continuation(reader, cont_addr, cont_len, &mut messages)?;
        }

        Ok(ObjectHeader {
            version: 1,
            flags: 0,
            refcount,
            atime: None,
            mtime: None,
            ctime: None,
            btime: None,
            max_compact_attrs: None,
            min_dense_attrs: None,
            messages,
        })
    }

    fn read_v1_messages<R: Read + Seek>(
        reader: &mut HdfReader<R>,
        chunk_end: u64,
        _num_messages: u16,
        messages: &mut Vec<RawMessage>,
        continuations: &mut Vec<(u64, u64)>,
    ) -> Result<()> {
        while reader.position()? < chunk_end {
            let pos = reader.position()?;
            if pos + 8 > chunk_end {
                break;
            }

            let msg_type = reader.read_u16()?;
            let msg_size = reader.read_u16()? as u64;
            let msg_flags = reader.read_u8()?;
            // 3 reserved bytes
            reader.skip(3)?;

            // Aligned message size (v1 messages are 8-byte aligned)
            let aligned_size = (msg_size + 7) & !7;

            if msg_type == MSG_NIL {
                reader.skip(aligned_size)?;
                continue;
            }

            if msg_type == MSG_HEADER_CONTINUATION {
                // Continuation message: contains offset + length
                let cont_offset = reader.read_addr()?;
                let cont_length = reader.read_length()?;
                let remaining = aligned_size - reader.sizeof_addr() as u64 - reader.sizeof_size() as u64;
                if remaining > 0 {
                    reader.skip(remaining)?;
                }
                continuations.push((cont_offset, cont_length));
                continue;
            }

            let data = reader.read_bytes(msg_size as usize)?;
            // Skip padding to alignment
            let padding = aligned_size - msg_size;
            if padding > 0 {
                reader.skip(padding)?;
            }

            messages.push(RawMessage {
                msg_type,
                flags: msg_flags,
                creation_index: None,
                data,
            });
        }

        Ok(())
    }

    fn read_v1_continuation<R: Read + Seek>(
        reader: &mut HdfReader<R>,
        addr: u64,
        length: u64,
        messages: &mut Vec<RawMessage>,
    ) -> Result<()> {
        reader.seek(addr)?;

        // V1 continuation chunks are just raw messages, no header.
        let chunk_end = addr + length;
        let mut continuations = Vec::new();
        Self::read_v1_messages(reader, chunk_end, 0, messages, &mut continuations)?;

        for (cont_addr, cont_len) in continuations {
            Self::read_v1_continuation(reader, cont_addr, cont_len, messages)?;
        }

        Ok(())
    }

    /// Read a v2 object header. Assumes the "OHDR" magic has already been read.
    fn read_v2<R: Read + Seek>(reader: &mut HdfReader<R>, header_addr: u64) -> Result<Self> {
        let version = reader.read_u8()?;
        if version != 2 {
            return Err(Error::InvalidFormat(format!(
                "expected object header v2, got {version}"
            )));
        }

        let flags = reader.read_u8()?;

        // Optional timestamps
        let (atime, mtime, ctime, btime) = if flags & HDR_STORE_TIMES != 0 {
            (
                Some(reader.read_u32()?),
                Some(reader.read_u32()?),
                Some(reader.read_u32()?),
                Some(reader.read_u32()?),
            )
        } else {
            (None, None, None, None)
        };

        // Optional attribute phase change
        let (max_compact_attrs, min_dense_attrs) = if flags & HDR_ATTR_STORE_PHASE_CHANGE != 0 {
            (Some(reader.read_u16()?), Some(reader.read_u16()?))
        } else {
            (None, None)
        };

        // Chunk 0 data size (1, 2, 4, or 8 bytes based on flags)
        let chunk0_size_bytes = 1u8 << (flags & HDR_CHUNK0_SIZE_MASK);
        let chunk0_data_size = reader.read_uint(chunk0_size_bytes)?;

        // Now we know where chunk 0 data starts and where its checksum is
        let chunk0_data_start = reader.position()?;
        let chunk0_data_end = chunk0_data_start + chunk0_data_size;

        // Verify checksum: it covers from "OHDR" magic to just before the checksum
        let checksum_pos = chunk0_data_end;
        // Read the stored checksum
        reader.seek(checksum_pos)?;
        let stored_checksum = reader.read_u32()?;

        // Compute checksum over header_addr .. checksum_pos
        let check_len = (checksum_pos - header_addr) as usize;
        reader.seek(header_addr)?;
        let check_data = reader.read_bytes(check_len)?;
        let computed = checksum_metadata(&check_data);

        if stored_checksum != computed {
            return Err(Error::InvalidFormat(format!(
                "object header checksum mismatch: stored={stored_checksum:#010x}, computed={computed:#010x}"
            )));
        }

        // Now parse messages from chunk 0 data
        reader.seek(chunk0_data_start)?;

        let has_crt_order = flags & HDR_ATTR_CRT_ORDER_TRACKED != 0;
        let mut messages = Vec::new();
        let mut continuations = Vec::new();

        Self::read_v2_messages(reader, chunk0_data_end, has_crt_order, &mut messages, &mut continuations)?;

        // Process continuation chunks
        for (cont_addr, cont_len) in continuations {
            Self::read_v2_continuation(reader, cont_addr, cont_len, has_crt_order, &mut messages)?;
        }

        Ok(ObjectHeader {
            version: 2,
            flags,
            refcount: 1, // v2 headers may have a separate refcount message
            atime,
            mtime,
            ctime,
            btime,
            max_compact_attrs,
            min_dense_attrs,
            messages,
        })
    }

    fn read_v2_messages<R: Read + Seek>(
        reader: &mut HdfReader<R>,
        chunk_data_end: u64,
        has_crt_order: bool,
        messages: &mut Vec<RawMessage>,
        continuations: &mut Vec<(u64, u64)>,
    ) -> Result<()> {
        while reader.position()? < chunk_data_end {
            let pos = reader.position()?;
            // Minimum message header size: 4 bytes (type:1, size:2, flags:1)
            if pos + 4 > chunk_data_end {
                break;
            }

            let msg_type = reader.read_u8()? as u16;
            let msg_size = reader.read_u16()? as u64;
            let msg_flags = reader.read_u8()?;

            let creation_index = if has_crt_order {
                Some(reader.read_u16()?)
            } else {
                None
            };

            if msg_type == MSG_NIL as u16 {
                reader.skip(msg_size)?;
                continue;
            }

            if msg_type == MSG_HEADER_CONTINUATION as u16 {
                let cont_offset = reader.read_addr()?;
                let cont_length = reader.read_length()?;
                let used = reader.sizeof_addr() as u64 + reader.sizeof_size() as u64;
                if msg_size > used {
                    reader.skip(msg_size - used)?;
                }
                continuations.push((cont_offset, cont_length));
                continue;
            }

            let data = reader.read_bytes(msg_size as usize)?;

            messages.push(RawMessage {
                msg_type,
                flags: msg_flags,
                creation_index,
                data,
            });
        }

        Ok(())
    }

    fn read_v2_continuation<R: Read + Seek>(
        reader: &mut HdfReader<R>,
        addr: u64,
        length: u64,
        has_crt_order: bool,
        messages: &mut Vec<RawMessage>,
    ) -> Result<()> {
        reader.seek(addr)?;

        // V2 continuation chunks start with "OCHK" magic
        let magic = reader.read_bytes(4)?;
        if magic != OCHK_MAGIC {
            return Err(Error::InvalidFormat(
                "invalid continuation chunk magic".into(),
            ));
        }

        // Data runs from after magic to before checksum
        let _data_start = reader.position()?;
        let data_end = addr + length - 4; // minus checksum

        let mut continuations = Vec::new();
        Self::read_v2_messages(reader, data_end, has_crt_order, messages, &mut continuations)?;

        // Verify checksum
        reader.seek(data_end)?;
        let stored_checksum = reader.read_u32()?;
        let check_len = (data_end - addr) as usize;
        reader.seek(addr)?;
        let check_data = reader.read_bytes(check_len)?;
        let computed = checksum_metadata(&check_data);

        if stored_checksum != computed {
            return Err(Error::InvalidFormat(
                "continuation chunk checksum mismatch".into(),
            ));
        }

        // Process nested continuations
        for (cont_addr, cont_len) in continuations {
            Self::read_v2_continuation(reader, cont_addr, cont_len, has_crt_order, messages)?;
        }

        Ok(())
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::fs::File;
    use std::io::BufReader;

    #[test]
    fn test_parse_v0_root_object_header() {
        let f = File::open("tests/data/simple_v0.h5").unwrap();
        let mut reader = HdfReader::new(BufReader::new(f));
        let sb = crate::format::superblock::Superblock::read(&mut reader).unwrap();

        let oh = ObjectHeader::read_at(&mut reader, sb.root_addr).unwrap();
        println!("v0 root OH: version={}, refcount={}, messages:", oh.version, oh.refcount);
        for msg in &oh.messages {
            println!("  type={:#06x}, flags={:#04x}, len={}", msg.msg_type, msg.flags, msg.data.len());
        }

        assert_eq!(oh.version, 1);
        // Root group should have a symbol table message
        assert!(oh.messages.iter().any(|m| m.msg_type == MSG_SYMBOL_TABLE));
    }

    #[test]
    fn test_parse_v3_root_object_header() {
        let f = File::open("tests/data/simple_v2.h5").unwrap();
        let mut reader = HdfReader::new(BufReader::new(f));
        let sb = crate::format::superblock::Superblock::read(&mut reader).unwrap();

        let oh = ObjectHeader::read_at(&mut reader, sb.root_addr).unwrap();
        println!("v3 root OH: version={}, flags={:#04x}, messages:", oh.version, oh.flags);
        for msg in &oh.messages {
            println!("  type={:#06x}, flags={:#04x}, len={}", msg.msg_type, msg.flags, msg.data.len());
        }

        assert_eq!(oh.version, 2);
        // V2 root group should have link messages or link info
        let has_links = oh.messages.iter().any(|m| m.msg_type == MSG_LINK || m.msg_type == MSG_LINK_INFO);
        assert!(has_links, "v2 root group should have link or link info messages");
    }
}