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