Skip to main content

rust_hdf5/format/
object_header.rs

1/// Object Header v2 encode/decode.
2///
3/// The Object Header is the primary metadata container in HDF5. Every named
4/// object (group, dataset, committed datatype) has one. Version 2 headers use
5/// the "OHDR" signature and end with a Jenkins checksum.
6///
7/// Layout of the header prefix (before messages):
8/// ```text
9/// "OHDR" (4 bytes)
10/// Version: 2 (1 byte)
11/// Flags (1 byte):
12///   bits 0-1: chunk#0 data-size encoding (0=1B, 1=2B, 2=4B, 3=8B)
13///   bit 2:    attribute creation order tracked
14///   bit 3:    attribute creation order indexed
15///   bit 4:    non-default attribute storage phase-change thresholds
16///   bit 5:    store access/modify/change/birth timestamps
17/// [if bit 5 set: 4x uint32 timestamps (16 bytes)]
18/// [if bit 4 set: max_compact(u16) + min_dense(u16) (4 bytes)]
19/// chunk0_data_size: 1/2/4/8 bytes depending on bits 0-1
20/// <messages>
21/// Checksum (4 bytes)
22/// ```
23///
24/// Each message (v2 format):
25/// ```text
26/// msg_type:       u8
27/// msg_data_size:  u16 LE
28/// msg_flags:      u8
29/// [if obj header flags bit 2: creation_order: u16 LE]
30/// msg_data:       [u8; msg_data_size]
31/// ```
32use crate::format::checksum::checksum_metadata;
33use crate::format::{FormatError, FormatResult};
34
35/// The 4-byte object header v2 signature.
36pub const OHDR_SIGNATURE: [u8; 4] = *b"OHDR";
37
38/// Object header version 2.
39pub const OHDR_VERSION: u8 = 2;
40
41// Flag bit masks
42const FLAG_SIZE_MASK: u8 = 0x03;
43const FLAG_ATTR_CREATION_ORDER_TRACKED: u8 = 0x04;
44// const FLAG_ATTR_CREATION_ORDER_INDEXED: u8 = 0x08; // bit 3
45const FLAG_NON_DEFAULT_ATTR_THRESHOLDS: u8 = 0x10;
46const FLAG_STORE_TIMESTAMPS: u8 = 0x20;
47
48/// A single message within an object header.
49#[derive(Debug, Clone, PartialEq, Eq)]
50pub struct ObjectHeaderMessage {
51    /// Message type ID (e.g., 0x01 = Dataspace, 0x03 = Datatype, etc.)
52    pub msg_type: u8,
53    /// Per-message flags (bit 0 = constant, bit 1 = shared, etc.)
54    pub flags: u8,
55    /// Raw message payload.
56    pub data: Vec<u8>,
57}
58
59/// Object Header v2.
60#[derive(Debug, Clone, PartialEq, Eq)]
61pub struct ObjectHeader {
62    /// Header flags byte. Bits 0-1 control chunk0 size encoding. Other bits
63    /// control optional fields (timestamps, attr thresholds, creation order).
64    pub flags: u8,
65    /// The ordered list of header messages.
66    pub messages: Vec<ObjectHeaderMessage>,
67}
68
69impl ObjectHeader {
70    /// Create a new, empty object header with default flags.
71    ///
72    /// Defaults: bits 0-1 = 2 (4-byte chunk size encoding), no timestamps,
73    /// no attribute creation order, no non-default thresholds.
74    pub fn new() -> Self {
75        Self {
76            flags: 0x02, // bits 0-1 = 2 => 4-byte chunk0 size
77            messages: Vec::new(),
78        }
79    }
80
81    /// Append a message to the object header.
82    pub fn add_message(&mut self, msg_type: u8, flags: u8, data: Vec<u8>) {
83        self.messages.push(ObjectHeaderMessage {
84            msg_type,
85            flags,
86            data,
87        });
88    }
89
90    /// Returns the number of bytes used to encode chunk0's data size, based on
91    /// flags bits 0-1.
92    fn chunk0_size_bytes(&self) -> usize {
93        match self.flags & FLAG_SIZE_MASK {
94            0 => 1,
95            1 => 2,
96            2 => 4,
97            3 => 8,
98            _ => unreachable!(),
99        }
100    }
101
102    /// Whether attribute creation order tracking is enabled (flags bit 2).
103    fn has_creation_order(&self) -> bool {
104        self.flags & FLAG_ATTR_CREATION_ORDER_TRACKED != 0
105    }
106
107    /// Compute the byte size of the messages region (chunk0 data).
108    fn messages_data_size(&self) -> usize {
109        let per_msg_overhead = if self.has_creation_order() {
110            1 + 2 + 1 + 2 // type + size + flags + creation_order
111        } else {
112            1 + 2 + 1 // type + size + flags
113        };
114        self.messages
115            .iter()
116            .map(|m| per_msg_overhead + m.data.len())
117            .sum()
118    }
119
120    /// Encode the object header to a byte vector, including "OHDR" signature
121    /// and trailing checksum.
122    pub fn encode(&self) -> Vec<u8> {
123        let messages_size = self.messages_data_size();
124
125        // Estimate total size for pre-allocation
126        let mut prefix_size: usize = 4 + 1 + 1; // OHDR + version + flags
127        if self.flags & FLAG_STORE_TIMESTAMPS != 0 {
128            prefix_size += 16; // 4 x u32
129        }
130        if self.flags & FLAG_NON_DEFAULT_ATTR_THRESHOLDS != 0 {
131            prefix_size += 4; // max_compact(u16) + min_dense(u16)
132        }
133        prefix_size += self.chunk0_size_bytes(); // chunk0 data size field
134        let total = prefix_size + messages_size + 4; // + checksum
135
136        let mut buf = Vec::with_capacity(total);
137
138        // Signature
139        buf.extend_from_slice(&OHDR_SIGNATURE);
140        // Version
141        buf.push(OHDR_VERSION);
142        // Flags
143        buf.push(self.flags);
144
145        // Optional timestamps (bit 5) -- for MVP we write zeros if enabled
146        if self.flags & FLAG_STORE_TIMESTAMPS != 0 {
147            buf.extend_from_slice(&[0u8; 16]);
148        }
149
150        // Optional attr storage thresholds (bit 4) -- write defaults if enabled
151        if self.flags & FLAG_NON_DEFAULT_ATTR_THRESHOLDS != 0 {
152            // max_compact = 8, min_dense = 6 (HDF5 defaults)
153            buf.extend_from_slice(&8u16.to_le_bytes());
154            buf.extend_from_slice(&6u16.to_le_bytes());
155        }
156
157        // Chunk0 data size
158        let chunk0_data_size = messages_size as u64;
159        let csb = self.chunk0_size_bytes();
160        buf.extend_from_slice(&chunk0_data_size.to_le_bytes()[..csb]);
161
162        // Messages
163        for msg in &self.messages {
164            buf.push(msg.msg_type);
165            buf.extend_from_slice(&(msg.data.len() as u16).to_le_bytes());
166            buf.push(msg.flags);
167            if self.has_creation_order() {
168                // We don't track actual creation order values in the MVP --
169                // write 0.
170                buf.extend_from_slice(&0u16.to_le_bytes());
171            }
172            buf.extend_from_slice(&msg.data);
173        }
174
175        // Checksum over everything before the checksum
176        let cksum = checksum_metadata(&buf);
177        buf.extend_from_slice(&cksum.to_le_bytes());
178
179        debug_assert_eq!(buf.len(), total);
180        buf
181    }
182
183    /// Decode an object header from a byte buffer. Returns the parsed header
184    /// and the number of bytes consumed from the buffer.
185    pub fn decode(buf: &[u8]) -> FormatResult<(Self, usize)> {
186        // Minimum: OHDR(4) + version(1) + flags(1) + chunk0_size(1) + checksum(4) = 11
187        if buf.len() < 11 {
188            return Err(FormatError::BufferTooShort {
189                needed: 11,
190                available: buf.len(),
191            });
192        }
193
194        // Signature
195        if buf[0..4] != OHDR_SIGNATURE {
196            return Err(FormatError::InvalidSignature);
197        }
198
199        // Version
200        let version = buf[4];
201        if version != OHDR_VERSION {
202            return Err(FormatError::InvalidVersion(version));
203        }
204
205        let flags = buf[5];
206        let mut pos: usize = 6;
207
208        // Optional timestamps (bit 5)
209        if flags & FLAG_STORE_TIMESTAMPS != 0 {
210            if buf.len() < pos + 16 {
211                return Err(FormatError::BufferTooShort {
212                    needed: pos + 16,
213                    available: buf.len(),
214                });
215            }
216            // Skip timestamps for now (MVP doesn't use them)
217            pos += 16;
218        }
219
220        // Optional attr storage thresholds (bit 4)
221        if flags & FLAG_NON_DEFAULT_ATTR_THRESHOLDS != 0 {
222            if buf.len() < pos + 4 {
223                return Err(FormatError::BufferTooShort {
224                    needed: pos + 4,
225                    available: buf.len(),
226                });
227            }
228            // Skip thresholds for now
229            pos += 4;
230        }
231
232        // Chunk0 data size
233        let chunk0_size_bytes = match flags & FLAG_SIZE_MASK {
234            0 => 1,
235            1 => 2,
236            2 => 4,
237            3 => 8,
238            _ => unreachable!(),
239        };
240
241        if buf.len() < pos + chunk0_size_bytes {
242            return Err(FormatError::BufferTooShort {
243                needed: pos + chunk0_size_bytes,
244                available: buf.len(),
245            });
246        }
247
248        let chunk0_data_size =
249            crate::format::bytes::read_le_uint(&buf[pos..], chunk0_size_bytes) as usize;
250        pos += chunk0_size_bytes;
251
252        // We need chunk0_data_size bytes of messages + 4 bytes of checksum.
253        // chunk0_data_size is a file field up to 8 bytes wide; guard the
254        // addition so a crafted absurd value yields a clean error instead of
255        // an overflow panic (debug) or wrap (release).
256        let total_consumed = pos
257            .checked_add(chunk0_data_size)
258            .and_then(|x| x.checked_add(4))
259            .ok_or_else(|| {
260                FormatError::InvalidData("object header chunk-0 size overflows usize".into())
261            })?;
262        if buf.len() < total_consumed {
263            return Err(FormatError::BufferTooShort {
264                needed: total_consumed,
265                available: buf.len(),
266            });
267        }
268
269        // Verify checksum: covers everything from start up to (but not
270        // including) the 4-byte checksum.
271        let data_end = total_consumed - 4;
272        let stored_cksum = u32::from_le_bytes([
273            buf[data_end],
274            buf[data_end + 1],
275            buf[data_end + 2],
276            buf[data_end + 3],
277        ]);
278        let computed_cksum = checksum_metadata(&buf[..data_end]);
279        if stored_cksum != computed_cksum {
280            return Err(FormatError::ChecksumMismatch {
281                expected: stored_cksum,
282                computed: computed_cksum,
283            });
284        }
285
286        // Parse messages
287        let has_creation_order = flags & FLAG_ATTR_CREATION_ORDER_TRACKED != 0;
288        let messages_end = pos + chunk0_data_size;
289        let mut messages = Vec::new();
290
291        while pos < messages_end {
292            // Each message: type(1) + size(2) + flags(1) [+ creation_order(2)]
293            let msg_header_size = if has_creation_order { 6 } else { 4 };
294            if pos + msg_header_size > messages_end {
295                // libhdf5 (H5O__chunk_deserialize) permits a gap smaller than
296                // one message header at the end of a v2 chunk; treat the
297                // remaining bytes as such a gap rather than an error.
298                break;
299            }
300
301            let msg_type = buf[pos];
302            let msg_data_size = u16::from_le_bytes([buf[pos + 1], buf[pos + 2]]) as usize;
303            let msg_flags = buf[pos + 3];
304            pos += 4;
305
306            if has_creation_order {
307                // Skip creation_order for now
308                pos += 2;
309            }
310
311            if pos + msg_data_size > messages_end {
312                return Err(FormatError::InvalidData(format!(
313                    "message data ({} bytes) extends past chunk0 boundary",
314                    msg_data_size
315                )));
316            }
317
318            let data = buf[pos..pos + msg_data_size].to_vec();
319            pos += msg_data_size;
320
321            messages.push(ObjectHeaderMessage {
322                msg_type,
323                flags: msg_flags,
324                data,
325            });
326        }
327
328        Ok((ObjectHeader { flags, messages }, total_consumed))
329    }
330}
331
332impl Default for ObjectHeader {
333    fn default() -> Self {
334        Self::new()
335    }
336}
337
338// =========================================================================
339// Object Header v1 — decode only (for reading legacy HDF5 files)
340// =========================================================================
341
342impl ObjectHeader {
343    /// Decode a v1 object header from a byte buffer.
344    ///
345    /// v1 headers do NOT have the "OHDR" signature or a checksum. The layout is:
346    /// ```text
347    /// Byte 0: version = 1
348    /// Byte 1: reserved
349    /// Bytes 2-3: num_messages (u16 LE)
350    /// Bytes 4-7: obj_ref_count (u32 LE)
351    /// Bytes 8-11: header_data_size (u32 LE) — size of message data in first chunk
352    /// Messages follow, each:
353    ///   type: u16 LE
354    ///   data_size: u16 LE
355    ///   flags: u8
356    ///   reserved: 3 bytes
357    ///   data: data_size bytes (padded to 8-byte alignment)
358    /// ```
359    pub fn decode_v1(buf: &[u8]) -> FormatResult<(Self, usize)> {
360        // V1 header prefix is 16 bytes: version(1) + reserved(1) + num_msg(2)
361        // + ref_count(4) + chunk0_data_size(4) + reserved_padding(4)
362        if buf.len() < 16 {
363            return Err(FormatError::BufferTooShort {
364                needed: 16,
365                available: buf.len(),
366            });
367        }
368
369        let version = buf[0];
370        if version != 1 {
371            return Err(FormatError::InvalidVersion(version));
372        }
373
374        // buf[1] = reserved
375        let num_messages = u16::from_le_bytes([buf[2], buf[3]]) as usize;
376        let _obj_ref_count = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
377        let header_data_size = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]) as usize;
378        // buf[12..16] = reserved alignment padding
379
380        let total_consumed = 16 + header_data_size;
381        if buf.len() < total_consumed {
382            return Err(FormatError::BufferTooShort {
383                needed: total_consumed,
384                available: buf.len(),
385            });
386        }
387
388        let msg_data_start = 16; // offset where message data begins (after 16-byte prefix)
389        let mut pos = msg_data_start;
390        let messages_end = msg_data_start + header_data_size;
391        let mut messages = Vec::with_capacity(num_messages);
392
393        for _ in 0..num_messages {
394            if pos + 8 > messages_end {
395                break; // no more room for a message header
396            }
397
398            let msg_type = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
399            let data_size = u16::from_le_bytes([buf[pos + 2], buf[pos + 3]]) as usize;
400            let msg_flags = buf[pos + 4];
401            // bytes pos+5..pos+8 are reserved
402            pos += 8;
403
404            if pos + data_size > messages_end {
405                return Err(FormatError::InvalidData(format!(
406                    "v1 message data ({} bytes) extends past header boundary",
407                    data_size
408                )));
409            }
410
411            let data = buf[pos..pos + data_size].to_vec();
412            pos += data_size;
413
414            // In v1, messages are padded to 8-byte alignment relative to
415            // the start of the message data region.
416            let rel = pos - msg_data_start;
417            let aligned_rel = (rel + 7) & !7;
418            let aligned_pos = msg_data_start + aligned_rel;
419            if aligned_pos <= messages_end {
420                pos = aligned_pos;
421            }
422
423            // Skip null/padding messages (type 0)
424            if msg_type == 0 {
425                continue;
426            }
427
428            messages.push(ObjectHeaderMessage {
429                msg_type: msg_type as u8,
430                flags: msg_flags,
431                data,
432            });
433        }
434
435        Ok((
436            ObjectHeader {
437                flags: 0x02, // default flags (not meaningful for v1)
438                messages,
439            },
440            total_consumed,
441        ))
442    }
443
444    /// Auto-detect and decode either v1 or v2 object header.
445    ///
446    /// Checks for the "OHDR" signature to decide v2; otherwise tries v1.
447    pub fn decode_any(buf: &[u8]) -> FormatResult<(Self, usize)> {
448        if buf.len() >= 4 && buf[0..4] == OHDR_SIGNATURE {
449            Self::decode(buf)
450        } else if !buf.is_empty() && buf[0] == 1 {
451            Self::decode_v1(buf)
452        } else {
453            // Try v2 first (will fail with proper error)
454            Self::decode(buf)
455        }
456    }
457}
458
459#[cfg(test)]
460mod tests_v1 {
461    use super::*;
462
463    /// Build a minimal v1 object header with given messages.
464    fn build_v1_header(messages: &[(u16, u8, &[u8])]) -> Vec<u8> {
465        let mut msg_data = Vec::new();
466        for (msg_type, flags, data) in messages {
467            msg_data.extend_from_slice(&msg_type.to_le_bytes());
468            msg_data.extend_from_slice(&(data.len() as u16).to_le_bytes());
469            msg_data.push(*flags);
470            msg_data.extend_from_slice(&[0u8; 3]); // reserved
471            msg_data.extend_from_slice(data);
472            // Pad to 8-byte alignment
473            let aligned = (msg_data.len() + 7) & !7;
474            msg_data.resize(aligned, 0);
475        }
476
477        let mut buf = Vec::new();
478        buf.push(1); // version
479        buf.push(0); // reserved
480        buf.extend_from_slice(&(messages.len() as u16).to_le_bytes());
481        buf.extend_from_slice(&1u32.to_le_bytes()); // ref count
482        buf.extend_from_slice(&(msg_data.len() as u32).to_le_bytes());
483        buf.extend_from_slice(&[0u8; 4]); // reserved padding (align to 16 bytes)
484        buf.extend_from_slice(&msg_data);
485        buf
486    }
487
488    #[test]
489    fn test_decode_v1_empty() {
490        let buf = build_v1_header(&[]);
491        let (hdr, consumed) = ObjectHeader::decode_v1(&buf).unwrap();
492        assert_eq!(consumed, 16); // 16-byte prefix, no messages
493        assert!(hdr.messages.is_empty());
494    }
495
496    #[test]
497    fn test_decode_v1_single_message() {
498        let data = vec![0xAA, 0xBB, 0xCC];
499        let buf = build_v1_header(&[(0x03, 0x00, &data)]);
500        let (hdr, _consumed) = ObjectHeader::decode_v1(&buf).unwrap();
501        assert_eq!(hdr.messages.len(), 1);
502        assert_eq!(hdr.messages[0].msg_type, 0x03);
503        assert_eq!(hdr.messages[0].data, data);
504    }
505
506    #[test]
507    fn test_decode_v1_multiple_messages() {
508        let buf = build_v1_header(&[
509            (0x01, 0x00, &[1, 2, 3, 4]),
510            (0x03, 0x01, &[10, 20]),
511            (0x08, 0x00, &[0xFF; 16]),
512        ]);
513        let (hdr, _) = ObjectHeader::decode_v1(&buf).unwrap();
514        assert_eq!(hdr.messages.len(), 3);
515        assert_eq!(hdr.messages[0].msg_type, 0x01);
516        assert_eq!(hdr.messages[1].msg_type, 0x03);
517        assert_eq!(hdr.messages[2].msg_type, 0x08);
518        assert_eq!(hdr.messages[2].data, vec![0xFF; 16]);
519    }
520
521    #[test]
522    fn test_decode_v1_skips_null_messages() {
523        let buf = build_v1_header(&[
524            (0x00, 0x00, &[0; 8]), // null message (type 0)
525            (0x03, 0x00, &[1, 2]),
526        ]);
527        let (hdr, _) = ObjectHeader::decode_v1(&buf).unwrap();
528        assert_eq!(hdr.messages.len(), 1);
529        assert_eq!(hdr.messages[0].msg_type, 0x03);
530    }
531
532    #[test]
533    fn test_decode_any_v2() {
534        let mut hdr = ObjectHeader::new();
535        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
536        let encoded = hdr.encode();
537        let (decoded, _) = ObjectHeader::decode_any(&encoded).unwrap();
538        assert_eq!(decoded.messages.len(), 1);
539    }
540
541    #[test]
542    fn test_decode_any_v1() {
543        let buf = build_v1_header(&[(0x03, 0x00, &[1, 2])]);
544        let (decoded, _) = ObjectHeader::decode_any(&buf).unwrap();
545        assert_eq!(decoded.messages.len(), 1);
546        assert_eq!(decoded.messages[0].msg_type, 0x03);
547    }
548
549    #[test]
550    fn test_decode_v1_bad_version() {
551        let mut buf = build_v1_header(&[]);
552        buf[0] = 5;
553        assert!(matches!(
554            ObjectHeader::decode_v1(&buf).unwrap_err(),
555            FormatError::InvalidVersion(5)
556        ));
557    }
558
559    #[test]
560    fn test_decode_v1_buffer_too_short() {
561        assert!(matches!(
562            ObjectHeader::decode_v1(&[1, 0, 0]).unwrap_err(),
563            FormatError::BufferTooShort { .. }
564        ));
565    }
566}
567
568#[cfg(test)]
569mod tests {
570    use super::*;
571
572    #[test]
573    fn test_empty_header_roundtrip() {
574        let hdr = ObjectHeader::new();
575        let encoded = hdr.encode();
576
577        // OHDR(4) + version(1) + flags(1) + chunk0_size(4) + checksum(4) = 14
578        assert_eq!(encoded.len(), 14);
579        assert_eq!(&encoded[..4], b"OHDR");
580        assert_eq!(encoded[4], 2); // version
581
582        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
583        assert_eq!(consumed, encoded.len());
584        assert_eq!(decoded, hdr);
585    }
586
587    #[test]
588    fn test_single_message_roundtrip() {
589        let mut hdr = ObjectHeader::new();
590        hdr.add_message(0x01, 0x00, vec![0xAA, 0xBB, 0xCC]);
591
592        let encoded = hdr.encode();
593        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
594        assert_eq!(consumed, encoded.len());
595        assert_eq!(decoded.messages.len(), 1);
596        assert_eq!(decoded.messages[0].msg_type, 0x01);
597        assert_eq!(decoded.messages[0].flags, 0x00);
598        assert_eq!(decoded.messages[0].data, vec![0xAA, 0xBB, 0xCC]);
599    }
600
601    #[test]
602    fn test_multiple_messages_roundtrip() {
603        let mut hdr = ObjectHeader::new();
604        hdr.add_message(0x01, 0x00, vec![1, 2, 3, 4]);
605        hdr.add_message(0x03, 0x01, vec![10, 20]);
606        hdr.add_message(0x0C, 0x00, vec![]);
607
608        let encoded = hdr.encode();
609        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
610        assert_eq!(consumed, encoded.len());
611        assert_eq!(decoded.messages.len(), 3);
612        assert_eq!(decoded, hdr);
613    }
614
615    #[test]
616    fn test_with_creation_order() {
617        let mut hdr = ObjectHeader {
618            flags: 0x02 | FLAG_ATTR_CREATION_ORDER_TRACKED,
619            messages: Vec::new(),
620        };
621        hdr.add_message(0x01, 0x00, vec![0xFF; 8]);
622        hdr.add_message(0x03, 0x00, vec![0xEE; 4]);
623
624        let encoded = hdr.encode();
625        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
626        assert_eq!(consumed, encoded.len());
627        assert_eq!(decoded.messages.len(), 2);
628        assert_eq!(decoded.messages[0].data, vec![0xFF; 8]);
629        assert_eq!(decoded.messages[1].data, vec![0xEE; 4]);
630    }
631
632    #[test]
633    fn test_chunk0_size_1byte() {
634        // flags bits 0-1 = 0 => 1-byte chunk0 size
635        let mut hdr = ObjectHeader {
636            flags: 0x00,
637            messages: Vec::new(),
638        };
639        hdr.add_message(0x01, 0x00, vec![42]);
640
641        let encoded = hdr.encode();
642        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
643        assert_eq!(consumed, encoded.len());
644        assert_eq!(decoded.messages[0].data, vec![42]);
645    }
646
647    #[test]
648    fn test_chunk0_size_2byte() {
649        // flags bits 0-1 = 1 => 2-byte chunk0 size
650        let mut hdr = ObjectHeader {
651            flags: 0x01,
652            messages: Vec::new(),
653        };
654        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
655
656        let encoded = hdr.encode();
657        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
658        assert_eq!(consumed, encoded.len());
659        assert_eq!(decoded.messages[0].data, vec![1, 2, 3]);
660    }
661
662    #[test]
663    fn test_chunk0_size_8byte() {
664        // flags bits 0-1 = 3 => 8-byte chunk0 size
665        let mut hdr = ObjectHeader {
666            flags: 0x03,
667            messages: Vec::new(),
668        };
669        hdr.add_message(0x01, 0x00, vec![0xDE, 0xAD]);
670
671        let encoded = hdr.encode();
672        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
673        assert_eq!(consumed, encoded.len());
674        assert_eq!(decoded.messages[0].data, vec![0xDE, 0xAD]);
675    }
676
677    #[test]
678    fn test_decode_bad_signature() {
679        let mut data = vec![0u8; 20];
680        data[0..4].copy_from_slice(b"XHDR");
681        let err = ObjectHeader::decode(&data).unwrap_err();
682        assert!(matches!(err, FormatError::InvalidSignature));
683    }
684
685    #[test]
686    fn test_decode_bad_version() {
687        let hdr = ObjectHeader::new();
688        let mut encoded = hdr.encode();
689        encoded[4] = 99; // corrupt version
690        let err = ObjectHeader::decode(&encoded).unwrap_err();
691        assert!(matches!(err, FormatError::InvalidVersion(99)));
692    }
693
694    #[test]
695    fn test_decode_checksum_mismatch() {
696        let mut hdr = ObjectHeader::new();
697        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
698        let mut encoded = hdr.encode();
699        // Corrupt a message byte
700        let last_data = encoded.len() - 5;
701        encoded[last_data] ^= 0xFF;
702        let err = ObjectHeader::decode(&encoded).unwrap_err();
703        assert!(matches!(err, FormatError::ChecksumMismatch { .. }));
704    }
705
706    #[test]
707    fn test_decode_buffer_too_short() {
708        let err = ObjectHeader::decode(&[0u8; 5]).unwrap_err();
709        assert!(matches!(err, FormatError::BufferTooShort { .. }));
710    }
711
712    #[test]
713    fn test_decode_with_trailing_data() {
714        let mut hdr = ObjectHeader::new();
715        hdr.add_message(0x01, 0x00, vec![7, 8, 9]);
716        let mut encoded = hdr.encode();
717        let original_len = encoded.len();
718        encoded.extend_from_slice(&[0xBB; 50]); // trailing garbage
719
720        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
721        assert_eq!(consumed, original_len);
722        assert_eq!(decoded, hdr);
723    }
724
725    #[test]
726    fn test_large_message_payload() {
727        let mut hdr = ObjectHeader::new();
728        let big_data = vec![0x42; 1000];
729        hdr.add_message(0x0C, 0x00, big_data.clone());
730
731        let encoded = hdr.encode();
732        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
733        assert_eq!(consumed, encoded.len());
734        assert_eq!(decoded.messages[0].data.len(), 1000);
735        assert_eq!(decoded.messages[0].data, big_data);
736    }
737
738    #[test]
739    fn test_default() {
740        let hdr = ObjectHeader::default();
741        assert_eq!(hdr.flags, 0x02);
742        assert!(hdr.messages.is_empty());
743    }
744}