Skip to main content

rustyhdf5_format/
object_header.rs

1//! HDF5 Object Header parsing (v1 and v2).
2
3#[cfg(not(feature = "std"))]
4use alloc::vec::Vec;
5
6use byteorder::{ByteOrder, LittleEndian};
7
8use crate::error::FormatError;
9use crate::message_type::MessageType;
10
11/// OHDR signature for v2 object headers.
12const OHDR_SIGNATURE: [u8; 4] = [b'O', b'H', b'D', b'R'];
13
14/// OCHK signature for v2 continuation chunks.
15const OCHK_SIGNATURE: [u8; 4] = [b'O', b'C', b'H', b'K'];
16
17/// A single parsed header message.
18#[derive(Debug, Clone)]
19pub struct HeaderMessage {
20    /// The message type.
21    pub msg_type: MessageType,
22    /// Size of the message data in bytes.
23    pub size: usize,
24    /// Message flags byte.
25    pub flags: u8,
26    /// Creation order (v2 only, when tracking is enabled).
27    pub creation_order: Option<u16>,
28    /// Raw message data bytes.
29    pub data: Vec<u8>,
30}
31
32/// Parsed HDF5 object header.
33#[derive(Debug, Clone)]
34pub struct ObjectHeader {
35    /// Header version (1 or 2).
36    pub version: u8,
37    /// All non-NIL messages collected from all chunks.
38    pub messages: Vec<HeaderMessage>,
39    /// Object reference count (v1 only).
40    pub reference_count: Option<u32>,
41    /// Object header flags (v2 only; 0 for v1).
42    pub flags: u8,
43    /// Access time (v2, when flags bit 2 set).
44    pub access_time: Option<u32>,
45    /// Modification time (v2, when flags bit 2 set).
46    pub modification_time: Option<u32>,
47    /// Change time (v2, when flags bit 2 set).
48    pub change_time: Option<u32>,
49    /// Birth time (v2, when flags bit 2 set).
50    pub birth_time: Option<u32>,
51}
52
53fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> {
54    match offset.checked_add(needed) {
55        Some(end) if end <= data.len() => Ok(()),
56        _ => Err(FormatError::UnexpectedEof {
57            expected: offset.saturating_add(needed),
58            available: data.len(),
59        }),
60    }
61}
62
63fn read_offset(data: &[u8], pos: usize, size: u8) -> Result<u64, FormatError> {
64    let s = size as usize;
65    ensure_len(data, pos, s)?;
66    let slice = &data[pos..pos + s];
67    Ok(match size {
68        2 => LittleEndian::read_u16(slice) as u64,
69        4 => LittleEndian::read_u32(slice) as u64,
70        8 => LittleEndian::read_u64(slice),
71        1 => slice[0] as u64,
72        _ => {
73            return Err(FormatError::InvalidOffsetSize(size));
74        }
75    })
76}
77
78impl ObjectHeader {
79    /// Parse an object header at the given offset in the data buffer.
80    ///
81    /// `offset_size` and `length_size` come from the superblock.
82    pub fn parse(
83        data: &[u8],
84        offset: usize,
85        offset_size: u8,
86        length_size: u8,
87    ) -> Result<ObjectHeader, FormatError> {
88        ensure_len(data, offset, 4)?;
89        if data[offset..offset + 4] == OHDR_SIGNATURE {
90            Self::parse_v2(data, offset, offset_size, length_size)
91        } else {
92            Self::parse_v1(data, offset, offset_size, length_size)
93        }
94    }
95
96    fn parse_v1(
97        data: &[u8],
98        offset: usize,
99        offset_size: u8,
100        length_size: u8,
101    ) -> Result<ObjectHeader, FormatError> {
102        // version(1) + reserved(1) + num_messages(2) + ref_count(4) + header_size(4) = 12
103        // then pad to 8-byte alignment from start of header
104        ensure_len(data, offset, 12)?;
105
106        let version = data[offset];
107        if version != 1 {
108            return Err(FormatError::InvalidObjectHeaderVersion(version));
109        }
110
111        let num_messages = LittleEndian::read_u16(&data[offset + 2..offset + 4]);
112        let reference_count = LittleEndian::read_u32(&data[offset + 4..offset + 8]);
113        let header_data_size = LittleEndian::read_u32(&data[offset + 8..offset + 12]) as usize;
114
115        // Pad to 8-byte alignment: header prefix is 12 bytes, pad to 16
116        let padding = 4; // pad 12-byte prefix to 16-byte alignment
117        let msg_start = offset.checked_add(12 + padding).ok_or(FormatError::UnexpectedEof {
118            expected: usize::MAX,
119            available: data.len(),
120        })?;
121
122        ensure_len(data, msg_start, header_data_size)?;
123
124        let mut messages = Vec::new();
125        let mut pos = msg_start;
126        let msg_end = msg_start.checked_add(header_data_size).ok_or(FormatError::UnexpectedEof {
127            expected: usize::MAX,
128            available: data.len(),
129        })?;
130
131        for _ in 0..num_messages {
132            if pos + 8 > msg_end {
133                break;
134            }
135            let msg_type_raw = LittleEndian::read_u16(&data[pos..pos + 2]);
136            let msg_data_size = LittleEndian::read_u16(&data[pos + 2..pos + 4]) as usize;
137            let msg_flags = data[pos + 4];
138            // reserved(3) at pos+5..pos+8
139            pos += 8;
140
141            ensure_len(data, pos, msg_data_size)?;
142            let msg_type = MessageType::from_u16(msg_type_raw);
143
144            // Check if unknown + must-understand (bit 3 of msg_flags)
145            if let MessageType::Unknown(id) = msg_type {
146                if msg_flags & 0x08 != 0 {
147                    return Err(FormatError::UnsupportedMessage(id));
148                }
149            }
150
151            if msg_type != MessageType::Nil {
152                messages.push(HeaderMessage {
153                    msg_type,
154                    size: msg_data_size,
155                    flags: msg_flags,
156                    creation_order: None,
157                    data: data[pos..pos + msg_data_size].to_vec(),
158                });
159            }
160
161            pos += msg_data_size;
162
163            // Follow continuations
164            if msg_type == MessageType::ObjectHeaderContinuation {
165                let cont_msg_data = &messages.last()
166                    .ok_or(FormatError::InvalidObjectHeaderSignature)?.data;
167                if cont_msg_data.len() >= (offset_size as usize + length_size as usize) {
168                    let cont_offset =
169                        read_offset(cont_msg_data, 0, offset_size)? as usize;
170                    let cont_length =
171                        read_offset(cont_msg_data, offset_size as usize, length_size)?
172                            as usize;
173                    // Parse continuation block (v1: just raw messages, no signature)
174                    let cont_msgs = Self::parse_v1_continuation(
175                        data,
176                        cont_offset,
177                        cont_length,
178                        offset_size,
179                        length_size,
180                        32, // max continuation depth
181                    )?;
182                    messages.extend(cont_msgs);
183                }
184            }
185        }
186
187        Ok(ObjectHeader {
188            version: 1,
189            messages,
190            reference_count: Some(reference_count),
191            flags: 0,
192            access_time: None,
193            modification_time: None,
194            change_time: None,
195            birth_time: None,
196        })
197    }
198
199    fn parse_v1_continuation(
200        data: &[u8],
201        offset: usize,
202        length: usize,
203        offset_size: u8,
204        length_size: u8,
205        depth_remaining: u16,
206    ) -> Result<Vec<HeaderMessage>, FormatError> {
207        if depth_remaining == 0 {
208            return Err(FormatError::NestingDepthExceeded);
209        }
210        ensure_len(data, offset, length)?;
211        let mut messages = Vec::new();
212        let mut pos = offset;
213        let end = offset.saturating_add(length);
214
215        while pos + 8 <= end {
216            let msg_type_raw = LittleEndian::read_u16(&data[pos..pos + 2]);
217            let msg_data_size = LittleEndian::read_u16(&data[pos + 2..pos + 4]) as usize;
218            let msg_flags = data[pos + 4];
219            pos += 8;
220
221            if pos + msg_data_size > end {
222                break;
223            }
224
225            let msg_type = MessageType::from_u16(msg_type_raw);
226
227            if let MessageType::Unknown(id) = msg_type {
228                if msg_flags & 0x08 != 0 {
229                    return Err(FormatError::UnsupportedMessage(id));
230                }
231            }
232
233            if msg_type != MessageType::Nil {
234                messages.push(HeaderMessage {
235                    msg_type,
236                    size: msg_data_size,
237                    flags: msg_flags,
238                    creation_order: None,
239                    data: data[pos..pos + msg_data_size].to_vec(),
240                });
241            }
242
243            pos += msg_data_size;
244
245            // Recursive continuations
246            if msg_type == MessageType::ObjectHeaderContinuation {
247                let cont_msg_data = &messages.last()
248                    .ok_or(FormatError::InvalidObjectHeaderSignature)?.data;
249                if cont_msg_data.len() >= (offset_size as usize + length_size as usize) {
250                    let cont_offset =
251                        read_offset(cont_msg_data, 0, offset_size)? as usize;
252                    let cont_length =
253                        read_offset(cont_msg_data, offset_size as usize, length_size)?
254                            as usize;
255                    let cont_msgs = Self::parse_v1_continuation(
256                        data, cont_offset, cont_length, offset_size, length_size,
257                        depth_remaining - 1,
258                    )?;
259                    messages.extend(cont_msgs);
260                }
261            }
262        }
263
264        Ok(messages)
265    }
266
267    fn parse_v2(
268        data: &[u8],
269        offset: usize,
270        offset_size: u8,
271        length_size: u8,
272    ) -> Result<ObjectHeader, FormatError> {
273        // signature(4) + version(1) + flags(1) = 6
274        ensure_len(data, offset, 6)?;
275
276        let version = data[offset + 4];
277        if version != 2 {
278            return Err(FormatError::InvalidObjectHeaderVersion(version));
279        }
280        let flags = data[offset + 5];
281
282        let mut pos = offset + 6;
283
284        // Optional timestamps (flags bit 5)
285        let (access_time, modification_time, change_time, birth_time) = if flags & 0x20 != 0 {
286            ensure_len(data, pos, 16)?;
287            let at = LittleEndian::read_u32(&data[pos..pos + 4]);
288            let mt = LittleEndian::read_u32(&data[pos + 4..pos + 8]);
289            let ct = LittleEndian::read_u32(&data[pos + 8..pos + 12]);
290            let bt = LittleEndian::read_u32(&data[pos + 12..pos + 16]);
291            pos += 16;
292            (Some(at), Some(mt), Some(ct), Some(bt))
293        } else {
294            (None, None, None, None)
295        };
296
297        // Optional attribute storage thresholds (flags bit 4)
298        if flags & 0x10 != 0 {
299            ensure_len(data, pos, 4)?;
300            // max_compact_attrs(2) + min_dense_attrs(2) — read but don't store for now
301            pos += 4;
302        }
303
304        // chunk0 size: width depends on flags bits 0-1
305        let chunk_size_width = match flags & 0x03 {
306            0 => 1u8,
307            1 => 2,
308            2 => 4,
309            3 => 8,
310            _ => unreachable!(),
311        };
312        ensure_len(data, pos, chunk_size_width as usize)?;
313        let chunk0_size = read_offset(data, pos, chunk_size_width)? as usize;
314        pos += chunk_size_width as usize;
315
316        let chunk0_msg_start = pos;
317        let chunk0_msg_end = pos.checked_add(chunk0_size).ok_or(FormatError::UnexpectedEof {
318            expected: usize::MAX,
319            available: data.len(),
320        })?;
321
322        // Validate checksum: from OHDR signature through all messages (before checksum)
323        ensure_len(data, chunk0_msg_end, 4)?;
324        #[cfg(feature = "checksum")]
325        {
326            let stored = LittleEndian::read_u32(&data[chunk0_msg_end..chunk0_msg_end + 4]);
327            let computed = crate::checksum::jenkins_lookup3(&data[offset..chunk0_msg_end]);
328            if computed != stored {
329                return Err(FormatError::ChecksumMismatch {
330                    expected: stored,
331                    computed,
332                });
333            }
334        }
335
336        // Bit 2: attribute creation order tracked → messages include creation order field
337        let has_creation_order = flags & 0x04 != 0;
338
339        // Parse messages from chunk0
340        let mut messages = Vec::new();
341        let mut continuations = Vec::new();
342        Self::parse_v2_messages(
343            data,
344            chunk0_msg_start,
345            chunk0_msg_end,
346            has_creation_order,
347            offset_size,
348            length_size,
349            &mut messages,
350            &mut continuations,
351        )?;
352
353        // Follow continuations (limit to prevent cycles in malformed data)
354        let mut cont_remaining = 256u16;
355        while let Some((cont_offset, cont_length)) = continuations.pop() {
356            if cont_remaining == 0 {
357                return Err(FormatError::NestingDepthExceeded);
358            }
359            cont_remaining -= 1;
360            Self::parse_v2_continuation(
361                data,
362                cont_offset,
363                cont_length,
364                has_creation_order,
365                offset_size,
366                length_size,
367                &mut messages,
368                &mut continuations,
369            )?;
370        }
371
372        Ok(ObjectHeader {
373            version: 2,
374            messages,
375            reference_count: None,
376            flags,
377            access_time,
378            modification_time,
379            change_time,
380            birth_time,
381        })
382    }
383
384    #[allow(clippy::too_many_arguments)]
385    fn parse_v2_messages(
386        data: &[u8],
387        start: usize,
388        end: usize,
389        has_creation_order: bool,
390        offset_size: u8,
391        length_size: u8,
392        messages: &mut Vec<HeaderMessage>,
393        continuations: &mut Vec<(usize, usize)>,
394    ) -> Result<(), FormatError> {
395        let msg_header_size = if has_creation_order { 6 } else { 4 };
396        let mut pos = start;
397
398        while pos + msg_header_size <= end {
399            let msg_type_raw = data[pos] as u16;
400            let msg_data_size = LittleEndian::read_u16(&data[pos + 1..pos + 3]) as usize;
401            let msg_flags = data[pos + 3];
402            let creation_order = if has_creation_order {
403                Some(LittleEndian::read_u16(&data[pos + 4..pos + 6]))
404            } else {
405                None
406            };
407            pos += msg_header_size;
408
409            if pos + msg_data_size > end {
410                // Could be padding at end of chunk
411                break;
412            }
413
414            let msg_type = MessageType::from_u16(msg_type_raw);
415
416            if let MessageType::Unknown(id) = msg_type {
417                if msg_flags & 0x08 != 0 {
418                    return Err(FormatError::UnsupportedMessage(id));
419                }
420            }
421
422            let msg_data = data[pos..pos + msg_data_size].to_vec();
423
424            if msg_type == MessageType::ObjectHeaderContinuation {
425                // Parse continuation offset/length from message data
426                if msg_data.len() >= (offset_size as usize + length_size as usize) {
427                    let cont_off = read_offset(&msg_data, 0, offset_size)? as usize;
428                    let cont_len =
429                        read_offset(&msg_data, offset_size as usize, length_size)? as usize;
430                    continuations.push((cont_off, cont_len));
431                }
432            } else if msg_type != MessageType::Nil {
433                messages.push(HeaderMessage {
434                    msg_type,
435                    size: msg_data_size,
436                    flags: msg_flags,
437                    creation_order,
438                    data: msg_data,
439                });
440            }
441
442            pos += msg_data_size;
443        }
444
445        Ok(())
446    }
447
448    #[allow(clippy::too_many_arguments)]
449    fn parse_v2_continuation(
450        data: &[u8],
451        offset: usize,
452        length: usize,
453        has_creation_order: bool,
454        offset_size: u8,
455        length_size: u8,
456        messages: &mut Vec<HeaderMessage>,
457        continuations: &mut Vec<(usize, usize)>,
458    ) -> Result<(), FormatError> {
459        // OCHK signature(4) + messages + checksum(4)
460        ensure_len(data, offset, length)?;
461        if length < 8 {
462            return Err(FormatError::UnexpectedEof {
463                expected: 8,
464                available: length,
465            });
466        }
467
468        ensure_len(data, offset, 4)?;
469        if data[offset..offset + 4] != OCHK_SIGNATURE {
470            return Err(FormatError::InvalidObjectHeaderSignature);
471        }
472
473        let msg_start = offset + 4;
474        let checksum_pos = offset + length - 4;
475
476        #[cfg(feature = "checksum")]
477        {
478            let stored = LittleEndian::read_u32(&data[checksum_pos..checksum_pos + 4]);
479            let computed = crate::checksum::jenkins_lookup3(&data[offset..checksum_pos]);
480            if computed != stored {
481                return Err(FormatError::ChecksumMismatch {
482                    expected: stored,
483                    computed,
484                });
485            }
486        }
487
488        Self::parse_v2_messages(
489            data,
490            msg_start,
491            checksum_pos,
492            has_creation_order,
493            offset_size,
494            length_size,
495            messages,
496            continuations,
497        )
498    }
499}
500
501#[cfg(test)]
502mod tests {
503    use super::*;
504
505    // Helper: build a v1 object header with given messages
506    fn build_v1_header(
507        messages: &[(u16, &[u8], u8)], // (type, data, flags)
508        offset_size: u8,
509        length_size: u8,
510    ) -> Vec<u8> {
511        let _ = (offset_size, length_size);
512        // Calculate total header message data size
513        let mut msg_bytes = Vec::new();
514        for (mtype, mdata, mflags) in messages {
515            msg_bytes.extend_from_slice(&mtype.to_le_bytes()); // type(2)
516            msg_bytes.extend_from_slice(&(mdata.len() as u16).to_le_bytes()); // size(2)
517            msg_bytes.push(*mflags); // flags(1)
518            msg_bytes.extend_from_slice(&[0u8; 3]); // reserved(3)
519            msg_bytes.extend_from_slice(mdata); // data
520        }
521
522        let mut buf = Vec::new();
523        buf.push(1); // version
524        buf.push(0); // reserved
525        buf.extend_from_slice(&(messages.len() as u16).to_le_bytes()); // num_messages
526        buf.extend_from_slice(&1u32.to_le_bytes()); // reference_count
527        buf.extend_from_slice(&(msg_bytes.len() as u32).to_le_bytes()); // header_data_size
528        // Pad to 8-byte alignment (12 bytes so far, pad 4)
529        buf.extend_from_slice(&[0u8; 4]);
530        buf.extend_from_slice(&msg_bytes);
531        buf
532    }
533
534    // Helper: build a v2 object header chunk0 with given messages
535    fn build_v2_header(
536        flags: u8,
537        messages: &[(u8, &[u8], u8)], // (type, data, msg_flags)
538        timestamps: Option<(u32, u32, u32, u32)>,
539    ) -> Vec<u8> {
540        let has_creation_order = flags & 0x04 != 0;
541        let has_timestamps = flags & 0x20 != 0;
542        let mut buf = Vec::new();
543        buf.extend_from_slice(&OHDR_SIGNATURE); // 4
544        buf.push(2); // version
545        buf.push(flags);
546
547        if has_timestamps {
548            if let Some((at, mt, ct, bt)) = timestamps {
549                buf.extend_from_slice(&at.to_le_bytes());
550                buf.extend_from_slice(&mt.to_le_bytes());
551                buf.extend_from_slice(&ct.to_le_bytes());
552                buf.extend_from_slice(&bt.to_le_bytes());
553            }
554        }
555
556        if flags & 0x10 != 0 {
557            buf.extend_from_slice(&8u16.to_le_bytes()); // max_compact
558            buf.extend_from_slice(&6u16.to_le_bytes()); // min_dense
559        }
560
561        // Build message bytes to get chunk size
562        let mut msg_bytes = Vec::new();
563        for (mtype, mdata, mflags) in messages {
564            msg_bytes.push(*mtype); // type(1)
565            msg_bytes.extend_from_slice(&(mdata.len() as u16).to_le_bytes()); // size(2)
566            msg_bytes.push(*mflags); // flags(1)
567            if has_creation_order {
568                msg_bytes.extend_from_slice(&0u16.to_le_bytes()); // creation_order(2)
569            }
570            msg_bytes.extend_from_slice(mdata);
571        }
572
573        let chunk_size = msg_bytes.len();
574        // Write chunk size based on flags bits 0-1
575        match flags & 0x03 {
576            0 => buf.push(chunk_size as u8),
577            1 => buf.extend_from_slice(&(chunk_size as u16).to_le_bytes()),
578            2 => buf.extend_from_slice(&(chunk_size as u32).to_le_bytes()),
579            3 => buf.extend_from_slice(&(chunk_size as u64).to_le_bytes()),
580            _ => unreachable!(),
581        }
582
583        buf.extend_from_slice(&msg_bytes);
584
585        // Checksum (CRC32C of everything from OHDR to here)
586        let checksum = crate::checksum::jenkins_lookup3(&buf);
587        buf.extend_from_slice(&checksum.to_le_bytes());
588        buf
589    }
590
591    #[test]
592    fn parse_v1_zero_messages() {
593        let data = build_v1_header(&[], 8, 8);
594        let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
595        assert_eq!(hdr.version, 1);
596        assert_eq!(hdr.messages.len(), 0);
597        assert_eq!(hdr.reference_count, Some(1));
598        assert_eq!(hdr.flags, 0);
599    }
600
601    #[test]
602    fn parse_v1_two_messages() {
603        let messages = [
604            (0x0001u16, &[1u8, 2, 3, 4][..], 0u8), // Dataspace
605            (0x0008, &[5u8, 6][..], 0),              // DataLayout
606        ];
607        let data = build_v1_header(&messages, 8, 8);
608        let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
609        assert_eq!(hdr.messages.len(), 2);
610        assert_eq!(hdr.messages[0].msg_type, MessageType::Dataspace);
611        assert_eq!(hdr.messages[0].data, vec![1, 2, 3, 4]);
612        assert_eq!(hdr.messages[1].msg_type, MessageType::DataLayout);
613        assert_eq!(hdr.messages[1].data, vec![5, 6]);
614    }
615
616    #[test]
617    fn parse_v1_unknown_message_ok() {
618        let messages = [(0x00FFu16, &[0xAA, 0xBB][..], 0u8)];
619        let data = build_v1_header(&messages, 8, 8);
620        let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
621        assert_eq!(hdr.messages.len(), 1);
622        assert_eq!(hdr.messages[0].msg_type, MessageType::Unknown(0x00FF));
623    }
624
625    #[test]
626    fn parse_v1_unknown_must_understand_errors() {
627        // Bit 3 of msg_flags = must understand
628        let messages = [(0x00FFu16, &[0xAA][..], 0x08u8)];
629        let data = build_v1_header(&messages, 8, 8);
630        let err = ObjectHeader::parse(&data, 0, 8, 8).unwrap_err();
631        assert_eq!(err, FormatError::UnsupportedMessage(0x00FF));
632    }
633
634    #[test]
635    fn parse_v2_no_timestamps_one_message() {
636        let data = build_v2_header(0x00, &[(0x01, &[10, 20], 0)], None);
637        let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
638        assert_eq!(hdr.version, 2);
639        assert_eq!(hdr.flags, 0);
640        assert_eq!(hdr.messages.len(), 1);
641        assert_eq!(hdr.messages[0].msg_type, MessageType::Dataspace);
642        assert_eq!(hdr.messages[0].data, vec![10, 20]);
643        assert!(hdr.access_time.is_none());
644    }
645
646    #[test]
647    fn parse_v2_with_timestamps() {
648        let data = build_v2_header(
649            0x20,
650            &[(0x01, &[1], 0)],
651            Some((100, 200, 300, 400)),
652        );
653        let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
654        assert_eq!(hdr.access_time, Some(100));
655        assert_eq!(hdr.modification_time, Some(200));
656        assert_eq!(hdr.change_time, Some(300));
657        assert_eq!(hdr.birth_time, Some(400));
658        assert_eq!(hdr.messages.len(), 1);
659        // flags bit 5 = timestamps, but bit 2 not set → no creation order in messages
660        assert!(hdr.messages[0].creation_order.is_none());
661    }
662
663    #[test]
664    fn parse_v2_creation_order() {
665        // flags bit 2 enables attribute/message creation order tracking
666        // flags bit 5 enables timestamps
667        // Use 0x24 = bit 2 + bit 5
668        let data = build_v2_header(
669            0x24,
670            &[(0x03, &[9], 0), (0x05, &[8], 0)],
671            Some((0, 0, 0, 0)),
672        );
673        let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
674        assert_eq!(hdr.messages.len(), 2);
675        assert!(hdr.messages[0].creation_order.is_some());
676        assert!(hdr.messages[1].creation_order.is_some());
677        assert_eq!(hdr.access_time, Some(0));
678    }
679
680    #[test]
681    fn parse_v2_checksum_valid() {
682        let data = build_v2_header(0x00, &[(0x01, &[1, 2, 3], 0)], None);
683        // Should succeed — checksum is valid
684        let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
685        assert_eq!(hdr.messages.len(), 1);
686    }
687
688    #[test]
689    fn parse_v2_checksum_invalid() {
690        let mut data = build_v2_header(0x00, &[(0x01, &[1, 2, 3], 0)], None);
691        // Corrupt checksum
692        let len = data.len();
693        data[len - 1] ^= 0xFF;
694        let err = ObjectHeader::parse(&data, 0, 8, 8).unwrap_err();
695        assert!(matches!(err, FormatError::ChecksumMismatch { .. }));
696    }
697
698    #[test]
699    fn parse_v2_nil_padding_skipped() {
700        let data = build_v2_header(
701            0x00,
702            &[
703                (0x00, &[0, 0, 0, 0], 0), // NIL
704                (0x01, &[42], 0),           // Dataspace
705            ],
706            None,
707        );
708        let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
709        assert_eq!(hdr.messages.len(), 1);
710        assert_eq!(hdr.messages[0].msg_type, MessageType::Dataspace);
711    }
712
713    #[test]
714    fn parse_v2_chunk_size_1byte() {
715        // flags bits 0-1 = 0 → 1-byte chunk size
716        let data = build_v2_header(0x00, &[(0x01, &[1], 0)], None);
717        let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
718        assert_eq!(hdr.messages.len(), 1);
719    }
720
721    #[test]
722    fn parse_v2_chunk_size_2byte() {
723        let data = build_v2_header(0x01, &[(0x01, &[1], 0)], None);
724        let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
725        assert_eq!(hdr.messages.len(), 1);
726    }
727
728    #[test]
729    fn parse_v2_chunk_size_4byte() {
730        let data = build_v2_header(0x02, &[(0x01, &[1], 0)], None);
731        let hdr = ObjectHeader::parse(&data, 0, 8, 8).unwrap();
732        assert_eq!(hdr.messages.len(), 1);
733    }
734
735    #[test]
736    fn parse_v2_continuation() {
737        // Build a continuation chunk (OCHK) at a known offset
738        let ochk_offset = 256usize;
739        let ochk_msg_type = 0x03u8; // Datatype
740        let ochk_msg_data = [0xDE, 0xAD];
741
742        // Build the OCHK chunk
743        let mut ochk_buf = Vec::new();
744        ochk_buf.extend_from_slice(&OCHK_SIGNATURE);
745        ochk_buf.push(ochk_msg_type);
746        ochk_buf.extend_from_slice(&(ochk_msg_data.len() as u16).to_le_bytes());
747        ochk_buf.push(0); // msg flags
748        ochk_buf.extend_from_slice(&ochk_msg_data);
749        let checksum = crate::checksum::jenkins_lookup3(&ochk_buf);
750        ochk_buf.extend_from_slice(&checksum.to_le_bytes());
751
752        let ochk_length = ochk_buf.len();
753
754        // Build continuation message data: offset(8 LE) + length(8 LE)
755        let mut cont_data = Vec::new();
756        cont_data.extend_from_slice(&(ochk_offset as u64).to_le_bytes());
757        cont_data.extend_from_slice(&(ochk_length as u64).to_le_bytes());
758
759        // Build main header with continuation message + a regular message
760        let header = build_v2_header(
761            0x00,
762            &[
763                (0x01, &[42], 0),       // Dataspace
764                (0x10, &cont_data, 0),   // Continuation
765            ],
766            None,
767        );
768
769        // Assemble full "file"
770        let total_size = ochk_offset + ochk_buf.len();
771        let mut file_data = vec![0u8; total_size];
772        file_data[..header.len()].copy_from_slice(&header);
773        file_data[ochk_offset..ochk_offset + ochk_buf.len()].copy_from_slice(&ochk_buf);
774
775        let hdr = ObjectHeader::parse(&file_data, 0, 8, 8).unwrap();
776        assert_eq!(hdr.messages.len(), 2);
777        assert_eq!(hdr.messages[0].msg_type, MessageType::Dataspace);
778        assert_eq!(hdr.messages[1].msg_type, MessageType::Datatype);
779        assert_eq!(hdr.messages[1].data, vec![0xDE, 0xAD]);
780    }
781
782    #[test]
783    fn truncated_v1_header() {
784        let data = vec![1u8, 0]; // version 1, but too short
785        let err = ObjectHeader::parse(&data, 0, 8, 8).unwrap_err();
786        assert!(matches!(err, FormatError::UnexpectedEof { .. }));
787    }
788
789    #[test]
790    fn truncated_v2_header() {
791        let data = [b'O', b'H', b'D', b'R', 2]; // signature + version, but no flags
792        let err = ObjectHeader::parse(&data, 0, 8, 8).unwrap_err();
793        assert!(matches!(err, FormatError::UnexpectedEof { .. }));
794    }
795}