Skip to main content

hdf5_reader/
object_header.rs

1//! HDF5 Object Header parser (v1 and v2).
2//!
3//! Object headers contain a collection of header messages that describe an
4//! HDF5 object (group, dataset, committed datatype, etc.).  Two on-disk
5//! formats exist:
6//!
7//! * **Version 1** (HDF5 < 1.8) — 16-byte fixed prefix, messages each have an
8//!   8-byte envelope (type u16 + size u16 + flags u8 + reserved 3).
9//! * **Version 2** (HDF5 >= 1.8) — begins with the `OHDR` signature, variable-
10//!   length prefix, messages have a 4-or-6-byte envelope, and every chunk is
11//!   checksummed with Jenkins lookup3.
12//!
13//! Continuation messages (type `0x0010`) cause the parser to follow an offset
14//! to an additional chunk of messages (an `OCHK` block in v2, or a raw message
15//! run in v1).
16
17use crate::checksum::jenkins_lookup3;
18use crate::error::{Error, Result};
19use crate::io::Cursor;
20use crate::messages::shared::SharedMessage;
21use crate::messages::{parse_message, HdfMessage};
22use crate::storage::Storage;
23
24/// Magic signature for v2 object headers.
25const OHDR_SIGNATURE: [u8; 4] = *b"OHDR";
26
27/// Magic signature for v2 continuation chunks.
28const OCHK_SIGNATURE: [u8; 4] = *b"OCHK";
29
30/// Header continuation message type id.
31const MSG_TYPE_CONTINUATION: u16 = 0x0010;
32
33/// Nil (padding) message type id.
34const MSG_TYPE_NIL: u16 = 0x0000;
35
36/// Parsed object header with all its messages.
37#[derive(Debug, Clone)]
38pub struct ObjectHeader {
39    /// Object header format version (1 or 2).
40    pub version: u8,
41    /// All parsed header messages, collected from every chunk.
42    pub messages: Vec<HdfMessage>,
43    /// Object reference count.
44    pub reference_count: u32,
45    /// Modification time in seconds since the UNIX epoch (v2 only, when the
46    /// "times stored" flag is set).
47    pub modification_time: Option<u32>,
48}
49
50impl ObjectHeader {
51    /// Parse an object header at the given absolute file address.
52    ///
53    /// `data` is the entire file mapped into memory, `address` is the byte
54    /// offset where the object header starts, and `offset_size` / `length_size`
55    /// come from the superblock.
56    pub fn parse_at(data: &[u8], address: u64, offset_size: u8, length_size: u8) -> Result<Self> {
57        let mut cursor = Cursor::new(data);
58        cursor.set_position(address);
59
60        // Peek at the first four bytes to decide v1 vs v2.
61        let sig = cursor.peek_bytes(4)?;
62        if sig == OHDR_SIGNATURE {
63            Self::parse_v2(&cursor, address, offset_size, length_size)
64        } else {
65            Self::parse_v1(&cursor, address, offset_size, length_size)
66        }
67    }
68
69    /// Parse an object header from random-access storage.
70    pub fn parse_at_storage(
71        storage: &dyn Storage,
72        address: u64,
73        offset_size: u8,
74        length_size: u8,
75    ) -> Result<Self> {
76        let prefix = storage.read_range(address, 64)?;
77        if prefix.len() < 5 {
78            return Err(Error::UnexpectedEof {
79                offset: address,
80                needed: 5,
81                available: prefix.len() as u64,
82            });
83        }
84
85        if prefix.as_ref()[..4] == OHDR_SIGNATURE {
86            Self::parse_v2_storage(storage, address, offset_size, length_size)
87        } else {
88            Self::parse_v1_storage(storage, address, offset_size, length_size)
89        }
90    }
91
92    /// Resolve shared messages by following references to other object headers.
93    ///
94    /// For `SharedInOhdr`, the referenced object header is parsed and the first
95    /// matching message type is extracted. `SharedInSohm` returns an error (rare).
96    pub fn resolve_shared_messages(
97        &mut self,
98        data: &[u8],
99        offset_size: u8,
100        length_size: u8,
101    ) -> Result<()> {
102        let old_messages = std::mem::take(&mut self.messages);
103        let mut resolved = Vec::with_capacity(old_messages.len());
104        for msg in old_messages {
105            match msg {
106                HdfMessage::Shared(SharedMessage::SharedInOhdr { address }) => {
107                    match Self::parse_at(data, address, offset_size, length_size) {
108                        Ok(target_header) => {
109                            // Extract the actual message(s) from the target header.
110                            // Typically there is exactly one "real" message (the
111                            // committed datatype, fill value, etc.).
112                            for target_msg in target_header.messages {
113                                match target_msg {
114                                    HdfMessage::Nil
115                                    | HdfMessage::ObjectHeaderContinuation
116                                    | HdfMessage::Shared(_) => continue,
117                                    other => {
118                                        resolved.push(other);
119                                        break;
120                                    }
121                                }
122                            }
123                        }
124                        Err(_) => {
125                            // If we can't parse the target, keep the shared ref
126                            resolved
127                                .push(HdfMessage::Shared(SharedMessage::SharedInOhdr { address }));
128                        }
129                    }
130                }
131                HdfMessage::Shared(SharedMessage::SharedInSohm { .. }) => {
132                    self.messages = resolved;
133                    return Err(Error::Other(
134                        "SOHM table lookup not yet supported — file uses shared object header messages".to_string(),
135                    ));
136                }
137                other => resolved.push(other),
138            }
139        }
140        self.messages = resolved;
141        Ok(())
142    }
143
144    /// Resolve shared messages by following references via random-access storage.
145    pub fn resolve_shared_messages_storage(
146        &mut self,
147        storage: &dyn Storage,
148        offset_size: u8,
149        length_size: u8,
150    ) -> Result<()> {
151        let old_messages = std::mem::take(&mut self.messages);
152        let mut resolved = Vec::with_capacity(old_messages.len());
153        for msg in old_messages {
154            match msg {
155                HdfMessage::Shared(SharedMessage::SharedInOhdr { address }) => {
156                    match Self::parse_at_storage(storage, address, offset_size, length_size) {
157                        Ok(target_header) => {
158                            for target_msg in target_header.messages {
159                                match target_msg {
160                                    HdfMessage::Nil
161                                    | HdfMessage::ObjectHeaderContinuation
162                                    | HdfMessage::Shared(_) => continue,
163                                    other => {
164                                        resolved.push(other);
165                                        break;
166                                    }
167                                }
168                            }
169                        }
170                        Err(_) => {
171                            resolved
172                                .push(HdfMessage::Shared(SharedMessage::SharedInOhdr { address }));
173                        }
174                    }
175                }
176                HdfMessage::Shared(SharedMessage::SharedInSohm { .. }) => {
177                    self.messages = resolved;
178                    return Err(Error::Other(
179                        "SOHM table lookup not yet supported — file uses shared object header messages".to_string(),
180                    ));
181                }
182                other => resolved.push(other),
183            }
184        }
185        self.messages = resolved;
186        Ok(())
187    }
188
189    // ------------------------------------------------------------------
190    // Version 1
191    // ------------------------------------------------------------------
192
193    /// Parse a version-1 object header.
194    ///
195    /// Layout (16 bytes total):
196    /// ```text
197    ///   version          u8    (must be 1)
198    ///   reserved         u8
199    ///   num_messages     u16
200    ///   ref_count        u32
201    ///   header_data_size u32   (byte count of the message run)
202    ///   reserved         u32   (alignment padding)
203    /// ```
204    fn parse_v1(base: &Cursor<'_>, address: u64, offset_size: u8, length_size: u8) -> Result<Self> {
205        let mut cursor = base.at_offset(address)?;
206
207        let version = cursor.read_u8()?;
208        if version != 1 {
209            return Err(Error::UnsupportedObjectHeaderVersion(version));
210        }
211
212        let _reserved = cursor.read_u8()?;
213        let num_messages = cursor.read_u16_le()?;
214        let reference_count = cursor.read_u32_le()?;
215        let header_data_size = cursor.read_u32_le()? as u64;
216        let _reserved2 = cursor.read_u32_le()?; // alignment padding
217
218        // Messages start right after the 16-byte prefix.
219        let messages_start = cursor.position();
220        let messages_end = messages_start + header_data_size;
221
222        let mut messages: Vec<HdfMessage> = Vec::with_capacity(num_messages as usize);
223        let mut continuations: Vec<(u64, u64)> = Vec::new();
224
225        Self::read_v1_messages(
226            base,
227            messages_start,
228            messages_end,
229            offset_size,
230            length_size,
231            &mut messages,
232            &mut continuations,
233        )?;
234
235        // Follow continuation messages.
236        while let Some((cont_offset, cont_length)) = continuations.pop() {
237            let cont_end = cont_offset + cont_length;
238            Self::read_v1_messages(
239                base,
240                cont_offset,
241                cont_end,
242                offset_size,
243                length_size,
244                &mut messages,
245                &mut continuations,
246            )?;
247        }
248
249        Ok(ObjectHeader {
250            version: 1,
251            messages,
252            reference_count,
253            modification_time: None,
254        })
255    }
256
257    fn parse_v1_storage(
258        storage: &dyn Storage,
259        address: u64,
260        offset_size: u8,
261        length_size: u8,
262    ) -> Result<Self> {
263        let header = storage.read_range(address, 16)?;
264        let mut cursor = Cursor::new(header.as_ref());
265
266        let version = cursor.read_u8()?;
267        if version != 1 {
268            return Err(Error::UnsupportedObjectHeaderVersion(version));
269        }
270
271        let _reserved = cursor.read_u8()?;
272        let num_messages = cursor.read_u16_le()?;
273        let reference_count = cursor.read_u32_le()?;
274        let header_data_size = cursor.read_u32_le()? as u64;
275        let _reserved2 = cursor.read_u32_le()?;
276
277        let first_chunk = storage.read_range(address, (16 + header_data_size) as usize)?;
278        let mut messages = Vec::with_capacity(num_messages as usize);
279        let mut continuations = Vec::new();
280        Self::read_v1_messages_from_slice(
281            &first_chunk.as_ref()[16..],
282            offset_size,
283            length_size,
284            &mut messages,
285            &mut continuations,
286        )?;
287
288        while let Some((cont_offset, cont_length)) = continuations.pop() {
289            let chunk = storage.read_range(cont_offset, cont_length as usize)?;
290            Self::read_v1_messages_from_slice(
291                chunk.as_ref(),
292                offset_size,
293                length_size,
294                &mut messages,
295                &mut continuations,
296            )?;
297        }
298
299        Ok(ObjectHeader {
300            version: 1,
301            messages,
302            reference_count,
303            modification_time: None,
304        })
305    }
306
307    /// Read v1 header messages from `start..end`, appending to `messages`.
308    /// Any continuation messages encountered are pushed onto `continuations`
309    /// for the caller to follow.
310    fn read_v1_messages(
311        base: &Cursor<'_>,
312        start: u64,
313        end: u64,
314        offset_size: u8,
315        length_size: u8,
316        messages: &mut Vec<HdfMessage>,
317        continuations: &mut Vec<(u64, u64)>,
318    ) -> Result<()> {
319        let mut cursor = base.at_offset(start)?;
320
321        while cursor.position() + 8 <= end {
322            let msg_type = cursor.read_u16_le()?;
323            let msg_data_size = cursor.read_u16_le()? as usize;
324            let msg_flags = cursor.read_u8()?;
325            let _reserved = cursor.read_bytes(3)?; // 3 reserved bytes
326
327            // Bounds-check the message data within this chunk.
328            if cursor.position() + msg_data_size as u64 > end {
329                return Err(Error::InvalidData(format!(
330                    "v1 message data ({} bytes) extends past header chunk end",
331                    msg_data_size
332                )));
333            }
334
335            if msg_type == MSG_TYPE_NIL {
336                // Nil / padding — skip the data bytes.
337                cursor.skip(msg_data_size)?;
338                messages.push(HdfMessage::Nil);
339                continue;
340            }
341
342            let msg_data = cursor.read_bytes(msg_data_size)?;
343            let is_shared = (msg_flags & 0x02) != 0;
344
345            if is_shared {
346                // Shared message — the stored bytes are a shared-message
347                // reference, not the message payload itself.
348                let shared_msg = crate::messages::shared::parse(
349                    &mut Cursor::new(msg_data),
350                    offset_size,
351                    length_size,
352                    msg_data_size,
353                )?;
354                messages.push(HdfMessage::Shared(shared_msg));
355            } else if msg_type == MSG_TYPE_CONTINUATION {
356                // Parse the continuation message to get offset + length, then
357                // enqueue it for later processing.
358                let cont = crate::messages::continuation::parse(
359                    &mut Cursor::new(msg_data),
360                    offset_size,
361                    length_size,
362                    msg_data_size,
363                )?;
364                continuations.push((cont.offset, cont.length));
365                messages.push(HdfMessage::ObjectHeaderContinuation);
366            } else {
367                let parsed = parse_message(
368                    msg_type,
369                    msg_data.len(),
370                    &mut Cursor::new(msg_data),
371                    offset_size,
372                    length_size,
373                )?;
374                messages.push(parsed);
375            }
376        }
377
378        Ok(())
379    }
380
381    // ------------------------------------------------------------------
382    // Version 2
383    // ------------------------------------------------------------------
384
385    /// Parse a version-2 object header.
386    ///
387    /// Layout:
388    /// ```text
389    ///   signature  4 bytes  ("OHDR")
390    ///   version    u8       (must be 2)
391    ///   flags      u8
392    ///   [optional timestamps — 4 x u32 if bit 5 of flags]
393    ///   [optional attr phase change — 2 x u16 if bit 4 of flags]
394    ///   chunk0_size  1/2/4/8 bytes (encoded size depends on bits 0-1 of flags)
395    ///   <messages for chunk 0>
396    ///   checksum   u32      (Jenkins lookup3 from "OHDR" through last byte before checksum)
397    /// ```
398    fn parse_v2(base: &Cursor<'_>, address: u64, offset_size: u8, length_size: u8) -> Result<Self> {
399        let mut cursor = base.at_offset(address)?;
400
401        // ---- Fixed prefix ----
402        let sig = cursor.read_bytes(4)?;
403        if sig != OHDR_SIGNATURE {
404            return Err(Error::InvalidObjectHeaderSignature);
405        }
406        let version = cursor.read_u8()?;
407        if version != 2 {
408            return Err(Error::UnsupportedObjectHeaderVersion(version));
409        }
410        let flags = cursor.read_u8()?;
411
412        // Bit 5 — timestamps stored.
413        let modification_time = if (flags & 0x20) != 0 {
414            let _access_time = cursor.read_u32_le()?;
415            let mod_time = cursor.read_u32_le()?;
416            let _change_time = cursor.read_u32_le()?;
417            let _birth_time = cursor.read_u32_le()?;
418            Some(mod_time)
419        } else {
420            None
421        };
422
423        // Bit 4 — non-default attribute storage phase change values.
424        if (flags & 0x10) != 0 {
425            let _max_compact = cursor.read_u16_le()?;
426            let _min_dense = cursor.read_u16_le()?;
427        }
428
429        // Chunk#0 size — width depends on bits 0-1 of flags.
430        let size_field_width = 1usize << (flags & 0x03);
431        let chunk0_data_size = cursor.read_uvar(size_field_width)?;
432
433        // Bit 2 — attribute creation order tracked (affects per-message envelope).
434        let creation_order_tracked = (flags & 0x04) != 0;
435
436        // Messages for chunk 0 run from the current position for
437        // `chunk0_data_size` bytes.  The last 4 bytes of that range are the
438        // checksum.
439        let messages_start = cursor.position();
440        let chunk0_end = messages_start + chunk0_data_size;
441
442        // The checksum covers everything from "OHDR" through the last byte
443        // before the checksum field.
444        let checksum_start = address as usize;
445        let checksum_end = chunk0_end as usize; // the checksum itself sits at chunk0_end
446        let stored_checksum = {
447            let mut ck = base.at_offset(chunk0_end)?;
448            ck.read_u32_le()?
449        };
450        let computed = jenkins_lookup3(&base.data()[checksum_start..checksum_end]);
451        if computed != stored_checksum {
452            return Err(Error::ChecksumMismatch {
453                expected: stored_checksum,
454                actual: computed,
455            });
456        }
457
458        let mut messages: Vec<HdfMessage> = Vec::new();
459        let mut continuations: Vec<(u64, u64)> = Vec::new();
460
461        Self::read_v2_messages(
462            base,
463            messages_start,
464            chunk0_end,
465            offset_size,
466            length_size,
467            creation_order_tracked,
468            &mut messages,
469            &mut continuations,
470        )?;
471
472        // Follow continuation chunks.
473        while let Some((cont_offset, cont_length)) = continuations.pop() {
474            Self::read_v2_continuation_chunk(
475                base,
476                cont_offset,
477                cont_length,
478                offset_size,
479                length_size,
480                creation_order_tracked,
481                &mut messages,
482                &mut continuations,
483            )?;
484        }
485
486        Ok(ObjectHeader {
487            version: 2,
488            messages,
489            reference_count: 0, // v2 does not store a reference count in the header
490            modification_time,
491        })
492    }
493
494    fn parse_v2_storage(
495        storage: &dyn Storage,
496        address: u64,
497        offset_size: u8,
498        length_size: u8,
499    ) -> Result<Self> {
500        let prefix = storage.read_range(address, 64)?;
501        let mut cursor = Cursor::new(prefix.as_ref());
502
503        let sig = cursor.read_bytes(4)?;
504        if sig != OHDR_SIGNATURE {
505            return Err(Error::InvalidObjectHeaderSignature);
506        }
507        let version = cursor.read_u8()?;
508        if version != 2 {
509            return Err(Error::UnsupportedObjectHeaderVersion(version));
510        }
511        let flags = cursor.read_u8()?;
512
513        let modification_time = if (flags & 0x20) != 0 {
514            let _access_time = cursor.read_u32_le()?;
515            let mod_time = cursor.read_u32_le()?;
516            let _change_time = cursor.read_u32_le()?;
517            let _birth_time = cursor.read_u32_le()?;
518            Some(mod_time)
519        } else {
520            None
521        };
522
523        if (flags & 0x10) != 0 {
524            let _max_compact = cursor.read_u16_le()?;
525            let _min_dense = cursor.read_u16_le()?;
526        }
527
528        let size_field_width = 1usize << (flags & 0x03);
529        let chunk0_data_size = cursor.read_uvar(size_field_width)?;
530        let creation_order_tracked = (flags & 0x04) != 0;
531        let messages_start = cursor.position() as usize;
532        let chunk0_end = messages_start + chunk0_data_size as usize;
533
534        let chunk = storage.read_range(address, chunk0_end + 4)?;
535        let stored_checksum = u32::from_le_bytes(
536            chunk.as_ref()[chunk0_end..chunk0_end + 4]
537                .try_into()
538                .unwrap(),
539        );
540        let computed = jenkins_lookup3(&chunk.as_ref()[..chunk0_end]);
541        if computed != stored_checksum {
542            return Err(Error::ChecksumMismatch {
543                expected: stored_checksum,
544                actual: computed,
545            });
546        }
547
548        let mut messages = Vec::new();
549        let mut continuations = Vec::new();
550        Self::read_v2_messages_from_slice(
551            &chunk.as_ref()[messages_start..chunk0_end],
552            offset_size,
553            length_size,
554            creation_order_tracked,
555            &mut messages,
556            &mut continuations,
557        )?;
558
559        while let Some((cont_offset, cont_length)) = continuations.pop() {
560            Self::read_v2_continuation_chunk_storage(
561                storage,
562                cont_offset,
563                cont_length,
564                offset_size,
565                length_size,
566                creation_order_tracked,
567                &mut messages,
568                &mut continuations,
569            )?;
570        }
571
572        Ok(ObjectHeader {
573            version: 2,
574            messages,
575            reference_count: 0,
576            modification_time,
577        })
578    }
579
580    /// Read v2 messages from `start..end`.
581    #[allow(clippy::too_many_arguments)]
582    fn read_v2_messages(
583        base: &Cursor<'_>,
584        start: u64,
585        end: u64,
586        offset_size: u8,
587        length_size: u8,
588        creation_order_tracked: bool,
589        messages: &mut Vec<HdfMessage>,
590        continuations: &mut Vec<(u64, u64)>,
591    ) -> Result<()> {
592        let mut cursor = base.at_offset(start)?;
593
594        // Minimum envelope: type(1) + size(2) + flags(1) = 4 bytes, optionally
595        // +2 for creation order.
596        let min_envelope = if creation_order_tracked { 6 } else { 4 };
597
598        while cursor.position() + min_envelope as u64 <= end {
599            let msg_type = cursor.read_u8()? as u16;
600            let msg_data_size = cursor.read_u16_le()? as usize;
601            let msg_flags = cursor.read_u8()?;
602
603            if creation_order_tracked {
604                let _creation_order = cursor.read_u16_le()?;
605            }
606
607            if msg_type == MSG_TYPE_NIL {
608                if msg_data_size == 0
609                    && base.data()[cursor.position() as usize..end as usize]
610                        .iter()
611                        .all(|byte| *byte == 0)
612                {
613                    break;
614                }
615                cursor.skip(msg_data_size)?;
616                messages.push(HdfMessage::Nil);
617                continue;
618            }
619
620            if cursor.position() + msg_data_size as u64 > end {
621                return Err(Error::InvalidData(format!(
622                    "v2 message data ({} bytes) extends past chunk end",
623                    msg_data_size
624                )));
625            }
626
627            let msg_data = cursor.read_bytes(msg_data_size)?;
628            let is_shared = (msg_flags & 0x02) != 0;
629
630            if is_shared {
631                let shared_msg = crate::messages::shared::parse(
632                    &mut Cursor::new(msg_data),
633                    offset_size,
634                    length_size,
635                    msg_data_size,
636                )?;
637                messages.push(HdfMessage::Shared(shared_msg));
638            } else if msg_type == MSG_TYPE_CONTINUATION {
639                let cont = crate::messages::continuation::parse(
640                    &mut Cursor::new(msg_data),
641                    offset_size,
642                    length_size,
643                    msg_data_size,
644                )?;
645                continuations.push((cont.offset, cont.length));
646                messages.push(HdfMessage::ObjectHeaderContinuation);
647            } else {
648                let parsed = parse_message(
649                    msg_type,
650                    msg_data.len(),
651                    &mut Cursor::new(msg_data),
652                    offset_size,
653                    length_size,
654                )?;
655                messages.push(parsed);
656            }
657        }
658
659        Ok(())
660    }
661
662    fn read_v1_messages_from_slice(
663        data: &[u8],
664        offset_size: u8,
665        length_size: u8,
666        messages: &mut Vec<HdfMessage>,
667        continuations: &mut Vec<(u64, u64)>,
668    ) -> Result<()> {
669        let mut cursor = Cursor::new(data);
670        while cursor.remaining() >= 8 {
671            let msg_type = cursor.read_u16_le()?;
672            let msg_data_size = cursor.read_u16_le()? as usize;
673            let msg_flags = cursor.read_u8()?;
674            let _reserved = cursor.read_bytes(3)?;
675
676            if cursor.remaining() < msg_data_size as u64 {
677                return Err(Error::InvalidData(format!(
678                    "v1 message data ({} bytes) extends past header chunk end",
679                    msg_data_size
680                )));
681            }
682
683            if msg_type == MSG_TYPE_NIL {
684                cursor.skip(msg_data_size)?;
685                messages.push(HdfMessage::Nil);
686                continue;
687            }
688
689            let msg_data = cursor.read_bytes(msg_data_size)?;
690            let is_shared = (msg_flags & 0x02) != 0;
691            if is_shared {
692                let shared_msg = crate::messages::shared::parse(
693                    &mut Cursor::new(msg_data),
694                    offset_size,
695                    length_size,
696                    msg_data_size,
697                )?;
698                messages.push(HdfMessage::Shared(shared_msg));
699            } else if msg_type == MSG_TYPE_CONTINUATION {
700                let cont = crate::messages::continuation::parse(
701                    &mut Cursor::new(msg_data),
702                    offset_size,
703                    length_size,
704                    msg_data_size,
705                )?;
706                continuations.push((cont.offset, cont.length));
707                messages.push(HdfMessage::ObjectHeaderContinuation);
708            } else {
709                let parsed = parse_message(
710                    msg_type,
711                    msg_data.len(),
712                    &mut Cursor::new(msg_data),
713                    offset_size,
714                    length_size,
715                )?;
716                messages.push(parsed);
717            }
718        }
719        Ok(())
720    }
721
722    fn read_v2_messages_from_slice(
723        data: &[u8],
724        offset_size: u8,
725        length_size: u8,
726        creation_order_tracked: bool,
727        messages: &mut Vec<HdfMessage>,
728        continuations: &mut Vec<(u64, u64)>,
729    ) -> Result<()> {
730        let mut cursor = Cursor::new(data);
731        let min_envelope = if creation_order_tracked { 6 } else { 4 };
732
733        while cursor.remaining() >= min_envelope as u64 {
734            let msg_type = cursor.read_u8()? as u16;
735            let msg_data_size = cursor.read_u16_le()? as usize;
736            let msg_flags = cursor.read_u8()?;
737
738            if creation_order_tracked {
739                let _creation_order = cursor.read_u16_le()?;
740            }
741
742            if msg_type == MSG_TYPE_NIL {
743                if msg_data_size == 0
744                    && data[cursor.position() as usize..]
745                        .iter()
746                        .all(|byte| *byte == 0)
747                {
748                    break;
749                }
750                cursor.skip(msg_data_size)?;
751                messages.push(HdfMessage::Nil);
752                continue;
753            }
754
755            if cursor.remaining() < msg_data_size as u64 {
756                return Err(Error::InvalidData(format!(
757                    "v2 message data ({} bytes) extends past chunk end",
758                    msg_data_size
759                )));
760            }
761
762            let msg_data = cursor.read_bytes(msg_data_size)?;
763            let is_shared = (msg_flags & 0x02) != 0;
764            if is_shared {
765                let shared_msg = crate::messages::shared::parse(
766                    &mut Cursor::new(msg_data),
767                    offset_size,
768                    length_size,
769                    msg_data_size,
770                )?;
771                messages.push(HdfMessage::Shared(shared_msg));
772            } else if msg_type == MSG_TYPE_CONTINUATION {
773                let cont = crate::messages::continuation::parse(
774                    &mut Cursor::new(msg_data),
775                    offset_size,
776                    length_size,
777                    msg_data_size,
778                )?;
779                continuations.push((cont.offset, cont.length));
780                messages.push(HdfMessage::ObjectHeaderContinuation);
781            } else {
782                let parsed = parse_message(
783                    msg_type,
784                    msg_data.len(),
785                    &mut Cursor::new(msg_data),
786                    offset_size,
787                    length_size,
788                )?;
789                messages.push(parsed);
790            }
791        }
792
793        Ok(())
794    }
795
796    #[allow(clippy::too_many_arguments)]
797    fn read_v2_continuation_chunk_storage(
798        storage: &dyn Storage,
799        cont_offset: u64,
800        cont_length: u64,
801        offset_size: u8,
802        length_size: u8,
803        creation_order_tracked: bool,
804        messages: &mut Vec<HdfMessage>,
805        continuations: &mut Vec<(u64, u64)>,
806    ) -> Result<()> {
807        let chunk = storage.read_range(cont_offset, cont_length as usize)?;
808        if chunk.len() < 8 || chunk.as_ref()[..4] != OCHK_SIGNATURE {
809            return Err(Error::InvalidObjectHeaderSignature);
810        }
811        let messages_end = chunk.len() - 4;
812        let stored_checksum = u32::from_le_bytes(
813            chunk.as_ref()[messages_end..messages_end + 4]
814                .try_into()
815                .unwrap(),
816        );
817        let computed = jenkins_lookup3(&chunk.as_ref()[..messages_end]);
818        if computed != stored_checksum {
819            return Err(Error::ChecksumMismatch {
820                expected: stored_checksum,
821                actual: computed,
822            });
823        }
824
825        Self::read_v2_messages_from_slice(
826            &chunk.as_ref()[4..messages_end],
827            offset_size,
828            length_size,
829            creation_order_tracked,
830            messages,
831            continuations,
832        )
833    }
834
835    /// Read and verify a v2 continuation chunk (`OCHK`).
836    #[allow(clippy::too_many_arguments)]
837    ///
838    /// Layout:
839    /// ```text
840    ///   "OCHK"    4 bytes
841    ///   messages  (cont_length - 4 - 4) bytes
842    ///   checksum  u32
843    /// ```
844    fn read_v2_continuation_chunk(
845        base: &Cursor<'_>,
846        cont_offset: u64,
847        cont_length: u64,
848        offset_size: u8,
849        length_size: u8,
850        creation_order_tracked: bool,
851        messages: &mut Vec<HdfMessage>,
852        continuations: &mut Vec<(u64, u64)>,
853    ) -> Result<()> {
854        let mut cursor = base.at_offset(cont_offset)?;
855
856        let sig = cursor.read_bytes(4)?;
857        if sig != OCHK_SIGNATURE {
858            return Err(Error::InvalidObjectHeaderSignature);
859        }
860
861        let chunk_end = cont_offset + cont_length;
862        // The last 4 bytes of the chunk are the checksum.
863        let messages_end = chunk_end - 4;
864        let messages_start = cursor.position(); // right after "OCHK"
865
866        // Verify checksum: covers "OCHK" through the byte before the checksum.
867        let checksum_start = cont_offset as usize;
868        let checksum_end = messages_end as usize;
869        let stored_checksum = {
870            let mut ck = base.at_offset(messages_end)?;
871            ck.read_u32_le()?
872        };
873        let computed = jenkins_lookup3(&base.data()[checksum_start..checksum_end]);
874        if computed != stored_checksum {
875            return Err(Error::ChecksumMismatch {
876                expected: stored_checksum,
877                actual: computed,
878            });
879        }
880
881        Self::read_v2_messages(
882            base,
883            messages_start,
884            messages_end,
885            offset_size,
886            length_size,
887            creation_order_tracked,
888            messages,
889            continuations,
890        )
891    }
892}
893
894#[cfg(test)]
895mod tests {
896    use super::*;
897    use crate::checksum::jenkins_lookup3;
898
899    // ------------------------------------------------------------------
900    // Helpers
901    // ------------------------------------------------------------------
902
903    /// Build a v1 object header containing the given pre-encoded messages.
904    /// Each entry in `raw_messages` is `(type_id, flags, payload)`.
905    fn build_v1_header(raw_messages: &[(u16, u8, &[u8])], ref_count: u32) -> Vec<u8> {
906        // Compute total message data size.
907        let data_size: usize = raw_messages
908            .iter()
909            .map(|(_, _, payload)| 8 + payload.len()) // 8-byte envelope per message
910            .sum();
911
912        let mut buf = Vec::new();
913        // Version
914        buf.push(1);
915        // Reserved
916        buf.push(0);
917        // Number of messages
918        buf.extend_from_slice(&(raw_messages.len() as u16).to_le_bytes());
919        // Reference count
920        buf.extend_from_slice(&ref_count.to_le_bytes());
921        // Header data size
922        buf.extend_from_slice(&(data_size as u32).to_le_bytes());
923        // Reserved padding (4 bytes)
924        buf.extend_from_slice(&[0u8; 4]);
925
926        // Messages
927        for (type_id, flags, payload) in raw_messages {
928            buf.extend_from_slice(&type_id.to_le_bytes());
929            buf.extend_from_slice(&(payload.len() as u16).to_le_bytes());
930            buf.push(*flags);
931            buf.extend_from_slice(&[0u8; 3]); // reserved
932            buf.extend_from_slice(payload);
933        }
934
935        buf
936    }
937
938    /// Build a v2 OHDR chunk#0 with the given raw messages.
939    /// `flags` controls the header flags byte.  Timestamps and phase-change
940    /// values are added automatically when the corresponding flag bits are set.
941    /// Each entry in `raw_messages` is `(type_id, flags, payload)`.
942    /// Returns the complete OHDR block including the trailing checksum.
943    fn build_v2_header(
944        header_flags: u8,
945        raw_messages: &[(u8, u8, &[u8])],
946        timestamps: Option<[u32; 4]>,
947        phase_change: Option<(u16, u16)>,
948    ) -> Vec<u8> {
949        let creation_order = (header_flags & 0x04) != 0;
950
951        // Compute message data size.
952        let envelope_size: usize = if creation_order { 6 } else { 4 };
953        let msg_data_size: usize = raw_messages
954            .iter()
955            .map(|(_, _, payload)| envelope_size + payload.len())
956            .sum();
957
958        let mut buf = Vec::new();
959        // Signature
960        buf.extend_from_slice(&OHDR_SIGNATURE);
961        // Version
962        buf.push(2);
963        // Flags
964        buf.push(header_flags);
965
966        // Timestamps (bit 5)
967        if let Some(ts) = timestamps {
968            for &t in &ts {
969                buf.extend_from_slice(&t.to_le_bytes());
970            }
971        }
972
973        // Phase change (bit 4)
974        if let Some((max_compact, min_dense)) = phase_change {
975            buf.extend_from_slice(&max_compact.to_le_bytes());
976            buf.extend_from_slice(&min_dense.to_le_bytes());
977        }
978
979        // Chunk#0 size field — encode using the width dictated by bits 0-1.
980        let size_width = 1usize << (header_flags & 0x03);
981        match size_width {
982            1 => buf.push(msg_data_size as u8),
983            2 => buf.extend_from_slice(&(msg_data_size as u16).to_le_bytes()),
984            4 => buf.extend_from_slice(&(msg_data_size as u32).to_le_bytes()),
985            8 => buf.extend_from_slice(&(msg_data_size as u64).to_le_bytes()),
986            _ => unreachable!(),
987        }
988
989        // Messages
990        for (type_id, mflags, payload) in raw_messages {
991            buf.push(*type_id);
992            buf.extend_from_slice(&(payload.len() as u16).to_le_bytes());
993            buf.push(*mflags);
994            if creation_order {
995                buf.extend_from_slice(&0u16.to_le_bytes());
996            }
997            buf.extend_from_slice(payload);
998        }
999
1000        // Checksum — covers everything so far.
1001        let ck = jenkins_lookup3(&buf);
1002        buf.extend_from_slice(&ck.to_le_bytes());
1003
1004        buf
1005    }
1006
1007    /// Build a v2 OCHK continuation chunk containing the given raw messages.
1008    fn build_v2_ochk(raw_messages: &[(u8, u8, &[u8])], creation_order: bool) -> Vec<u8> {
1009        let mut buf = Vec::new();
1010        // Signature
1011        buf.extend_from_slice(&OCHK_SIGNATURE);
1012
1013        // Messages
1014        for (type_id, mflags, payload) in raw_messages {
1015            buf.push(*type_id);
1016            buf.extend_from_slice(&(payload.len() as u16).to_le_bytes());
1017            buf.push(*mflags);
1018            if creation_order {
1019                buf.extend_from_slice(&0u16.to_le_bytes());
1020            }
1021            buf.extend_from_slice(payload);
1022        }
1023
1024        // Checksum over everything before the checksum itself.
1025        let ck = jenkins_lookup3(&buf);
1026        buf.extend_from_slice(&ck.to_le_bytes());
1027
1028        buf
1029    }
1030
1031    // ------------------------------------------------------------------
1032    // Tests — Version 1
1033    // ------------------------------------------------------------------
1034
1035    #[test]
1036    fn v1_empty_header() {
1037        let data = build_v1_header(&[], 1);
1038        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1039        assert_eq!(hdr.version, 1);
1040        assert_eq!(hdr.reference_count, 1);
1041        assert!(hdr.messages.is_empty());
1042        assert!(hdr.modification_time.is_none());
1043    }
1044
1045    #[test]
1046    fn v1_nil_message() {
1047        // A single nil message with 4 bytes of padding payload.
1048        let data = build_v1_header(&[(0x0000, 0, &[0u8; 4])], 1);
1049        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1050        assert_eq!(hdr.messages.len(), 1);
1051        assert!(matches!(hdr.messages[0], HdfMessage::Nil));
1052    }
1053
1054    #[test]
1055    fn v1_unknown_message() {
1056        // An unknown message type should be stored as HdfMessage::Unknown.
1057        let payload = [0xAA, 0xBB, 0xCC];
1058        let data = build_v1_header(&[(0x00FF, 0, &payload)], 2);
1059        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1060        assert_eq!(hdr.reference_count, 2);
1061        assert_eq!(hdr.messages.len(), 1);
1062        match &hdr.messages[0] {
1063            HdfMessage::Unknown { type_id, data } => {
1064                assert_eq!(*type_id, 0x00FF);
1065                assert_eq!(data.as_slice(), &payload);
1066            }
1067            other => panic!("expected Unknown, got {:?}", other),
1068        }
1069    }
1070
1071    #[test]
1072    fn v1_symbol_table_message() {
1073        // Type 0x0011 — symbol table message.
1074        // Payload: btree address (8 bytes) + heap address (8 bytes).
1075        let mut payload = Vec::new();
1076        payload.extend_from_slice(&0x1000u64.to_le_bytes());
1077        payload.extend_from_slice(&0x2000u64.to_le_bytes());
1078
1079        let data = build_v1_header(&[(0x0011, 0, &payload)], 1);
1080        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1081        assert_eq!(hdr.messages.len(), 1);
1082        match &hdr.messages[0] {
1083            HdfMessage::SymbolTable(st) => {
1084                assert_eq!(st.btree_address, 0x1000);
1085                assert_eq!(st.heap_address, 0x2000);
1086            }
1087            other => panic!("expected SymbolTable, got {:?}", other),
1088        }
1089    }
1090
1091    #[test]
1092    fn v1_continuation_message() {
1093        // Build a continuation payload that points to a second chunk.
1094        // The second chunk contains one unknown message.
1095        let unknown_payload = [0xDD; 2];
1096
1097        // Build the continuation target (a raw v1 message run, no header prefix).
1098        let mut cont_chunk = Vec::new();
1099        // message type 0x00FE
1100        cont_chunk.extend_from_slice(&0x00FEu16.to_le_bytes());
1101        // message data size
1102        cont_chunk.extend_from_slice(&(unknown_payload.len() as u16).to_le_bytes());
1103        // flags
1104        cont_chunk.push(0);
1105        // reserved
1106        cont_chunk.extend_from_slice(&[0u8; 3]);
1107        // payload
1108        cont_chunk.extend_from_slice(&unknown_payload);
1109
1110        // We will place the continuation chunk after the main header.
1111        // First build the main header with a continuation message.
1112        let main_header_base_size = 16; // v1 prefix
1113                                        // The continuation message envelope = 8, payload = offset_size + length_size.
1114                                        // With offset_size=8, length_size=8, the continuation payload is 16 bytes.
1115        let cont_msg_envelope_size = 8 + 16; // 24
1116        let cont_chunk_offset = (main_header_base_size + cont_msg_envelope_size) as u64;
1117
1118        let mut cont_payload = Vec::new();
1119        cont_payload.extend_from_slice(&cont_chunk_offset.to_le_bytes()); // offset
1120        cont_payload.extend_from_slice(&(cont_chunk.len() as u64).to_le_bytes()); // length
1121
1122        let main_header = build_v1_header(&[(MSG_TYPE_CONTINUATION, 0, &cont_payload)], 1);
1123
1124        // Concatenate main header + continuation chunk.
1125        let mut file_data = main_header;
1126        assert_eq!(file_data.len() as u64, cont_chunk_offset);
1127        file_data.extend_from_slice(&cont_chunk);
1128
1129        let hdr = ObjectHeader::parse_at(&file_data, 0, 8, 8).unwrap();
1130        // Should have the continuation marker + the unknown message from the continuation chunk.
1131        assert_eq!(hdr.messages.len(), 2);
1132        assert!(matches!(
1133            hdr.messages[0],
1134            HdfMessage::ObjectHeaderContinuation
1135        ));
1136        match &hdr.messages[1] {
1137            HdfMessage::Unknown { type_id, data } => {
1138                assert_eq!(*type_id, 0x00FE);
1139                assert_eq!(data.as_slice(), &unknown_payload);
1140            }
1141            other => panic!("expected Unknown from continuation, got {:?}", other),
1142        }
1143    }
1144
1145    #[test]
1146    fn v1_nonzero_address_offset() {
1147        // Place the header at a non-zero offset in the file.
1148        let prefix_pad = vec![0xFFu8; 64];
1149        let header = build_v1_header(&[(0x00AA, 0, &[0x01])], 3);
1150
1151        let mut file_data = prefix_pad;
1152        file_data.extend_from_slice(&header);
1153
1154        let hdr = ObjectHeader::parse_at(&file_data, 64, 8, 8).unwrap();
1155        assert_eq!(hdr.version, 1);
1156        assert_eq!(hdr.reference_count, 3);
1157        assert_eq!(hdr.messages.len(), 1);
1158    }
1159
1160    #[test]
1161    fn v1_bad_version() {
1162        let mut data = build_v1_header(&[], 1);
1163        data[0] = 3; // corrupt version to 3
1164        let err = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap_err();
1165        assert!(matches!(err, Error::UnsupportedObjectHeaderVersion(3)));
1166    }
1167
1168    // ------------------------------------------------------------------
1169    // Tests — Version 2
1170    // ------------------------------------------------------------------
1171
1172    #[test]
1173    fn v2_empty_header() {
1174        // Flags=0 → 1-byte size field, no timestamps, no phase change, no creation order.
1175        let data = build_v2_header(0x00, &[], None, None);
1176        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1177        assert_eq!(hdr.version, 2);
1178        assert!(hdr.messages.is_empty());
1179        assert!(hdr.modification_time.is_none());
1180    }
1181
1182    #[test]
1183    fn v2_nil_message() {
1184        let data = build_v2_header(0x00, &[(0x00, 0, &[0u8; 3])], None, None);
1185        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1186        assert_eq!(hdr.messages.len(), 1);
1187        assert!(matches!(hdr.messages[0], HdfMessage::Nil));
1188    }
1189
1190    #[test]
1191    fn v2_unknown_message() {
1192        let payload = [0x11, 0x22];
1193        let data = build_v2_header(0x00, &[(0xFE, 0, &payload)], None, None);
1194        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1195        assert_eq!(hdr.messages.len(), 1);
1196        match &hdr.messages[0] {
1197            HdfMessage::Unknown { type_id, data } => {
1198                assert_eq!(*type_id, 0x00FE);
1199                assert_eq!(data.as_slice(), &payload);
1200            }
1201            other => panic!("expected Unknown, got {:?}", other),
1202        }
1203    }
1204
1205    #[test]
1206    fn v2_with_timestamps() {
1207        // Flags: bit 5 (timestamps) + bits 0-1 = 0 (1-byte size field).
1208        let flags = 0x20;
1209        let ts = [1000u32, 2000, 3000, 4000]; // access, modification, change, birth
1210        let data = build_v2_header(flags, &[], Some(ts), None);
1211        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1212        assert_eq!(hdr.modification_time, Some(2000));
1213    }
1214
1215    #[test]
1216    fn v2_with_phase_change() {
1217        // Flags: bit 4 (phase change) + bits 0-1 = 0.
1218        let flags = 0x10;
1219        let data = build_v2_header(flags, &[], None, Some((8, 6)));
1220        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1221        assert!(hdr.messages.is_empty());
1222    }
1223
1224    #[test]
1225    fn v2_with_creation_order() {
1226        // Flags: bit 2 (creation order tracked) + bits 0-1 = 0.
1227        let flags = 0x04;
1228        let payload = [0xAA];
1229        let data = build_v2_header(flags, &[(0xFE, 0, &payload)], None, None);
1230        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1231        assert_eq!(hdr.messages.len(), 1);
1232        match &hdr.messages[0] {
1233            HdfMessage::Unknown { type_id, .. } => assert_eq!(*type_id, 0x00FE),
1234            other => panic!("expected Unknown, got {:?}", other),
1235        }
1236    }
1237
1238    #[test]
1239    fn v2_2byte_size_field() {
1240        // bits 0-1 = 1 → 2-byte size field.
1241        let flags = 0x01;
1242        let payload = [0x42; 5];
1243        let data = build_v2_header(flags, &[(0xFE, 0, &payload)], None, None);
1244        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1245        assert_eq!(hdr.messages.len(), 1);
1246    }
1247
1248    #[test]
1249    fn v2_4byte_size_field() {
1250        // bits 0-1 = 2 → 4-byte size field.
1251        let flags = 0x02;
1252        let data = build_v2_header(flags, &[(0xFE, 0, &[0x01])], None, None);
1253        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1254        assert_eq!(hdr.messages.len(), 1);
1255    }
1256
1257    #[test]
1258    fn v2_8byte_size_field() {
1259        // bits 0-1 = 3 → 8-byte size field.
1260        let flags = 0x03;
1261        let data = build_v2_header(flags, &[(0xFE, 0, &[0x01])], None, None);
1262        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1263        assert_eq!(hdr.messages.len(), 1);
1264    }
1265
1266    #[test]
1267    fn v2_checksum_mismatch() {
1268        let mut data = build_v2_header(0x00, &[(0xFE, 0, &[0x01])], None, None);
1269        // Corrupt the last byte (part of checksum).
1270        let last = data.len() - 1;
1271        data[last] ^= 0xFF;
1272        let err = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap_err();
1273        assert!(matches!(err, Error::ChecksumMismatch { .. }));
1274    }
1275
1276    #[test]
1277    fn v2_continuation_chunk() {
1278        // Build a continuation chunk (OCHK) that holds one unknown message.
1279        let unknown_payload = [0xCC; 3];
1280        let ochk = build_v2_ochk(&[(0xFD, 0, &unknown_payload)], false);
1281
1282        // The continuation message payload is offset(8) + length(8) = 16 bytes.
1283        // We will compute the offset of the OCHK after building the main OHDR.
1284        // Strategy: build OHDR first with a placeholder, measure its size,
1285        // set the actual offset, then rebuild.
1286
1287        // Placeholder continuation payload (will rewrite).
1288        let mut cont_payload = vec![0u8; 16];
1289
1290        // Build OHDR with the continuation message.  The OHDR occupies:
1291        //   4 (sig) + 1 (ver) + 1 (flags) + 1 (size field, flags=0) + messages + 4 (checksum)
1292        // Message envelope: type(1) + size(2) + flags(1) = 4; payload = 16.
1293        // Total OHDR = 4 + 1 + 1 + 1 + 4 + 16 + 4 = 31 bytes.
1294        // The OCHK starts at byte 31.
1295
1296        // We need the offset to be the byte where OCHK starts.
1297        // OHDR: sig(4) + ver(1) + flags(1) + size(1) + [envelope(4)+payload(16)] + checksum(4) = 31
1298        let ohdr_size = 4 + 1 + 1 + 1 + (4 + cont_payload.len()) + 4;
1299        let ochk_offset = ohdr_size as u64;
1300
1301        // Rebuild continuation payload with correct offset.
1302        cont_payload.clear();
1303        cont_payload.extend_from_slice(&ochk_offset.to_le_bytes());
1304        cont_payload.extend_from_slice(&(ochk.len() as u64).to_le_bytes());
1305
1306        let ohdr = build_v2_header(0x00, &[(0x10, 0, &cont_payload)], None, None);
1307        assert_eq!(ohdr.len(), ohdr_size);
1308
1309        let mut file_data = ohdr;
1310        file_data.extend_from_slice(&ochk);
1311
1312        let hdr = ObjectHeader::parse_at(&file_data, 0, 8, 8).unwrap();
1313        // Should have: continuation marker + unknown message from OCHK.
1314        assert_eq!(hdr.messages.len(), 2);
1315        assert!(matches!(
1316            hdr.messages[0],
1317            HdfMessage::ObjectHeaderContinuation
1318        ));
1319        match &hdr.messages[1] {
1320            HdfMessage::Unknown { type_id, data } => {
1321                assert_eq!(*type_id, 0x00FD);
1322                assert_eq!(data.as_slice(), &unknown_payload);
1323            }
1324            other => panic!("expected Unknown from OCHK, got {:?}", other),
1325        }
1326    }
1327
1328    #[test]
1329    fn v2_ochk_checksum_mismatch() {
1330        let unknown_payload = [0xCC; 3];
1331        let mut ochk = build_v2_ochk(&[(0xFD, 0, &unknown_payload)], false);
1332        // Corrupt OCHK checksum.
1333        let last = ochk.len() - 1;
1334        ochk[last] ^= 0xFF;
1335
1336        let ohdr_size = 4 + 1 + 1 + 1 + (4 + 16) + 4; // 31
1337        let ochk_offset = ohdr_size as u64;
1338
1339        let mut cont_payload = Vec::new();
1340        cont_payload.extend_from_slice(&ochk_offset.to_le_bytes());
1341        cont_payload.extend_from_slice(&(ochk.len() as u64).to_le_bytes());
1342
1343        let ohdr = build_v2_header(0x00, &[(0x10, 0, &cont_payload)], None, None);
1344        let mut file_data = ohdr;
1345        file_data.extend_from_slice(&ochk);
1346
1347        let err = ObjectHeader::parse_at(&file_data, 0, 8, 8).unwrap_err();
1348        assert!(matches!(err, Error::ChecksumMismatch { .. }));
1349    }
1350
1351    #[test]
1352    fn v2_multiple_messages() {
1353        // Two unknown messages in the same chunk.
1354        let p1 = [0x01, 0x02];
1355        let p2 = [0x03, 0x04, 0x05];
1356        let data = build_v2_header(0x00, &[(0xA0, 0, &p1), (0xA1, 0, &p2)], None, None);
1357        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1358        assert_eq!(hdr.messages.len(), 2);
1359        match &hdr.messages[0] {
1360            HdfMessage::Unknown { type_id, .. } => assert_eq!(*type_id, 0x00A0),
1361            other => panic!("expected Unknown 0xA0, got {:?}", other),
1362        }
1363        match &hdr.messages[1] {
1364            HdfMessage::Unknown { type_id, .. } => assert_eq!(*type_id, 0x00A1),
1365            other => panic!("expected Unknown 0xA1, got {:?}", other),
1366        }
1367    }
1368
1369    #[test]
1370    fn v2_zero_length_nil_before_more_messages() {
1371        let p1 = [0xAA];
1372        let p2 = [0xBB];
1373        let data = build_v2_header(
1374            0x04,
1375            &[(0xFE, 0, &p1), (0x00, 0, &[]), (0xFD, 0, &p2)],
1376            None,
1377            None,
1378        );
1379        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1380        assert_eq!(hdr.messages.len(), 3);
1381        assert!(matches!(hdr.messages[0], HdfMessage::Unknown { .. }));
1382        assert!(matches!(hdr.messages[1], HdfMessage::Nil));
1383        assert!(matches!(hdr.messages[2], HdfMessage::Unknown { .. }));
1384    }
1385
1386    #[test]
1387    fn v2_nonzero_address() {
1388        // Place the OHDR at offset 128 in a larger buffer.
1389        let prefix_pad = vec![0u8; 128];
1390        let ohdr = build_v2_header(0x00, &[(0xFE, 0, &[0x42])], None, None);
1391
1392        let mut file_data = prefix_pad;
1393        file_data.extend_from_slice(&ohdr);
1394
1395        let hdr = ObjectHeader::parse_at(&file_data, 128, 8, 8).unwrap();
1396        assert_eq!(hdr.version, 2);
1397        assert_eq!(hdr.messages.len(), 1);
1398    }
1399
1400    #[test]
1401    fn v2_all_flags_combined() {
1402        // Combine timestamps (0x20) + phase change (0x10) + creation order (0x04) + 2-byte size (0x01).
1403        let flags = 0x20 | 0x10 | 0x04 | 0x01;
1404        let ts = [100u32, 200, 300, 400];
1405        let payload = [0xBB];
1406        let data = build_v2_header(flags, &[(0xFE, 0, &payload)], Some(ts), Some((12, 8)));
1407        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1408        assert_eq!(hdr.version, 2);
1409        assert_eq!(hdr.modification_time, Some(200));
1410        assert_eq!(hdr.messages.len(), 1);
1411    }
1412
1413    #[test]
1414    fn v1_multiple_messages() {
1415        // Two messages in a single v1 header.
1416        let p1 = [0xAA; 4];
1417        let p2 = [0xBB; 8];
1418        let data = build_v1_header(&[(0x00FF, 0, &p1), (0x00FE, 0, &p2)], 5);
1419        let hdr = ObjectHeader::parse_at(&data, 0, 8, 8).unwrap();
1420        assert_eq!(hdr.version, 1);
1421        assert_eq!(hdr.reference_count, 5);
1422        assert_eq!(hdr.messages.len(), 2);
1423    }
1424
1425    #[test]
1426    fn v1_4byte_offsets() {
1427        // Verify correct operation with 4-byte offset/length sizes.
1428        // Symbol table message with 4-byte addresses.
1429        let mut payload = Vec::new();
1430        payload.extend_from_slice(&0x1000u32.to_le_bytes());
1431        payload.extend_from_slice(&0x2000u32.to_le_bytes());
1432
1433        let data = build_v1_header(&[(0x0011, 0, &payload)], 1);
1434        let hdr = ObjectHeader::parse_at(&data, 0, 4, 4).unwrap();
1435        assert_eq!(hdr.messages.len(), 1);
1436        match &hdr.messages[0] {
1437            HdfMessage::SymbolTable(st) => {
1438                assert_eq!(st.btree_address, 0x1000);
1439                assert_eq!(st.heap_address, 0x2000);
1440            }
1441            other => panic!("expected SymbolTable, got {:?}", other),
1442        }
1443    }
1444}