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};
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}
144
145/// Object Header v2.
146#[derive(Debug, Clone, PartialEq, Eq)]
147pub struct ObjectHeader {
148    /// Header flags byte. Bits 0-1 control chunk0 size encoding. Other bits
149    /// control optional fields (attr thresholds, creation order).
150    ///
151    /// Bit 5 (`H5O_HDR_STORE_TIMES`) is *not* held here: it is derived from
152    /// [`times`](Self::times) at encode and stripped at decode, so the flag and
153    /// the four values it announces cannot disagree. Setting it by hand here
154    /// does nothing — the encoder's flag byte comes from `times`.
155    pub flags: u8,
156    /// The stored times, when this object tracks them.
157    pub times: Option<ObjectTimes>,
158    /// The ordered list of header messages.
159    pub messages: Vec<ObjectHeaderMessage>,
160}
161
162impl ObjectHeader {
163    /// Create a new, empty object header with default flags.
164    ///
165    /// Defaults: bits 0-1 = 2 (4-byte chunk size encoding), no timestamps,
166    /// no attribute creation order, no non-default thresholds.
167    pub fn new() -> Self {
168        Self {
169            flags: 0x02, // bits 0-1 = 2 => 4-byte chunk0 size
170            times: None,
171            messages: Vec::new(),
172        }
173    }
174
175    /// The times this header records, wherever its version keeps them.
176    ///
177    /// One answer for both versions, which store a different number of times
178    /// in different places: version 2 keeps all four in the prefix under
179    /// `H5O_HDR_STORE_TIMES`, and version 1 keeps at most one, in an
180    /// `H5O_MTIME_NEW` message. `None` means the object was created with
181    /// `H5Pset_obj_track_times(false)` — or is a version-1 group or committed
182    /// datatype, whose header has nowhere to record a time even while the
183    /// property is on, since only `H5D__update_oh_info` calls `H5O_touch_oh`
184    /// with the `force` that creates the message (H5Dint.c:1022-1026).
185    ///
186    /// The one time a version-1 header stores fills all four fields. Only
187    /// [`ObjectTimes::change`] is written back for that version — it is the
188    /// field `H5O_touch_oh` moves to now on both versions (H5Oint.c:1290-1345)
189    /// — so the other three are there to keep one struct across both versions
190    /// rather than to claim the file said anything about them.
191    pub fn recorded_times(&self) -> Option<ObjectTimes> {
192        if let Some(times) = self.times {
193            return Some(times);
194        }
195        self.messages
196            .iter()
197            .find(|m| m.msg_type == crate::format::messages::MSG_MOD_TIME)
198            .and_then(|m| crate::format::messages::mod_time::ModificationTime::decode(&m.data).ok())
199            .map(|t| ObjectTimes::created_at(t.0))
200    }
201
202    /// Append a message to the object header.
203    pub fn add_message(&mut self, msg_type: u8, flags: u8, data: Vec<u8>) {
204        self.add_message_indexed(msg_type, flags, data, 0);
205    }
206
207    /// Append a message carrying a creation index.
208    ///
209    /// The index reaches the file only when the header's flags bit 2 says the
210    /// creation order is tracked; libhdf5 does not encode the field otherwise
211    /// (`H5O_SIZEOF_MSGHDR_OH`).
212    pub fn add_message_indexed(
213        &mut self,
214        msg_type: u8,
215        flags: u8,
216        data: Vec<u8>,
217        creation_index: u16,
218    ) {
219        self.messages.push(ObjectHeaderMessage {
220            msg_type,
221            flags,
222            creation_index,
223            data,
224        });
225    }
226
227    /// Declare `order` as this object's attribute creation-order policy.
228    ///
229    /// `H5Pget_attr_creation_order` reads these two bits back out of the
230    /// header, not out of the Attribute Info message (`H5Pocpl.c`), so they
231    /// are what makes an object report its attributes as creation-ordered.
232    /// Setting `TRACKED` also widens every message envelope by the two-byte
233    /// creation index.
234    pub fn set_attribute_creation_order(&mut self, order: CreationOrder) {
235        self.flags &= !(FLAG_ATTR_CREATION_ORDER_TRACKED | FLAG_ATTR_CREATION_ORDER_INDEXED);
236        if order.is_tracked() {
237            self.flags |= FLAG_ATTR_CREATION_ORDER_TRACKED;
238        }
239        if order.is_indexed() {
240            self.flags |= FLAG_ATTR_CREATION_ORDER_INDEXED;
241        }
242    }
243
244    /// This object's attribute creation-order policy, as its flag bits
245    /// declare it — the reverse of
246    /// [`set_attribute_creation_order`](Self::set_attribute_creation_order),
247    /// and what a reopen must consult so a rewrite re-declares what the file
248    /// already says.
249    pub fn attribute_creation_order(&self) -> CreationOrder {
250        CreationOrder::from_flags(
251            self.flags & FLAG_ATTR_CREATION_ORDER_TRACKED != 0,
252            self.flags & FLAG_ATTR_CREATION_ORDER_INDEXED != 0,
253        )
254    }
255
256    /// The flags byte as it reaches the file: everything [`flags`](Self::flags)
257    /// holds, with `H5O_HDR_STORE_TIMES` taken from
258    /// [`times`](Self::times) — the one place the two are joined, so a header
259    /// can neither claim times it does not have nor carry times it does not
260    /// declare.
261    fn encoded_flags(&self) -> u8 {
262        let base = self.flags & !FLAG_STORE_TIMESTAMPS;
263        match self.times {
264            Some(_) => base | FLAG_STORE_TIMESTAMPS,
265            None => base,
266        }
267    }
268
269    /// Returns the number of bytes used to encode chunk0's data size, based on
270    /// flags bits 0-1.
271    fn chunk0_size_bytes(&self) -> usize {
272        match self.flags & FLAG_SIZE_MASK {
273            0 => 1,
274            1 => 2,
275            2 => 4,
276            3 => 8,
277            _ => unreachable!(),
278        }
279    }
280
281    /// Whether attribute creation order tracking is enabled (flags bit 2).
282    pub fn has_creation_order(&self) -> bool {
283        self.flags & FLAG_ATTR_CREATION_ORDER_TRACKED != 0
284    }
285
286    /// Bytes a message envelope takes: type, size and flags, plus the creation
287    /// index when this header tracks one (`H5O_SIZEOF_MSGHDR_OH`).
288    pub fn message_envelope_size(&self) -> usize {
289        if self.has_creation_order() {
290            1 + 2 + 1 + 2 // type + size + flags + creation_order
291        } else {
292            1 + 2 + 1 // type + size + flags
293        }
294    }
295
296    /// Bytes `messages` occupy in a chunk, envelopes included.
297    fn messages_size(&self, messages: &[ObjectHeaderMessage]) -> usize {
298        messages
299            .iter()
300            .map(|m| self.message_envelope_size() + m.data.len())
301            .sum()
302    }
303
304    /// Compute the byte size of the messages region (chunk0 data).
305    fn messages_data_size(&self) -> usize {
306        self.messages_size(&self.messages)
307    }
308
309    /// Bytes chunk 0 spends before its message area: signature, version,
310    /// flags, the optional prefix fields, and the chunk-0 size field.
311    fn prefix_size(&self) -> usize {
312        let mut size = 4 + 1 + 1; // OHDR + version + flags
313        if self.times.is_some() {
314            size += 16; // 4 x u32
315        }
316        if self.flags & FLAG_NON_DEFAULT_ATTR_THRESHOLDS != 0 {
317            size += 4; // max_compact(u16) + min_dense(u16)
318        }
319        size + self.chunk0_size_bytes()
320    }
321
322    /// Reject a message whose payload the `u16` size field cannot express.
323    ///
324    /// Writing such a message would record its length modulo 65536: the reader
325    /// would then take the payload's own tail for the next message envelope and
326    /// every message after it would decode as garbage. Nothing downstream can
327    /// detect that — the checksum is computed over the truncated image and
328    /// matches — so the check has to happen before any bytes are produced.
329    fn check_message_sizes(messages: &[ObjectHeaderMessage]) -> FormatResult<()> {
330        for msg in messages {
331            if msg.data.len() > MAX_MESSAGE_SIZE {
332                return Err(FormatError::InvalidData(format!(
333                    "object header message type 0x{:02X} is {} bytes, over the \
334                     {MAX_MESSAGE_SIZE}-byte limit the message size field can express",
335                    msg.msg_type,
336                    msg.data.len()
337                )));
338            }
339        }
340        Ok(())
341    }
342
343    /// Append `messages` in wire form, then pad to `data_size` bytes.
344    ///
345    /// Space left over is filled the way `H5O__chunk_serialize` leaves a
346    /// partly used chunk: a NIL message covering the rest when its envelope
347    /// fits, and otherwise a "gap" of zero bytes too short to hold any message
348    /// header at all (`H5O_SIZEOF_MSGHDR_OH`, H5Ocache.c).
349    fn write_messages(
350        &self,
351        buf: &mut Vec<u8>,
352        messages: &[ObjectHeaderMessage],
353        data_size: usize,
354    ) {
355        let envelope = self.message_envelope_size();
356        let mut write = |msg_type: u8, flags: u8, creation_index: u16, data: &[u8]| {
357            buf.push(msg_type);
358            // Checked against MAX_MESSAGE_SIZE by `check_message_sizes`.
359            buf.extend_from_slice(&(data.len() as u16).to_le_bytes());
360            buf.push(flags);
361            if self.has_creation_order() {
362                buf.extend_from_slice(&creation_index.to_le_bytes());
363            }
364            buf.extend_from_slice(data);
365        };
366        for msg in messages {
367            write(msg.msg_type, msg.flags, msg.creation_index, &msg.data);
368        }
369        let spare = data_size - self.messages_size(messages);
370        if spare >= envelope {
371            write(MSG_NIL, 0x00, 0, &vec![0u8; spare - envelope]);
372        } else {
373            buf.extend(std::iter::repeat_n(0u8, spare));
374        }
375    }
376
377    /// Encode the object header to a byte vector, including "OHDR" signature
378    /// and trailing checksum, with every message in chunk 0.
379    ///
380    /// Fails when any message payload exceeds [`MAX_MESSAGE_SIZE`] — see
381    /// `check_message_sizes`.
382    pub fn encode(&self) -> FormatResult<Vec<u8>> {
383        self.encode_chunk0(&self.messages, self.messages_data_size())
384    }
385
386    /// Chunk 0 holding `messages`, with a message area of exactly `data_size`
387    /// bytes — the sole producer of a version-2 chunk-0 image.
388    fn encode_chunk0(
389        &self,
390        messages: &[ObjectHeaderMessage],
391        data_size: usize,
392    ) -> FormatResult<Vec<u8>> {
393        Self::check_message_sizes(messages)?;
394        debug_assert!(data_size >= self.messages_size(messages));
395        let total = self.prefix_size() + data_size + 4; // + checksum
396        let mut buf = Vec::with_capacity(total);
397
398        buf.extend_from_slice(&OHDR_SIGNATURE);
399        buf.push(OHDR_VERSION);
400        buf.push(self.encoded_flags());
401
402        // Optional timestamps (bit 5), in `H5O__cache_serialize` order.
403        if let Some(t) = self.times {
404            for field in [t.access, t.modification, t.change, t.birth] {
405                buf.extend_from_slice(&field.to_le_bytes());
406            }
407        }
408
409        // Optional attr storage thresholds (bit 4) -- write defaults if enabled
410        if self.flags & FLAG_NON_DEFAULT_ATTR_THRESHOLDS != 0 {
411            // max_compact = 8, min_dense = 6 (HDF5 defaults)
412            buf.extend_from_slice(&8u16.to_le_bytes());
413            buf.extend_from_slice(&6u16.to_le_bytes());
414        }
415
416        let csb = self.chunk0_size_bytes();
417        buf.extend_from_slice(&(data_size as u64).to_le_bytes()[..csb]);
418
419        self.write_messages(&mut buf, messages, data_size);
420
421        // Checksum over everything before the checksum
422        let cksum = checksum_metadata(&buf);
423        buf.extend_from_slice(&cksum.to_le_bytes());
424
425        debug_assert_eq!(buf.len(), total);
426        Ok(buf)
427    }
428
429    /// A continuation chunk holding `messages`: the `"OCHK"` signature, the
430    /// messages, and the Jenkins checksum over both, exactly as
431    /// `H5O__chunk_serialize` writes one. Sized to fit, so it needs no
432    /// padding.
433    fn encode_continuation(&self, messages: &[ObjectHeaderMessage]) -> FormatResult<Vec<u8>> {
434        Self::check_message_sizes(messages)?;
435        let data_size = self.messages_size(messages);
436        let mut buf = Vec::with_capacity(OCHK_SIGNATURE.len() + data_size + 4);
437        buf.extend_from_slice(&OCHK_SIGNATURE);
438        self.write_messages(&mut buf, messages, data_size);
439        let cksum = checksum_metadata(&buf);
440        buf.extend_from_slice(&cksum.to_le_bytes());
441        Ok(buf)
442    }
443
444    /// How this header divides between chunk 0 and its continuation chunk
445    /// when chunk 0's message area holds at most `capacity` bytes.
446    ///
447    /// The whole point of a plan is that both sizes are known before either
448    /// chunk has an address: the caller allocates the continuation from
449    /// [`continuation_size`](ChunkPlan::continuation_size) and hands the
450    /// address back to [`encode_chunked`](Self::encode_chunked), which is the
451    /// only way chunk 0 can name a block that does not exist yet.
452    ///
453    /// Messages fill chunk 0 in order and the rest go to the continuation, so
454    /// a message never moves ahead of one that was written before it.
455    pub fn plan_chunks(&self, capacity: usize, ctx: &FormatContext) -> FormatResult<ChunkPlan> {
456        let exact = self.messages_data_size();
457        if exact <= capacity {
458            return Ok(ChunkPlan {
459                split: self.messages.len(),
460                chunk0_size: self.prefix_size() + exact + 4,
461                continuation_size: 0,
462            });
463        }
464        // Chunk 0 has to keep room for the message naming the continuation,
465        // whose body is the block's address and length (`H5O_CONT_ID`).
466        let envelope = self.message_envelope_size();
467        let continuation_message = envelope + ctx.sizeof_addr as usize + ctx.sizeof_size as usize;
468        if capacity < continuation_message {
469            return Err(FormatError::InvalidData(format!(
470                "an object header chunk-0 capacity of {capacity} bytes cannot hold the \
471                 {continuation_message}-byte message naming its continuation chunk"
472            )));
473        }
474        let mut used = continuation_message;
475        let mut split = 0;
476        for msg in &self.messages {
477            let size = envelope + msg.data.len();
478            if used + size > capacity {
479                break;
480            }
481            used += size;
482            split += 1;
483        }
484        let spilled = self.messages_size(&self.messages[split..]);
485        Ok(ChunkPlan {
486            split,
487            chunk0_size: self.prefix_size() + capacity + 4,
488            continuation_size: OCHK_SIGNATURE.len() + spilled + 4,
489        })
490    }
491
492    /// Encode this header as `plan` divides it, with the continuation chunk at
493    /// `continuation_addr`.
494    ///
495    /// Returns chunk 0 and, when the plan spills, the continuation chunk's
496    /// image. `continuation_addr` is ignored for a plan that does not spill.
497    pub fn encode_chunked(
498        &self,
499        plan: &ChunkPlan,
500        ctx: &FormatContext,
501        continuation_addr: u64,
502    ) -> FormatResult<(Vec<u8>, Option<Vec<u8>>)> {
503        if plan.continuation_size == 0 {
504            return Ok((self.encode()?, None));
505        }
506        let mut chunk0: Vec<ObjectHeaderMessage> = self.messages[..plan.split].to_vec();
507        let sa = ctx.sizeof_addr as usize;
508        let ss = ctx.sizeof_size as usize;
509        let mut body = Vec::with_capacity(sa + ss);
510        body.extend_from_slice(&continuation_addr.to_le_bytes()[..sa]);
511        body.extend_from_slice(&(plan.continuation_size as u64).to_le_bytes()[..ss]);
512        chunk0.push(ObjectHeaderMessage {
513            msg_type: MSG_CONTINUATION,
514            flags: 0x00,
515            creation_index: 0,
516            data: body,
517        });
518        let data_size = plan.chunk0_size - self.prefix_size() - 4;
519        let continuation = self.encode_continuation(&self.messages[plan.split..])?;
520        debug_assert_eq!(continuation.len(), plan.continuation_size);
521        Ok((self.encode_chunk0(&chunk0, data_size)?, Some(continuation)))
522    }
523
524    /// Decode an object header from a byte buffer. Returns the parsed header
525    /// and the number of bytes consumed from the buffer.
526    pub fn decode(buf: &[u8]) -> FormatResult<(Self, usize)> {
527        // Minimum: OHDR(4) + version(1) + flags(1) + chunk0_size(1) + checksum(4) = 11
528        if buf.len() < 11 {
529            return Err(FormatError::BufferTooShort {
530                needed: 11,
531                available: buf.len(),
532            });
533        }
534
535        // Signature
536        if buf[0..4] != OHDR_SIGNATURE {
537            return Err(FormatError::InvalidSignature);
538        }
539
540        // Version
541        let version = buf[4];
542        if version != OHDR_VERSION {
543            return Err(FormatError::InvalidVersion(version));
544        }
545
546        // Bit 5 is stripped here and carried by `times` instead, so the two
547        // can only ever agree — see [`ObjectHeader::flags`].
548        let flags = buf[5] & !FLAG_STORE_TIMESTAMPS;
549        let mut pos: usize = 6;
550
551        // Optional timestamps (bit 5)
552        let times = if buf[5] & FLAG_STORE_TIMESTAMPS != 0 {
553            if buf.len() < pos + 16 {
554                return Err(FormatError::BufferTooShort {
555                    needed: pos + 16,
556                    available: buf.len(),
557                });
558            }
559            let read = |off: usize| {
560                u32::from_le_bytes([buf[off], buf[off + 1], buf[off + 2], buf[off + 3]])
561            };
562            let t = ObjectTimes {
563                access: read(pos),
564                modification: read(pos + 4),
565                change: read(pos + 8),
566                birth: read(pos + 12),
567            };
568            pos += 16;
569            Some(t)
570        } else {
571            None
572        };
573
574        // Optional attr storage thresholds (bit 4)
575        if flags & FLAG_NON_DEFAULT_ATTR_THRESHOLDS != 0 {
576            if buf.len() < pos + 4 {
577                return Err(FormatError::BufferTooShort {
578                    needed: pos + 4,
579                    available: buf.len(),
580                });
581            }
582            // Skip thresholds for now
583            pos += 4;
584        }
585
586        // Chunk0 data size
587        let chunk0_size_bytes = match flags & FLAG_SIZE_MASK {
588            0 => 1,
589            1 => 2,
590            2 => 4,
591            3 => 8,
592            _ => unreachable!(),
593        };
594
595        if buf.len() < pos + chunk0_size_bytes {
596            return Err(FormatError::BufferTooShort {
597                needed: pos + chunk0_size_bytes,
598                available: buf.len(),
599            });
600        }
601
602        let chunk0_data_size =
603            crate::format::bytes::read_le_uint(&buf[pos..], chunk0_size_bytes) as usize;
604        pos += chunk0_size_bytes;
605
606        // We need chunk0_data_size bytes of messages + 4 bytes of checksum.
607        // chunk0_data_size is a file field up to 8 bytes wide; guard the
608        // addition so a crafted absurd value yields a clean error instead of
609        // an overflow panic (debug) or wrap (release).
610        let total_consumed = pos
611            .checked_add(chunk0_data_size)
612            .and_then(|x| x.checked_add(4))
613            .ok_or_else(|| {
614                FormatError::InvalidData("object header chunk-0 size overflows usize".into())
615            })?;
616        if buf.len() < total_consumed {
617            return Err(FormatError::BufferTooShort {
618                needed: total_consumed,
619                available: buf.len(),
620            });
621        }
622
623        // Verify checksum: covers everything from start up to (but not
624        // including) the 4-byte checksum.
625        let data_end = total_consumed - 4;
626        let stored_cksum = u32::from_le_bytes([
627            buf[data_end],
628            buf[data_end + 1],
629            buf[data_end + 2],
630            buf[data_end + 3],
631        ]);
632        let computed_cksum = checksum_metadata(&buf[..data_end]);
633        if stored_cksum != computed_cksum {
634            return Err(FormatError::ChecksumMismatch {
635                expected: stored_cksum,
636                computed: computed_cksum,
637            });
638        }
639
640        // Parse messages
641        let has_creation_order = flags & FLAG_ATTR_CREATION_ORDER_TRACKED != 0;
642        let messages_end = pos + chunk0_data_size;
643        let mut messages = Vec::new();
644
645        while pos < messages_end {
646            // Each message: type(1) + size(2) + flags(1) [+ creation_order(2)]
647            let msg_header_size = if has_creation_order { 6 } else { 4 };
648            if pos + msg_header_size > messages_end {
649                // libhdf5 (H5O__chunk_deserialize) permits a gap smaller than
650                // one message header at the end of a v2 chunk; treat the
651                // remaining bytes as such a gap rather than an error.
652                break;
653            }
654
655            let msg_type = buf[pos];
656            let msg_data_size = u16::from_le_bytes([buf[pos + 1], buf[pos + 2]]) as usize;
657            let msg_flags = buf[pos + 3];
658            pos += 4;
659
660            let creation_index = if has_creation_order {
661                let v = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
662                pos += 2;
663                v
664            } else {
665                0
666            };
667
668            if pos + msg_data_size > messages_end {
669                return Err(FormatError::InvalidData(format!(
670                    "message data ({} bytes) extends past chunk0 boundary",
671                    msg_data_size
672                )));
673            }
674
675            let data = buf[pos..pos + msg_data_size].to_vec();
676            pos += msg_data_size;
677
678            messages.push(ObjectHeaderMessage {
679                msg_type,
680                flags: msg_flags,
681                creation_index,
682                data,
683            });
684        }
685
686        Ok((
687            ObjectHeader {
688                flags,
689                times,
690                messages,
691            },
692            total_consumed,
693        ))
694    }
695}
696
697impl Default for ObjectHeader {
698    fn default() -> Self {
699        Self::new()
700    }
701}
702
703// =========================================================================
704// Object Header v1 — for reading and writing legacy HDF5 files
705// =========================================================================
706
707/// `H5O_ALIGN_OLD` (H5Opkg.h:57): version 1 rounds every message body, and the
708/// header prefix, up to a multiple of 8.
709fn align_old(n: usize) -> usize {
710    (n + 7) & !7
711}
712
713/// `H5O_SIZEOF_HDR` for version 1 (H5Opkg.h:85): `H5O_ALIGN_OLD(1 + 1 + 2 + 4
714/// + 4)` — the four trailing bytes are the alignment pad, not a field.
715const V1_PREFIX_SIZE: usize = 16;
716
717/// `H5O_SIZEOF_MSGHDR_VERS` for version 1 (H5Opkg.h:112):
718/// `H5O_ALIGN_OLD(2 + 2 + 1 + 3)`.
719const V1_MSG_HEADER_SIZE: usize = 8;
720
721impl ObjectHeader {
722    /// Encode this header in the version-1 format, with `nlink` as the object
723    /// reference count.
724    ///
725    /// The differences from [`encode`](Self::encode) are all
726    /// `H5O_ALIGN_OLD`'s doing: no signature and no checksum, a two-byte
727    /// message type, three reserved bytes where version 2 puts the optional
728    /// creation index, and every message body padded out to eight bytes with
729    /// the *padded* length in the size field (`H5O_msg_flush` writes
730    /// `mesg->raw_size`, which `H5O__alloc` already aligned). The message
731    /// count is the count of messages in the whole header, and this writer
732    /// emits one chunk, so it is `self.messages.len()`.
733    ///
734    /// Refuses a header carrying attribute-creation-order flags: those bits
735    /// live in the version-2 flags byte, which version 1 does not have, so
736    /// encoding such a header would silently drop the policy the caller set.
737    ///
738    /// Refuses a header carrying [`times`](Self::times) for the same reason.
739    /// The four times and the `H5O_HDR_STORE_TIMES` bit that announces them
740    /// are version-2 prefix fields; a version-1 header records at most a
741    /// modification time, in an `H5O_MTIME_NEW` message among the messages
742    /// below. Refusing keeps the version gate at the one encoder that knows
743    /// which prefix it is writing, instead of letting a v2-shaped header reach
744    /// `encode_v1` and come back out with its times gone.
745    pub fn encode_v1(&self, nlink: u32) -> FormatResult<Vec<u8>> {
746        if self.flags & (FLAG_ATTR_CREATION_ORDER_TRACKED | FLAG_ATTR_CREATION_ORDER_INDEXED) != 0 {
747            return Err(FormatError::InvalidData(
748                "a version-1 object header cannot record attribute creation order: \
749                 the tracking flags exist only in the version-2 header prefix"
750                    .into(),
751            ));
752        }
753        if self.times.is_some() {
754            return Err(FormatError::InvalidData(
755                "a version-1 object header cannot store access/modification/change/birth \
756                 times: they are version-2 prefix fields, and version 1 carries only a \
757                 modification time, as an H5O_MTIME_NEW message"
758                    .into(),
759            ));
760        }
761        let mut data_size = 0usize;
762        for msg in &self.messages {
763            let padded = align_old(msg.data.len());
764            if padded > MAX_MESSAGE_SIZE {
765                return Err(FormatError::InvalidData(format!(
766                    "object header message type 0x{:02X} is {} bytes, {padded} once \
767                     aligned to 8, over the {MAX_MESSAGE_SIZE}-byte limit the message \
768                     size field can express",
769                    msg.msg_type,
770                    msg.data.len()
771                )));
772            }
773            data_size += V1_MSG_HEADER_SIZE + padded;
774        }
775        let Ok(chunk0_size) = u32::try_from(data_size) else {
776            return Err(FormatError::InvalidData(format!(
777                "version-1 object header chunk 0 is {data_size} bytes, over the 4-byte \
778                 size field's range"
779            )));
780        };
781        let Ok(nmesgs) = u16::try_from(self.messages.len()) else {
782            return Err(FormatError::InvalidData(format!(
783                "version-1 object header holds {} messages, over the 2-byte count \
784                 field's range",
785                self.messages.len()
786            )));
787        };
788
789        let total = V1_PREFIX_SIZE + data_size;
790        let mut buf = Vec::with_capacity(total);
791        buf.push(1); // version
792        buf.push(0); // reserved
793        buf.extend_from_slice(&nmesgs.to_le_bytes());
794        buf.extend_from_slice(&nlink.to_le_bytes());
795        buf.extend_from_slice(&chunk0_size.to_le_bytes());
796        buf.extend_from_slice(&[0u8; 4]); // pad to H5O_ALIGN_OLD(12)
797
798        for msg in &self.messages {
799            let padded = align_old(msg.data.len());
800            buf.extend_from_slice(&u16::from(msg.msg_type).to_le_bytes());
801            buf.extend_from_slice(&(padded as u16).to_le_bytes());
802            buf.push(msg.flags);
803            buf.extend_from_slice(&[0u8; 3]); // reserved
804            buf.extend_from_slice(&msg.data);
805            buf.resize(buf.len() + (padded - msg.data.len()), 0);
806        }
807
808        debug_assert_eq!(buf.len(), total);
809        Ok(buf)
810    }
811
812    /// Encode this header in the version `format` calls for.
813    ///
814    /// `nlink` reaches the file only in the version-1 layout; the version-2
815    /// header has no reference-count field (an object with more than one hard
816    /// link carries an Object Reference Count message instead).
817    pub fn encode_for(
818        &self,
819        format: crate::format::ObjectFormat,
820        nlink: u32,
821    ) -> FormatResult<Vec<u8>> {
822        match format {
823            crate::format::ObjectFormat::Legacy => self.encode_v1(nlink),
824            crate::format::ObjectFormat::Modern => self.encode(),
825        }
826    }
827}
828
829impl ObjectHeader {
830    /// Decode a v1 object header from a byte buffer.
831    ///
832    /// v1 headers do NOT have the "OHDR" signature or a checksum. The layout is:
833    /// ```text
834    /// Byte 0: version = 1
835    /// Byte 1: reserved
836    /// Bytes 2-3: num_messages (u16 LE)
837    /// Bytes 4-7: obj_ref_count (u32 LE)
838    /// Bytes 8-11: header_data_size (u32 LE) — size of message data in first chunk
839    /// Messages follow, each:
840    ///   type: u16 LE
841    ///   data_size: u16 LE
842    ///   flags: u8
843    ///   reserved: 3 bytes
844    ///   data: data_size bytes (padded to 8-byte alignment)
845    /// ```
846    pub fn decode_v1(buf: &[u8]) -> FormatResult<(Self, usize)> {
847        // V1 header prefix is 16 bytes: version(1) + reserved(1) + num_msg(2)
848        // + ref_count(4) + chunk0_data_size(4) + reserved_padding(4)
849        if buf.len() < 16 {
850            return Err(FormatError::BufferTooShort {
851                needed: 16,
852                available: buf.len(),
853            });
854        }
855
856        let version = buf[0];
857        if version != 1 {
858            return Err(FormatError::InvalidVersion(version));
859        }
860
861        // buf[1] = reserved
862        let num_messages = u16::from_le_bytes([buf[2], buf[3]]) as usize;
863        let _obj_ref_count = u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]);
864        let header_data_size = u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]) as usize;
865        // buf[12..16] = reserved alignment padding
866
867        let total_consumed = 16 + header_data_size;
868        if buf.len() < total_consumed {
869            return Err(FormatError::BufferTooShort {
870                needed: total_consumed,
871                available: buf.len(),
872            });
873        }
874
875        let msg_data_start = 16; // offset where message data begins (after 16-byte prefix)
876        let mut pos = msg_data_start;
877        let messages_end = msg_data_start + header_data_size;
878        let mut messages = Vec::with_capacity(num_messages);
879
880        for _ in 0..num_messages {
881            if pos + 8 > messages_end {
882                break; // no more room for a message header
883            }
884
885            let msg_type = u16::from_le_bytes([buf[pos], buf[pos + 1]]);
886            let data_size = u16::from_le_bytes([buf[pos + 2], buf[pos + 3]]) as usize;
887            let msg_flags = buf[pos + 4];
888            // bytes pos+5..pos+8 are reserved
889            pos += 8;
890
891            if pos + data_size > messages_end {
892                return Err(FormatError::InvalidData(format!(
893                    "v1 message data ({} bytes) extends past header boundary",
894                    data_size
895                )));
896            }
897
898            let data = buf[pos..pos + data_size].to_vec();
899            pos += data_size;
900
901            // In v1, messages are padded to 8-byte alignment relative to
902            // the start of the message data region.
903            let rel = pos - msg_data_start;
904            let aligned_rel = (rel + 7) & !7;
905            let aligned_pos = msg_data_start + aligned_rel;
906            if aligned_pos <= messages_end {
907                pos = aligned_pos;
908            }
909
910            // Skip null/padding messages (type 0)
911            if msg_type == 0 {
912                continue;
913            }
914
915            messages.push(ObjectHeaderMessage {
916                msg_type: msg_type as u8,
917                flags: msg_flags,
918                // A version-1 message envelope has no creation index.
919                creation_index: 0,
920                data,
921            });
922        }
923
924        Ok((
925            ObjectHeader {
926                flags: 0x02, // default flags (not meaningful for v1)
927                // A v1 header keeps its modification time in a message
928                // (`H5O_MSG_MTIME`), never in the prefix.
929                times: None,
930                messages,
931            },
932            total_consumed,
933        ))
934    }
935
936    /// Auto-detect and decode either v1 or v2 object header.
937    ///
938    /// Checks for the "OHDR" signature to decide v2; otherwise tries v1.
939    pub fn decode_any(buf: &[u8]) -> FormatResult<(Self, usize)> {
940        if buf.len() >= 4 && buf[0..4] == OHDR_SIGNATURE {
941            Self::decode(buf)
942        } else if !buf.is_empty() && buf[0] == 1 {
943            Self::decode_v1(buf)
944        } else {
945            // Try v2 first (will fail with proper error)
946            Self::decode(buf)
947        }
948    }
949}
950
951#[cfg(test)]
952mod tests_v1 {
953    use super::*;
954
955    /// Build a minimal v1 object header with given messages.
956    fn build_v1_header(messages: &[(u16, u8, &[u8])]) -> Vec<u8> {
957        let mut msg_data = Vec::new();
958        for (msg_type, flags, data) in messages {
959            msg_data.extend_from_slice(&msg_type.to_le_bytes());
960            msg_data.extend_from_slice(&(data.len() as u16).to_le_bytes());
961            msg_data.push(*flags);
962            msg_data.extend_from_slice(&[0u8; 3]); // reserved
963            msg_data.extend_from_slice(data);
964            // Pad to 8-byte alignment
965            let aligned = (msg_data.len() + 7) & !7;
966            msg_data.resize(aligned, 0);
967        }
968
969        let mut buf = Vec::new();
970        buf.push(1); // version
971        buf.push(0); // reserved
972        buf.extend_from_slice(&(messages.len() as u16).to_le_bytes());
973        buf.extend_from_slice(&1u32.to_le_bytes()); // ref count
974        buf.extend_from_slice(&(msg_data.len() as u32).to_le_bytes());
975        buf.extend_from_slice(&[0u8; 4]); // reserved padding (align to 16 bytes)
976        buf.extend_from_slice(&msg_data);
977        buf
978    }
979
980    #[test]
981    fn test_decode_v1_empty() {
982        let buf = build_v1_header(&[]);
983        let (hdr, consumed) = ObjectHeader::decode_v1(&buf).unwrap();
984        assert_eq!(consumed, 16); // 16-byte prefix, no messages
985        assert!(hdr.messages.is_empty());
986    }
987
988    #[test]
989    fn test_decode_v1_single_message() {
990        let data = vec![0xAA, 0xBB, 0xCC];
991        let buf = build_v1_header(&[(0x03, 0x00, &data)]);
992        let (hdr, _consumed) = ObjectHeader::decode_v1(&buf).unwrap();
993        assert_eq!(hdr.messages.len(), 1);
994        assert_eq!(hdr.messages[0].msg_type, 0x03);
995        assert_eq!(hdr.messages[0].data, data);
996    }
997
998    #[test]
999    fn test_decode_v1_multiple_messages() {
1000        let buf = build_v1_header(&[
1001            (0x01, 0x00, &[1, 2, 3, 4]),
1002            (0x03, 0x01, &[10, 20]),
1003            (0x08, 0x00, &[0xFF; 16]),
1004        ]);
1005        let (hdr, _) = ObjectHeader::decode_v1(&buf).unwrap();
1006        assert_eq!(hdr.messages.len(), 3);
1007        assert_eq!(hdr.messages[0].msg_type, 0x01);
1008        assert_eq!(hdr.messages[1].msg_type, 0x03);
1009        assert_eq!(hdr.messages[2].msg_type, 0x08);
1010        assert_eq!(hdr.messages[2].data, vec![0xFF; 16]);
1011    }
1012
1013    /// The exact bytes h5py 3.15/libhdf5 1.14.6 wrote for the header of a
1014    /// contiguous `<i4` dataset of shape (6,) in a default (superblock-0)
1015    /// file: five messages, the last a null pad, chunk 0 of 256 bytes. Only
1016    /// the first four are re-encoded here — the pad is libhdf5 pre-allocating
1017    /// room to grow, not content — so the assertion is on the prefix shape and
1018    /// on each message's aligned envelope.
1019    #[test]
1020    fn an_encoded_v1_header_matches_the_envelope_libhdf5_writes() {
1021        let dataspace = vec![
1022            0x01, 0x01, 0x01, 0x00, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0, 6, 0, 0, 0, 0, 0, 0, 0,
1023        ];
1024        let datatype = vec![0x10, 0x08, 0, 0, 0x04, 0, 0, 0, 0, 0, 0x20, 0, 0, 0, 0, 0];
1025        let fill = vec![0x02, 0x02, 0x02, 0x01, 0, 0, 0, 0];
1026        // 18 raw bytes: version 3 contiguous layout, address then size.
1027        let layout = vec![
1028            0x03, 0x01, 0, 0x08, 0, 0, 0, 0, 0, 0, 0x18, 0, 0, 0, 0, 0, 0, 0,
1029        ];
1030        let mut header = ObjectHeader::new();
1031        header.add_message(0x01, 0x00, dataspace);
1032        header.add_message(0x03, 0x01, datatype);
1033        header.add_message(0x05, 0x01, fill);
1034        header.add_message(0x08, 0x00, layout);
1035
1036        let buf = header.encode_v1(1).unwrap();
1037        assert_eq!(buf[0], 1, "version");
1038        assert_eq!(u16::from_le_bytes([buf[2], buf[3]]), 4, "message count");
1039        assert_eq!(
1040            u32::from_le_bytes([buf[4], buf[5], buf[6], buf[7]]),
1041            1,
1042            "reference count"
1043        );
1044        // (8 + 24) + (8 + 16) + (8 + 8) + (8 + 24), the layout body aligned
1045        // from 18 to 24.
1046        assert_eq!(
1047            u32::from_le_bytes([buf[8], buf[9], buf[10], buf[11]]),
1048            104,
1049            "chunk 0 data size"
1050        );
1051        assert_eq!(buf.len(), 16 + 104);
1052        // The layout message's size field records the aligned length.
1053        let layout_at = 16 + 32 + 24 + 16;
1054        assert_eq!(u16::from_le_bytes([buf[layout_at], buf[layout_at + 1]]), 8);
1055        assert_eq!(
1056            u16::from_le_bytes([buf[layout_at + 2], buf[layout_at + 3]]),
1057            24
1058        );
1059        assert_eq!(&buf[layout_at + 8 + 18..layout_at + 8 + 24], &[0u8; 6]);
1060    }
1061
1062    #[test]
1063    fn a_v1_header_round_trips_through_its_own_decoder() {
1064        let mut header = ObjectHeader::new();
1065        header.add_message(0x11, 0x00, vec![0xAB; 16]);
1066        header.add_message(0x0C, 0x00, vec![0xCD; 21]);
1067        let buf = header.encode_v1(3).unwrap();
1068        let (back, consumed) = ObjectHeader::decode_v1(&buf).unwrap();
1069        assert_eq!(consumed, buf.len());
1070        assert_eq!(back.messages.len(), 2);
1071        assert_eq!(back.messages[0].data, vec![0xAB; 16]);
1072        // The 21-byte body came back padded to 24, so re-encoding is stable.
1073        assert_eq!(back.messages[1].data.len(), 24);
1074        assert_eq!(back.encode_v1(3).unwrap(), buf);
1075        // `decode_any` must not mistake it for a version-2 header.
1076        assert_eq!(ObjectHeader::decode_any(&buf).unwrap().1, buf.len());
1077    }
1078
1079    #[test]
1080    fn a_v1_header_refuses_to_drop_attribute_creation_order() {
1081        let mut header = ObjectHeader::new();
1082        header.set_attribute_creation_order(CreationOrder::Tracked);
1083        assert!(matches!(
1084            header.encode_v1(1).unwrap_err(),
1085            FormatError::InvalidData(_)
1086        ));
1087    }
1088
1089    /// The times gate is the header version, not the caller: a version-1
1090    /// prefix has nowhere to put them, so `encode_v1` says so instead of
1091    /// returning a header whose times are gone.
1092    #[test]
1093    fn a_v1_header_refuses_to_drop_its_stored_times() {
1094        let mut header = ObjectHeader::new();
1095        header.add_message(0x11, 0x00, vec![0u8; 16]);
1096        assert!(header.encode_v1(1).is_ok());
1097
1098        header.times = Some(ObjectTimes::created_at(0x1234_5678));
1099        assert!(matches!(
1100            header.encode_v1(1).unwrap_err(),
1101            FormatError::InvalidData(_)
1102        ));
1103        // The same header is fine as version 2, where the prefix holds them.
1104        let v2 = header.encode().unwrap();
1105        assert_eq!(v2[5] & FLAG_STORE_TIMESTAMPS, FLAG_STORE_TIMESTAMPS);
1106    }
1107
1108    #[test]
1109    fn encode_for_picks_the_version_the_format_calls_for() {
1110        use crate::format::ObjectFormat;
1111        let mut header = ObjectHeader::new();
1112        header.add_message(0x11, 0x00, vec![0u8; 16]);
1113        assert_eq!(header.encode_for(ObjectFormat::Legacy, 1).unwrap()[0], 1);
1114        assert_eq!(
1115            &header.encode_for(ObjectFormat::Modern, 1).unwrap()[0..4],
1116            &OHDR_SIGNATURE
1117        );
1118    }
1119
1120    #[test]
1121    fn test_decode_v1_skips_null_messages() {
1122        let buf = build_v1_header(&[
1123            (0x00, 0x00, &[0; 8]), // null message (type 0)
1124            (0x03, 0x00, &[1, 2]),
1125        ]);
1126        let (hdr, _) = ObjectHeader::decode_v1(&buf).unwrap();
1127        assert_eq!(hdr.messages.len(), 1);
1128        assert_eq!(hdr.messages[0].msg_type, 0x03);
1129    }
1130
1131    #[test]
1132    fn test_decode_any_v2() {
1133        let mut hdr = ObjectHeader::new();
1134        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
1135        let encoded = hdr.encode().unwrap();
1136        let (decoded, _) = ObjectHeader::decode_any(&encoded).unwrap();
1137        assert_eq!(decoded.messages.len(), 1);
1138    }
1139
1140    #[test]
1141    fn test_decode_any_v1() {
1142        let buf = build_v1_header(&[(0x03, 0x00, &[1, 2])]);
1143        let (decoded, _) = ObjectHeader::decode_any(&buf).unwrap();
1144        assert_eq!(decoded.messages.len(), 1);
1145        assert_eq!(decoded.messages[0].msg_type, 0x03);
1146    }
1147
1148    #[test]
1149    fn test_decode_v1_bad_version() {
1150        let mut buf = build_v1_header(&[]);
1151        buf[0] = 5;
1152        assert!(matches!(
1153            ObjectHeader::decode_v1(&buf).unwrap_err(),
1154            FormatError::InvalidVersion(5)
1155        ));
1156    }
1157
1158    #[test]
1159    fn test_decode_v1_buffer_too_short() {
1160        assert!(matches!(
1161            ObjectHeader::decode_v1(&[1, 0, 0]).unwrap_err(),
1162            FormatError::BufferTooShort { .. }
1163        ));
1164    }
1165}
1166
1167#[cfg(test)]
1168mod tests {
1169    use super::*;
1170
1171    #[test]
1172    fn test_empty_header_roundtrip() {
1173        let hdr = ObjectHeader::new();
1174        let encoded = hdr.encode().unwrap();
1175
1176        // OHDR(4) + version(1) + flags(1) + chunk0_size(4) + checksum(4) = 14
1177        assert_eq!(encoded.len(), 14);
1178        assert_eq!(&encoded[..4], b"OHDR");
1179        assert_eq!(encoded[4], 2); // version
1180
1181        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1182        assert_eq!(consumed, encoded.len());
1183        assert_eq!(decoded, hdr);
1184    }
1185
1186    #[test]
1187    fn test_single_message_roundtrip() {
1188        let mut hdr = ObjectHeader::new();
1189        hdr.add_message(0x01, 0x00, vec![0xAA, 0xBB, 0xCC]);
1190
1191        let encoded = hdr.encode().unwrap();
1192        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1193        assert_eq!(consumed, encoded.len());
1194        assert_eq!(decoded.messages.len(), 1);
1195        assert_eq!(decoded.messages[0].msg_type, 0x01);
1196        assert_eq!(decoded.messages[0].flags, 0x00);
1197        assert_eq!(decoded.messages[0].data, vec![0xAA, 0xBB, 0xCC]);
1198    }
1199
1200    #[test]
1201    fn test_multiple_messages_roundtrip() {
1202        let mut hdr = ObjectHeader::new();
1203        hdr.add_message(0x01, 0x00, vec![1, 2, 3, 4]);
1204        hdr.add_message(0x03, 0x01, vec![10, 20]);
1205        hdr.add_message(0x0C, 0x00, vec![]);
1206
1207        let encoded = hdr.encode().unwrap();
1208        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1209        assert_eq!(consumed, encoded.len());
1210        assert_eq!(decoded.messages.len(), 3);
1211        assert_eq!(decoded, hdr);
1212    }
1213
1214    #[test]
1215    fn test_with_creation_order() {
1216        let mut hdr = ObjectHeader {
1217            flags: 0x02 | FLAG_ATTR_CREATION_ORDER_TRACKED,
1218            times: None,
1219            messages: Vec::new(),
1220        };
1221        hdr.add_message(0x01, 0x00, vec![0xFF; 8]);
1222        hdr.add_message(0x03, 0x00, vec![0xEE; 4]);
1223
1224        let encoded = hdr.encode().unwrap();
1225        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1226        assert_eq!(consumed, encoded.len());
1227        assert_eq!(decoded.messages.len(), 2);
1228        assert_eq!(decoded.messages[0].data, vec![0xFF; 8]);
1229        assert_eq!(decoded.messages[1].data, vec![0xEE; 4]);
1230    }
1231
1232    /// The per-message creation index survives a round trip, and lands where
1233    /// libhdf5 puts it: right after the message flags byte, ahead of the
1234    /// message data.
1235    #[test]
1236    fn a_tracked_header_round_trips_each_message_creation_index() {
1237        let mut hdr = ObjectHeader::new();
1238        hdr.set_attribute_creation_order(CreationOrder::Indexed);
1239        hdr.add_message_indexed(0x0C, 0x00, vec![0xAA; 6], 0);
1240        hdr.add_message_indexed(0x0C, 0x00, vec![0xBB; 6], 1);
1241        hdr.add_message_indexed(0x0C, 0x00, vec![0xCC; 6], 2);
1242
1243        let encoded = hdr.encode().unwrap();
1244        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1245        assert_eq!(consumed, encoded.len());
1246        assert_eq!(decoded, hdr);
1247        let indices: Vec<u16> = decoded.messages.iter().map(|m| m.creation_index).collect();
1248        assert_eq!(indices, vec![0, 1, 2]);
1249
1250        // Off: no index is written, and every message decodes with 0.
1251        let mut plain = ObjectHeader::new();
1252        plain.add_message(0x0C, 0x00, vec![0xAA; 6]);
1253        assert!(plain.encode().unwrap().len() < encoded.len());
1254        let (plain_back, _) = ObjectHeader::decode(&plain.encode().unwrap()).unwrap();
1255        assert_eq!(plain_back.messages[0].creation_index, 0);
1256    }
1257
1258    /// The two flag bits are set and read back independently, so a header that
1259    /// tracks without indexing survives a decode as exactly that — the state
1260    /// `H5Pset_attr_creation_order(H5P_CRT_ORDER_TRACKED)` produces.
1261    #[test]
1262    fn each_attribute_creation_order_state_round_trips_through_the_flags() {
1263        for order in [
1264            CreationOrder::Untracked,
1265            CreationOrder::Tracked,
1266            CreationOrder::Indexed,
1267        ] {
1268            let mut hdr = ObjectHeader::new();
1269            hdr.set_attribute_creation_order(order);
1270            hdr.add_message_indexed(0x0C, 0x00, vec![0xAA; 6], 3);
1271            let (decoded, _) = ObjectHeader::decode(&hdr.encode().unwrap()).unwrap();
1272            assert_eq!(decoded.attribute_creation_order(), order);
1273            let want_index = if order.is_tracked() { 3 } else { 0 };
1274            assert_eq!(decoded.messages[0].creation_index, want_index);
1275        }
1276    }
1277
1278    /// Setting a policy clears whatever the previous one left behind, so a
1279    /// header recovered as indexed and re-declared untracked does not keep a
1280    /// stale bit.
1281    #[test]
1282    fn setting_a_weaker_policy_clears_the_stronger_one() {
1283        let mut hdr = ObjectHeader::new();
1284        hdr.set_attribute_creation_order(CreationOrder::Indexed);
1285        hdr.set_attribute_creation_order(CreationOrder::Untracked);
1286        assert_eq!(hdr.attribute_creation_order(), CreationOrder::Untracked);
1287        // Bits 0-1 (the chunk0 size encoding) are untouched.
1288        assert_eq!(hdr.flags, 0x02);
1289    }
1290
1291    /// The four times survive a round trip in `H5O__cache_serialize` order,
1292    /// and their presence is what sets `H5O_HDR_STORE_TIMES` in the file.
1293    #[test]
1294    fn stored_times_round_trip_and_set_the_flag() {
1295        let mut hdr = ObjectHeader::new();
1296        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
1297        let without = hdr.encode().unwrap();
1298
1299        hdr.times = Some(ObjectTimes {
1300            access: 0x0A0A_0A0A,
1301            modification: 0x0B0B_0B0B,
1302            change: 0x0C0C_0C0C,
1303            birth: 0x0D0D_0D0D,
1304        });
1305        let with = hdr.encode().unwrap();
1306
1307        assert_eq!(with.len(), without.len() + 16);
1308        assert_eq!(with[5] & FLAG_STORE_TIMESTAMPS, FLAG_STORE_TIMESTAMPS);
1309        assert_eq!(&with[6..10], &0x0A0A_0A0Au32.to_le_bytes());
1310        assert_eq!(&with[10..14], &0x0B0B_0B0Bu32.to_le_bytes());
1311        assert_eq!(&with[14..18], &0x0C0C_0C0Cu32.to_le_bytes());
1312        assert_eq!(&with[18..22], &0x0D0D_0D0Du32.to_le_bytes());
1313
1314        let (decoded, consumed) = ObjectHeader::decode(&with).expect("decode failed");
1315        assert_eq!(consumed, with.len());
1316        assert_eq!(decoded, hdr);
1317    }
1318
1319    /// The flag cannot be set without the times behind it: bit 5 poked into
1320    /// `flags` by hand is dropped at encode rather than announcing sixteen
1321    /// bytes that are not there — the shape that made a rewrite emit zero
1322    /// timestamps.
1323    #[test]
1324    fn the_timestamps_flag_is_never_written_without_times() {
1325        let mut hdr = ObjectHeader::new();
1326        hdr.flags |= FLAG_STORE_TIMESTAMPS;
1327        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
1328
1329        let encoded = hdr.encode().unwrap();
1330        assert_eq!(encoded[5] & FLAG_STORE_TIMESTAMPS, 0);
1331        let (decoded, _) = ObjectHeader::decode(&encoded).expect("decode failed");
1332        assert_eq!(decoded.times, None);
1333        assert_eq!(decoded.flags & FLAG_STORE_TIMESTAMPS, 0);
1334    }
1335
1336    /// `H5O_touch_oh` on a version-2 header moves access and change time to
1337    /// now and leaves modification and birth time alone.
1338    #[test]
1339    fn touching_moves_access_and_change_time_only() {
1340        let before = ObjectTimes {
1341            access: 100,
1342            modification: 200,
1343            change: 300,
1344            birth: 400,
1345        };
1346        assert_eq!(
1347            before.touched(999),
1348            ObjectTimes {
1349                access: 999,
1350                modification: 200,
1351                change: 999,
1352                birth: 400,
1353            }
1354        );
1355        assert_eq!(
1356            ObjectTimes::created_at(7).touched(7),
1357            ObjectTimes::created_at(7)
1358        );
1359    }
1360
1361    #[test]
1362    fn test_chunk0_size_1byte() {
1363        // flags bits 0-1 = 0 => 1-byte chunk0 size
1364        let mut hdr = ObjectHeader {
1365            flags: 0x00,
1366            times: None,
1367            messages: Vec::new(),
1368        };
1369        hdr.add_message(0x01, 0x00, vec![42]);
1370
1371        let encoded = hdr.encode().unwrap();
1372        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1373        assert_eq!(consumed, encoded.len());
1374        assert_eq!(decoded.messages[0].data, vec![42]);
1375    }
1376
1377    #[test]
1378    fn test_chunk0_size_2byte() {
1379        // flags bits 0-1 = 1 => 2-byte chunk0 size
1380        let mut hdr = ObjectHeader {
1381            flags: 0x01,
1382            times: None,
1383            messages: Vec::new(),
1384        };
1385        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
1386
1387        let encoded = hdr.encode().unwrap();
1388        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1389        assert_eq!(consumed, encoded.len());
1390        assert_eq!(decoded.messages[0].data, vec![1, 2, 3]);
1391    }
1392
1393    #[test]
1394    fn test_chunk0_size_8byte() {
1395        // flags bits 0-1 = 3 => 8-byte chunk0 size
1396        let mut hdr = ObjectHeader {
1397            flags: 0x03,
1398            times: None,
1399            messages: Vec::new(),
1400        };
1401        hdr.add_message(0x01, 0x00, vec![0xDE, 0xAD]);
1402
1403        let encoded = hdr.encode().unwrap();
1404        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1405        assert_eq!(consumed, encoded.len());
1406        assert_eq!(decoded.messages[0].data, vec![0xDE, 0xAD]);
1407    }
1408
1409    #[test]
1410    fn test_decode_bad_signature() {
1411        let mut data = vec![0u8; 20];
1412        data[0..4].copy_from_slice(b"XHDR");
1413        let err = ObjectHeader::decode(&data).unwrap_err();
1414        assert!(matches!(err, FormatError::InvalidSignature));
1415    }
1416
1417    #[test]
1418    fn test_decode_bad_version() {
1419        let hdr = ObjectHeader::new();
1420        let mut encoded = hdr.encode().unwrap();
1421        encoded[4] = 99; // corrupt version
1422        let err = ObjectHeader::decode(&encoded).unwrap_err();
1423        assert!(matches!(err, FormatError::InvalidVersion(99)));
1424    }
1425
1426    #[test]
1427    fn test_decode_checksum_mismatch() {
1428        let mut hdr = ObjectHeader::new();
1429        hdr.add_message(0x01, 0x00, vec![1, 2, 3]);
1430        let mut encoded = hdr.encode().unwrap();
1431        // Corrupt a message byte
1432        let last_data = encoded.len() - 5;
1433        encoded[last_data] ^= 0xFF;
1434        let err = ObjectHeader::decode(&encoded).unwrap_err();
1435        assert!(matches!(err, FormatError::ChecksumMismatch { .. }));
1436    }
1437
1438    #[test]
1439    fn test_decode_buffer_too_short() {
1440        let err = ObjectHeader::decode(&[0u8; 5]).unwrap_err();
1441        assert!(matches!(err, FormatError::BufferTooShort { .. }));
1442    }
1443
1444    #[test]
1445    fn test_decode_with_trailing_data() {
1446        let mut hdr = ObjectHeader::new();
1447        hdr.add_message(0x01, 0x00, vec![7, 8, 9]);
1448        let mut encoded = hdr.encode().unwrap();
1449        let original_len = encoded.len();
1450        encoded.extend_from_slice(&[0xBB; 50]); // trailing garbage
1451
1452        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1453        assert_eq!(consumed, original_len);
1454        assert_eq!(decoded, hdr);
1455    }
1456
1457    #[test]
1458    fn test_large_message_payload() {
1459        let mut hdr = ObjectHeader::new();
1460        let big_data = vec![0x42; 1000];
1461        hdr.add_message(0x0C, 0x00, big_data.clone());
1462
1463        let encoded = hdr.encode().unwrap();
1464        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1465        assert_eq!(consumed, encoded.len());
1466        assert_eq!(decoded.messages[0].data.len(), 1000);
1467        assert_eq!(decoded.messages[0].data, big_data);
1468    }
1469
1470    /// The largest payload the size field can express still round-trips.
1471    #[test]
1472    fn test_message_payload_at_size_limit() {
1473        let mut hdr = ObjectHeader::new();
1474        hdr.add_message(0x0C, 0x00, vec![0x42; MAX_MESSAGE_SIZE]);
1475
1476        let encoded = hdr.encode().expect("encode at the limit must succeed");
1477        let (decoded, consumed) = ObjectHeader::decode(&encoded).expect("decode failed");
1478        assert_eq!(consumed, encoded.len());
1479        assert_eq!(decoded.messages[0].data.len(), MAX_MESSAGE_SIZE);
1480    }
1481
1482    /// One byte past the limit is refused. Encoding it would store the length
1483    /// modulo 65536 — here 0 — and every message after it would decode from
1484    /// the middle of this one's payload, with a checksum that still matches.
1485    #[test]
1486    fn test_message_payload_over_size_limit_is_refused() {
1487        let mut hdr = ObjectHeader::new();
1488        hdr.add_message(0x01, 0x00, vec![7; 4]);
1489        hdr.add_message(0x0C, 0x00, vec![0x42; MAX_MESSAGE_SIZE + 1]);
1490
1491        let err = hdr.encode().expect_err("over the limit must not encode");
1492        let msg = err.to_string();
1493        assert!(msg.contains("0x0C"), "{msg}");
1494        assert!(msg.contains(&(MAX_MESSAGE_SIZE + 1).to_string()), "{msg}");
1495    }
1496
1497    #[test]
1498    fn test_default() {
1499        let hdr = ObjectHeader::default();
1500        assert_eq!(hdr.flags, 0x02);
1501        assert!(hdr.messages.is_empty());
1502    }
1503
1504    /// A header with three 40-byte messages, for the chunking tests below.
1505    fn chunked_header() -> ObjectHeader {
1506        let mut hdr = ObjectHeader::new();
1507        for (i, t) in [0x02u8, 0x0A, 0x0C].iter().enumerate() {
1508            hdr.add_message(*t, 0x00, vec![i as u8; 40]);
1509        }
1510        hdr
1511    }
1512
1513    /// A capacity that covers every message leaves one chunk, and the image is
1514    /// the one `encode` produces on its own.
1515    #[test]
1516    fn a_capacity_that_fits_every_message_plans_one_chunk() {
1517        let hdr = chunked_header();
1518        let ctx = FormatContext::default_v3();
1519        let plan = hdr.plan_chunks(1024, &ctx).unwrap();
1520        assert_eq!(plan.continuation_size, 0);
1521        let (chunk0, continuation) = hdr.encode_chunked(&plan, &ctx, 0x1000).unwrap();
1522        assert!(continuation.is_none());
1523        assert_eq!(chunk0, hdr.encode().unwrap());
1524        assert_eq!(chunk0.len(), plan.chunk0_size);
1525    }
1526
1527    /// Past the capacity the tail of the message list moves into an `OCHK`
1528    /// chunk, chunk 0 names it by address and length, and both chunks carry a
1529    /// checksum over their own image.
1530    #[test]
1531    fn messages_past_the_capacity_move_into_a_continuation_chunk() {
1532        let hdr = chunked_header();
1533        let ctx = FormatContext::default_v3();
1534        // Room for one 44-byte message and the 20-byte continuation message.
1535        let plan = hdr.plan_chunks(64, &ctx).unwrap();
1536        assert_eq!(plan.continuation_size, 4 + 2 * 44 + 4);
1537        let (chunk0, continuation) = hdr.encode_chunked(&plan, &ctx, 0x2000).unwrap();
1538        let continuation = continuation.unwrap();
1539        assert_eq!(chunk0.len(), plan.chunk0_size);
1540        assert_eq!(continuation.len(), plan.continuation_size);
1541        assert_eq!(&continuation[..4], &OCHK_SIGNATURE);
1542
1543        let (decoded, consumed) = ObjectHeader::decode(&chunk0).unwrap();
1544        assert_eq!(consumed, chunk0.len());
1545        assert_eq!(
1546            decoded.messages.len(),
1547            2,
1548            "the first message and the pointer"
1549        );
1550        assert_eq!(decoded.messages[0], hdr.messages[0]);
1551        let pointer = &decoded.messages[1];
1552        assert_eq!(pointer.msg_type, MSG_CONTINUATION);
1553        assert_eq!(
1554            u64::from_le_bytes(pointer.data[..8].try_into().unwrap()),
1555            0x2000
1556        );
1557        assert_eq!(
1558            u64::from_le_bytes(pointer.data[8..16].try_into().unwrap()),
1559            plan.continuation_size as u64
1560        );
1561
1562        let body = &continuation[..continuation.len() - 4];
1563        let stored = u32::from_le_bytes(continuation[continuation.len() - 4..].try_into().unwrap());
1564        assert_eq!(stored, checksum_metadata(body));
1565    }
1566
1567    /// Space left in chunk 0 becomes a NIL message when a message envelope
1568    /// fits in it, and zero bytes — a "gap" — when it does not.
1569    #[test]
1570    fn leftover_chunk_zero_space_is_a_nil_message_or_a_gap() {
1571        let hdr = chunked_header();
1572        let ctx = FormatContext::default_v3();
1573
1574        // 44 (message) + 20 (continuation) + 8 leaves room for a NIL.
1575        let (chunk0, _) = hdr
1576            .encode_chunked(&hdr.plan_chunks(72, &ctx).unwrap(), &ctx, 0x2000)
1577            .unwrap();
1578        let tail = &chunk0[chunk0.len() - 4 - 8..chunk0.len() - 4];
1579        assert_eq!(tail, [MSG_NIL, 4, 0, 0, 0, 0, 0, 0]);
1580
1581        // Three bytes over is one short of an envelope, so they stay a gap.
1582        let (chunk0, _) = hdr
1583            .encode_chunked(&hdr.plan_chunks(67, &ctx).unwrap(), &ctx, 0x2000)
1584            .unwrap();
1585        assert_eq!(&chunk0[chunk0.len() - 4 - 3..chunk0.len() - 4], [0, 0, 0]);
1586        // A gap is not a message: the decoder stops at it.
1587        let (decoded, _) = ObjectHeader::decode(&chunk0).unwrap();
1588        assert_eq!(decoded.messages.len(), 2);
1589    }
1590
1591    /// A capacity with no room for the message naming the continuation cannot
1592    /// be planned: chunk 0 would spill with nothing pointing at the spill.
1593    #[test]
1594    fn a_capacity_below_the_continuation_message_is_refused() {
1595        let hdr = chunked_header();
1596        let err = hdr
1597            .plan_chunks(19, &FormatContext::default_v3())
1598            .expect_err("19 bytes cannot hold a 20-byte continuation message");
1599        assert!(err.to_string().contains("continuation chunk"), "{err}");
1600    }
1601
1602    /// The times a version-2 header stores sit in its prefix, ahead of the
1603    /// message area a plan divides — so a header carrying them reserves
1604    /// sixteen more bytes for chunk 0 and spills at exactly the same message.
1605    ///
1606    /// `chunk0_capacity` in the writer budgets the *message* area, and the
1607    /// prefix is added on top of it here; a plan that folded the times into
1608    /// that budget would size chunk 0 sixteen bytes short of the image
1609    /// `encode_chunked` then produces, which `check_header_size` refuses.
1610    #[test]
1611    fn the_times_prefix_widens_chunk_zero_without_moving_the_split() {
1612        let ctx = FormatContext::default_v3();
1613        let plain = chunked_header();
1614        let mut timed = chunked_header();
1615        timed.times = Some(ObjectTimes::created_at(0x5EED_1234));
1616
1617        for capacity in [72usize, 120, 1024] {
1618            let a = plain.plan_chunks(capacity, &ctx).unwrap();
1619            let b = timed.plan_chunks(capacity, &ctx).unwrap();
1620            assert_eq!(a.split, b.split, "capacity {capacity}: same split");
1621            assert_eq!(
1622                a.continuation_size, b.continuation_size,
1623                "capacity {capacity}: same continuation"
1624            );
1625            assert_eq!(
1626                b.chunk0_size,
1627                a.chunk0_size + 16,
1628                "capacity {capacity}: four times of four bytes"
1629            );
1630        }
1631
1632        // And the plan describes the image: chunk 0 is the length the plan
1633        // said, times included.
1634        let plan = timed.plan_chunks(72, &ctx).unwrap();
1635        let (chunk0, continuation) = timed.encode_chunked(&plan, &ctx, 0x2000).unwrap();
1636        assert_eq!(chunk0.len(), plan.chunk0_size);
1637        assert_eq!(continuation.unwrap().len(), plan.continuation_size);
1638        let (decoded, _) = ObjectHeader::decode(&chunk0).unwrap();
1639        assert_eq!(decoded.times, timed.times);
1640    }
1641
1642    /// A version-1 header has its own continuation rules (`H5O__chunk_deserialize`
1643    /// reads a v1 chunk with no `OCHK` signature and no checksum), so the
1644    /// version-2 planner must never be applied to one. `encode_for` is where
1645    /// that holds — and it is the whole of `Hdf5Writer::encode_header_at`'s
1646    /// legacy arm: the plan is never built, every message goes in chunk 0, and
1647    /// the two version-2 prefix fields are refused outright rather than
1648    /// planned around (`tests_v1::a_v1_header_refuses_to_drop_its_stored_times`,
1649    /// `tests_v1::a_v1_header_refuses_to_drop_attribute_creation_order`).
1650    #[test]
1651    fn a_version_one_header_is_never_planned_into_chunks() {
1652        let hdr = chunked_header();
1653        // Far past any plausible chunk-0 estimate, and still one chunk.
1654        let v1 = hdr
1655            .encode_for(crate::format::ObjectFormat::Legacy, 1)
1656            .unwrap();
1657        assert_eq!(v1[0], 1, "version-1 prefix");
1658        assert!(
1659            !v1.windows(4).any(|w| w == OCHK_SIGNATURE),
1660            "a version-1 header holds no OCHK chunk"
1661        );
1662        let (decoded, _) = ObjectHeader::decode_v1(&v1).unwrap();
1663        assert_eq!(decoded.messages.len(), hdr.messages.len());
1664        assert!(
1665            !decoded
1666                .messages
1667                .iter()
1668                .any(|m| m.msg_type == MSG_CONTINUATION),
1669            "no continuation message"
1670        );
1671    }
1672}