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 mut size_bytes = [0u8; 8];
249        size_bytes[..chunk0_size_bytes].copy_from_slice(&buf[pos..pos + chunk0_size_bytes]);
250        let chunk0_data_size = u64::from_le_bytes(size_bytes) as usize;
251        pos += chunk0_size_bytes;
252
253        // We need chunk0_data_size bytes of messages + 4 bytes of checksum
254        let total_consumed = pos + chunk0_data_size + 4;
255        if buf.len() < total_consumed {
256            return Err(FormatError::BufferTooShort {
257                needed: total_consumed,
258                available: buf.len(),
259            });
260        }
261
262        // Verify checksum: covers everything from start up to (but not
263        // including) the 4-byte checksum.
264        let data_end = total_consumed - 4;
265        let stored_cksum = u32::from_le_bytes([
266            buf[data_end],
267            buf[data_end + 1],
268            buf[data_end + 2],
269            buf[data_end + 3],
270        ]);
271        let computed_cksum = checksum_metadata(&buf[..data_end]);
272        if stored_cksum != computed_cksum {
273            return Err(FormatError::ChecksumMismatch {
274                expected: stored_cksum,
275                computed: computed_cksum,
276            });
277        }
278
279        // Parse messages
280        let has_creation_order = flags & FLAG_ATTR_CREATION_ORDER_TRACKED != 0;
281        let messages_end = pos + chunk0_data_size;
282        let mut messages = Vec::new();
283
284        while pos < messages_end {
285            // Each message: type(1) + size(2) + flags(1) [+ creation_order(2)]
286            let msg_header_size = if has_creation_order { 6 } else { 4 };
287            if pos + msg_header_size > messages_end {
288                return Err(FormatError::InvalidData(
289                    "truncated message header in object header".into(),
290                ));
291            }
292
293            let msg_type = buf[pos];
294            let msg_data_size = u16::from_le_bytes([buf[pos + 1], buf[pos + 2]]) as usize;
295            let msg_flags = buf[pos + 3];
296            pos += 4;
297
298            if has_creation_order {
299                // Skip creation_order for now
300                pos += 2;
301            }
302
303            if pos + msg_data_size > messages_end {
304                return Err(FormatError::InvalidData(format!(
305                    "message data ({} bytes) extends past chunk0 boundary",
306                    msg_data_size
307                )));
308            }
309
310            let data = buf[pos..pos + msg_data_size].to_vec();
311            pos += msg_data_size;
312
313            messages.push(ObjectHeaderMessage {
314                msg_type,
315                flags: msg_flags,
316                data,
317            });
318        }
319
320        Ok((ObjectHeader { flags, messages }, total_consumed))
321    }
322}
323
324impl Default for ObjectHeader {
325    fn default() -> Self {
326        Self::new()
327    }
328}
329
330// =========================================================================
331// Object Header v1 — decode only (for reading legacy HDF5 files)
332// =========================================================================
333
334impl ObjectHeader {
335    /// Decode a v1 object header from a byte buffer.
336    ///
337    /// v1 headers do NOT have the "OHDR" signature or a checksum. The layout is:
338    /// ```text
339    /// Byte 0: version = 1
340    /// Byte 1: reserved
341    /// Bytes 2-3: num_messages (u16 LE)
342    /// Bytes 4-7: obj_ref_count (u32 LE)
343    /// Bytes 8-11: header_data_size (u32 LE) — size of message data in first chunk
344    /// Messages follow, each:
345    ///   type: u16 LE
346    ///   data_size: u16 LE
347    ///   flags: u8
348    ///   reserved: 3 bytes
349    ///   data: data_size bytes (padded to 8-byte alignment)
350    /// ```
351    pub fn decode_v1(buf: &[u8]) -> FormatResult<(Self, usize)> {
352        // V1 header prefix is 16 bytes: version(1) + reserved(1) + num_msg(2)
353        // + ref_count(4) + chunk0_data_size(4) + reserved_padding(4)
354        if buf.len() < 16 {
355            return Err(FormatError::BufferTooShort {
356                needed: 16,
357                available: buf.len(),
358            });
359        }
360
361        let version = buf[0];
362        if version != 1 {
363            return Err(FormatError::InvalidVersion(version));
364        }
365
366        // buf[1] = reserved
367        let num_messages = u16::from_le_bytes([buf[2], buf[3]]) as usize;
368        let _obj_ref_count = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
369        let header_data_size = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]) as usize;
370        // buf[12..16] = reserved alignment padding
371
372        let total_consumed = 16 + header_data_size;
373        if buf.len() < total_consumed {
374            return Err(FormatError::BufferTooShort {
375                needed: total_consumed,
376                available: buf.len(),
377            });
378        }
379
380        let msg_data_start = 16; // offset where message data begins (after 16-byte prefix)
381        let mut pos = msg_data_start;
382        let messages_end = msg_data_start + header_data_size;
383        let mut messages = Vec::with_capacity(num_messages);
384
385        for _ in 0..num_messages {
386            if pos + 8 > messages_end {
387                break; // no more room for a message header
388            }
389
390            let msg_type = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
391            let data_size = u16::from_le_bytes([buf[pos + 2], buf[pos + 3]]) as usize;
392            let msg_flags = buf[pos + 4];
393            // bytes pos+5..pos+8 are reserved
394            pos += 8;
395
396            if pos + data_size > messages_end {
397                return Err(FormatError::InvalidData(format!(
398                    "v1 message data ({} bytes) extends past header boundary",
399                    data_size
400                )));
401            }
402
403            let data = buf[pos..pos + data_size].to_vec();
404            pos += data_size;
405
406            // In v1, messages are padded to 8-byte alignment relative to
407            // the start of the message data region.
408            let rel = pos - msg_data_start;
409            let aligned_rel = (rel + 7) & !7;
410            let aligned_pos = msg_data_start + aligned_rel;
411            if aligned_pos <= messages_end {
412                pos = aligned_pos;
413            }
414
415            // Skip null/padding messages (type 0)
416            if msg_type == 0 {
417                continue;
418            }
419
420            messages.push(ObjectHeaderMessage {
421                msg_type: msg_type as u8,
422                flags: msg_flags,
423                data,
424            });
425        }
426
427        Ok((
428            ObjectHeader {
429                flags: 0x02, // default flags (not meaningful for v1)
430                messages,
431            },
432            total_consumed,
433        ))
434    }
435
436    /// Auto-detect and decode either v1 or v2 object header.
437    ///
438    /// Checks for the "OHDR" signature to decide v2; otherwise tries v1.
439    pub fn decode_any(buf: &[u8]) -> FormatResult<(Self, usize)> {
440        if buf.len() >= 4 && buf[0..4] == OHDR_SIGNATURE {
441            Self::decode(buf)
442        } else if !buf.is_empty() && buf[0] == 1 {
443            Self::decode_v1(buf)
444        } else {
445            // Try v2 first (will fail with proper error)
446            Self::decode(buf)
447        }
448    }
449}
450
451#[cfg(test)]
452mod tests_v1 {
453    use super::*;
454
455    /// Build a minimal v1 object header with given messages.
456    fn build_v1_header(messages: &[(u16, u8, &[u8])]) -> Vec<u8> {
457        let mut msg_data = Vec::new();
458        for (msg_type, flags, data) in messages {
459            msg_data.extend_from_slice(&msg_type.to_le_bytes());
460            msg_data.extend_from_slice(&(data.len() as u16).to_le_bytes());
461            msg_data.push(*flags);
462            msg_data.extend_from_slice(&[0u8; 3]); // reserved
463            msg_data.extend_from_slice(data);
464            // Pad to 8-byte alignment
465            let aligned = (msg_data.len() + 7) & !7;
466            msg_data.resize(aligned, 0);
467        }
468
469        let mut buf = Vec::new();
470        buf.push(1); // version
471        buf.push(0); // reserved
472        buf.extend_from_slice(&(messages.len() as u16).to_le_bytes());
473        buf.extend_from_slice(&1u32.to_le_bytes()); // ref count
474        buf.extend_from_slice(&(msg_data.len() as u32).to_le_bytes());
475        buf.extend_from_slice(&[0u8; 4]); // reserved padding (align to 16 bytes)
476        buf.extend_from_slice(&msg_data);
477        buf
478    }
479
480    #[test]
481    fn test_decode_v1_empty() {
482        let buf = build_v1_header(&[]);
483        let (hdr, consumed) = ObjectHeader::decode_v1(&buf).unwrap();
484        assert_eq!(consumed, 16); // 16-byte prefix, no messages
485        assert!(hdr.messages.is_empty());
486    }
487
488    #[test]
489    fn test_decode_v1_single_message() {
490        let data = vec![0xAA, 0xBB, 0xCC];
491        let buf = build_v1_header(&[(0x03, 0x00, &data)]);
492        let (hdr, _consumed) = ObjectHeader::decode_v1(&buf).unwrap();
493        assert_eq!(hdr.messages.len(), 1);
494        assert_eq!(hdr.messages[0].msg_type, 0x03);
495        assert_eq!(hdr.messages[0].data, data);
496    }
497
498    #[test]
499    fn test_decode_v1_multiple_messages() {
500        let buf = build_v1_header(&[
501            (0x01, 0x00, &[1, 2, 3, 4]),
502            (0x03, 0x01, &[10, 20]),
503            (0x08, 0x00, &[0xFF; 16]),
504        ]);
505        let (hdr, _) = ObjectHeader::decode_v1(&buf).unwrap();
506        assert_eq!(hdr.messages.len(), 3);
507        assert_eq!(hdr.messages[0].msg_type, 0x01);
508        assert_eq!(hdr.messages[1].msg_type, 0x03);
509        assert_eq!(hdr.messages[2].msg_type, 0x08);
510        assert_eq!(hdr.messages[2].data, vec![0xFF; 16]);
511    }
512
513    #[test]
514    fn test_decode_v1_skips_null_messages() {
515        let buf = build_v1_header(&[
516            (0x00, 0x00, &[0; 8]), // null message (type 0)
517            (0x03, 0x00, &[1, 2]),
518        ]);
519        let (hdr, _) = ObjectHeader::decode_v1(&buf).unwrap();
520        assert_eq!(hdr.messages.len(), 1);
521        assert_eq!(hdr.messages[0].msg_type, 0x03);
522    }
523
524    #[test]
525    fn test_decode_any_v2() {
526        let mut hdr = ObjectHeader::new();
527        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
528        let encoded = hdr.encode();
529        let (decoded, _) = ObjectHeader::decode_any(&encoded).unwrap();
530        assert_eq!(decoded.messages.len(), 1);
531    }
532
533    #[test]
534    fn test_decode_any_v1() {
535        let buf = build_v1_header(&[(0x03, 0x00, &[1, 2])]);
536        let (decoded, _) = ObjectHeader::decode_any(&buf).unwrap();
537        assert_eq!(decoded.messages.len(), 1);
538        assert_eq!(decoded.messages[0].msg_type, 0x03);
539    }
540
541    #[test]
542    fn test_decode_v1_bad_version() {
543        let mut buf = build_v1_header(&[]);
544        buf[0] = 5;
545        assert!(matches!(
546            ObjectHeader::decode_v1(&buf).unwrap_err(),
547            FormatError::InvalidVersion(5)
548        ));
549    }
550
551    #[test]
552    fn test_decode_v1_buffer_too_short() {
553        assert!(matches!(
554            ObjectHeader::decode_v1(&[1, 0, 0]).unwrap_err(),
555            FormatError::BufferTooShort { .. }
556        ));
557    }
558}
559
560#[cfg(test)]
561mod tests {
562    use super::*;
563
564    #[test]
565    fn test_empty_header_roundtrip() {
566        let hdr = ObjectHeader::new();
567        let encoded = hdr.encode();
568
569        // OHDR(4) + version(1) + flags(1) + chunk0_size(4) + checksum(4) = 14
570        assert_eq!(encoded.len(), 14);
571        assert_eq!(&encoded[..4], b"OHDR");
572        assert_eq!(encoded[4], 2); // version
573
574        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
575        assert_eq!(consumed, encoded.len());
576        assert_eq!(decoded, hdr);
577    }
578
579    #[test]
580    fn test_single_message_roundtrip() {
581        let mut hdr = ObjectHeader::new();
582        hdr.add_message(0x01, 0x00, vec![0xAA, 0xBB, 0xCC]);
583
584        let encoded = hdr.encode();
585        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
586        assert_eq!(consumed, encoded.len());
587        assert_eq!(decoded.messages.len(), 1);
588        assert_eq!(decoded.messages[0].msg_type, 0x01);
589        assert_eq!(decoded.messages[0].flags, 0x00);
590        assert_eq!(decoded.messages[0].data, vec![0xAA, 0xBB, 0xCC]);
591    }
592
593    #[test]
594    fn test_multiple_messages_roundtrip() {
595        let mut hdr = ObjectHeader::new();
596        hdr.add_message(0x01, 0x00, vec![1, 2, 3, 4]);
597        hdr.add_message(0x03, 0x01, vec![10, 20]);
598        hdr.add_message(0x0C, 0x00, vec![]);
599
600        let encoded = hdr.encode();
601        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
602        assert_eq!(consumed, encoded.len());
603        assert_eq!(decoded.messages.len(), 3);
604        assert_eq!(decoded, hdr);
605    }
606
607    #[test]
608    fn test_with_creation_order() {
609        let mut hdr = ObjectHeader {
610            flags: 0x02 | FLAG_ATTR_CREATION_ORDER_TRACKED,
611            messages: Vec::new(),
612        };
613        hdr.add_message(0x01, 0x00, vec![0xFF; 8]);
614        hdr.add_message(0x03, 0x00, vec![0xEE; 4]);
615
616        let encoded = hdr.encode();
617        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
618        assert_eq!(consumed, encoded.len());
619        assert_eq!(decoded.messages.len(), 2);
620        assert_eq!(decoded.messages[0].data, vec![0xFF; 8]);
621        assert_eq!(decoded.messages[1].data, vec![0xEE; 4]);
622    }
623
624    #[test]
625    fn test_chunk0_size_1byte() {
626        // flags bits 0-1 = 0 => 1-byte chunk0 size
627        let mut hdr = ObjectHeader {
628            flags: 0x00,
629            messages: Vec::new(),
630        };
631        hdr.add_message(0x01, 0x00, vec![42]);
632
633        let encoded = hdr.encode();
634        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
635        assert_eq!(consumed, encoded.len());
636        assert_eq!(decoded.messages[0].data, vec![42]);
637    }
638
639    #[test]
640    fn test_chunk0_size_2byte() {
641        // flags bits 0-1 = 1 => 2-byte chunk0 size
642        let mut hdr = ObjectHeader {
643            flags: 0x01,
644            messages: Vec::new(),
645        };
646        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
647
648        let encoded = hdr.encode();
649        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
650        assert_eq!(consumed, encoded.len());
651        assert_eq!(decoded.messages[0].data, vec![1, 2, 3]);
652    }
653
654    #[test]
655    fn test_chunk0_size_8byte() {
656        // flags bits 0-1 = 3 => 8-byte chunk0 size
657        let mut hdr = ObjectHeader {
658            flags: 0x03,
659            messages: Vec::new(),
660        };
661        hdr.add_message(0x01, 0x00, vec![0xDE, 0xAD]);
662
663        let encoded = hdr.encode();
664        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
665        assert_eq!(consumed, encoded.len());
666        assert_eq!(decoded.messages[0].data, vec![0xDE, 0xAD]);
667    }
668
669    #[test]
670    fn test_decode_bad_signature() {
671        let mut data = vec![0u8; 20];
672        data[0..4].copy_from_slice(b"XHDR");
673        let err = ObjectHeader::decode(&data).unwrap_err();
674        assert!(matches!(err, FormatError::InvalidSignature));
675    }
676
677    #[test]
678    fn test_decode_bad_version() {
679        let hdr = ObjectHeader::new();
680        let mut encoded = hdr.encode();
681        encoded[4] = 99; // corrupt version
682        let err = ObjectHeader::decode(&encoded).unwrap_err();
683        assert!(matches!(err, FormatError::InvalidVersion(99)));
684    }
685
686    #[test]
687    fn test_decode_checksum_mismatch() {
688        let mut hdr = ObjectHeader::new();
689        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
690        let mut encoded = hdr.encode();
691        // Corrupt a message byte
692        let last_data = encoded.len() - 5;
693        encoded[last_data] ^= 0xFF;
694        let err = ObjectHeader::decode(&encoded).unwrap_err();
695        assert!(matches!(err, FormatError::ChecksumMismatch { .. }));
696    }
697
698    #[test]
699    fn test_decode_buffer_too_short() {
700        let err = ObjectHeader::decode(&[0u8; 5]).unwrap_err();
701        assert!(matches!(err, FormatError::BufferTooShort { .. }));
702    }
703
704    #[test]
705    fn test_decode_with_trailing_data() {
706        let mut hdr = ObjectHeader::new();
707        hdr.add_message(0x01, 0x00, vec![7, 8, 9]);
708        let mut encoded = hdr.encode();
709        let original_len = encoded.len();
710        encoded.extend_from_slice(&[0xBB; 50]); // trailing garbage
711
712        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
713        assert_eq!(consumed, original_len);
714        assert_eq!(decoded, hdr);
715    }
716
717    #[test]
718    fn test_large_message_payload() {
719        let mut hdr = ObjectHeader::new();
720        let big_data = vec![0x42; 1000];
721        hdr.add_message(0x0C, 0x00, big_data.clone());
722
723        let encoded = hdr.encode();
724        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
725        assert_eq!(consumed, encoded.len());
726        assert_eq!(decoded.messages[0].data.len(), 1000);
727        assert_eq!(decoded.messages[0].data, big_data);
728    }
729
730    #[test]
731    fn test_default() {
732        let hdr = ObjectHeader::default();
733        assert_eq!(hdr.flags, 0x02);
734        assert!(hdr.messages.is_empty());
735    }
736}