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::creation_order::CreationOrder;
34use crate::format::{FormatContext, FormatError, FormatResult, ObjectFormat};
35
36/// The 4-byte object header v2 signature.
37pub const OHDR_SIGNATURE: [u8; 4] = *b"OHDR";
38
39/// The 4-byte signature of a version-2 object header continuation chunk.
40pub const OCHK_SIGNATURE: [u8; 4] = *b"OCHK";
41
42/// `H5O_NULL_ID` — the message that covers space a chunk holds but does not
43/// use.
44const MSG_NIL: u8 = 0x00;
45
46/// `H5O_CONT_ID` — the message naming a continuation chunk.
47const MSG_CONTINUATION: u8 = 0x10;
48
49/// Object header version 2.
50pub const OHDR_VERSION: u8 = 2;
51
52/// Largest payload one object header message can carry.
53///
54/// The message envelope encodes the payload length in a `u16`, so this is a
55/// hard on-disk ceiling, not a policy: libhdf5 refuses the same sizes through
56/// `H5O_MESG_MAX_SIZE` (65536) and moves anything that reaches it out of the
57/// header — `H5O__attr_create` switches such an attribute to dense storage.
58pub const MAX_MESSAGE_SIZE: usize = u16::MAX as usize;
59
60// Flag bit masks
61const FLAG_SIZE_MASK: u8 = 0x03;
62const FLAG_ATTR_CREATION_ORDER_TRACKED: u8 = 0x04;
63const FLAG_ATTR_CREATION_ORDER_INDEXED: u8 = 0x08;
64const FLAG_NON_DEFAULT_ATTR_THRESHOLDS: u8 = 0x10;
65const FLAG_STORE_TIMESTAMPS: u8 = 0x20;
66
67/// A single message within an object header.
68#[derive(Debug, Clone, PartialEq, Eq)]
69pub struct ObjectHeaderMessage {
70    /// Message type ID (e.g., 0x01 = Dataspace, 0x03 = Datatype, etc.)
71    pub msg_type: u8,
72    /// Per-message flags (bit 0 = constant, bit 1 = shared, etc.)
73    pub flags: u8,
74    /// Creation index, written only when the header tracks attribute creation
75    /// order (flags bit 2). Only the attribute message class has one in
76    /// libhdf5 — `H5O_msg_class_t::get_crt_index` is null for every other
77    /// type, leaving the field zero (`H5O_msg_append_real`).
78    pub creation_index: u16,
79    /// Raw message payload.
80    pub data: Vec<u8>,
81}
82
83/// The four times a version-2 object header stores, in the order
84/// `H5O__cache_serialize` writes them: seconds since the epoch, as `H5_now`
85/// produces them.
86///
87/// Their presence *is* the `H5O_HDR_STORE_TIMES` flag — see
88/// [`ObjectHeader::times`] — so an object created with
89/// `H5Pset_obj_track_times(true)` cannot be encoded with the flag set and no
90/// times behind it.
91#[derive(Debug, Clone, Copy, PartialEq, Eq)]
92pub struct ObjectTimes {
93    /// Access time (`oh->atime`).
94    pub access: u32,
95    /// Modification time (`oh->mtime`).
96    pub modification: u32,
97    /// Change time (`oh->ctime`).
98    pub change: u32,
99    /// Birth time (`oh->btime`).
100    pub birth: u32,
101}
102
103impl ObjectTimes {
104    /// All four set to `now` — what `H5O_create_ohdr` does for an object
105    /// created with timestamps enabled.
106    pub fn created_at(now: u32) -> Self {
107        Self {
108            access: now,
109            modification: now,
110            change: now,
111            birth: now,
112        }
113    }
114
115    /// These times after a real modification of the object.
116    ///
117    /// `H5O_touch_oh` moves access and change time to `now` for a version-2
118    /// header and leaves modification and birth time as they were — the
119    /// modification time is what its own `XXX` comment says is not updated
120    /// yet. Following it means a rewrite reports the same times libhdf5 would.
121    pub fn touched(self, now: u32) -> Self {
122        Self {
123            access: now,
124            change: now,
125            ..self
126        }
127    }
128}
129
130/// How an object header's messages divide between chunk 0 and one
131/// continuation chunk, produced by [`ObjectHeader::plan_chunks`] and consumed
132/// by [`ObjectHeader::encode_chunked`].
133#[derive(Debug, Clone, Copy, PartialEq, Eq)]
134pub struct ChunkPlan {
135    /// Messages before this index go in chunk 0, the rest in the continuation
136    /// chunk.
137    split: usize,
138    /// Bytes chunk 0 occupies, prefix and checksum included.
139    pub chunk0_size: usize,
140    /// Bytes the continuation chunk occupies, signature and checksum
141    /// included; zero when every message fits chunk 0.
142    pub continuation_size: usize,
143    /// The flags byte chunk 0 is encoded under: the header's own with the
144    /// chunk-0 size field width that fits the area — the narrowest that
145    /// expresses it, or, in a plan into an existing block
146    /// ([`ObjectHeader::plan_chunks_in`]), the one that makes chunk 0 the
147    /// block's length.
148    flags: u8,
149}
150
151/// Object Header v2.
152#[derive(Debug, Clone, PartialEq, Eq)]
153pub struct ObjectHeader {
154    /// Header flags byte: the optional prefix fields (attribute thresholds)
155    /// and the attribute creation-order policy.
156    ///
157    /// Two groups of bits are *not* held here, because each describes the
158    /// image rather than the header. Bit 5 (`H5O_HDR_STORE_TIMES`) is derived
159    /// from [`times`](Self::times) at encode and stripped at decode, so the
160    /// flag and the four values it announces cannot disagree. Bits 0-1, the
161    /// chunk-0 size field width, are chosen at encode for the size chunk 0
162    /// turns out to have — the narrowest field that expresses it, as
163    /// `H5O_apply_ohdr` picks it (H5Oint.c:459-464) — and stripped at decode
164    /// for the same reason. Setting either by hand here does nothing.
165    pub flags: u8,
166    /// The stored times, when this object tracks them.
167    pub times: Option<ObjectTimes>,
168    /// The ordered list of header messages.
169    pub messages: Vec<ObjectHeaderMessage>,
170}
171
172impl ObjectHeader {
173    /// Create a new, empty object header with default flags: no timestamps,
174    /// no attribute creation order, no non-default thresholds.
175    pub fn new() -> Self {
176        Self {
177            flags: 0,
178            times: None,
179            messages: Vec::new(),
180        }
181    }
182
183    /// The times this header records, wherever its version keeps them.
184    ///
185    /// One answer for both versions, which store a different number of times
186    /// in different places: version 2 keeps all four in the prefix under
187    /// `H5O_HDR_STORE_TIMES`, and version 1 keeps at most one, in an
188    /// `H5O_MTIME_NEW` message. `None` means the object was created with
189    /// `H5Pset_obj_track_times(false)` — or is a version-1 group or committed
190    /// datatype, whose header has nowhere to record a time even while the
191    /// property is on, since only `H5D__update_oh_info` calls `H5O_touch_oh`
192    /// with the `force` that creates the message (H5Dint.c:1022-1026).
193    ///
194    /// The one time a version-1 header stores fills all four fields. Only
195    /// [`ObjectTimes::change`] is written back for that version — it is the
196    /// field `H5O_touch_oh` moves to now on both versions (H5Oint.c:1290-1345)
197    /// — so the other three are there to keep one struct across both versions
198    /// rather than to claim the file said anything about them.
199    pub fn recorded_times(&self) -> Option<ObjectTimes> {
200        if let Some(times) = self.times {
201            return Some(times);
202        }
203        self.messages
204            .iter()
205            .find(|m| m.msg_type == crate::format::messages::MSG_MOD_TIME)
206            .and_then(|m| crate::format::messages::mod_time::ModificationTime::decode(&m.data).ok())
207            .map(|t| ObjectTimes::created_at(t.0))
208    }
209
210    /// Append a message to the object header.
211    pub fn add_message(&mut self, msg_type: u8, flags: u8, data: Vec<u8>) {
212        self.add_message_indexed(msg_type, flags, data, 0);
213    }
214
215    /// Append a message carrying a creation index.
216    ///
217    /// The index reaches the file only when the header's flags bit 2 says the
218    /// creation order is tracked; libhdf5 does not encode the field otherwise
219    /// (`H5O_SIZEOF_MSGHDR_OH`).
220    pub fn add_message_indexed(
221        &mut self,
222        msg_type: u8,
223        flags: u8,
224        data: Vec<u8>,
225        creation_index: u16,
226    ) {
227        self.messages.push(ObjectHeaderMessage {
228            msg_type,
229            flags,
230            creation_index,
231            data,
232        });
233    }
234
235    /// Declare `order` as this object's attribute creation-order policy.
236    ///
237    /// `H5Pget_attr_creation_order` reads these two bits back out of the
238    /// header, not out of the Attribute Info message (`H5Pocpl.c`), so they
239    /// are what makes an object report its attributes as creation-ordered.
240    /// Setting `TRACKED` also widens every message envelope by the two-byte
241    /// creation index.
242    pub fn set_attribute_creation_order(&mut self, order: CreationOrder) {
243        self.flags &= !(FLAG_ATTR_CREATION_ORDER_TRACKED | FLAG_ATTR_CREATION_ORDER_INDEXED);
244        if order.is_tracked() {
245            self.flags |= FLAG_ATTR_CREATION_ORDER_TRACKED;
246        }
247        if order.is_indexed() {
248            self.flags |= FLAG_ATTR_CREATION_ORDER_INDEXED;
249        }
250    }
251
252    /// This object's attribute creation-order policy, as its flag bits
253    /// declare it — the reverse of
254    /// [`set_attribute_creation_order`](Self::set_attribute_creation_order),
255    /// and what a reopen must consult so a rewrite re-declares what the file
256    /// already says.
257    pub fn attribute_creation_order(&self) -> CreationOrder {
258        CreationOrder::from_flags(
259            self.flags & FLAG_ATTR_CREATION_ORDER_TRACKED != 0,
260            self.flags & FLAG_ATTR_CREATION_ORDER_INDEXED != 0,
261        )
262    }
263
264    /// The flags byte as it reaches the file: everything [`flags`](Self::flags)
265    /// holds, with `H5O_HDR_STORE_TIMES` taken from
266    /// [`times`](Self::times) — the one place the two are joined, so a header
267    /// can neither claim times it does not have nor carry times it does not
268    /// declare.
269    fn encoded_flags(&self, flags: u8) -> u8 {
270        let base = flags & !FLAG_STORE_TIMESTAMPS;
271        match self.times {
272            Some(_) => base | FLAG_STORE_TIMESTAMPS,
273            None => base,
274        }
275    }
276
277    /// The flags chunk 0 is encoded under when its message area is `area`
278    /// bytes: this header's, with bits 0-1 naming the narrowest size field
279    /// that expresses the area — `H5O_apply_ohdr`'s choice for a new header
280    /// (H5Oint.c:459-464), and what `H5O__alloc_extend_chunk` widens to as
281    /// chunk 0 grows (H5Oalloc.c:538-557).
282    fn flags_for_area(&self, area: usize) -> u8 {
283        let bits = match area as u64 {
284            0..=0xFF => 0,
285            0x100..=0xFFFF => 1,
286            0x1_0000..=0xFFFF_FFFF => 2,
287            _ => 3,
288        };
289        (self.flags & !FLAG_SIZE_MASK) | bits
290    }
291
292    /// Whether attribute creation order tracking is enabled (flags bit 2).
293    pub fn has_creation_order(&self) -> bool {
294        self.flags & FLAG_ATTR_CREATION_ORDER_TRACKED != 0
295    }
296
297    /// Bytes a message envelope takes: type, size and flags, plus the creation
298    /// index when this header tracks one (`H5O_SIZEOF_MSGHDR_OH`).
299    pub fn message_envelope_size(&self) -> usize {
300        if self.has_creation_order() {
301            1 + 2 + 1 + 2 // type + size + flags + creation_order
302        } else {
303            1 + 2 + 1 // type + size + flags
304        }
305    }
306
307    /// Bytes `messages` occupy in a chunk, envelopes included.
308    fn messages_size(&self, messages: &[ObjectHeaderMessage]) -> usize {
309        messages
310            .iter()
311            .map(|m| self.message_envelope_size() + m.data.len())
312            .sum()
313    }
314
315    /// Compute the byte size of the messages region (chunk0 data).
316    fn messages_data_size(&self) -> usize {
317        self.messages_size(&self.messages)
318    }
319
320    /// Bytes chunk 0 spends before its message area when encoded under
321    /// `flags`: signature, version, flags, the optional prefix fields, and
322    /// the chunk-0 size field.
323    fn prefix_size_under(&self, flags: u8) -> usize {
324        let mut size = 4 + 1 + 1; // OHDR + version + flags
325        if self.times.is_some() {
326            size += 16; // 4 x u32
327        }
328        if flags & FLAG_NON_DEFAULT_ATTR_THRESHOLDS != 0 {
329            size += 4; // max_compact(u16) + min_dense(u16)
330        }
331        size + Self::size_field_width(flags)
332    }
333
334    /// The chunk-0 size field width `flags` bits 0-1 name.
335    fn size_field_width(flags: u8) -> usize {
336        match flags & FLAG_SIZE_MASK {
337            0 => 1,
338            1 => 2,
339            2 => 4,
340            3 => 8,
341            _ => unreachable!(),
342        }
343    }
344
345    /// Reject a message whose payload the `u16` size field cannot express.
346    ///
347    /// Writing such a message would record its length modulo 65536: the reader
348    /// would then take the payload's own tail for the next message envelope and
349    /// every message after it would decode as garbage. Nothing downstream can
350    /// detect that — the checksum is computed over the truncated image and
351    /// matches — so the check has to happen before any bytes are produced.
352    fn check_message_sizes(messages: &[ObjectHeaderMessage]) -> FormatResult<()> {
353        for msg in messages {
354            if msg.data.len() > MAX_MESSAGE_SIZE {
355                return Err(FormatError::InvalidData(format!(
356                    "object header message type 0x{:02X} is {} bytes, over the \
357                     {MAX_MESSAGE_SIZE}-byte limit the message size field can express",
358                    msg.msg_type,
359                    msg.data.len()
360                )));
361            }
362        }
363        Ok(())
364    }
365
366    /// Append `messages` in wire form, then pad to `data_size` bytes.
367    ///
368    /// Space left over is filled the way `H5O__chunk_serialize` leaves a
369    /// partly used chunk: a NIL message covering the rest when its envelope
370    /// fits, and otherwise a "gap" of zero bytes too short to hold any message
371    /// header at all (`H5O_SIZEOF_MSGHDR_OH`, H5Ocache.c).
372    fn write_messages(
373        &self,
374        buf: &mut Vec<u8>,
375        messages: &[ObjectHeaderMessage],
376        data_size: usize,
377    ) {
378        let envelope = self.message_envelope_size();
379        let mut write = |msg_type: u8, flags: u8, creation_index: u16, data: &[u8]| {
380            buf.push(msg_type);
381            // Checked against MAX_MESSAGE_SIZE by `check_message_sizes`.
382            buf.extend_from_slice(&(data.len() as u16).to_le_bytes());
383            buf.push(flags);
384            if self.has_creation_order() {
385                buf.extend_from_slice(&creation_index.to_le_bytes());
386            }
387            buf.extend_from_slice(data);
388        };
389        for msg in messages {
390            write(msg.msg_type, msg.flags, msg.creation_index, &msg.data);
391        }
392        let spare = data_size - self.messages_size(messages);
393        if spare >= envelope {
394            write(MSG_NIL, 0x00, 0, &vec![0u8; spare - envelope]);
395        } else {
396            buf.extend(std::iter::repeat_n(0u8, spare));
397        }
398    }
399
400    /// Encode the object header to a byte vector, including "OHDR" signature
401    /// and trailing checksum, with every message in chunk 0.
402    ///
403    /// Fails when any message payload exceeds [`MAX_MESSAGE_SIZE`] — see
404    /// `check_message_sizes`.
405    pub fn encode(&self) -> FormatResult<Vec<u8>> {
406        let exact = self.messages_data_size();
407        self.encode_chunk0(&self.messages, exact, self.flags_for_area(exact))
408    }
409
410    /// Chunk 0 holding `messages`, with a message area of exactly `data_size`
411    /// bytes, encoded under `flags` — the sole producer of a version-2
412    /// chunk-0 image.
413    fn encode_chunk0(
414        &self,
415        messages: &[ObjectHeaderMessage],
416        data_size: usize,
417        flags: u8,
418    ) -> FormatResult<Vec<u8>> {
419        Self::check_message_sizes(messages)?;
420        debug_assert!(data_size >= self.messages_size(messages));
421        let total = self.prefix_size_under(flags) + data_size + 4; // + checksum
422        let mut buf = Vec::with_capacity(total);
423
424        buf.extend_from_slice(&OHDR_SIGNATURE);
425        buf.push(OHDR_VERSION);
426        buf.push(self.encoded_flags(flags));
427
428        // Optional timestamps (bit 5), in `H5O__cache_serialize` order.
429        if let Some(t) = self.times {
430            for field in [t.access, t.modification, t.change, t.birth] {
431                buf.extend_from_slice(&field.to_le_bytes());
432            }
433        }
434
435        // Optional attr storage thresholds (bit 4) -- write defaults if enabled
436        if flags & FLAG_NON_DEFAULT_ATTR_THRESHOLDS != 0 {
437            // max_compact = 8, min_dense = 6 (HDF5 defaults)
438            buf.extend_from_slice(&8u16.to_le_bytes());
439            buf.extend_from_slice(&6u16.to_le_bytes());
440        }
441
442        let csb = Self::size_field_width(flags);
443        buf.extend_from_slice(&(data_size as u64).to_le_bytes()[..csb]);
444
445        self.write_messages(&mut buf, messages, data_size);
446
447        // Checksum over everything before the checksum
448        let cksum = checksum_metadata(&buf);
449        buf.extend_from_slice(&cksum.to_le_bytes());
450
451        debug_assert_eq!(buf.len(), total);
452        Ok(buf)
453    }
454
455    /// A continuation chunk holding `messages`: the `"OCHK"` signature, the
456    /// messages, and the Jenkins checksum over both, exactly as
457    /// `H5O__chunk_serialize` writes one. Sized to fit, so it needs no
458    /// padding.
459    fn encode_continuation(&self, messages: &[ObjectHeaderMessage]) -> FormatResult<Vec<u8>> {
460        Self::check_message_sizes(messages)?;
461        let data_size = self.messages_size(messages);
462        let mut buf = Vec::with_capacity(OCHK_SIGNATURE.len() + data_size + 4);
463        buf.extend_from_slice(&OCHK_SIGNATURE);
464        self.write_messages(&mut buf, messages, data_size);
465        let cksum = checksum_metadata(&buf);
466        buf.extend_from_slice(&cksum.to_le_bytes());
467        Ok(buf)
468    }
469
470    /// Bytes `msg` occupies in a chunk of version `format`, envelope included.
471    ///
472    /// Version 1 spends eight bytes on the envelope and rounds the body up to
473    /// eight (`H5O_ALIGN_OLD`); version 2 spends exactly the envelope.
474    fn message_size_for(&self, format: ObjectFormat, msg: &ObjectHeaderMessage) -> usize {
475        match format {
476            ObjectFormat::Legacy => V1_MSG_HEADER_SIZE + align_old(msg.data.len()),
477            ObjectFormat::Modern => self.message_envelope_size() + msg.data.len(),
478        }
479    }
480
481    /// Bytes `messages` occupy in a chunk of version `format`.
482    fn messages_size_for(&self, format: ObjectFormat, messages: &[ObjectHeaderMessage]) -> usize {
483        messages
484            .iter()
485            .map(|m| self.message_size_for(format, m))
486            .sum()
487    }
488
489    /// Bytes chunk 0 spends outside its message area in version `format`
490    /// under `flags`: the prefix, and the checksum version 2 appends.
491    fn chunk0_overhead(&self, format: ObjectFormat, flags: u8) -> usize {
492        match format {
493            ObjectFormat::Legacy => V1_PREFIX_SIZE,
494            ObjectFormat::Modern => self.prefix_size_under(flags) + 4,
495        }
496    }
497
498    /// Bytes a continuation chunk spends outside its message area: nothing in
499    /// version 1, the `OCHK` signature and a checksum in version 2.
500    fn continuation_overhead(format: ObjectFormat) -> usize {
501        match format {
502            ObjectFormat::Legacy => 0,
503            ObjectFormat::Modern => OCHK_SIGNATURE.len() + 4,
504        }
505    }
506
507    /// The message naming a continuation chunk of `size` bytes at `addr`
508    /// (`H5O_CONT_ID`: the block's address, then its length).
509    fn continuation_message(ctx: &FormatContext, addr: u64, size: usize) -> ObjectHeaderMessage {
510        let sa = ctx.sizeof_addr as usize;
511        let ss = ctx.sizeof_size as usize;
512        let mut body = Vec::with_capacity(sa + ss);
513        body.extend_from_slice(&addr.to_le_bytes()[..sa]);
514        body.extend_from_slice(&(size as u64).to_le_bytes()[..ss]);
515        ObjectHeaderMessage {
516            msg_type: MSG_CONTINUATION,
517            flags: 0x00,
518            creation_index: 0,
519            data: body,
520        }
521    }
522
523    /// How this header divides between chunk 0 and its continuation chunk
524    /// when chunk 0's message area holds at most `capacity` bytes.
525    ///
526    /// The whole point of a plan is that both sizes are known before either
527    /// chunk has an address: the caller allocates the continuation from
528    /// [`continuation_size`](ChunkPlan::continuation_size) and hands the
529    /// address back to [`encode_chunked`](Self::encode_chunked), which is the
530    /// only way chunk 0 can name a block that does not exist yet.
531    ///
532    /// Messages fill chunk 0 in order and the rest go to the continuation, so
533    /// a message never moves ahead of one that was written before it.
534    pub fn plan_chunks(
535        &self,
536        format: ObjectFormat,
537        capacity: usize,
538        ctx: &FormatContext,
539    ) -> FormatResult<ChunkPlan> {
540        self.plan_under(format, capacity, ctx, None)
541    }
542
543    /// [`plan_chunks`](Self::plan_chunks) with chunk 0 encoded under `flags`
544    /// when given, and otherwise under the narrowest size field for the area
545    /// the plan gives chunk 0.
546    fn plan_under(
547        &self,
548        format: ObjectFormat,
549        capacity: usize,
550        ctx: &FormatContext,
551        flags: Option<u8>,
552    ) -> FormatResult<ChunkPlan> {
553        let exact = self.messages_size_for(format, &self.messages);
554        if exact <= capacity {
555            let flags = flags.unwrap_or_else(|| self.flags_for_area(exact));
556            return Ok(ChunkPlan {
557                split: self.messages.len(),
558                chunk0_size: self.chunk0_overhead(format, flags) + exact,
559                continuation_size: 0,
560                flags,
561            });
562        }
563        let flags = flags.unwrap_or_else(|| self.flags_for_area(capacity));
564        // Chunk 0 has to keep room for the message naming the continuation,
565        // whose body is the block's address and length (`H5O_CONT_ID`).
566        let continuation_message =
567            self.message_size_for(format, &Self::continuation_message(ctx, 0, 0));
568        if capacity < continuation_message {
569            return Err(FormatError::InvalidData(format!(
570                "an object header chunk-0 capacity of {capacity} bytes cannot hold the \
571                 {continuation_message}-byte message naming its continuation chunk"
572            )));
573        }
574        let mut used = continuation_message;
575        let mut split = 0;
576        for msg in &self.messages {
577            let size = self.message_size_for(format, msg);
578            if used + size > capacity {
579                break;
580            }
581            used += size;
582            split += 1;
583        }
584        let spilled = self.messages_size_for(format, &self.messages[split..]);
585        Ok(ChunkPlan {
586            split,
587            chunk0_size: self.chunk0_overhead(format, flags) + capacity,
588            continuation_size: Self::continuation_overhead(format) + spilled,
589            flags,
590        })
591    }
592
593    /// How this header divides when chunk 0 is written over a `block`-byte
594    /// block — the one an existing header occupies, which a rewrite keeps so
595    /// that every reference naming the header stays good.
596    ///
597    /// Messages that fit leave the rest of the block padded, as
598    /// `H5O__chunk_serialize` leaves a chunk whose messages shrank; past it,
599    /// the plan is [`plan_chunks`](Self::plan_chunks)'s over the block's
600    /// message area. A version-2 chunk 0 is sized under the narrowest chunk-0
601    /// size field that can express its area, the one that leaves the most of
602    /// the block to messages, so a header libhdf5 sized exactly for its
603    /// messages takes them back without spilling. Refused for a block the
604    /// chunk cannot describe: one too short
605    /// for the prefix, or, in version 1, one whose message area is not a
606    /// multiple of eight — version 1 has no gap, so every byte of the area
607    /// has to belong to a message.
608    pub fn plan_chunks_in(
609        &self,
610        format: ObjectFormat,
611        block: usize,
612        ctx: &FormatContext,
613    ) -> FormatResult<ChunkPlan> {
614        let too_short = || {
615            FormatError::InvalidData(format!(
616                "a {block}-byte block cannot hold this object header's chunk 0 prefix"
617            ))
618        };
619        let (area, flags) = match format {
620            ObjectFormat::Legacy => {
621                let area = block.checked_sub(V1_PREFIX_SIZE).ok_or_else(too_short)?;
622                if area % 8 != 0 {
623                    return Err(FormatError::InvalidData(format!(
624                        "a version-1 object header cannot hold a {area}-byte message area: \
625                         version 1 aligns every message to eight bytes"
626                    )));
627                }
628                (area, self.flags)
629            }
630            ObjectFormat::Modern => {
631                let mut fit = None;
632                for bits in 0..=3u8 {
633                    let flags = (self.flags & !FLAG_SIZE_MASK) | bits;
634                    let Some(area) = block.checked_sub(self.chunk0_overhead(format, flags)) else {
635                        break;
636                    };
637                    if area as u64 <= u64::MAX >> (64 - 8 * Self::size_field_width(flags)) {
638                        fit = Some((area, flags));
639                        break;
640                    }
641                }
642                fit.ok_or_else(too_short)?
643            }
644        };
645        let mut plan = self.plan_under(format, area, ctx, Some(flags))?;
646        if plan.continuation_size == 0 {
647            plan.chunk0_size = block;
648        }
649        Ok(plan)
650    }
651
652    /// Encode this header as `plan` divides it, in version `format`, with the
653    /// continuation chunk at `continuation_addr`.
654    ///
655    /// Returns chunk 0 and, when the plan spills, the continuation chunk's
656    /// image. `continuation_addr` is ignored for a plan that does not spill,
657    /// and `nlink` reaches the file only in the version-1 prefix (see
658    /// [`encode_for`](Self::encode_for)).
659    pub fn encode_chunked(
660        &self,
661        plan: &ChunkPlan,
662        format: ObjectFormat,
663        ctx: &FormatContext,
664        continuation_addr: u64,
665        nlink: u32,
666    ) -> FormatResult<(Vec<u8>, Option<Vec<u8>>)> {
667        let area = plan.chunk0_size - self.chunk0_overhead(format, plan.flags);
668        if plan.continuation_size == 0 {
669            let chunk0 = match format {
670                ObjectFormat::Legacy => self.encode_v1_chunk0(&self.messages, area, nlink, 0)?,
671                ObjectFormat::Modern => self.encode_chunk0(&self.messages, area, plan.flags)?,
672            };
673            return Ok((chunk0, None));
674        }
675        let mut chunk0 = self.messages[..plan.split].to_vec();
676        chunk0.push(Self::continuation_message(
677            ctx,
678            continuation_addr,
679            plan.continuation_size,
680        ));
681        let spilled = &self.messages[plan.split..];
682        let (chunk0, continuation) = match format {
683            ObjectFormat::Legacy => (
684                self.encode_v1_chunk0(&chunk0, area, nlink, spilled.len())?,
685                self.encode_v1_continuation(spilled)?,
686            ),
687            ObjectFormat::Modern => (
688                self.encode_chunk0(&chunk0, area, plan.flags)?,
689                self.encode_continuation(spilled)?,
690            ),
691        };
692        debug_assert_eq!(continuation.len(), plan.continuation_size);
693        Ok((chunk0, Some(continuation)))
694    }
695
696    /// Decode an object header from a byte buffer. Returns the parsed header
697    /// and the number of bytes consumed from the buffer.
698    pub fn decode(buf: &[u8]) -> FormatResult<(Self, usize)> {
699        // Minimum: OHDR(4) + version(1) + flags(1) + chunk0_size(1) + checksum(4) = 11
700        if buf.len() < 11 {
701            return Err(FormatError::BufferTooShort {
702                needed: 11,
703                available: buf.len(),
704            });
705        }
706
707        // Signature
708        if buf[0..4] != OHDR_SIGNATURE {
709            return Err(FormatError::InvalidSignature);
710        }
711
712        // Version
713        let version = buf[4];
714        if version != OHDR_VERSION {
715            return Err(FormatError::InvalidVersion(version));
716        }
717
718        // Bit 5 is stripped here and carried by `times` instead, and bits 0-1
719        // are stripped and carried by the image's own width, so neither can
720        // disagree with what it describes — see [`ObjectHeader::flags`].
721        let flags = buf[5] & !(FLAG_STORE_TIMESTAMPS | FLAG_SIZE_MASK);
722        let mut pos: usize = 6;
723
724        // Optional timestamps (bit 5)
725        let times = if buf[5] & FLAG_STORE_TIMESTAMPS != 0 {
726            if buf.len() < pos + 16 {
727                return Err(FormatError::BufferTooShort {
728                    needed: pos + 16,
729                    available: buf.len(),
730                });
731            }
732            let read = |off: usize| {
733                u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
734            };
735            let t = ObjectTimes {
736                access: read(pos),
737                modification: read(pos + 4),
738                change: read(pos + 8),
739                birth: read(pos + 12),
740            };
741            pos += 16;
742            Some(t)
743        } else {
744            None
745        };
746
747        // Optional attr storage thresholds (bit 4)
748        if flags & FLAG_NON_DEFAULT_ATTR_THRESHOLDS != 0 {
749            if buf.len() < pos + 4 {
750                return Err(FormatError::BufferTooShort {
751                    needed: pos + 4,
752                    available: buf.len(),
753                });
754            }
755            // Skip thresholds for now
756            pos += 4;
757        }
758
759        // Chunk0 data size
760        let chunk0_size_bytes = Self::size_field_width(buf[5]);
761
762        if buf.len() < pos + chunk0_size_bytes {
763            return Err(FormatError::BufferTooShort {
764                needed: pos + chunk0_size_bytes,
765                available: buf.len(),
766            });
767        }
768
769        let chunk0_data_size =
770            crate::format::bytes::read_le_uint(&buf[pos..], chunk0_size_bytes) as usize;
771        pos += chunk0_size_bytes;
772
773        // We need chunk0_data_size bytes of messages + 4 bytes of checksum.
774        // chunk0_data_size is a file field up to 8 bytes wide; guard the
775        // addition so a crafted absurd value yields a clean error instead of
776        // an overflow panic (debug) or wrap (release).
777        let total_consumed = pos
778            .checked_add(chunk0_data_size)
779            .and_then(|x| x.checked_add(4))
780            .ok_or_else(|| {
781                FormatError::InvalidData("object header chunk-0 size overflows usize".into())
782            })?;
783        if buf.len() < total_consumed {
784            return Err(FormatError::BufferTooShort {
785                needed: total_consumed,
786                available: buf.len(),
787            });
788        }
789
790        // Verify checksum: covers everything from start up to (but not
791        // including) the 4-byte checksum.
792        let data_end = total_consumed - 4;
793        let stored_cksum = u32::from_le_bytes([
794            buf[data_end],
795            buf[data_end + 1],
796            buf[data_end + 2],
797            buf[data_end + 3],
798        ]);
799        let computed_cksum = checksum_metadata(&buf[..data_end]);
800        if stored_cksum != computed_cksum {
801            return Err(FormatError::ChecksumMismatch {
802                expected: stored_cksum,
803                computed: computed_cksum,
804            });
805        }
806
807        // Parse messages
808        let has_creation_order = flags & FLAG_ATTR_CREATION_ORDER_TRACKED != 0;
809        let messages_end = pos + chunk0_data_size;
810        let mut messages = Vec::new();
811
812        while pos < messages_end {
813            // Each message: type(1) + size(2) + flags(1) [+ creation_order(2)]
814            let msg_header_size = if has_creation_order { 6 } else { 4 };
815            if pos + msg_header_size > messages_end {
816                // libhdf5 (H5O__chunk_deserialize) permits a gap smaller than
817                // one message header at the end of a v2 chunk; treat the
818                // remaining bytes as such a gap rather than an error.
819                break;
820            }
821
822            let msg_type = buf[pos];
823            let msg_data_size = u16::from_le_bytes([buf[pos + 1], buf[pos + 2]]) as usize;
824            let msg_flags = buf[pos + 3];
825            pos += 4;
826
827            let creation_index = if has_creation_order {
828                let v = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
829                pos += 2;
830                v
831            } else {
832                0
833            };
834
835            if pos + msg_data_size > messages_end {
836                return Err(FormatError::InvalidData(format!(
837                    "message data ({} bytes) extends past chunk0 boundary",
838                    msg_data_size
839                )));
840            }
841
842            let data = buf[pos..pos + msg_data_size].to_vec();
843            pos += msg_data_size;
844
845            messages.push(ObjectHeaderMessage {
846                msg_type,
847                flags: msg_flags,
848                creation_index,
849                data,
850            });
851        }
852
853        Ok((
854            ObjectHeader {
855                flags,
856                times,
857                messages,
858            },
859            total_consumed,
860        ))
861    }
862}
863
864impl Default for ObjectHeader {
865    fn default() -> Self {
866        Self::new()
867    }
868}
869
870// =========================================================================
871// Object Header v1 — for reading and writing legacy HDF5 files
872// =========================================================================
873
874/// `H5O_ALIGN_OLD` (H5Opkg.h:57): version 1 rounds every message body, and the
875/// header prefix, up to a multiple of 8.
876fn align_old(n: usize) -> usize {
877    (n + 7) & !7
878}
879
880/// `H5O_SIZEOF_HDR` for version 1 (H5Opkg.h:85): `H5O_ALIGN_OLD(1 + 1 + 2 + 4
881/// + 4)` — the four trailing bytes are the alignment pad, not a field.
882const V1_PREFIX_SIZE: usize = 16;
883
884/// `H5O_SIZEOF_MSGHDR_VERS` for version 1 (H5Opkg.h:112):
885/// `H5O_ALIGN_OLD(2 + 2 + 1 + 3)`.
886const V1_MSG_HEADER_SIZE: usize = 8;
887
888impl ObjectHeader {
889    /// Why this header cannot be written in version 1, when it cannot.
890    ///
891    /// A header carrying attribute-creation-order flags is refused: those bits
892    /// live in the version-2 flags byte, which version 1 does not have, so
893    /// encoding such a header would silently drop the policy the caller set.
894    ///
895    /// A header carrying [`times`](Self::times) is refused for the same
896    /// reason. The four times and the `H5O_HDR_STORE_TIMES` bit that
897    /// announces them are version-2 prefix fields; a version-1 header records
898    /// at most a modification time, in an `H5O_MTIME_NEW` message among the
899    /// messages. Refusing keeps the version gate at the one encoder that
900    /// knows which prefix it is writing, instead of letting a v2-shaped header
901    /// reach it and come back out with its times gone.
902    fn check_v1_encodable(&self) -> FormatResult<()> {
903        if self.flags & (FLAG_ATTR_CREATION_ORDER_TRACKED | FLAG_ATTR_CREATION_ORDER_INDEXED) != 0 {
904            return Err(FormatError::InvalidData(
905                "a version-1 object header cannot record attribute creation order: \
906                 the tracking flags exist only in the version-2 header prefix"
907                    .into(),
908            ));
909        }
910        if self.times.is_some() {
911            return Err(FormatError::InvalidData(
912                "a version-1 object header cannot store access/modification/change/birth \
913                 times: they are version-2 prefix fields, and version 1 carries only a \
914                 modification time, as an H5O_MTIME_NEW message"
915                    .into(),
916            ));
917        }
918        Ok(())
919    }
920
921    /// Reject a message whose body, once aligned to eight, the version-1 size
922    /// field cannot express — the version-1 counterpart of
923    /// `check_message_sizes`, which the *padded* length is what matters to.
924    fn check_v1_message_sizes(messages: &[ObjectHeaderMessage]) -> FormatResult<()> {
925        for msg in messages {
926            let padded = align_old(msg.data.len());
927            if padded > MAX_MESSAGE_SIZE {
928                return Err(FormatError::InvalidData(format!(
929                    "object header message type 0x{:02X} is {} bytes, {padded} once \
930                     aligned to 8, over the {MAX_MESSAGE_SIZE}-byte limit the message \
931                     size field can express",
932                    msg.msg_type,
933                    msg.data.len()
934                )));
935            }
936        }
937        Ok(())
938    }
939
940    /// Append `messages` in version-1 wire form: a two-byte type, the
941    /// *padded* body length in the size field (`H5O_msg_flush` writes
942    /// `mesg->raw_size`, which `H5O__alloc` already aligned), the flags,
943    /// three reserved bytes where version 2 puts the creation index, and the
944    /// body padded out to eight bytes.
945    fn write_messages_v1(buf: &mut Vec<u8>, messages: &[ObjectHeaderMessage]) {
946        for msg in messages {
947            let padded = align_old(msg.data.len());
948            buf.extend_from_slice(&u16::from(msg.msg_type).to_le_bytes());
949            buf.extend_from_slice(&(padded as u16).to_le_bytes());
950            buf.push(msg.flags);
951            buf.extend_from_slice(&[0u8; 3]); // reserved
952            buf.extend_from_slice(&msg.data);
953            buf.resize(buf.len() + (padded - msg.data.len()), 0);
954        }
955    }
956
957    /// Chunk 0 of a version-1 header holding `messages` in a message area of
958    /// exactly `data_size` bytes, with `spilled` more messages in its
959    /// continuation chunk — the sole producer of a version-1 chunk-0 image.
960    ///
961    /// Space left in the area is one NIL message, the only padding version 1
962    /// has: with no signature and no checksum, the prefix is the version, the
963    /// reference count, and the two counts libhdf5 checks the chunks against —
964    /// the area's size, and the number of messages in the *whole* header
965    /// (`H5O_protect` compares it with what every chunk yielded, H5Oint.c:1094),
966    /// the NIL padding and the continuation message included.
967    fn encode_v1_chunk0(
968        &self,
969        messages: &[ObjectHeaderMessage],
970        data_size: usize,
971        nlink: u32,
972        spilled: usize,
973    ) -> FormatResult<Vec<u8>> {
974        self.check_v1_encodable()?;
975        Self::check_v1_message_sizes(messages)?;
976        let used = self.messages_size_for(ObjectFormat::Legacy, messages);
977        debug_assert!(data_size >= used);
978        let spare = data_size - used;
979        // Every version-1 size is a multiple of eight, so spare space is
980        // either nothing or room for a NIL message's envelope.
981        debug_assert_eq!(spare % 8, 0);
982        let padded = spare >= V1_MSG_HEADER_SIZE;
983        let Ok(chunk0_size) = u32::try_from(data_size) else {
984            return Err(FormatError::InvalidData(format!(
985                "version-1 object header chunk 0 is {data_size} bytes, over the 4-byte \
986                 size field's range"
987            )));
988        };
989        let total_messages = messages.len() + usize::from(padded) + spilled;
990        let Ok(nmesgs) = u16::try_from(total_messages) else {
991            return Err(FormatError::InvalidData(format!(
992                "version-1 object header holds {total_messages} messages, over the 2-byte \
993                 count field's range"
994            )));
995        };
996
997        let total = V1_PREFIX_SIZE + data_size;
998        let mut buf = Vec::with_capacity(total);
999        buf.push(1); // version
1000        buf.push(0); // reserved
1001        buf.extend_from_slice(&nmesgs.to_le_bytes());
1002        buf.extend_from_slice(&nlink.to_le_bytes());
1003        buf.extend_from_slice(&chunk0_size.to_le_bytes());
1004        buf.extend_from_slice(&[0u8; 4]); // pad to H5O_ALIGN_OLD(12)
1005
1006        Self::write_messages_v1(&mut buf, messages);
1007        if padded {
1008            buf.extend_from_slice(&u16::from(MSG_NIL).to_le_bytes());
1009            buf.extend_from_slice(&((spare - V1_MSG_HEADER_SIZE) as u16).to_le_bytes());
1010            buf.push(0);
1011            buf.extend_from_slice(&[0u8; 3]);
1012            buf.resize(total, 0);
1013        }
1014
1015        debug_assert_eq!(buf.len(), total);
1016        Ok(buf)
1017    }
1018
1019    /// A version-1 continuation chunk holding `messages`: bare messages, with
1020    /// no signature and no checksum, which is all `H5O__chunk_deserialize`
1021    /// expects of one. Sized to fit, so it needs no padding.
1022    fn encode_v1_continuation(&self, messages: &[ObjectHeaderMessage]) -> FormatResult<Vec<u8>> {
1023        Self::check_v1_message_sizes(messages)?;
1024        let mut buf = Vec::with_capacity(self.messages_size_for(ObjectFormat::Legacy, messages));
1025        Self::write_messages_v1(&mut buf, messages);
1026        Ok(buf)
1027    }
1028
1029    /// Encode this header in the version-1 format, with `nlink` as the object
1030    /// reference count and every message in chunk 0.
1031    ///
1032    /// The differences from [`encode`](Self::encode) are all
1033    /// `H5O_ALIGN_OLD`'s doing: no signature and no checksum, a two-byte
1034    /// message type, three reserved bytes where version 2 puts the optional
1035    /// creation index, and every message body padded out to eight bytes with
1036    /// the *padded* length in the size field. Refuses the two version-2
1037    /// prefix fields — see `check_v1_encodable`.
1038    pub fn encode_v1(&self, nlink: u32) -> FormatResult<Vec<u8>> {
1039        let exact = self.messages_size_for(ObjectFormat::Legacy, &self.messages);
1040        self.encode_v1_chunk0(&self.messages, exact, nlink, 0)
1041    }
1042
1043    /// Encode this header in the version `format` calls for.
1044    ///
1045    /// `nlink` reaches the file only in the version-1 layout; the version-2
1046    /// header has no reference-count field (an object with more than one hard
1047    /// link carries an Object Reference Count message instead).
1048    pub fn encode_for(&self, format: ObjectFormat, nlink: u32) -> FormatResult<Vec<u8>> {
1049        match format {
1050            ObjectFormat::Legacy => self.encode_v1(nlink),
1051            ObjectFormat::Modern => self.encode(),
1052        }
1053    }
1054}
1055
1056impl ObjectHeader {
1057    /// Decode a v1 object header from a byte buffer.
1058    ///
1059    /// v1 headers do NOT have the "OHDR" signature or a checksum. The layout is:
1060    /// ```text
1061    /// Byte 0: version = 1
1062    /// Byte 1: reserved
1063    /// Bytes 2-3: num_messages (u16 LE)
1064    /// Bytes 4-7: obj_ref_count (u32 LE)
1065    /// Bytes 8-11: header_data_size (u32 LE) — size of message data in first chunk
1066    /// Messages follow, each:
1067    ///   type: u16 LE
1068    ///   data_size: u16 LE
1069    ///   flags: u8
1070    ///   reserved: 3 bytes
1071    ///   data: data_size bytes (padded to 8-byte alignment)
1072    /// ```
1073    pub fn decode_v1(buf: &[u8]) -> FormatResult<(Self, usize)> {
1074        // V1 header prefix is 16 bytes: version(1) + reserved(1) + num_msg(2)
1075        // + ref_count(4) + chunk0_data_size(4) + reserved_padding(4)
1076        if buf.len() < 16 {
1077            return Err(FormatError::BufferTooShort {
1078                needed: 16,
1079                available: buf.len(),
1080            });
1081        }
1082
1083        let version = buf[0];
1084        if version != 1 {
1085            return Err(FormatError::InvalidVersion(version));
1086        }
1087
1088        // buf[1] = reserved
1089        let num_messages = u16::from_le_bytes([buf[2], buf[3]]) as usize;
1090        let _obj_ref_count = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
1091        let header_data_size = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]) as usize;
1092        // buf[12..16] = reserved alignment padding
1093
1094        let total_consumed = 16 + header_data_size;
1095        if buf.len() < total_consumed {
1096            return Err(FormatError::BufferTooShort {
1097                needed: total_consumed,
1098                available: buf.len(),
1099            });
1100        }
1101
1102        let msg_data_start = 16; // offset where message data begins (after 16-byte prefix)
1103        let mut pos = msg_data_start;
1104        let messages_end = msg_data_start + header_data_size;
1105        let mut messages = Vec::with_capacity(num_messages);
1106
1107        for _ in 0..num_messages {
1108            if pos + 8 > messages_end {
1109                break; // no more room for a message header
1110            }
1111
1112            let msg_type = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
1113            let data_size = u16::from_le_bytes([buf[pos + 2], buf[pos + 3]]) as usize;
1114            let msg_flags = buf[pos + 4];
1115            // bytes pos+5..pos+8 are reserved
1116            pos += 8;
1117
1118            if pos + data_size > messages_end {
1119                return Err(FormatError::InvalidData(format!(
1120                    "v1 message data ({} bytes) extends past header boundary",
1121                    data_size
1122                )));
1123            }
1124
1125            let data = buf[pos..pos + data_size].to_vec();
1126            pos += data_size;
1127
1128            // In v1, messages are padded to 8-byte alignment relative to
1129            // the start of the message data region.
1130            let rel = pos - msg_data_start;
1131            let aligned_rel = (rel + 7) & !7;
1132            let aligned_pos = msg_data_start + aligned_rel;
1133            if aligned_pos <= messages_end {
1134                pos = aligned_pos;
1135            }
1136
1137            // Skip null/padding messages (type 0)
1138            if msg_type == 0 {
1139                continue;
1140            }
1141
1142            messages.push(ObjectHeaderMessage {
1143                msg_type: msg_type as u8,
1144                flags: msg_flags,
1145                // A version-1 message envelope has no creation index.
1146                creation_index: 0,
1147                data,
1148            });
1149        }
1150
1151        Ok((
1152            ObjectHeader {
1153                flags: 0,
1154                // A v1 header keeps its modification time in a message
1155                // (`H5O_MSG_MTIME`), never in the prefix.
1156                times: None,
1157                messages,
1158            },
1159            total_consumed,
1160        ))
1161    }
1162
1163    /// Auto-detect and decode either v1 or v2 object header.
1164    ///
1165    /// Checks for the "OHDR" signature to decide v2; otherwise tries v1.
1166    pub fn decode_any(buf: &[u8]) -> FormatResult<(Self, usize)> {
1167        if buf.len() >= 4 && buf[0..4] == OHDR_SIGNATURE {
1168            Self::decode(buf)
1169        } else if !buf.is_empty() && buf[0] == 1 {
1170            Self::decode_v1(buf)
1171        } else {
1172            // Try v2 first (will fail with proper error)
1173            Self::decode(buf)
1174        }
1175    }
1176}
1177
1178#[cfg(test)]
1179mod tests_v1 {
1180    use super::*;
1181
1182    /// Build a minimal v1 object header with given messages.
1183    fn build_v1_header(messages: &[(u16, u8, &[u8])]) -> Vec<u8> {
1184        let mut msg_data = Vec::new();
1185        for (msg_type, flags, data) in messages {
1186            msg_data.extend_from_slice(&msg_type.to_le_bytes());
1187            msg_data.extend_from_slice(&(data.len() as u16).to_le_bytes());
1188            msg_data.push(*flags);
1189            msg_data.extend_from_slice(&[0u8; 3]); // reserved
1190            msg_data.extend_from_slice(data);
1191            // Pad to 8-byte alignment
1192            let aligned = (msg_data.len() + 7) & !7;
1193            msg_data.resize(aligned, 0);
1194        }
1195
1196        let mut buf = Vec::new();
1197        buf.push(1); // version
1198        buf.push(0); // reserved
1199        buf.extend_from_slice(&(messages.len() as u16).to_le_bytes());
1200        buf.extend_from_slice(&1u32.to_le_bytes()); // ref count
1201        buf.extend_from_slice(&(msg_data.len() as u32).to_le_bytes());
1202        buf.extend_from_slice(&[0u8; 4]); // reserved padding (align to 16 bytes)
1203        buf.extend_from_slice(&msg_data);
1204        buf
1205    }
1206
1207    #[test]
1208    fn test_decode_v1_empty() {
1209        let buf = build_v1_header(&[]);
1210        let (hdr, consumed) = ObjectHeader::decode_v1(&buf).unwrap();
1211        assert_eq!(consumed, 16); // 16-byte prefix, no messages
1212        assert!(hdr.messages.is_empty());
1213    }
1214
1215    #[test]
1216    fn test_decode_v1_single_message() {
1217        let data = vec![0xAA, 0xBB, 0xCC];
1218        let buf = build_v1_header(&[(0x03, 0x00, &data)]);
1219        let (hdr, _consumed) = ObjectHeader::decode_v1(&buf).unwrap();
1220        assert_eq!(hdr.messages.len(), 1);
1221        assert_eq!(hdr.messages[0].msg_type, 0x03);
1222        assert_eq!(hdr.messages[0].data, data);
1223    }
1224
1225    #[test]
1226    fn test_decode_v1_multiple_messages() {
1227        let buf = build_v1_header(&[
1228            (0x01, 0x00, &[1, 2, 3, 4]),
1229            (0x03, 0x01, &[10, 20]),
1230            (0x08, 0x00, &[0xFF; 16]),
1231        ]);
1232        let (hdr, _) = ObjectHeader::decode_v1(&buf).unwrap();
1233        assert_eq!(hdr.messages.len(), 3);
1234        assert_eq!(hdr.messages[0].msg_type, 0x01);
1235        assert_eq!(hdr.messages[1].msg_type, 0x03);
1236        assert_eq!(hdr.messages[2].msg_type, 0x08);
1237        assert_eq!(hdr.messages[2].data, vec![0xFF; 16]);
1238    }
1239
1240    /// The exact bytes h5py 3.15/libhdf5 1.14.6 wrote for the header of a
1241    /// contiguous `<i4` dataset of shape (6,) in a default (superblock-0)
1242    /// file: five messages, the last a null pad, chunk 0 of 256 bytes. Only
1243    /// the first four are re-encoded here — the pad is libhdf5 pre-allocating
1244    /// room to grow, not content — so the assertion is on the prefix shape and
1245    /// on each message's aligned envelope.
1246    #[test]
1247    fn an_encoded_v1_header_matches_the_envelope_libhdf5_writes() {
1248        let dataspace = vec![
1249            0x01, 0x01, 0x01, 0x00, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0,
1250        ];
1251        let datatype = vec![0x10, 0x08, 0, 0, 0x04, 0, 0, 0, 0, 0, 0x20, 0, 0, 0, 0, 0];
1252        let fill = vec![0x02, 0x02, 0x02, 0x01, 0, 0, 0, 0];
1253        // 18 raw bytes: version 3 contiguous layout, address then size.
1254        let layout = vec![
1255            0x03, 0x01, 0, 0x08, 0, 0, 0, 0, 0, 0, 0x18, 0, 0, 0, 0, 0, 0, 0,
1256        ];
1257        let mut header = ObjectHeader::new();
1258        header.add_message(0x01, 0x00, dataspace);
1259        header.add_message(0x03, 0x01, datatype);
1260        header.add_message(0x05, 0x01, fill);
1261        header.add_message(0x08, 0x00, layout);
1262
1263        let buf = header.encode_v1(1).unwrap();
1264        assert_eq!(buf[0], 1, "version");
1265        assert_eq!(u16::from_le_bytes([buf[2], buf[3]]), 4, "message count");
1266        assert_eq!(
1267            u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
1268            1,
1269            "reference count"
1270        );
1271        // (8 + 24) + (8 + 16) + (8 + 8) + (8 + 24), the layout body aligned
1272        // from 18 to 24.
1273        assert_eq!(
1274            u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]),
1275            104,
1276            "chunk 0 data size"
1277        );
1278        assert_eq!(buf.len(), 16 + 104);
1279        // The layout message's size field records the aligned length.
1280        let layout_at = 16 + 32 + 24 + 16;
1281        assert_eq!(u16::from_le_bytes([buf[layout_at], buf[layout_at + 1]]), 8);
1282        assert_eq!(
1283            u16::from_le_bytes([buf[layout_at + 2], buf[layout_at + 3]]),
1284            24
1285        );
1286        assert_eq!(&buf[layout_at + 8 + 18..layout_at + 8 + 24], &[0u8; 6]);
1287    }
1288
1289    #[test]
1290    fn a_v1_header_round_trips_through_its_own_decoder() {
1291        let mut header = ObjectHeader::new();
1292        header.add_message(0x11, 0x00, vec![0xAB; 16]);
1293        header.add_message(0x0C, 0x00, vec![0xCD; 21]);
1294        let buf = header.encode_v1(3).unwrap();
1295        let (back, consumed) = ObjectHeader::decode_v1(&buf).unwrap();
1296        assert_eq!(consumed, buf.len());
1297        assert_eq!(back.messages.len(), 2);
1298        assert_eq!(back.messages[0].data, vec![0xAB; 16]);
1299        // The 21-byte body came back padded to 24, so re-encoding is stable.
1300        assert_eq!(back.messages[1].data.len(), 24);
1301        assert_eq!(back.encode_v1(3).unwrap(), buf);
1302        // `decode_any` must not mistake it for a version-2 header.
1303        assert_eq!(ObjectHeader::decode_any(&buf).unwrap().1, buf.len());
1304    }
1305
1306    #[test]
1307    fn a_v1_header_refuses_to_drop_attribute_creation_order() {
1308        let mut header = ObjectHeader::new();
1309        header.set_attribute_creation_order(CreationOrder::Tracked);
1310        assert!(matches!(
1311            header.encode_v1(1).unwrap_err(),
1312            FormatError::InvalidData(_)
1313        ));
1314    }
1315
1316    /// The times gate is the header version, not the caller: a version-1
1317    /// prefix has nowhere to put them, so `encode_v1` says so instead of
1318    /// returning a header whose times are gone.
1319    #[test]
1320    fn a_v1_header_refuses_to_drop_its_stored_times() {
1321        let mut header = ObjectHeader::new();
1322        header.add_message(0x11, 0x00, vec![0u8; 16]);
1323        assert!(header.encode_v1(1).is_ok());
1324
1325        header.times = Some(ObjectTimes::created_at(0x1234_5678));
1326        assert!(matches!(
1327            header.encode_v1(1).unwrap_err(),
1328            FormatError::InvalidData(_)
1329        ));
1330        // The same header is fine as version 2, where the prefix holds them.
1331        let v2 = header.encode().unwrap();
1332        assert_eq!(v2[5] & FLAG_STORE_TIMESTAMPS, FLAG_STORE_TIMESTAMPS);
1333    }
1334
1335    #[test]
1336    fn encode_for_picks_the_version_the_format_calls_for() {
1337        use crate::format::ObjectFormat;
1338        let mut header = ObjectHeader::new();
1339        header.add_message(0x11, 0x00, vec![0u8; 16]);
1340        assert_eq!(header.encode_for(ObjectFormat::Legacy, 1).unwrap()[0], 1);
1341        assert_eq!(
1342            &header.encode_for(ObjectFormat::Modern, 1).unwrap()[0..4],
1343            &OHDR_SIGNATURE
1344        );
1345    }
1346
1347    #[test]
1348    fn test_decode_v1_skips_null_messages() {
1349        let buf = build_v1_header(&[
1350            (0x00, 0x00, &[0; 8]), // null message (type 0)
1351            (0x03, 0x00, &[1, 2]),
1352        ]);
1353        let (hdr, _) = ObjectHeader::decode_v1(&buf).unwrap();
1354        assert_eq!(hdr.messages.len(), 1);
1355        assert_eq!(hdr.messages[0].msg_type, 0x03);
1356    }
1357
1358    #[test]
1359    fn test_decode_any_v2() {
1360        let mut hdr = ObjectHeader::new();
1361        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
1362        let encoded = hdr.encode().unwrap();
1363        let (decoded, _) = ObjectHeader::decode_any(&encoded).unwrap();
1364        assert_eq!(decoded.messages.len(), 1);
1365    }
1366
1367    #[test]
1368    fn test_decode_any_v1() {
1369        let buf = build_v1_header(&[(0x03, 0x00, &[1, 2])]);
1370        let (decoded, _) = ObjectHeader::decode_any(&buf).unwrap();
1371        assert_eq!(decoded.messages.len(), 1);
1372        assert_eq!(decoded.messages[0].msg_type, 0x03);
1373    }
1374
1375    #[test]
1376    fn test_decode_v1_bad_version() {
1377        let mut buf = build_v1_header(&[]);
1378        buf[0] = 5;
1379        assert!(matches!(
1380            ObjectHeader::decode_v1(&buf).unwrap_err(),
1381            FormatError::InvalidVersion(5)
1382        ));
1383    }
1384
1385    #[test]
1386    fn test_decode_v1_buffer_too_short() {
1387        assert!(matches!(
1388            ObjectHeader::decode_v1(&[1, 0, 0]).unwrap_err(),
1389            FormatError::BufferTooShort { .. }
1390        ));
1391    }
1392}
1393
1394#[cfg(test)]
1395mod tests {
1396    use super::*;
1397
1398    #[test]
1399    fn test_empty_header_roundtrip() {
1400        let hdr = ObjectHeader::new();
1401        let encoded = hdr.encode().unwrap();
1402
1403        // OHDR(4) + version(1) + flags(1) + chunk0_size(1) + checksum(4) = 11:
1404        // no messages, so the narrowest size field, as `H5O_apply_ohdr`
1405        // picks it.
1406        assert_eq!(encoded.len(), 11);
1407        assert_eq!(&encoded[..4], b"OHDR");
1408        assert_eq!(encoded[4], 2); // version
1409        assert_eq!(encoded[5] & FLAG_SIZE_MASK, 0);
1410
1411        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1412        assert_eq!(consumed, encoded.len());
1413        assert_eq!(decoded, hdr);
1414    }
1415
1416    #[test]
1417    fn test_single_message_roundtrip() {
1418        let mut hdr = ObjectHeader::new();
1419        hdr.add_message(0x01, 0x00, vec![0xAA, 0xBB, 0xCC]);
1420
1421        let encoded = hdr.encode().unwrap();
1422        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1423        assert_eq!(consumed, encoded.len());
1424        assert_eq!(decoded.messages.len(), 1);
1425        assert_eq!(decoded.messages[0].msg_type, 0x01);
1426        assert_eq!(decoded.messages[0].flags, 0x00);
1427        assert_eq!(decoded.messages[0].data, vec![0xAA, 0xBB, 0xCC]);
1428    }
1429
1430    #[test]
1431    fn test_multiple_messages_roundtrip() {
1432        let mut hdr = ObjectHeader::new();
1433        hdr.add_message(0x01, 0x00, vec![1, 2, 3, 4]);
1434        hdr.add_message(0x03, 0x01, vec![10, 20]);
1435        hdr.add_message(0x0C, 0x00, vec![]);
1436
1437        let encoded = hdr.encode().unwrap();
1438        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1439        assert_eq!(consumed, encoded.len());
1440        assert_eq!(decoded.messages.len(), 3);
1441        assert_eq!(decoded, hdr);
1442    }
1443
1444    #[test]
1445    fn test_with_creation_order() {
1446        let mut hdr = ObjectHeader {
1447            flags: 0x02 | FLAG_ATTR_CREATION_ORDER_TRACKED,
1448            times: None,
1449            messages: Vec::new(),
1450        };
1451        hdr.add_message(0x01, 0x00, vec![0xFF; 8]);
1452        hdr.add_message(0x03, 0x00, vec![0xEE; 4]);
1453
1454        let encoded = hdr.encode().unwrap();
1455        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1456        assert_eq!(consumed, encoded.len());
1457        assert_eq!(decoded.messages.len(), 2);
1458        assert_eq!(decoded.messages[0].data, vec![0xFF; 8]);
1459        assert_eq!(decoded.messages[1].data, vec![0xEE; 4]);
1460    }
1461
1462    /// The per-message creation index survives a round trip, and lands where
1463    /// libhdf5 puts it: right after the message flags byte, ahead of the
1464    /// message data.
1465    #[test]
1466    fn a_tracked_header_round_trips_each_message_creation_index() {
1467        let mut hdr = ObjectHeader::new();
1468        hdr.set_attribute_creation_order(CreationOrder::Indexed);
1469        hdr.add_message_indexed(0x0C, 0x00, vec![0xAA; 6], 0);
1470        hdr.add_message_indexed(0x0C, 0x00, vec![0xBB; 6], 1);
1471        hdr.add_message_indexed(0x0C, 0x00, vec![0xCC; 6], 2);
1472
1473        let encoded = hdr.encode().unwrap();
1474        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1475        assert_eq!(consumed, encoded.len());
1476        assert_eq!(decoded, hdr);
1477        let indices: Vec<u16> = decoded.messages.iter().map(|m| m.creation_index).collect();
1478        assert_eq!(indices, vec![0, 1, 2]);
1479
1480        // Off: no index is written, and every message decodes with 0.
1481        let mut plain = ObjectHeader::new();
1482        plain.add_message(0x0C, 0x00, vec![0xAA; 6]);
1483        assert!(plain.encode().unwrap().len() < encoded.len());
1484        let (plain_back, _) = ObjectHeader::decode(&plain.encode().unwrap()).unwrap();
1485        assert_eq!(plain_back.messages[0].creation_index, 0);
1486    }
1487
1488    /// The two flag bits are set and read back independently, so a header that
1489    /// tracks without indexing survives a decode as exactly that — the state
1490    /// `H5Pset_attr_creation_order(H5P_CRT_ORDER_TRACKED)` produces.
1491    #[test]
1492    fn each_attribute_creation_order_state_round_trips_through_the_flags() {
1493        for order in [
1494            CreationOrder::Untracked,
1495            CreationOrder::Tracked,
1496            CreationOrder::Indexed,
1497        ] {
1498            let mut hdr = ObjectHeader::new();
1499            hdr.set_attribute_creation_order(order);
1500            hdr.add_message_indexed(0x0C, 0x00, vec![0xAA; 6], 3);
1501            let (decoded, _) = ObjectHeader::decode(&hdr.encode().unwrap()).unwrap();
1502            assert_eq!(decoded.attribute_creation_order(), order);
1503            let want_index = if order.is_tracked() { 3 } else { 0 };
1504            assert_eq!(decoded.messages[0].creation_index, want_index);
1505        }
1506    }
1507
1508    /// Setting a policy clears whatever the previous one left behind, so a
1509    /// header recovered as indexed and re-declared untracked does not keep a
1510    /// stale bit.
1511    #[test]
1512    fn setting_a_weaker_policy_clears_the_stronger_one() {
1513        let mut hdr = ObjectHeader::new();
1514        hdr.set_attribute_creation_order(CreationOrder::Indexed);
1515        hdr.set_attribute_creation_order(CreationOrder::Untracked);
1516        assert_eq!(hdr.attribute_creation_order(), CreationOrder::Untracked);
1517        assert_eq!(hdr.flags, 0);
1518    }
1519
1520    /// The four times survive a round trip in `H5O__cache_serialize` order,
1521    /// and their presence is what sets `H5O_HDR_STORE_TIMES` in the file.
1522    #[test]
1523    fn stored_times_round_trip_and_set_the_flag() {
1524        let mut hdr = ObjectHeader::new();
1525        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
1526        let without = hdr.encode().unwrap();
1527
1528        hdr.times = Some(ObjectTimes {
1529            access: 0x0A0A_0A0A,
1530            modification: 0x0B0B_0B0B,
1531            change: 0x0C0C_0C0C,
1532            birth: 0x0D0D_0D0D,
1533        });
1534        let with = hdr.encode().unwrap();
1535
1536        assert_eq!(with.len(), without.len() + 16);
1537        assert_eq!(with[5] & FLAG_STORE_TIMESTAMPS, FLAG_STORE_TIMESTAMPS);
1538        assert_eq!(&with[6..10], &0x0A0A_0A0Au32.to_le_bytes());
1539        assert_eq!(&with[10..14], &0x0B0B_0B0Bu32.to_le_bytes());
1540        assert_eq!(&with[14..18], &0x0C0C_0C0Cu32.to_le_bytes());
1541        assert_eq!(&with[18..22], &0x0D0D_0D0Du32.to_le_bytes());
1542
1543        let (decoded, consumed) = ObjectHeader::decode(&with).expect("decode failed");
1544        assert_eq!(consumed, with.len());
1545        assert_eq!(decoded, hdr);
1546    }
1547
1548    /// The flag cannot be set without the times behind it: bit 5 poked into
1549    /// `flags` by hand is dropped at encode rather than announcing sixteen
1550    /// bytes that are not there — the shape that made a rewrite emit zero
1551    /// timestamps.
1552    #[test]
1553    fn the_timestamps_flag_is_never_written_without_times() {
1554        let mut hdr = ObjectHeader::new();
1555        hdr.flags |= FLAG_STORE_TIMESTAMPS;
1556        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
1557
1558        let encoded = hdr.encode().unwrap();
1559        assert_eq!(encoded[5] & FLAG_STORE_TIMESTAMPS, 0);
1560        let (decoded, _) = ObjectHeader::decode(&encoded).expect("decode failed");
1561        assert_eq!(decoded.times, None);
1562        assert_eq!(decoded.flags & FLAG_STORE_TIMESTAMPS, 0);
1563    }
1564
1565    /// `H5O_touch_oh` on a version-2 header moves access and change time to
1566    /// now and leaves modification and birth time alone.
1567    #[test]
1568    fn touching_moves_access_and_change_time_only() {
1569        let before = ObjectTimes {
1570            access: 100,
1571            modification: 200,
1572            change: 300,
1573            birth: 400,
1574        };
1575        assert_eq!(
1576            before.touched(999),
1577            ObjectTimes {
1578                access: 999,
1579                modification: 200,
1580                change: 999,
1581                birth: 400,
1582            }
1583        );
1584        assert_eq!(
1585            ObjectTimes::created_at(7).touched(7),
1586            ObjectTimes::created_at(7)
1587        );
1588    }
1589
1590    #[test]
1591    fn test_chunk0_size_1byte() {
1592        // flags bits 0-1 = 0 => 1-byte chunk0 size
1593        let mut hdr = ObjectHeader {
1594            flags: 0x00,
1595            times: None,
1596            messages: Vec::new(),
1597        };
1598        hdr.add_message(0x01, 0x00, vec![42]);
1599
1600        let encoded = hdr.encode().unwrap();
1601        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1602        assert_eq!(consumed, encoded.len());
1603        assert_eq!(decoded.messages[0].data, vec![42]);
1604    }
1605
1606    #[test]
1607    fn test_chunk0_size_2byte() {
1608        // flags bits 0-1 = 1 => 2-byte chunk0 size
1609        let mut hdr = ObjectHeader {
1610            flags: 0x01,
1611            times: None,
1612            messages: Vec::new(),
1613        };
1614        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
1615
1616        let encoded = hdr.encode().unwrap();
1617        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1618        assert_eq!(consumed, encoded.len());
1619        assert_eq!(decoded.messages[0].data, vec![1, 2, 3]);
1620    }
1621
1622    #[test]
1623    fn test_chunk0_size_8byte() {
1624        // flags bits 0-1 = 3 => 8-byte chunk0 size
1625        let mut hdr = ObjectHeader {
1626            flags: 0x03,
1627            times: None,
1628            messages: Vec::new(),
1629        };
1630        hdr.add_message(0x01, 0x00, vec![0xDE, 0xAD]);
1631
1632        let encoded = hdr.encode().unwrap();
1633        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1634        assert_eq!(consumed, encoded.len());
1635        assert_eq!(decoded.messages[0].data, vec![0xDE, 0xAD]);
1636    }
1637
1638    #[test]
1639    fn test_decode_bad_signature() {
1640        let mut data = vec![0u8; 20];
1641        data[0..4].copy_from_slice(b"XHDR");
1642        let err = ObjectHeader::decode(&data).unwrap_err();
1643        assert!(matches!(err, FormatError::InvalidSignature));
1644    }
1645
1646    #[test]
1647    fn test_decode_bad_version() {
1648        let hdr = ObjectHeader::new();
1649        let mut encoded = hdr.encode().unwrap();
1650        encoded[4] = 99; // corrupt version
1651        let err = ObjectHeader::decode(&encoded).unwrap_err();
1652        assert!(matches!(err, FormatError::InvalidVersion(99)));
1653    }
1654
1655    #[test]
1656    fn test_decode_checksum_mismatch() {
1657        let mut hdr = ObjectHeader::new();
1658        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
1659        let mut encoded = hdr.encode().unwrap();
1660        // Corrupt a message byte
1661        let last_data = encoded.len() - 5;
1662        encoded[last_data] ^= 0xFF;
1663        let err = ObjectHeader::decode(&encoded).unwrap_err();
1664        assert!(matches!(err, FormatError::ChecksumMismatch { .. }));
1665    }
1666
1667    #[test]
1668    fn test_decode_buffer_too_short() {
1669        let err = ObjectHeader::decode(&[0u8; 5]).unwrap_err();
1670        assert!(matches!(err, FormatError::BufferTooShort { .. }));
1671    }
1672
1673    #[test]
1674    fn test_decode_with_trailing_data() {
1675        let mut hdr = ObjectHeader::new();
1676        hdr.add_message(0x01, 0x00, vec![7, 8, 9]);
1677        let mut encoded = hdr.encode().unwrap();
1678        let original_len = encoded.len();
1679        encoded.extend_from_slice(&[0xBB; 50]); // trailing garbage
1680
1681        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1682        assert_eq!(consumed, original_len);
1683        assert_eq!(decoded, hdr);
1684    }
1685
1686    #[test]
1687    fn test_large_message_payload() {
1688        let mut hdr = ObjectHeader::new();
1689        let big_data = vec![0x42; 1000];
1690        hdr.add_message(0x0C, 0x00, big_data.clone());
1691
1692        let encoded = hdr.encode().unwrap();
1693        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1694        assert_eq!(consumed, encoded.len());
1695        assert_eq!(decoded.messages[0].data.len(), 1000);
1696        assert_eq!(decoded.messages[0].data, big_data);
1697    }
1698
1699    /// The largest payload the size field can express still round-trips.
1700    #[test]
1701    fn test_message_payload_at_size_limit() {
1702        let mut hdr = ObjectHeader::new();
1703        hdr.add_message(0x0C, 0x00, vec![0x42; MAX_MESSAGE_SIZE]);
1704
1705        let encoded = hdr.encode().expect("encode at the limit must succeed");
1706        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1707        assert_eq!(consumed, encoded.len());
1708        assert_eq!(decoded.messages[0].data.len(), MAX_MESSAGE_SIZE);
1709    }
1710
1711    /// One byte past the limit is refused. Encoding it would store the length
1712    /// modulo 65536 — here 0 — and every message after it would decode from
1713    /// the middle of this one's payload, with a checksum that still matches.
1714    #[test]
1715    fn test_message_payload_over_size_limit_is_refused() {
1716        let mut hdr = ObjectHeader::new();
1717        hdr.add_message(0x01, 0x00, vec![7; 4]);
1718        hdr.add_message(0x0C, 0x00, vec![0x42; MAX_MESSAGE_SIZE + 1]);
1719
1720        let err = hdr.encode().expect_err("over the limit must not encode");
1721        let msg = err.to_string();
1722        assert!(msg.contains("0x0C"), "{msg}");
1723        assert!(msg.contains(&(MAX_MESSAGE_SIZE + 1).to_string()), "{msg}");
1724    }
1725
1726    #[test]
1727    fn test_default() {
1728        let hdr = ObjectHeader::default();
1729        assert_eq!(hdr.flags, 0);
1730        assert!(hdr.messages.is_empty());
1731    }
1732
1733    /// A header with three 40-byte messages, for the chunking tests below.
1734    fn chunked_header() -> ObjectHeader {
1735        let mut hdr = ObjectHeader::new();
1736        for (i, t) in [0x02u8, 0x0A, 0x0C].iter().enumerate() {
1737            hdr.add_message(*t, 0x00, vec![i as u8; 40]);
1738        }
1739        hdr
1740    }
1741
1742    /// A capacity that covers every message leaves one chunk, and the image is
1743    /// the one `encode` produces on its own.
1744    #[test]
1745    fn a_capacity_that_fits_every_message_plans_one_chunk() {
1746        let hdr = chunked_header();
1747        let ctx = FormatContext::default_v3();
1748        let plan = hdr.plan_chunks(ObjectFormat::Modern, 1024, &ctx).unwrap();
1749        assert_eq!(plan.continuation_size, 0);
1750        let (chunk0, continuation) = hdr
1751            .encode_chunked(&plan, ObjectFormat::Modern, &ctx, 0x1000, 1)
1752            .unwrap();
1753        assert!(continuation.is_none());
1754        assert_eq!(chunk0, hdr.encode().unwrap());
1755        assert_eq!(chunk0.len(), plan.chunk0_size);
1756    }
1757
1758    /// Past the capacity the tail of the message list moves into an `OCHK`
1759    /// chunk, chunk 0 names it by address and length, and both chunks carry a
1760    /// checksum over their own image.
1761    #[test]
1762    fn messages_past_the_capacity_move_into_a_continuation_chunk() {
1763        let hdr = chunked_header();
1764        let ctx = FormatContext::default_v3();
1765        // Room for one 44-byte message and the 20-byte continuation message.
1766        let plan = hdr.plan_chunks(ObjectFormat::Modern, 64, &ctx).unwrap();
1767        assert_eq!(plan.continuation_size, 4 + 2 * 44 + 4);
1768        let (chunk0, continuation) = hdr
1769            .encode_chunked(&plan, ObjectFormat::Modern, &ctx, 0x2000, 1)
1770            .unwrap();
1771        let continuation = continuation.unwrap();
1772        assert_eq!(chunk0.len(), plan.chunk0_size);
1773        assert_eq!(continuation.len(), plan.continuation_size);
1774        assert_eq!(&continuation[..4], &OCHK_SIGNATURE);
1775
1776        let (decoded, consumed) = ObjectHeader::decode(&chunk0).unwrap();
1777        assert_eq!(consumed, chunk0.len());
1778        assert_eq!(
1779            decoded.messages.len(),
1780            2,
1781            "the first message and the pointer"
1782        );
1783        assert_eq!(decoded.messages[0], hdr.messages[0]);
1784        let pointer = &decoded.messages[1];
1785        assert_eq!(pointer.msg_type, MSG_CONTINUATION);
1786        assert_eq!(
1787            u64::from_le_bytes(pointer.data[..8].try_into().unwrap()),
1788            0x2000
1789        );
1790        assert_eq!(
1791            u64::from_le_bytes(pointer.data[8..16].try_into().unwrap()),
1792            plan.continuation_size as u64
1793        );
1794
1795        let body = &continuation[..continuation.len() - 4];
1796        let stored = u32::from_le_bytes(continuation[continuation.len() - 4..].try_into().unwrap());
1797        assert_eq!(stored, checksum_metadata(body));
1798    }
1799
1800    /// Space left in chunk 0 becomes a NIL message when a message envelope
1801    /// fits in it, and zero bytes — a "gap" — when it does not.
1802    #[test]
1803    fn leftover_chunk_zero_space_is_a_nil_message_or_a_gap() {
1804        let hdr = chunked_header();
1805        let ctx = FormatContext::default_v3();
1806
1807        // 44 (message) + 20 (continuation) + 8 leaves room for a NIL.
1808        let (chunk0, _) = hdr
1809            .encode_chunked(
1810                &hdr.plan_chunks(ObjectFormat::Modern, 72, &ctx).unwrap(),
1811                ObjectFormat::Modern,
1812                &ctx,
1813                0x2000,
1814                1,
1815            )
1816            .unwrap();
1817        let tail = &chunk0[chunk0.len() - 4 - 8..chunk0.len() - 4];
1818        assert_eq!(tail, [MSG_NIL, 4, 0, 0, 0, 0, 0, 0]);
1819
1820        // Three bytes over is one short of an envelope, so they stay a gap.
1821        let (chunk0, _) = hdr
1822            .encode_chunked(
1823                &hdr.plan_chunks(ObjectFormat::Modern, 67, &ctx).unwrap(),
1824                ObjectFormat::Modern,
1825                &ctx,
1826                0x2000,
1827                1,
1828            )
1829            .unwrap();
1830        assert_eq!(&chunk0[chunk0.len() - 4 - 3..chunk0.len() - 4], [0, 0, 0]);
1831        // A gap is not a message: the decoder stops at it.
1832        let (decoded, _) = ObjectHeader::decode(&chunk0).unwrap();
1833        assert_eq!(decoded.messages.len(), 2);
1834    }
1835
1836    /// A capacity with no room for the message naming the continuation cannot
1837    /// be planned: chunk 0 would spill with nothing pointing at the spill.
1838    #[test]
1839    fn a_capacity_below_the_continuation_message_is_refused() {
1840        let hdr = chunked_header();
1841        let err = hdr
1842            .plan_chunks(ObjectFormat::Modern, 19, &FormatContext::default_v3())
1843            .expect_err("19 bytes cannot hold a 20-byte continuation message");
1844        assert!(err.to_string().contains("continuation chunk"), "{err}");
1845    }
1846
1847    /// The times a version-2 header stores sit in its prefix, ahead of the
1848    /// message area a plan divides — so a header carrying them reserves
1849    /// sixteen more bytes for chunk 0 and spills at exactly the same message.
1850    ///
1851    /// `chunk0_capacity` in the writer budgets the *message* area, and the
1852    /// prefix is added on top of it here; a plan that folded the times into
1853    /// that budget would size chunk 0 sixteen bytes short of the image
1854    /// `encode_chunked` then produces, which `check_header_size` refuses.
1855    #[test]
1856    fn the_times_prefix_widens_chunk_zero_without_moving_the_split() {
1857        let ctx = FormatContext::default_v3();
1858        let plain = chunked_header();
1859        let mut timed = chunked_header();
1860        timed.times = Some(ObjectTimes::created_at(0x5EED_1234));
1861
1862        for capacity in [72usize, 120, 1024] {
1863            let a = plain
1864                .plan_chunks(ObjectFormat::Modern, capacity, &ctx)
1865                .unwrap();
1866            let b = timed
1867                .plan_chunks(ObjectFormat::Modern, capacity, &ctx)
1868                .unwrap();
1869            assert_eq!(a.split, b.split, "capacity {capacity}: same split");
1870            assert_eq!(
1871                a.continuation_size, b.continuation_size,
1872                "capacity {capacity}: same continuation"
1873            );
1874            assert_eq!(
1875                b.chunk0_size,
1876                a.chunk0_size + 16,
1877                "capacity {capacity}: four times of four bytes"
1878            );
1879        }
1880
1881        // And the plan describes the image: chunk 0 is the length the plan
1882        // said, times included.
1883        let plan = timed.plan_chunks(ObjectFormat::Modern, 72, &ctx).unwrap();
1884        let (chunk0, continuation) = timed
1885            .encode_chunked(&plan, ObjectFormat::Modern, &ctx, 0x2000, 1)
1886            .unwrap();
1887        assert_eq!(chunk0.len(), plan.chunk0_size);
1888        assert_eq!(continuation.unwrap().len(), plan.continuation_size);
1889        let (decoded, _) = ObjectHeader::decode(&chunk0).unwrap();
1890        assert_eq!(decoded.times, timed.times);
1891    }
1892
1893    /// A version-1 header planned with no bound is one chunk, and the image
1894    /// is the one `encode_v1` produces on its own — which is how the writer
1895    /// lays out a fresh legacy header whatever it holds.
1896    #[test]
1897    fn a_version_one_plan_with_no_bound_is_one_chunk() {
1898        let hdr = chunked_header();
1899        let ctx = FormatContext::default_v3();
1900        let plan = hdr
1901            .plan_chunks(ObjectFormat::Legacy, usize::MAX, &ctx)
1902            .unwrap();
1903        assert_eq!(plan.continuation_size, 0);
1904        let (chunk0, continuation) = hdr
1905            .encode_chunked(&plan, ObjectFormat::Legacy, &ctx, 0x2000, 3)
1906            .unwrap();
1907        assert!(continuation.is_none());
1908        assert_eq!(chunk0, hdr.encode_v1(3).unwrap());
1909        assert_eq!(chunk0.len(), plan.chunk0_size);
1910    }
1911
1912    /// Past the capacity a version-1 header spills into a bare continuation
1913    /// chunk — messages with no signature and no checksum — and chunk 0's
1914    /// prefix counts every message in the header: its own, the one naming
1915    /// the continuation, the NIL filling the rest of the area, and the
1916    /// spilled ones, which is the count `H5O_protect` checks the chunks
1917    /// against (H5Oint.c:1094).
1918    #[test]
1919    fn a_version_one_header_spills_into_a_bare_continuation_chunk() {
1920        let hdr = chunked_header();
1921        let ctx = FormatContext::default_v3();
1922        // Each message is 8 + 40 bytes; the continuation message is 8 + 16.
1923        // 80 bytes hold one of each and leave 8: a NIL with an empty body.
1924        let plan = hdr.plan_chunks(ObjectFormat::Legacy, 80, &ctx).unwrap();
1925        assert_eq!(plan.chunk0_size, 16 + 80);
1926        assert_eq!(plan.continuation_size, 2 * 48);
1927        let (chunk0, continuation) = hdr
1928            .encode_chunked(&plan, ObjectFormat::Legacy, &ctx, 0x2000, 1)
1929            .unwrap();
1930        let continuation = continuation.unwrap();
1931        assert_eq!(chunk0.len(), plan.chunk0_size);
1932        assert_eq!(continuation.len(), plan.continuation_size);
1933        assert_eq!(u16::from_le_bytes([chunk0[2], chunk0[3]]), 5, "nmesgs");
1934        assert_eq!(&chunk0[8..12], &80u32.to_le_bytes());
1935        // The NIL closes the area: type 0, an empty padded body.
1936        assert_eq!(&chunk0[16 + 48 + 24..], &[0u8; 8]);
1937
1938        let (decoded, consumed) = ObjectHeader::decode_v1(&chunk0).unwrap();
1939        assert_eq!(consumed, chunk0.len());
1940        assert_eq!(
1941            decoded.messages.len(),
1942            2,
1943            "the first message and the pointer"
1944        );
1945        assert_eq!(decoded.messages[0], hdr.messages[0]);
1946        let pointer = &decoded.messages[1];
1947        assert_eq!(pointer.msg_type, MSG_CONTINUATION);
1948        assert_eq!(
1949            u64::from_le_bytes(pointer.data[..8].try_into().unwrap()),
1950            0x2000
1951        );
1952        assert_eq!(
1953            u64::from_le_bytes(pointer.data[8..16].try_into().unwrap()),
1954            plan.continuation_size as u64
1955        );
1956        // Bare messages: the second message's envelope opens the chunk.
1957        assert_eq!(&continuation[..4], &[0x0A, 0, 40, 0]);
1958        assert_eq!(&continuation[8..48], &hdr.messages[1].data[..]);
1959        assert_eq!(&continuation[48..52], &[0x0C, 0, 40, 0]);
1960    }
1961
1962    /// A plan into an existing block holds chunk 0 to the block: a header
1963    /// that fits leaves the rest as a NIL message (version 2) or a NIL whose
1964    /// padded body fills it (version 1), and a block the messages outgrow
1965    /// spills exactly as a capacity would.
1966    #[test]
1967    fn a_plan_into_a_block_holds_chunk_zero_to_it() {
1968        let hdr = chunked_header();
1969        let ctx = FormatContext::default_v3();
1970
1971        // Version 2 under a one-byte size field: 4 + 1 + 1 + 1 of prefix, the
1972        // three 44-byte messages, 20 bytes to spare, and the checksum.
1973        let block = 7 + 3 * 44 + 20 + 4;
1974        let plan = hdr
1975            .plan_chunks_in(ObjectFormat::Modern, block, &ctx)
1976            .unwrap();
1977        assert_eq!((plan.chunk0_size, plan.continuation_size), (block, 0));
1978        let (chunk0, continuation) = hdr
1979            .encode_chunked(&plan, ObjectFormat::Modern, &ctx, 0, 1)
1980            .unwrap();
1981        assert!(continuation.is_none());
1982        assert_eq!(chunk0.len(), block);
1983        let (decoded, consumed) = ObjectHeader::decode(&chunk0).unwrap();
1984        assert_eq!(consumed, block);
1985        assert_eq!(decoded.messages[..3], hdr.messages[..]);
1986        assert_eq!(decoded.messages[3].msg_type, MSG_NIL);
1987        assert_eq!(decoded.messages[3].data.len(), 20 - 4);
1988
1989        // The same block split: 7 + area + 4 with area = 44 + 20 (pointer) + 8.
1990        let plan = hdr
1991            .plan_chunks_in(ObjectFormat::Modern, 7 + 72 + 4, &ctx)
1992            .unwrap();
1993        assert_eq!(plan.chunk0_size, 7 + 72 + 4);
1994        assert_eq!(plan.continuation_size, 4 + 2 * 44 + 4);
1995
1996        // Version 1: 16 of prefix, three 48-byte messages, and 16 to spare —
1997        // a NIL with an 8-byte padded body.
1998        let block = 16 + 3 * 48 + 16;
1999        let plan = hdr
2000            .plan_chunks_in(ObjectFormat::Legacy, block, &ctx)
2001            .unwrap();
2002        assert_eq!((plan.chunk0_size, plan.continuation_size), (block, 0));
2003        let (chunk0, _) = hdr
2004            .encode_chunked(&plan, ObjectFormat::Legacy, &ctx, 0, 1)
2005            .unwrap();
2006        assert_eq!(chunk0.len(), block);
2007        assert_eq!(u16::from_le_bytes([chunk0[2], chunk0[3]]), 4, "nmesgs");
2008        assert_eq!(&chunk0[block - 16..block - 12], &[0, 0, 8, 0]);
2009        let (decoded, consumed) = ObjectHeader::decode_v1(&chunk0).unwrap();
2010        assert_eq!(consumed, block);
2011        assert_eq!(decoded.messages, hdr.messages);
2012
2013        // A version-1 area that is not a multiple of eight has no legal
2014        // padding; a block too short for either prefix has no chunk 0.
2015        for (format, block) in [
2016            (ObjectFormat::Legacy, 16 + 3 * 48 + 12),
2017            (ObjectFormat::Legacy, 8),
2018            (ObjectFormat::Modern, 10),
2019        ] {
2020            hdr.plan_chunks_in(format, block, &ctx)
2021                .expect_err("an undescribable block");
2022        }
2023    }
2024
2025    /// A plan into a block takes the narrowest chunk-0 size field that can
2026    /// express the area, whatever width the header's own flags name: a
2027    /// header libhdf5 sized exactly for its messages — under a one-byte
2028    /// field — takes them back without spilling, and a block past 255 bytes
2029    /// of area widens the field rather than being refused.
2030    #[test]
2031    fn a_plan_into_a_block_takes_the_narrowest_size_field() {
2032        let mut hdr = chunked_header();
2033        // The header's own field would be the widest: the plan ignores it.
2034        hdr.flags |= 3;
2035        let ctx = FormatContext::default_v3();
2036
2037        // libhdf5's exact fit for these messages: a one-byte field.
2038        let exact = 7 + 3 * 44 + 4;
2039        let plan = hdr
2040            .plan_chunks_in(ObjectFormat::Modern, exact, &ctx)
2041            .unwrap();
2042        assert_eq!(plan.continuation_size, 0);
2043        let (chunk0, _) = hdr
2044            .encode_chunked(&plan, ObjectFormat::Modern, &ctx, 0, 1)
2045            .unwrap();
2046        assert_eq!(chunk0.len(), exact);
2047        assert_eq!(chunk0[5] & FLAG_SIZE_MASK, 0);
2048        let (decoded, _) = ObjectHeader::decode(&chunk0).unwrap();
2049        assert_eq!(decoded.messages, hdr.messages);
2050
2051        // 300 bytes of area need a two-byte field.
2052        let plan = hdr
2053            .plan_chunks_in(ObjectFormat::Modern, 8 + 300 + 4, &ctx)
2054            .unwrap();
2055        let (chunk0, _) = hdr
2056            .encode_chunked(&plan, ObjectFormat::Modern, &ctx, 0, 1)
2057            .unwrap();
2058        assert_eq!(chunk0.len(), 8 + 300 + 4);
2059        assert_eq!(chunk0[5] & FLAG_SIZE_MASK, 1);
2060        let (decoded, consumed) = ObjectHeader::decode(&chunk0).unwrap();
2061        assert_eq!(consumed, chunk0.len());
2062        assert_eq!(decoded.messages[..3], hdr.messages[..]);
2063
2064        // The header itself is left as it was.
2065        assert_eq!(hdr.flags & FLAG_SIZE_MASK, 3);
2066    }
2067}