Skip to main content

hdf5_pure/
datatype.rs

1//! HDF5 Datatype message parsing (message type 0x0003).
2//!
3//! Supports all 12 HDF5 type classes (0–11) with recursive parsing
4//! for compound, enumeration, variable-length, and array types.
5
6#[cfg(not(feature = "std"))]
7use alloc::{boxed::Box, string::String, vec, vec::Vec};
8
9use core::fmt;
10use core::num::{NonZeroU32, NonZeroUsize};
11
12use byteorder::{ByteOrder, LittleEndian};
13
14use crate::bytes::ensure_len;
15use crate::display::{DISPLAY_MAX_MEMBERS, Dims, EscapedName, QuotedBytes, write_elided};
16use crate::error::FormatError;
17
18/// Byte order of numeric data.
19#[derive(Debug, Clone, PartialEq)]
20pub enum DatatypeByteOrder {
21    LittleEndian,
22    BigEndian,
23    Vax,
24}
25
26/// String padding type.
27#[derive(Debug, Clone, PartialEq)]
28pub enum StringPadding {
29    NullTerminate,
30    NullPad,
31    SpacePad,
32}
33
34/// Character set encoding.
35#[derive(Debug, Clone, PartialEq)]
36pub enum CharacterSet {
37    Ascii,
38    Utf8,
39}
40
41/// Reference type.
42///
43/// Non-exhaustive: the format has gained reference kinds since (HDF5 1.12 added
44/// attribute references), so match with a `_` arm.
45#[derive(Debug, Clone, PartialEq)]
46#[non_exhaustive]
47pub enum ReferenceType {
48    Object,
49    DatasetRegion,
50}
51
52/// A member of a compound datatype.
53///
54/// Non-exhaustive: parsed from a datatype message, and
55/// [`CompoundTypeBuilder`](crate::CompoundTypeBuilder) builds one over an
56/// arbitrary offset and member datatype, so nothing needs to construct this
57/// directly.
58#[derive(Debug, Clone, PartialEq)]
59#[non_exhaustive]
60pub struct CompoundMember {
61    /// Member name.
62    pub name: String,
63    /// Byte offset within the compound.
64    pub byte_offset: u64,
65    /// Member datatype.
66    pub datatype: Datatype,
67}
68
69/// A member of an enumeration datatype.
70///
71/// Non-exhaustive: parsed from a datatype message, and
72/// [`EnumTypeBuilder`](crate::EnumTypeBuilder) builds one over any integer base
73/// type (`with_base` plus `raw_value`), so nothing needs to construct this
74/// directly.
75#[derive(Debug, Clone, PartialEq)]
76#[non_exhaustive]
77pub struct EnumMember {
78    /// Member name.
79    pub name: String,
80    /// Raw value bytes (length = base type size).
81    pub value: Vec<u8>,
82}
83
84/// Parsed HDF5 datatype.
85///
86/// Non-exhaustive: the format's class set is not closed (HDF5 1.14.6 added a
87/// complex-number class), so match with a `_` arm. Only the *class* set is
88/// sealed — the variants stay open, so an exotic type this crate has no
89/// constructor for can still be built as a literal, and surfacing a format field
90/// this crate currently discards (a fixed-point type's padding bits, say) would
91/// still be a breaking change.
92#[derive(Debug, Clone, PartialEq)]
93#[non_exhaustive]
94pub enum Datatype {
95    /// Class 0: Fixed-point (integer) types.
96    FixedPoint {
97        size: u32,
98        byte_order: DatatypeByteOrder,
99        signed: bool,
100        bit_offset: u16,
101        bit_precision: u16,
102    },
103    /// Class 1: Floating-point types.
104    FloatingPoint {
105        size: u32,
106        byte_order: DatatypeByteOrder,
107        bit_offset: u16,
108        bit_precision: u16,
109        exponent_location: u8,
110        exponent_size: u8,
111        mantissa_location: u8,
112        mantissa_size: u8,
113        exponent_bias: u32,
114    },
115    /// Class 2: Time type (rarely used).
116    Time {
117        size: u32,
118        byte_order: DatatypeByteOrder,
119        bit_precision: u16,
120    },
121    /// Class 3: Fixed-length string.
122    String {
123        size: u32,
124        padding: StringPadding,
125        charset: CharacterSet,
126    },
127    /// Class 4: Bit field.
128    BitField {
129        size: u32,
130        byte_order: DatatypeByteOrder,
131        bit_offset: u16,
132        bit_precision: u16,
133    },
134    /// Class 5: Opaque data.
135    Opaque { size: u32, tag: Vec<u8> },
136    /// Class 6: Compound type.
137    Compound {
138        size: u32,
139        members: Vec<CompoundMember>,
140    },
141    /// Class 7: Reference type.
142    Reference { size: u32, ref_type: ReferenceType },
143    /// Class 8: Enumeration type.
144    Enumeration {
145        size: u32,
146        base_type: Box<Datatype>,
147        members: Vec<EnumMember>,
148    },
149    /// Class 9: Variable-length type.
150    VariableLength {
151        is_string: bool,
152        padding: Option<StringPadding>,
153        charset: Option<CharacterSet>,
154        base_type: Box<Datatype>,
155    },
156    /// Class 10: Array type.
157    Array {
158        base_type: Box<Datatype>,
159        dimensions: Vec<u32>,
160    },
161}
162
163// ---- Display ----
164//
165// These types land in error messages, so `Display` is the short form: the width
166// and class, plus the fields that depart from the ordinary — a big-endian order,
167// a bit span narrower than the type. A string always names its charset and
168// padding, ordinary or not, because they decide how its bytes read. `Debug`
169// keeps the full record.
170
171impl fmt::Display for DatatypeByteOrder {
172    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
173        f.pad(match self {
174            Self::LittleEndian => "le",
175            Self::BigEndian => "be",
176            Self::Vax => "vax",
177        })
178    }
179}
180
181impl fmt::Display for StringPadding {
182    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
183        f.pad(match self {
184            Self::NullTerminate => "null-term",
185            Self::NullPad => "null-pad",
186            Self::SpacePad => "space-pad",
187        })
188    }
189}
190
191impl fmt::Display for CharacterSet {
192    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
193        f.pad(match self {
194            Self::Ascii => "ascii",
195            Self::Utf8 => "utf8",
196        })
197    }
198}
199
200impl fmt::Display for ReferenceType {
201    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
202        f.pad(match self {
203            Self::Object => "object_ref",
204            Self::DatasetRegion => "region_ref",
205        })
206    }
207}
208
209/// The width in bits of a `size`-byte type.
210///
211/// Widens first: `size` is an on-disk `u32`, so a crafted size near [`u32::MAX`]
212/// would overflow a `u32` multiply (issue #140).
213fn bit_width(size: u32) -> u64 {
214    u64::from(size) * 8
215}
216
217/// The bit span, written only when it is narrower than the whole type.
218fn write_bit_span(
219    f: &mut fmt::Formatter<'_>,
220    size: u32,
221    bit_offset: u16,
222    bit_precision: u16,
223) -> fmt::Result {
224    if bit_offset != 0 || u64::from(bit_precision) != bit_width(size) {
225        let end = u64::from(bit_offset) + u64::from(bit_precision);
226        write!(f, "(bits {bit_offset}..{end})")?;
227    }
228    Ok(())
229}
230
231/// The byte order, written only when it is not little-endian.
232fn write_byte_order(f: &mut fmt::Formatter<'_>, byte_order: &DatatypeByteOrder) -> fmt::Result {
233    if *byte_order != DatatypeByteOrder::LittleEndian {
234        write!(f, " {byte_order}")?;
235    }
236    Ok(())
237}
238
239impl fmt::Display for Datatype {
240    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
241        match self {
242            Self::FixedPoint {
243                size,
244                byte_order,
245                signed,
246                bit_offset,
247                bit_precision,
248            } => {
249                let sign = if *signed { 'i' } else { 'u' };
250                write!(f, "{sign}{}", bit_width(*size))?;
251                write_bit_span(f, *size, *bit_offset, *bit_precision)?;
252                write_byte_order(f, byte_order)
253            }
254            Self::FloatingPoint {
255                size,
256                byte_order,
257                bit_offset,
258                bit_precision,
259                ..
260            } => {
261                write!(f, "f{}", bit_width(*size))?;
262                write_bit_span(f, *size, *bit_offset, *bit_precision)?;
263                write_byte_order(f, byte_order)
264            }
265            Self::Time {
266                size,
267                byte_order,
268                bit_precision,
269            } => {
270                write!(f, "time{}", bit_width(*size))?;
271                write_bit_span(f, *size, 0, *bit_precision)?;
272                write_byte_order(f, byte_order)
273            }
274            Self::String {
275                size,
276                padding,
277                charset,
278            } => write!(f, "string[{size}] {charset} {padding}"),
279            Self::BitField {
280                size,
281                byte_order,
282                bit_offset,
283                bit_precision,
284            } => {
285                write!(f, "bitfield{}", bit_width(*size))?;
286                write_bit_span(f, *size, *bit_offset, *bit_precision)?;
287                write_byte_order(f, byte_order)
288            }
289            Self::Opaque { size, tag } => {
290                write!(f, "opaque[{size}]")?;
291                if !tag.is_empty() {
292                    write!(f, " {}", QuotedBytes(tag))?;
293                }
294                Ok(())
295            }
296            Self::Compound { members, .. } => {
297                f.write_str("compound{")?;
298                for (i, member) in members.iter().take(DISPLAY_MAX_MEMBERS).enumerate() {
299                    if i > 0 {
300                        f.write_str(", ")?;
301                    }
302                    write!(f, "{}: {}", EscapedName(&member.name), member.datatype)?;
303                }
304                write_elided(f, members.len().saturating_sub(DISPLAY_MAX_MEMBERS))?;
305                f.write_str("}")
306            }
307            Self::Reference { ref_type, .. } => write!(f, "{ref_type}"),
308            Self::Enumeration {
309                base_type, members, ..
310            } => {
311                write!(f, "enum<{base_type}>[")?;
312                for (i, member) in members.iter().take(DISPLAY_MAX_MEMBERS).enumerate() {
313                    if i > 0 {
314                        f.write_str(", ")?;
315                    }
316                    write!(f, "{}", EscapedName(&member.name))?;
317                }
318                write_elided(f, members.len().saturating_sub(DISPLAY_MAX_MEMBERS))?;
319                f.write_str("]")
320            }
321            Self::VariableLength {
322                is_string,
323                charset,
324                base_type,
325                ..
326            } => {
327                if *is_string {
328                    f.write_str("vlen_string")?;
329                    if let Some(charset) = charset {
330                        write!(f, " {charset}")?;
331                    }
332                    Ok(())
333                } else {
334                    write!(f, "vlen<{base_type}>")
335                }
336            }
337            Self::Array {
338                base_type,
339                dimensions,
340            } => write!(f, "array<{base_type}, {}>", Dims(dimensions)),
341        }
342    }
343}
344
345fn parse_string_padding(val: u8) -> Result<StringPadding, FormatError> {
346    match val {
347        0 => Ok(StringPadding::NullTerminate),
348        1 => Ok(StringPadding::NullPad),
349        2 => Ok(StringPadding::SpacePad),
350        _ => Err(FormatError::InvalidStringPadding(val)),
351    }
352}
353
354fn parse_charset(val: u8) -> Result<CharacterSet, FormatError> {
355    match val {
356        0 => Ok(CharacterSet::Ascii),
357        1 => Ok(CharacterSet::Utf8),
358        _ => Err(FormatError::InvalidCharacterSet(val)),
359    }
360}
361
362/// Read a null-terminated string from `data` starting at `offset`.
363/// Returns (string, bytes_consumed including the null terminator).
364fn read_null_terminated_string(data: &[u8], offset: usize) -> Result<(String, usize), FormatError> {
365    if offset >= data.len() {
366        return Err(FormatError::UnexpectedEof {
367            expected: offset + 1,
368            available: data.len(),
369        });
370    }
371    let remaining = &data[offset..];
372    let null_pos = remaining
373        .iter()
374        .position(|&b| b == 0)
375        .ok_or(FormatError::UnexpectedEof {
376            expected: offset + 1,
377            available: data.len(),
378        })?;
379    let name = String::from_utf8_lossy(&remaining[..null_pos]).into_owned();
380    Ok((name, null_pos + 1))
381}
382
383/// Determine how many bytes are needed to encode `compound_size` as a byte offset (v3).
384fn offset_bytes_for_size(compound_size: u32) -> usize {
385    if compound_size <= 0xFF {
386        1
387    } else if compound_size <= 0xFFFF {
388        2
389    } else {
390        4
391    }
392}
393
394/// Read an unsigned integer of 1, 2, 4, or 8 bytes (LE).
395fn read_uint(data: &[u8], offset: usize, nbytes: usize) -> Result<u64, FormatError> {
396    ensure_len(data, offset, nbytes)?;
397    let slice = &data[offset..offset + nbytes];
398    Ok(match nbytes {
399        1 => slice[0] as u64,
400        2 => LittleEndian::read_u16(slice) as u64,
401        4 => LittleEndian::read_u32(slice) as u64,
402        8 => LittleEndian::read_u64(slice),
403        _ => {
404            return Err(FormatError::UnexpectedEof {
405                expected: offset + nbytes,
406                available: data.len(),
407            });
408        }
409    })
410}
411
412impl Datatype {
413    /// Parse a datatype message from raw bytes.
414    ///
415    /// Returns `(Datatype, bytes_consumed)` for recursive parsing.
416    ///
417    /// Crate-internal: no public API hands out datatype-message bytes to feed it.
418    /// Read a dataset's type with [`Dataset::datatype`](crate::Dataset::datatype).
419    ///
420    /// A parsed type always has a non-zero [`type_size`](Self::type_size): no HDF5
421    /// type occupies zero bytes per element, and every reader divides raw bytes by
422    /// that size to recover an element count. Refusing it here — the one place an
423    /// untrusted datatype message becomes a `Datatype` — holds that invariant for
424    /// every reader of a file instead of asking each one to re-check it.
425    ///
426    /// It says nothing about a `Datatype` a caller builds and hands to the writer,
427    /// which never passes through here. `CompoundTypeBuilder::build` over no fields
428    /// yields a zero-size compound today, and the write path divides by the element
429    /// size just as the read path does.
430    pub(crate) fn parse(data: &[u8]) -> Result<(Datatype, usize), FormatError> {
431        // Minimum header: 4 bytes (class_and_version + 3 bytes bit field) + 4 bytes size = 8
432        ensure_len(data, 0, 8)?;
433
434        let class_and_version = data[0];
435        let class_id = class_and_version & 0x0F;
436        let version = (class_and_version >> 4) & 0x0F;
437
438        // 24-bit class bit field (little-endian)
439        let bf0 = data[1];
440        let bf1 = data[2];
441        let bf2 = data[3];
442        let _bit_field_24 = (bf0 as u32) | ((bf1 as u32) << 8) | ((bf2 as u32) << 16);
443
444        let size = LittleEndian::read_u32(&data[4..8]);
445        let mut pos = 8;
446
447        let parsed = match class_id {
448            0 => {
449                // Fixed-Point
450                ensure_len(data, pos, 4)?;
451                let byte_order = if bf0 & 0x01 == 0 {
452                    DatatypeByteOrder::LittleEndian
453                } else {
454                    DatatypeByteOrder::BigEndian
455                };
456                let signed = (bf0 >> 3) & 0x01 == 1;
457                let bit_offset = LittleEndian::read_u16(&data[pos..pos + 2]);
458                let bit_precision = LittleEndian::read_u16(&data[pos + 2..pos + 4]);
459                pos += 4;
460                Ok((
461                    Datatype::FixedPoint {
462                        size,
463                        byte_order,
464                        signed,
465                        bit_offset,
466                        bit_precision,
467                    },
468                    pos,
469                ))
470            }
471            1 => {
472                // Floating-Point
473                ensure_len(data, pos, 12)?;
474                let bo_low = bf0 & 0x01;
475                let bo_high = (bf0 >> 6) & 0x01;
476                let byte_order = match (bo_high, bo_low) {
477                    (0, 0) => DatatypeByteOrder::LittleEndian,
478                    (0, 1) => DatatypeByteOrder::BigEndian,
479                    (1, 0) => DatatypeByteOrder::Vax,
480                    (1, 1) => DatatypeByteOrder::Vax,
481                    _ => unreachable!(),
482                };
483                let bit_offset = LittleEndian::read_u16(&data[pos..pos + 2]);
484                let bit_precision = LittleEndian::read_u16(&data[pos + 2..pos + 4]);
485                let exponent_location = data[pos + 4];
486                let exponent_size = data[pos + 5];
487                let mantissa_location = data[pos + 6];
488                let mantissa_size = data[pos + 7];
489                let exponent_bias = LittleEndian::read_u32(&data[pos + 8..pos + 12]);
490                pos += 12;
491                Ok((
492                    Datatype::FloatingPoint {
493                        size,
494                        byte_order,
495                        bit_offset,
496                        bit_precision,
497                        exponent_location,
498                        exponent_size,
499                        mantissa_location,
500                        mantissa_size,
501                        exponent_bias,
502                    },
503                    pos,
504                ))
505            }
506            2 => {
507                // Time
508                ensure_len(data, pos, 2)?;
509                let byte_order = if bf0 & 0x01 == 0 {
510                    DatatypeByteOrder::LittleEndian
511                } else {
512                    DatatypeByteOrder::BigEndian
513                };
514                let bit_precision = LittleEndian::read_u16(&data[pos..pos + 2]);
515                pos += 2;
516                Ok((
517                    Datatype::Time {
518                        size,
519                        byte_order,
520                        bit_precision,
521                    },
522                    pos,
523                ))
524            }
525            3 => {
526                // String
527                let padding_val = bf0 & 0x0F;
528                let charset_val = (bf0 >> 4) & 0x0F;
529                let padding = parse_string_padding(padding_val)?;
530                let charset = parse_charset(charset_val)?;
531                Ok((
532                    Datatype::String {
533                        size,
534                        padding,
535                        charset,
536                    },
537                    pos,
538                ))
539            }
540            4 => {
541                // Bit Field
542                ensure_len(data, pos, 4)?;
543                let byte_order = if bf0 & 0x01 == 0 {
544                    DatatypeByteOrder::LittleEndian
545                } else {
546                    DatatypeByteOrder::BigEndian
547                };
548                let bit_offset = LittleEndian::read_u16(&data[pos..pos + 2]);
549                let bit_precision = LittleEndian::read_u16(&data[pos + 2..pos + 4]);
550                pos += 4;
551                Ok((
552                    Datatype::BitField {
553                        size,
554                        byte_order,
555                        bit_offset,
556                        bit_precision,
557                    },
558                    pos,
559                ))
560            }
561            5 => {
562                // Opaque
563                let tag_len = bf0 as usize;
564                ensure_len(data, pos, tag_len)?;
565                let tag = data[pos..pos + tag_len].to_vec();
566                // Tags are padded to multiple of 8 bytes
567                let padded = (tag_len + 7) & !7;
568                let pos = 8 + padded; // from start of properties
569                Ok((Datatype::Opaque { size, tag }, pos))
570            }
571            6 => {
572                // Compound
573                let num_members = (bf0 as u16) | ((bf1 as u16) << 8);
574                let mut members = Vec::with_capacity(num_members as usize);
575
576                // Versions 3 to 5 share one layout: version 4 added the revised
577                // reference types and version 5 the complex class, and neither
578                // changed a compound. HDF5 2.0 writes version 5 for every
579                // datatype under its latest library bounds.
580                if (3..=5).contains(&version) {
581                    let ob = offset_bytes_for_size(size);
582                    for _ in 0..num_members {
583                        let (name, name_len) = read_null_terminated_string(data, pos)?;
584                        pos += name_len;
585                        let byte_offset = read_uint(data, pos, ob)?;
586                        pos += ob;
587                        let (member_dt, consumed) = Datatype::parse(&data[pos..])?;
588                        pos += consumed;
589                        members.push(CompoundMember {
590                            name,
591                            byte_offset,
592                            datatype: member_dt,
593                        });
594                    }
595                } else if version == 1 || version == 2 {
596                    // v1 and v2: the member name is NUL-terminated and padded with
597                    // additional NULs to a multiple of 8 bytes, followed by a
598                    // 4-byte member byte offset. v1 then carries a fixed 28-byte
599                    // dimension block — dimensionality(1) + reserved(3) +
600                    // dimension permutation(4) + reserved(4) + dimension sizes(16)
601                    // — before the member datatype message; v2 drops that block.
602                    for _ in 0..num_members {
603                        let (name, name_len) = read_null_terminated_string(data, pos)?;
604                        let padded = (name_len + 7) & !7;
605                        pos += padded;
606                        ensure_len(data, pos, 4)?;
607                        let byte_offset = LittleEndian::read_u32(&data[pos..pos + 4]) as u64;
608                        pos += 4;
609                        if version == 1 {
610                            ensure_len(data, pos, 28)?;
611                            pos += 28;
612                        }
613                        let (member_dt, consumed) = Datatype::parse(&data[pos..])?;
614                        pos += consumed;
615                        members.push(CompoundMember {
616                            name,
617                            byte_offset,
618                            datatype: member_dt,
619                        });
620                    }
621                } else {
622                    return Err(FormatError::InvalidDatatypeVersion {
623                        class: class_id,
624                        version,
625                    });
626                }
627
628                Ok((Datatype::Compound { size, members }, pos))
629            }
630            7 => {
631                // Reference
632                let ref_type_val = bf0 & 0x0F;
633                let ref_type = match ref_type_val {
634                    0 => ReferenceType::Object,
635                    1 => ReferenceType::DatasetRegion,
636                    _ => return Err(FormatError::InvalidReferenceType(ref_type_val)),
637                };
638                Ok((Datatype::Reference { size, ref_type }, pos))
639            }
640            8 => {
641                // Enumeration
642                let num_members = (bf0 as u16) | ((bf1 as u16) << 8);
643                // Parse base type
644                let (base_type, base_consumed) = Datatype::parse(&data[pos..])?;
645                pos += base_consumed;
646                let base_size = base_type.type_size();
647                let mut members = Vec::with_capacity(num_members as usize);
648                // Enum layout: base_type, then all names (null-terminated), then all values
649                // v1/v2: names are padded to 8-byte boundaries
650                // v3: names are just null-terminated
651                let mut member_names = Vec::with_capacity(num_members as usize);
652                for _ in 0..num_members {
653                    let (name, name_len) = read_null_terminated_string(data, pos)?;
654                    if version < 3 {
655                        let padded = (name_len + 7) & !7;
656                        pos += padded;
657                    } else {
658                        pos += name_len;
659                    }
660                    member_names.push(name);
661                }
662                // Now values
663                for name in &member_names {
664                    ensure_len(data, pos, base_size as usize)?;
665                    let value = data[pos..pos + base_size as usize].to_vec();
666                    pos += base_size as usize;
667                    members.push(EnumMember {
668                        name: name.clone(),
669                        value,
670                    });
671                }
672                Ok((
673                    Datatype::Enumeration {
674                        size,
675                        base_type: Box::new(base_type),
676                        members,
677                    },
678                    pos,
679                ))
680            }
681            9 => {
682                // Variable-Length
683                let vl_type = bf0 & 0x0F;
684                let is_string = vl_type == 1;
685                let padding = if is_string {
686                    let pad_val = (bf0 >> 4) & 0x0F;
687                    Some(parse_string_padding(pad_val)?)
688                } else {
689                    None
690                };
691                let charset = if is_string {
692                    let cs_val = bf1 & 0x0F;
693                    Some(parse_charset(cs_val)?)
694                } else {
695                    None
696                };
697                let (base_type, consumed) = Datatype::parse(&data[pos..])?;
698                pos += consumed;
699                Ok((
700                    Datatype::VariableLength {
701                        is_string,
702                        padding,
703                        charset,
704                        base_type: Box::new(base_type),
705                    },
706                    pos,
707                ))
708            }
709            10 => {
710                // Array
711                if version == 2 {
712                    ensure_len(data, pos, 4)?;
713                    let ndims = data[pos] as usize;
714                    pos += 4; // ndims(1) + reserved(3)
715                    ensure_len(data, pos, ndims * 4 + ndims * 4)?;
716                    let mut dimensions = Vec::with_capacity(ndims);
717                    for _ in 0..ndims {
718                        dimensions.push(LittleEndian::read_u32(&data[pos..pos + 4]));
719                        pos += 4;
720                    }
721                    // skip permutation indices
722                    pos += ndims * 4;
723                    let (base_type, consumed) = Datatype::parse(&data[pos..])?;
724                    pos += consumed;
725                    Ok((
726                        Datatype::Array {
727                            base_type: Box::new(base_type),
728                            dimensions,
729                        },
730                        pos,
731                    ))
732                } else if (3..=5).contains(&version) {
733                    ensure_len(data, pos, 1)?;
734                    let ndims = data[pos] as usize;
735                    pos += 1;
736                    ensure_len(data, pos, ndims * 4)?;
737                    let mut dimensions = Vec::with_capacity(ndims);
738                    for _ in 0..ndims {
739                        dimensions.push(LittleEndian::read_u32(&data[pos..pos + 4]));
740                        pos += 4;
741                    }
742                    let (base_type, consumed) = Datatype::parse(&data[pos..])?;
743                    pos += consumed;
744                    Ok((
745                        Datatype::Array {
746                            base_type: Box::new(base_type),
747                            dimensions,
748                        },
749                        pos,
750                    ))
751                } else {
752                    Err(FormatError::InvalidDatatypeVersion {
753                        class: class_id,
754                        version,
755                    })
756                }
757            }
758            11 => {
759                // Complex number — store as compound of two floats internally
760                // Parse like compound with version 3 and 2 members
761                // But actually class 11 has no special properties beyond class 6 compound.
762                // It's just recognized as a separate class. For now parse the 2 members
763                // as compound.
764                let num_members = (bf0 as u16) | ((bf1 as u16) << 8);
765                let mut members = Vec::with_capacity(num_members as usize);
766                let ob = offset_bytes_for_size(size);
767                for _ in 0..num_members {
768                    let (name, name_len) = read_null_terminated_string(data, pos)?;
769                    pos += name_len;
770                    let byte_offset = read_uint(data, pos, ob)?;
771                    pos += ob;
772                    let (member_dt, consumed) = Datatype::parse(&data[pos..])?;
773                    pos += consumed;
774                    members.push(CompoundMember {
775                        name,
776                        byte_offset,
777                        datatype: member_dt,
778                    });
779                }
780                Ok((Datatype::Compound { size, members }, pos))
781            }
782            _ => Err(FormatError::InvalidDatatypeClass(class_id)),
783        };
784
785        // The declared size is checked through `type_size` rather than the header
786        // field, because the two differ: an array type derives its size from its
787        // base type and dimensions, so a zero dimension yields a zero-byte element
788        // from a non-zero header field.
789        let (datatype, consumed) = parsed?;
790        if datatype.type_size() == 0 {
791            return Err(FormatError::ZeroSizedDatatype { class: class_id });
792        }
793        Ok((datatype, consumed))
794    }
795
796    /// Serialize datatype to HDF5 message bytes.
797    ///
798    /// Crate-internal: hand a `Datatype` to
799    /// [`DatasetBuilder::with_dtype`](crate::DatasetBuilder::with_dtype) and the
800    /// writer encodes it. Widening this again is additive if a caller ever needs
801    /// the raw encoding.
802    pub(crate) fn serialize(&self) -> Vec<u8> {
803        match self {
804            Datatype::FixedPoint {
805                size,
806                byte_order,
807                signed,
808                bit_offset,
809                bit_precision,
810            } => {
811                let mut bf0 = 0u8;
812                if matches!(byte_order, DatatypeByteOrder::BigEndian) {
813                    bf0 |= 0x01;
814                }
815                if *signed {
816                    bf0 |= 0x08;
817                }
818                let mut buf = Self::build_header(0, 1, [bf0, 0, 0], *size);
819                buf.extend_from_slice(&bit_offset.to_le_bytes());
820                buf.extend_from_slice(&bit_precision.to_le_bytes());
821                buf
822            }
823            Datatype::FloatingPoint {
824                size,
825                byte_order,
826                bit_offset,
827                bit_precision,
828                exponent_location,
829                exponent_size,
830                mantissa_location,
831                mantissa_size,
832                exponent_bias,
833            } => {
834                let mut bf0 = 0x20u8; // bit 5: sign location bit (standard IEEE 754)
835                match byte_order {
836                    DatatypeByteOrder::BigEndian => {
837                        bf0 |= 0x01;
838                    }
839                    DatatypeByteOrder::Vax => {
840                        bf0 |= 0x40;
841                    }
842                    _ => {}
843                }
844                // bf[1] = sign bit location (bit position of sign in the value)
845                #[expect(
846                    clippy::cast_possible_truncation,
847                    reason = "size is an element byte size; *8-1 is a bit index that fits in a u8 (at most 63 for an 8-byte element)"
848                )]
849                let bf1 = (*size * 8 - 1) as u8;
850                let mut buf = Self::build_header(1, 1, [bf0, bf1, 0], *size);
851                buf.extend_from_slice(&bit_offset.to_le_bytes());
852                buf.extend_from_slice(&bit_precision.to_le_bytes());
853                buf.push(*exponent_location);
854                buf.push(*exponent_size);
855                buf.push(*mantissa_location);
856                buf.push(*mantissa_size);
857                buf.extend_from_slice(&exponent_bias.to_le_bytes());
858                buf
859            }
860            Datatype::String {
861                size,
862                padding,
863                charset,
864            } => {
865                let pad_val = match padding {
866                    StringPadding::NullTerminate => 0,
867                    StringPadding::NullPad => 1,
868                    StringPadding::SpacePad => 2,
869                };
870                let cs_val = match charset {
871                    CharacterSet::Ascii => 0,
872                    CharacterSet::Utf8 => 1,
873                };
874                let bf0 = pad_val | (cs_val << 4);
875                Self::build_header(3, 1, [bf0, 0, 0], *size)
876            }
877            Datatype::VariableLength {
878                is_string,
879                padding,
880                charset,
881                base_type,
882            } => {
883                let mut bf0 = if *is_string { 0x01u8 } else { 0x00 };
884                if *is_string && let Some(p) = padding {
885                    let pv = match p {
886                        StringPadding::NullTerminate => 0,
887                        StringPadding::NullPad => 1,
888                        StringPadding::SpacePad => 2,
889                    };
890                    bf0 |= pv << 4;
891                }
892                let bf1 = if *is_string {
893                    charset.as_ref().map_or(0, |c| match c {
894                        CharacterSet::Ascii => 0,
895                        CharacterSet::Utf8 => 1,
896                    })
897                } else {
898                    0
899                };
900                let mut buf = Self::build_header(9, 1, [bf0, bf1, 0], 16);
901                buf.extend_from_slice(&base_type.serialize());
902                buf
903            }
904            Datatype::Compound { size, members } => {
905                #[expect(
906                    clippy::cast_possible_truncation,
907                    reason = "compound member count is written into the 2-byte member-count field of the datatype message"
908                )]
909                let num = members.len() as u16;
910                let bf0 = (num & 0xFF) as u8;
911                let bf1 = ((num >> 8) & 0xFF) as u8;
912                let mut buf = Self::build_header(6, 3, [bf0, bf1, 0], *size);
913                let ob = offset_bytes_for_size(*size);
914                for m in members {
915                    // Null-terminated name
916                    buf.extend_from_slice(m.name.as_bytes());
917                    buf.push(0);
918                    // Byte offset (variable-width)
919                    #[expect(
920                        clippy::cast_possible_truncation,
921                        reason = "ob is the offset-byte width chosen to hold byte_offset, so each arm casts to a width that fits by construction"
922                    )]
923                    match ob {
924                        1 => buf.push(m.byte_offset as u8),
925                        2 => buf.extend_from_slice(&(m.byte_offset as u16).to_le_bytes()),
926                        _ => buf.extend_from_slice(&(m.byte_offset as u32).to_le_bytes()),
927                    }
928                    // Recursively serialize member datatype
929                    buf.extend_from_slice(&m.datatype.serialize());
930                }
931                buf
932            }
933            Datatype::Enumeration {
934                size,
935                base_type,
936                members,
937            } => {
938                #[expect(
939                    clippy::cast_possible_truncation,
940                    reason = "enumeration member count is written into the 2-byte member-count field of the datatype message"
941                )]
942                let num = members.len() as u16;
943                let bf0 = (num & 0xFF) as u8;
944                let bf1 = ((num >> 8) & 0xFF) as u8;
945                let mut buf = Self::build_header(8, 3, [bf0, bf1, 0], *size);
946                // Base type
947                buf.extend_from_slice(&base_type.serialize());
948                // All names (null-terminated)
949                for m in members {
950                    buf.extend_from_slice(m.name.as_bytes());
951                    buf.push(0);
952                }
953                // All values
954                for m in members {
955                    buf.extend_from_slice(&m.value);
956                }
957                buf
958            }
959            Datatype::Array {
960                base_type,
961                dimensions,
962            } => {
963                let mut buf = Self::build_header(10, 3, [0, 0, 0], self.type_size());
964                #[expect(
965                    clippy::cast_possible_truncation,
966                    reason = "array rank is written into the 1-byte dimensionality field; HDF5 caps array rank well below 255"
967                )]
968                buf.push(dimensions.len() as u8);
969                for &d in dimensions {
970                    buf.extend_from_slice(&d.to_le_bytes());
971                }
972                buf.extend_from_slice(&base_type.serialize());
973                buf
974            }
975            Datatype::Reference { size, ref_type } => {
976                let bf0 = match ref_type {
977                    ReferenceType::Object => 0,
978                    ReferenceType::DatasetRegion => 1,
979                };
980                Self::build_header(7, 1, [bf0, 0, 0], *size)
981            }
982            Datatype::Time {
983                size,
984                byte_order,
985                bit_precision,
986            } => {
987                // bf0 bit 0 is the byte order (0 = little-endian, 1 = big-endian).
988                let bf0 = if matches!(byte_order, DatatypeByteOrder::BigEndian) {
989                    0x01u8
990                } else {
991                    0
992                };
993                let mut buf = Self::build_header(2, 1, [bf0, 0, 0], *size);
994                buf.extend_from_slice(&bit_precision.to_le_bytes());
995                buf
996            }
997            Datatype::BitField {
998                size,
999                byte_order,
1000                bit_offset,
1001                bit_precision,
1002            } => {
1003                let bf0 = if matches!(byte_order, DatatypeByteOrder::BigEndian) {
1004                    0x01u8
1005                } else {
1006                    0
1007                };
1008                let mut buf = Self::build_header(4, 1, [bf0, 0, 0], *size);
1009                buf.extend_from_slice(&bit_offset.to_le_bytes());
1010                buf.extend_from_slice(&bit_precision.to_le_bytes());
1011                buf
1012            }
1013            Datatype::Opaque { size, tag } => {
1014                // bf0 carries the ASCII tag length; the tag is padded with zero
1015                // bytes to a multiple of 8, mirroring `parse`.
1016                #[expect(
1017                    clippy::cast_possible_truncation,
1018                    reason = "opaque tag length is written into the 1-byte tag-length bit field (bf0)"
1019                )]
1020                let bf0 = tag.len() as u8;
1021                let mut buf = Self::build_header(5, 1, [bf0, 0, 0], *size);
1022                buf.extend_from_slice(tag);
1023                let padded = (tag.len() + 7) & !7;
1024                buf.resize(buf.len() + (padded - tag.len()), 0);
1025                buf
1026            }
1027        }
1028    }
1029
1030    fn build_header(class: u8, version: u8, bf: [u8; 3], size: u32) -> Vec<u8> {
1031        let mut buf = vec![0u8; 8];
1032        buf[0] = (class & 0x0F) | ((version & 0x0F) << 4);
1033        buf[1] = bf[0];
1034        buf[2] = bf[1];
1035        buf[3] = bf[2];
1036        buf[4..8].copy_from_slice(&size.to_le_bytes());
1037        buf
1038    }
1039
1040    /// Return the size in bytes of one element of this type.
1041    pub fn type_size(&self) -> u32 {
1042        match self {
1043            Datatype::FixedPoint { size, .. } => *size,
1044            Datatype::FloatingPoint { size, .. } => *size,
1045            Datatype::Time { size, .. } => *size,
1046            Datatype::String { size, .. } => *size,
1047            Datatype::BitField { size, .. } => *size,
1048            Datatype::Opaque { size, .. } => *size,
1049            Datatype::Compound { size, .. } => *size,
1050            Datatype::Reference { size, .. } => *size,
1051            Datatype::Enumeration { size, .. } => *size,
1052            Datatype::VariableLength { .. } => 16, // typically pointer + length
1053            Datatype::Array {
1054                base_type,
1055                dimensions,
1056            } => {
1057                let elem_count: u32 = dimensions
1058                    .iter()
1059                    .copied()
1060                    .fold(1u32, |a, b| a.saturating_mul(b));
1061                base_type.type_size().saturating_mul(elem_count)
1062            }
1063        }
1064    }
1065
1066    /// The class code this type encodes as, the low nibble of a datatype
1067    /// message's first byte.
1068    ///
1069    /// Kept beside [`type_size`](Self::type_size) rather than read back out of
1070    /// [`serialize`](Self::serialize), so naming the class in an error costs no
1071    /// encoding.
1072    pub(crate) fn class_code(&self) -> u8 {
1073        match self {
1074            Datatype::FixedPoint { .. } => 0,
1075            Datatype::FloatingPoint { .. } => 1,
1076            Datatype::Time { .. } => 2,
1077            Datatype::String { .. } => 3,
1078            Datatype::BitField { .. } => 4,
1079            Datatype::Opaque { .. } => 5,
1080            Datatype::Compound { .. } => 6,
1081            Datatype::Reference { .. } => 7,
1082            Datatype::Enumeration { .. } => 8,
1083            Datatype::VariableLength { .. } => 9,
1084            Datatype::Array { .. } => 10,
1085        }
1086    }
1087
1088    /// The element size in bytes, proven non-zero.
1089    ///
1090    /// Prefer this to [`type_size`](Self::type_size) for any element size that
1091    /// is about to be divided or divided *by*: it returns the size as a
1092    /// [`NonZeroU32`], so the value carries its own proof and the code it is
1093    /// handed to cannot divide by zero. Every such site in this crate takes a
1094    /// non-zero size rather than re-checking one.
1095    ///
1096    /// The refusal has to live here rather than in the type because
1097    /// `type_size()` is *computed*: an [`Array`](Self::Array) reports its base
1098    /// type times its dimensions, so a zero dimension yields a zero-width
1099    /// element behind a header that claims otherwise, and the variants are
1100    /// deliberately open for a caller to build as a literal. A type read out of
1101    /// a file is already refused when its message is decoded; this is the same
1102    /// refusal for a constructed one, on the way into a writer.
1103    ///
1104    /// # Errors
1105    ///
1106    /// [`FormatError::ZeroSizedDatatype`] if the type occupies zero bytes per
1107    /// element.
1108    pub fn element_size(&self) -> Result<NonZeroU32, FormatError> {
1109        NonZeroU32::new(self.type_size()).ok_or(FormatError::ZeroSizedDatatype {
1110            class: self.class_code(),
1111        })
1112    }
1113
1114    /// The element size in bytes as a non-zero `usize`, for the byte arithmetic
1115    /// that indexes an in-memory buffer.
1116    ///
1117    /// The narrowing is the one [`convert`](crate::convert) describes: a `u32`
1118    /// fits `usize` on every target this crate supports, and the conversion is
1119    /// routed through a checked one anyway.
1120    ///
1121    /// # Errors
1122    ///
1123    /// [`FormatError::ZeroSizedDatatype`] if the type occupies zero bytes per
1124    /// element, or [`FormatError::ValueTooLargeForPlatform`] if the size does
1125    /// not fit this target's `usize`.
1126    pub(crate) fn element_size_usize(&self) -> Result<NonZeroUsize, FormatError> {
1127        crate::convert::nonzero_usize_from(self.element_size()?)
1128    }
1129}
1130/// Whether a datatype of this encoded class *could* hold an object address,
1131/// decided from the first byte of a datatype message rather than by parsing it.
1132///
1133/// A **necessary** condition for [`datatype_holds_object_address`] and never a
1134/// sufficient one: a compound of two integers has a qualifying class and holds
1135/// no address at all. It exists so a walk over every object in a file can reject
1136/// the overwhelmingly common cases — a fixed-point, floating-point, string, or
1137/// opaque dataset — without allocating a parsed [`Datatype`] for each. The
1138/// classes it admits are exactly the ones `datatype_holds_object_address`
1139/// recurses through, plus the reference itself; `class_predicate_admits_every_
1140/// reference_holding_type` in this module's tests is what holds the two together.
1141pub(crate) fn class_may_hold_object_address(class_and_version: u8) -> bool {
1142    matches!(
1143        class_and_version & 0x0F,
1144        COMPOUND_CLASS | REFERENCE_CLASS | ENUMERATION_CLASS | VARIABLE_LENGTH_CLASS | ARRAY_CLASS
1145    )
1146}
1147
1148/// Datatype message class ids, as the low nibble of a datatype message's first
1149/// byte. Only the classes that can carry an object address downward are named.
1150const COMPOUND_CLASS: u8 = 6;
1151const REFERENCE_CLASS: u8 = 7;
1152const ENUMERATION_CLASS: u8 = 8;
1153const VARIABLE_LENGTH_CLASS: u8 = 9;
1154const ARRAY_CLASS: u8 = 10;
1155
1156/// Whether `dt` reaches an **object address** anywhere in its structure — an
1157/// object or dataset-region reference, directly or through a compound member,
1158/// array entry, enumeration base, or the contents of a variable-length
1159/// sequence.
1160///
1161/// The paired half of [`embedded_reference_slots`], which locates the ones it
1162/// can address. This one recognises an object reference of any width and in any
1163/// position; that one maps only the 8-byte form reachable through compound
1164/// members and array entries. The gap between them is not an oversight but the
1165/// point: a datatype this accepts and that cannot map is one whose addresses
1166/// cannot be read, which callers must refuse rather than pass over. Their fall-
1167/// through arm consults this function so the two cannot drift apart.
1168///
1169/// A variable-length datatype counts only when what it *holds* is an object
1170/// reference. The heap itself is not at risk — a deletion frees object headers
1171/// and dataset storage and never a global heap collection, so a variable-length
1172/// string keeps pointing at data that is still there — but a `H5T_VLEN` of
1173/// `H5T_STD_REF_OBJ`, which the reference library writes, keeps its addresses in
1174/// the heap *contents*, where the element bytes hold only a heap id.
1175pub(crate) fn datatype_holds_object_address(dt: &Datatype) -> bool {
1176    match dt {
1177        // Both reference kinds name an object. An object reference *is* the
1178        // header address; a dataset-region reference is a global-heap id whose
1179        // heap object holds the address and a selection, so the address is one
1180        // indirection further out — out of reach of a screen that reads element
1181        // bytes, which is what makes it unmappable rather than absent.
1182        Datatype::Reference { .. } => true,
1183        Datatype::Compound { members, .. } => members
1184            .iter()
1185            .any(|m| datatype_holds_object_address(&m.datatype)),
1186        Datatype::Array { base_type, .. }
1187        | Datatype::Enumeration { base_type, .. }
1188        | Datatype::VariableLength { base_type, .. } => datatype_holds_object_address(base_type),
1189        _ => false,
1190    }
1191}
1192
1193/// Whether `dt`'s element bytes carry a **file-absolute address** at any depth:
1194/// a variable-length element (a global-heap collection address and index) or a
1195/// reference (an object address, or for a dataset-region reference a heap id),
1196/// directly or through a compound member, array entry, or enumeration base.
1197///
1198/// The union of the two addresses an element can hold, and deliberately not a
1199/// finer answer than that — both callers ask only whether an address is in
1200/// there at all:
1201///
1202/// - a **cross-file copy** (`reject_foreign_addresses`) refuses such a
1203///   datatype, since an address into the source file cannot be translated into
1204///   another one;
1205/// - the **heap-collection provenance** of a variable-length overwrite (issue
1206///   #321) gives up its record when a raw-bytes write could name a collection a
1207///   second time.
1208///
1209/// Both are one-sided: answering `true` too often costs a refusal or a reclaim,
1210/// answering `false` too often would cost correctness.
1211///
1212/// Distinct from [`datatype_holds_object_address`], which asks specifically
1213/// whether an *object header* address is reachable — so it answers `false` for
1214/// a variable-length string and `true` for a variable length *of* references,
1215/// where this one answers `true` for both.
1216pub(crate) fn datatype_holds_file_address(dt: &Datatype) -> bool {
1217    match dt {
1218        Datatype::VariableLength { .. } | Datatype::Reference { .. } => true,
1219        Datatype::Compound { members, .. } => members
1220            .iter()
1221            .any(|m| datatype_holds_file_address(&m.datatype)),
1222        Datatype::Array { base_type, .. } | Datatype::Enumeration { base_type, .. } => {
1223            datatype_holds_file_address(base_type)
1224        }
1225        _ => false,
1226    }
1227}
1228
1229/// Every 8-byte object reference `datatype` reaches through a compound member or
1230/// array entry, as byte offsets within one element, in declaration order.
1231///
1232/// Mirrors [`embedded_vlen_slots`](crate::vl_data::embedded_vlen_slots) for the
1233/// other kind of address a rewrite invalidates. A datatype that *is* an object
1234/// reference yields the single slot at offset 0, so callers handling that case
1235/// separately should test for it first.
1236///
1237/// Returns `None` when the element bytes cannot be walked safely: the offsets
1238/// found do not fit the datatype's declared element size, or the type reaches an
1239/// object reference this walker cannot address (see
1240/// [`datatype_holds_object_address`]). Both mean the same thing to a caller —
1241/// the addresses are not readable from here — so neither is reported as an empty
1242/// slot list, which would read as "this type holds none".
1243pub(crate) fn embedded_reference_slots(datatype: &Datatype) -> Option<Vec<usize>> {
1244    /// Returns `false` when the datatype cannot be walked on this target, for the
1245    /// reasons [`embedded_vlen_slots`]' walker documents.
1246    fn collect(datatype: &Datatype, base: usize, capacity: usize, out: &mut Vec<usize>) -> bool {
1247        if out.len() > capacity {
1248            return true;
1249        }
1250        match datatype {
1251            Datatype::Reference {
1252                ref_type: ReferenceType::Object,
1253                size: 8,
1254            } => {
1255                out.push(base);
1256                true
1257            }
1258            Datatype::Compound { members, .. } => {
1259                for m in members {
1260                    let Some(at) = usize::try_from(m.byte_offset)
1261                        .ok()
1262                        .and_then(|off| base.checked_add(off))
1263                    else {
1264                        return false;
1265                    };
1266                    if !collect(&m.datatype, at, capacity, out) {
1267                        return false;
1268                    }
1269                }
1270                true
1271            }
1272            Datatype::Array {
1273                base_type,
1274                dimensions,
1275            } => {
1276                // As in `embedded_vlen_slots`: probe once so that entries which can
1277                // never contribute do not drive a walk over huge declared
1278                // dimensions, and so every iteration below pushes at least one slot.
1279                // Walked once and translated per entry, for the reason
1280                // `embedded_vlen_slots` documents: re-walking is exponential in
1281                // nesting depth.
1282                let mut probe = Vec::new();
1283                if !collect(base_type, 0, capacity, &mut probe) {
1284                    return false;
1285                }
1286                if probe.is_empty() {
1287                    return true;
1288                }
1289                let count = dimensions
1290                    .iter()
1291                    .copied()
1292                    .fold(1u64, |a, b| a.saturating_mul(u64::from(b)));
1293                // As in `embedded_vlen_slots`: more entries than the element has
1294                // room for cannot fit, so reject without walking them.
1295                if count > capacity as u64 {
1296                    return false;
1297                }
1298                let entries = usize::try_from(count).unwrap_or(usize::MAX);
1299                let stride = base_type.type_size() as usize;
1300                for i in 0..entries {
1301                    let Some(at) = i.checked_mul(stride).and_then(|off| base.checked_add(off))
1302                    else {
1303                        return false;
1304                    };
1305                    for &slot in &probe {
1306                        let Some(off) = at.checked_add(slot) else {
1307                            return false;
1308                        };
1309                        out.push(off);
1310                        if out.len() > capacity {
1311                            return true;
1312                        }
1313                    }
1314                }
1315                true
1316            }
1317            // Anything this walker does not map. A type that nonetheless
1318            // reaches an object reference — a width other than 8, an
1319            // enumeration over one, a variable-length sequence *of* them — is
1320            // one whose addresses cannot be located in the element bytes, so
1321            // say so rather than report "no slots here" and let a caller read
1322            // that as "nothing to check". Asking the predicate rather than
1323            // restating its arms is what keeps the pair honest as either grows.
1324            _ => !datatype_holds_object_address(datatype),
1325        }
1326    }
1327
1328    let element_size = datatype.type_size() as usize;
1329    let capacity = element_size / 8;
1330    let mut slots = Vec::new();
1331    if !collect(datatype, 0, capacity, &mut slots) {
1332        return None;
1333    }
1334    // `checked_add`: an offset near the top of the address space would otherwise
1335    // wrap here and read as "fits".
1336    if slots.len() > capacity
1337        || slots
1338            .iter()
1339            .any(|&s| s.checked_add(8).is_none_or(|end| end > element_size))
1340    {
1341        return None;
1342    }
1343    Some(slots)
1344}
1345
1346/// Every 8-byte object reference stored in `raw`, as
1347/// `(byte offset within raw, the address stored there)`.
1348///
1349/// `slots` is [`embedded_reference_slots`] for the datatype `raw` holds elements
1350/// of, and `element_size` its `type_size`. Callers differ in what they do with
1351/// an address — screen it against what a commit vacates, rewrite it to where the
1352/// object moved — but not in how they find one, and this is the one place that
1353/// walk lives. A second copy of it would be free to disagree about the element
1354/// stride, about a trailing partial element, or about which slots exist.
1355///
1356/// A trailing run shorter than one element is skipped: `chunks_exact` yields
1357/// whole elements only, which is the same thing every reader of these bytes does
1358/// with a truncated tail.
1359pub(crate) fn stored_object_references<'a>(
1360    raw: &'a [u8],
1361    element_size: usize,
1362    slots: &'a [usize],
1363) -> impl Iterator<Item = (usize, u64)> + 'a {
1364    raw.chunks_exact(element_size.max(1))
1365        .enumerate()
1366        .flat_map(move |(i, element)| {
1367            slots.iter().map(move |&at| {
1368                let stored = u64::from_le_bytes(element[at..at + 8].try_into().expect(
1369                    "embedded_reference_slots keeps every slot 8 bytes inside the element",
1370                ));
1371                (i * element_size + at, stored)
1372            })
1373        })
1374}
1375
1376/// Build a datatype header (8 bytes) for testing.
1377#[cfg(test)]
1378fn build_dt_header(class: u8, version: u8, bf: [u8; 3], size: u32) -> Vec<u8> {
1379    let mut buf = vec![0u8; 8];
1380    buf[0] = (class & 0x0F) | ((version & 0x0F) << 4);
1381    buf[1] = bf[0];
1382    buf[2] = bf[1];
1383    buf[3] = bf[2];
1384    LittleEndian::write_u32(&mut buf[4..8], size);
1385    buf
1386}
1387
1388#[cfg(test)]
1389mod tests {
1390
1391    /// Every datatype that reaches an object address must have an encoded class
1392    /// [`class_may_hold_object_address`] admits.
1393    ///
1394    /// The two are a pair with one job between them: the class predicate is the
1395    /// cheap gate a whole-file walk applies before it will parse a datatype at
1396    /// all (`crate::reference_patch`), and the type predicate is the answer it
1397    /// gates. A type the gate rejects is never parsed, so if the gate ever
1398    /// rejected one that holds an address, the walk would pass over a reference
1399    /// in silence — no error, no refusal, just a stored address left dangling.
1400    /// Nothing in either function's code says the other exists; this is what
1401    /// says it.
1402    #[test]
1403    fn the_class_gate_admits_every_reference_holding_datatype() {
1404        let object_ref = || Datatype::Reference {
1405            size: 8,
1406            ref_type: ReferenceType::Object,
1407        };
1408        let i32_le = || Datatype::FixedPoint {
1409            size: 4,
1410            byte_order: DatatypeByteOrder::LittleEndian,
1411            signed: true,
1412            bit_offset: 0,
1413            bit_precision: 32,
1414        };
1415        let holds_an_address = [
1416            ("a bare object reference", object_ref()),
1417            (
1418                "a dataset-region reference",
1419                Datatype::Reference {
1420                    size: 12,
1421                    ref_type: ReferenceType::DatasetRegion,
1422                },
1423            ),
1424            (
1425                "a compound holding one",
1426                Datatype::Compound {
1427                    size: 12,
1428                    members: vec![
1429                        CompoundMember {
1430                            name: "r".into(),
1431                            byte_offset: 0,
1432                            datatype: object_ref(),
1433                        },
1434                        CompoundMember {
1435                            name: "i".into(),
1436                            byte_offset: 8,
1437                            datatype: i32_le(),
1438                        },
1439                    ],
1440                },
1441            ),
1442            (
1443                "an array of them",
1444                Datatype::Array {
1445                    base_type: Box::new(object_ref()),
1446                    dimensions: vec![2],
1447                },
1448            ),
1449            (
1450                "a variable length of them",
1451                Datatype::VariableLength {
1452                    is_string: false,
1453                    padding: None,
1454                    charset: None,
1455                    base_type: Box::new(object_ref()),
1456                },
1457            ),
1458            (
1459                "an enumeration over one",
1460                Datatype::Enumeration {
1461                    size: 8,
1462                    base_type: Box::new(object_ref()),
1463                    members: vec![EnumMember {
1464                        name: "a".into(),
1465                        value: vec![0; 8],
1466                    }],
1467                },
1468            ),
1469            (
1470                "one nested two deep",
1471                Datatype::Array {
1472                    base_type: Box::new(Datatype::Compound {
1473                        size: 8,
1474                        members: vec![CompoundMember {
1475                            name: "r".into(),
1476                            byte_offset: 0,
1477                            datatype: object_ref(),
1478                        }],
1479                    }),
1480                    dimensions: vec![3],
1481                },
1482            ),
1483        ];
1484        for (what, dt) in holds_an_address {
1485            assert!(
1486                datatype_holds_object_address(&dt),
1487                "{what} holds an object address"
1488            );
1489            let encoded = dt.serialize();
1490            assert!(
1491                class_may_hold_object_address(encoded[0]),
1492                "{what} encodes as class {}, which the gate rejects — a walk would \
1493                 never parse it and would pass over the address inside it",
1494                encoded[0] & 0x0F
1495            );
1496        }
1497    }
1498
1499    /// The gate is a *necessary* condition and nothing more: it admits types
1500    /// that hold no address, and that is not a defect. Stated so a later reading
1501    /// of it as "this type holds a reference" has something to contradict it.
1502    #[test]
1503    fn the_class_gate_is_necessary_and_not_sufficient() {
1504        let ints = Datatype::Compound {
1505            size: 8,
1506            members: vec![CompoundMember {
1507                name: "a".into(),
1508                byte_offset: 0,
1509                datatype: Datatype::FixedPoint {
1510                    size: 8,
1511                    byte_order: DatatypeByteOrder::LittleEndian,
1512                    signed: true,
1513                    bit_offset: 0,
1514                    bit_precision: 64,
1515                },
1516            }],
1517        };
1518        assert!(!datatype_holds_object_address(&ints));
1519        assert!(
1520            class_may_hold_object_address(ints.serialize()[0]),
1521            "a compound of integers is admitted by the class gate and holds no address"
1522        );
1523    }
1524
1525    use super::*;
1526
1527    // Helper to build a fixed-point datatype message
1528    fn build_fixed_point(
1529        size: u32,
1530        be: bool,
1531        signed: bool,
1532        bit_offset: u16,
1533        bit_precision: u16,
1534    ) -> Vec<u8> {
1535        let bf0 = if be { 0x01 } else { 0x00 } | if signed { 0x08 } else { 0x00 };
1536        let mut buf = build_dt_header(0, 1, [bf0, 0, 0], size);
1537        let mut props = [0u8; 4];
1538        LittleEndian::write_u16(&mut props[0..2], bit_offset);
1539        LittleEndian::write_u16(&mut props[2..4], bit_precision);
1540        buf.extend_from_slice(&props);
1541        buf
1542    }
1543
1544    // Helper to build a floating-point datatype message
1545    fn build_float(
1546        size: u32,
1547        exp_loc: u8,
1548        exp_size: u8,
1549        mant_loc: u8,
1550        mant_size: u8,
1551        exp_bias: u32,
1552    ) -> Vec<u8> {
1553        // LE byte order: bo_low=0, bo_high=0
1554        let bf0 = 0x00u8;
1555        let bf1 = 0x00u8;
1556        // mantissa norm = 2 (MSB not stored) in bits 24-31... wait, that's bf2
1557        let bf2 = 0x02u8; // norm = 2
1558        let mut buf = build_dt_header(1, 1, [bf0, bf1, bf2], size);
1559        let mut props = [0u8; 12];
1560        LittleEndian::write_u16(&mut props[0..2], 0); // bit_offset
1561        LittleEndian::write_u16(&mut props[2..4], (size * 8) as u16); // bit_precision
1562        props[4] = exp_loc;
1563        props[5] = exp_size;
1564        props[6] = mant_loc;
1565        props[7] = mant_size;
1566        LittleEndian::write_u32(&mut props[8..12], exp_bias);
1567        buf.extend_from_slice(&props);
1568        buf
1569    }
1570
1571    #[test]
1572    fn test_fixed_point_u8() {
1573        let data = build_fixed_point(1, false, false, 0, 8);
1574        let (dt, consumed) = Datatype::parse(&data).unwrap();
1575        assert_eq!(consumed, 12);
1576        assert_eq!(
1577            dt,
1578            Datatype::FixedPoint {
1579                size: 1,
1580                byte_order: DatatypeByteOrder::LittleEndian,
1581                signed: false,
1582                bit_offset: 0,
1583                bit_precision: 8,
1584            }
1585        );
1586    }
1587
1588    #[test]
1589    fn test_fixed_point_i16_le() {
1590        let data = build_fixed_point(2, false, true, 0, 16);
1591        let (dt, _) = Datatype::parse(&data).unwrap();
1592        assert_eq!(
1593            dt,
1594            Datatype::FixedPoint {
1595                size: 2,
1596                byte_order: DatatypeByteOrder::LittleEndian,
1597                signed: true,
1598                bit_offset: 0,
1599                bit_precision: 16,
1600            }
1601        );
1602    }
1603
1604    #[test]
1605    fn test_fixed_point_u32_be() {
1606        let data = build_fixed_point(4, true, false, 0, 32);
1607        let (dt, _) = Datatype::parse(&data).unwrap();
1608        match &dt {
1609            Datatype::FixedPoint {
1610                byte_order,
1611                signed,
1612                size,
1613                ..
1614            } => {
1615                assert_eq!(*byte_order, DatatypeByteOrder::BigEndian);
1616                assert!(!signed);
1617                assert_eq!(*size, 4);
1618            }
1619            _ => panic!("expected FixedPoint"),
1620        }
1621    }
1622
1623    #[test]
1624    fn test_fixed_point_i64_le() {
1625        let data = build_fixed_point(8, false, true, 0, 64);
1626        let (dt, _) = Datatype::parse(&data).unwrap();
1627        assert_eq!(
1628            dt,
1629            Datatype::FixedPoint {
1630                size: 8,
1631                byte_order: DatatypeByteOrder::LittleEndian,
1632                signed: true,
1633                bit_offset: 0,
1634                bit_precision: 64,
1635            }
1636        );
1637    }
1638
1639    #[test]
1640    fn test_float_f32_le() {
1641        // IEEE 754 f32: exp=8 bits at bit 23, mant=23 bits at bit 0, bias=127
1642        let data = build_float(4, 23, 8, 0, 23, 127);
1643        let (dt, consumed) = Datatype::parse(&data).unwrap();
1644        assert_eq!(consumed, 20);
1645        assert_eq!(
1646            dt,
1647            Datatype::FloatingPoint {
1648                size: 4,
1649                byte_order: DatatypeByteOrder::LittleEndian,
1650                bit_offset: 0,
1651                bit_precision: 32,
1652                exponent_location: 23,
1653                exponent_size: 8,
1654                mantissa_location: 0,
1655                mantissa_size: 23,
1656                exponent_bias: 127,
1657            }
1658        );
1659    }
1660
1661    #[test]
1662    fn test_float_f64_le() {
1663        let data = build_float(8, 52, 11, 0, 52, 1023);
1664        let (dt, _) = Datatype::parse(&data).unwrap();
1665        assert_eq!(
1666            dt,
1667            Datatype::FloatingPoint {
1668                size: 8,
1669                byte_order: DatatypeByteOrder::LittleEndian,
1670                bit_offset: 0,
1671                bit_precision: 64,
1672                exponent_location: 52,
1673                exponent_size: 11,
1674                mantissa_location: 0,
1675                mantissa_size: 52,
1676                exponent_bias: 1023,
1677            }
1678        );
1679    }
1680
1681    #[test]
1682    fn test_string_null_terminated_ascii() {
1683        let buf = build_dt_header(3, 1, [0x00, 0, 0], 10); // padding=0(nullterm), charset=0(ascii)
1684        let (dt, consumed) = Datatype::parse(&buf).unwrap();
1685        assert_eq!(consumed, 8);
1686        assert_eq!(
1687            dt,
1688            Datatype::String {
1689                size: 10,
1690                padding: StringPadding::NullTerminate,
1691                charset: CharacterSet::Ascii,
1692            }
1693        );
1694    }
1695
1696    #[test]
1697    fn test_string_space_padded_utf8() {
1698        // padding=2(space pad), charset=1(utf8) → bf0 = 0x12
1699        let buf = build_dt_header(3, 1, [0x12, 0, 0], 32);
1700        let (dt, _) = Datatype::parse(&buf).unwrap();
1701        assert_eq!(
1702            dt,
1703            Datatype::String {
1704                size: 32,
1705                padding: StringPadding::SpacePad,
1706                charset: CharacterSet::Utf8,
1707            }
1708        );
1709    }
1710
1711    #[test]
1712    fn test_opaque() {
1713        // tag_len = 4, tag = "BLOB"
1714        let mut buf = build_dt_header(5, 1, [4, 0, 0], 64);
1715        buf.extend_from_slice(b"BLOB");
1716        // Pad to 8 bytes
1717        buf.extend_from_slice(&[0, 0, 0, 0]);
1718        let (dt, consumed) = Datatype::parse(&buf).unwrap();
1719        assert_eq!(consumed, 16); // 8 header + 8 padded tag
1720        assert_eq!(
1721            dt,
1722            Datatype::Opaque {
1723                size: 64,
1724                tag: b"BLOB".to_vec(),
1725            }
1726        );
1727    }
1728
1729    #[test]
1730    fn test_compound_v3_two_members() {
1731        // Compound with size=12, 2 members: "x" u32 at offset 0, "y" f64 at offset 4
1732        // Size=12, so offset_bytes=1
1733        let mut buf = build_dt_header(6, 3, [2, 0, 0], 12); // 2 members
1734        // Member "x": name "x\0", offset=0, then u32 LE datatype
1735        buf.extend_from_slice(b"x\0");
1736        buf.push(0); // byte_offset = 0
1737        buf.extend_from_slice(&build_fixed_point(4, false, false, 0, 32));
1738        // Member "y": name "y\0", offset=4, then f64 LE datatype
1739        buf.extend_from_slice(b"y\0");
1740        buf.push(4); // byte_offset = 4
1741        buf.extend_from_slice(&build_float(8, 52, 11, 0, 52, 1023));
1742
1743        let (dt, _) = Datatype::parse(&buf).unwrap();
1744        match dt {
1745            Datatype::Compound { size, members } => {
1746                assert_eq!(size, 12);
1747                assert_eq!(members.len(), 2);
1748                assert_eq!(members[0].name, "x");
1749                assert_eq!(members[0].byte_offset, 0);
1750                assert_eq!(members[1].name, "y");
1751                assert_eq!(members[1].byte_offset, 4);
1752                match &members[0].datatype {
1753                    Datatype::FixedPoint {
1754                        size: 4,
1755                        signed: false,
1756                        ..
1757                    } => {}
1758                    other => panic!("expected u32, got {other:?}"),
1759                }
1760                match &members[1].datatype {
1761                    Datatype::FloatingPoint { size: 8, .. } => {}
1762                    other => panic!("expected f64, got {other:?}"),
1763                }
1764            }
1765            _ => panic!("expected Compound"),
1766        }
1767    }
1768
1769    #[test]
1770    fn test_compound_v1_complex_matlab_layout() {
1771        // MATLAB stores a complex value as a version-1 compound of two f64
1772        // members named "real" and "imag" at offsets 0 and 8. v1 members pad
1773        // the NUL-terminated name to a multiple of 8 bytes and carry a fixed
1774        // 28-byte dimension block — dimensionality(1) + reserved(3) +
1775        // dimension permutation(4) + reserved(4) + dimension sizes(16) —
1776        // between the byte offset and the member datatype message. Regression
1777        // test for a stride bug that skipped only 24 bytes (omitting the second
1778        // reserved field) and so misread every real-MATLAB complex compound.
1779        let mut buf = build_dt_header(6, 1, [2, 0, 0], 16); // v1, 2 members, size 16
1780        for (name, offset) in [(&b"real\0\0\0\0"[..], 0u32), (&b"imag\0\0\0\0"[..], 8)] {
1781            buf.extend_from_slice(name); // NUL-terminated, padded to 8
1782            let mut off = [0u8; 4];
1783            LittleEndian::write_u32(&mut off, offset);
1784            buf.extend_from_slice(&off);
1785            buf.extend_from_slice(&[0u8; 28]); // v1 dimension block
1786            buf.extend_from_slice(&build_float(8, 52, 11, 0, 52, 1023));
1787        }
1788
1789        let (dt, _) = Datatype::parse(&buf).unwrap();
1790        match dt {
1791            Datatype::Compound { size, members } => {
1792                assert_eq!(size, 16);
1793                assert_eq!(members.len(), 2);
1794                assert_eq!(members[0].name, "real");
1795                assert_eq!(members[0].byte_offset, 0);
1796                assert_eq!(members[1].name, "imag");
1797                assert_eq!(members[1].byte_offset, 8);
1798                for m in &members {
1799                    assert!(
1800                        matches!(m.datatype, Datatype::FloatingPoint { size: 8, .. }),
1801                        "expected f64 member, got {:?}",
1802                        m.datatype
1803                    );
1804                }
1805            }
1806            _ => panic!("expected Compound"),
1807        }
1808    }
1809
1810    #[test]
1811    fn test_reference_object() {
1812        let buf = build_dt_header(7, 1, [0, 0, 0], 8);
1813        let (dt, _) = Datatype::parse(&buf).unwrap();
1814        assert_eq!(
1815            dt,
1816            Datatype::Reference {
1817                size: 8,
1818                ref_type: ReferenceType::Object,
1819            }
1820        );
1821    }
1822
1823    #[test]
1824    fn test_reference_region() {
1825        let buf = build_dt_header(7, 1, [1, 0, 0], 12);
1826        let (dt, _) = Datatype::parse(&buf).unwrap();
1827        assert_eq!(
1828            dt,
1829            Datatype::Reference {
1830                size: 12,
1831                ref_type: ReferenceType::DatasetRegion,
1832            }
1833        );
1834    }
1835
1836    #[test]
1837    fn test_enumeration() {
1838        // Enum with base type i32 LE, 3 members
1839        let mut buf = build_dt_header(8, 3, [3, 0, 0], 4); // 3 members
1840        // Base type: i32 LE
1841        buf.extend_from_slice(&build_fixed_point(4, false, true, 0, 32));
1842        // Names: "RED\0", "GREEN\0", "BLUE\0"
1843        buf.extend_from_slice(b"RED\0");
1844        buf.extend_from_slice(b"GREEN\0");
1845        buf.extend_from_slice(b"BLUE\0");
1846        // Values: 0, 1, 2 (as i32 LE)
1847        buf.extend_from_slice(&0i32.to_le_bytes());
1848        buf.extend_from_slice(&1i32.to_le_bytes());
1849        buf.extend_from_slice(&2i32.to_le_bytes());
1850
1851        let (dt, _) = Datatype::parse(&buf).unwrap();
1852        match dt {
1853            Datatype::Enumeration {
1854                size,
1855                base_type,
1856                members,
1857            } => {
1858                assert_eq!(size, 4);
1859                assert_eq!(members.len(), 3);
1860                assert_eq!(members[0].name, "RED");
1861                assert_eq!(members[0].value, 0i32.to_le_bytes().to_vec());
1862                assert_eq!(members[1].name, "GREEN");
1863                assert_eq!(members[1].value, 1i32.to_le_bytes().to_vec());
1864                assert_eq!(members[2].name, "BLUE");
1865                assert_eq!(members[2].value, 2i32.to_le_bytes().to_vec());
1866                match *base_type {
1867                    Datatype::FixedPoint {
1868                        signed: true,
1869                        size: 4,
1870                        ..
1871                    } => {}
1872                    other => panic!("expected i32, got {other:?}"),
1873                }
1874            }
1875            _ => panic!("expected Enumeration"),
1876        }
1877    }
1878
1879    #[test]
1880    fn test_variable_length_string_utf8() {
1881        // VL string: type=1, padding=0(null term), charset=1(utf8)
1882        // bf0: bits 0-3 = 1 (string), bits 4-7 = 0 (null term) → 0x01
1883        // bf1: bits 0-3 = 1 (utf8) → 0x01
1884        let mut buf = build_dt_header(9, 1, [0x01, 0x01, 0], 16);
1885        // Base type: u8 (class 0, unsigned, size 1)
1886        buf.extend_from_slice(&build_fixed_point(1, false, false, 0, 8));
1887
1888        let (dt, _) = Datatype::parse(&buf).unwrap();
1889        match dt {
1890            Datatype::VariableLength {
1891                is_string,
1892                padding,
1893                charset,
1894                base_type,
1895            } => {
1896                assert!(is_string);
1897                assert_eq!(padding, Some(StringPadding::NullTerminate));
1898                assert_eq!(charset, Some(CharacterSet::Utf8));
1899                assert_eq!(base_type.type_size(), 1);
1900            }
1901            _ => panic!("expected VariableLength"),
1902        }
1903    }
1904
1905    #[test]
1906    fn test_variable_length_sequence_f32() {
1907        // VL sequence: type=0
1908        // bf0 = 0x00
1909        let mut buf = build_dt_header(9, 1, [0x00, 0x00, 0], 16);
1910        // Base type: f32 LE
1911        buf.extend_from_slice(&build_float(4, 23, 8, 0, 23, 127));
1912
1913        let (dt, _) = Datatype::parse(&buf).unwrap();
1914        match dt {
1915            Datatype::VariableLength {
1916                is_string,
1917                padding,
1918                charset,
1919                base_type,
1920            } => {
1921                assert!(!is_string);
1922                assert_eq!(padding, None);
1923                assert_eq!(charset, None);
1924                assert_eq!(base_type.type_size(), 4);
1925            }
1926            _ => panic!("expected VariableLength"),
1927        }
1928    }
1929
1930    /// HDF5 2.0 writes every datatype message with version 5 under its latest
1931    /// library bounds. For a compound the layout is version 3's: unpadded
1932    /// member names and offsets in the fewest bytes the size needs.
1933    #[test]
1934    fn a_version_5_compound_parses_like_version_3() {
1935        let members = |version: u8| {
1936            let mut buf = build_dt_header(6, version, [2, 0, 0], 16);
1937            buf.extend_from_slice(b"re\0");
1938            buf.push(0);
1939            buf.extend_from_slice(&build_float(8, 52, 11, 0, 52, 1023));
1940            buf.extend_from_slice(b"im\0");
1941            buf.push(8);
1942            buf.extend_from_slice(&build_float(8, 52, 11, 0, 52, 1023));
1943            buf
1944        };
1945        let (expected, expected_len) = Datatype::parse(&members(3)).unwrap();
1946        let (parsed, len) = Datatype::parse(&members(5)).unwrap();
1947        assert_eq!((parsed, len), (expected, expected_len));
1948    }
1949
1950    /// The same for an array: version 5 keeps version 3's layout.
1951    #[test]
1952    fn a_version_5_array_parses_like_version_3() {
1953        let array = |version: u8| {
1954            let mut buf = build_dt_header(10, version, [0, 0, 0], 48);
1955            buf.push(2);
1956            buf.extend_from_slice(&3u32.to_le_bytes());
1957            buf.extend_from_slice(&4u32.to_le_bytes());
1958            buf.extend_from_slice(&build_fixed_point(4, false, true, 0, 32));
1959            buf
1960        };
1961        let (expected, expected_len) = Datatype::parse(&array(3)).unwrap();
1962        let (parsed, len) = Datatype::parse(&array(5)).unwrap();
1963        assert_eq!((parsed, len), (expected, expected_len));
1964    }
1965
1966    #[test]
1967    fn test_array_2d() {
1968        // Array [3][4] of i32 LE, version 3
1969        let mut buf = build_dt_header(10, 3, [0, 0, 0], 48); // 3*4*4=48
1970        buf.push(2); // ndims=2
1971        buf.extend_from_slice(&3u32.to_le_bytes()); // dim 0
1972        buf.extend_from_slice(&4u32.to_le_bytes()); // dim 1
1973        // Base type: i32 LE
1974        buf.extend_from_slice(&build_fixed_point(4, false, true, 0, 32));
1975
1976        let (dt, _) = Datatype::parse(&buf).unwrap();
1977        match dt {
1978            Datatype::Array {
1979                base_type,
1980                dimensions,
1981            } => {
1982                assert_eq!(dimensions, vec![3, 4]);
1983                match *base_type {
1984                    Datatype::FixedPoint {
1985                        size: 4,
1986                        signed: true,
1987                        ..
1988                    } => {}
1989                    other => panic!("expected i32, got {other:?}"),
1990                }
1991            }
1992            _ => panic!("expected Array"),
1993        }
1994    }
1995
1996    /// Nothing in HDF5 occupies zero bytes per element, and the readers divide by
1997    /// the element size, so a declared zero is refused where an untrusted message
1998    /// becomes a `Datatype` rather than at each division (issue #268).
1999    #[test]
2000    fn a_zero_width_element_type_is_refused() {
2001        let buf = build_dt_header(3, 1, [0x01, 0, 0], 0); // fixed-length string of 0 bytes
2002        assert_eq!(
2003            Datatype::parse(&buf).unwrap_err(),
2004            FormatError::ZeroSizedDatatype { class: 3 }
2005        );
2006    }
2007
2008    /// An array's element size is its base type across its dimensions, not the
2009    /// size the header declares, and the two disagree: a zero dimension is a
2010    /// zero-width element behind a header that claims 48 bytes. Reading the
2011    /// declared field instead of the computed one lets this one through.
2012    #[test]
2013    fn an_array_with_a_zero_dimension_is_refused_despite_its_header_size() {
2014        let mut buf = build_dt_header(10, 3, [0, 0, 0], 48);
2015        buf.push(2); // ndims=2
2016        buf.extend_from_slice(&0u32.to_le_bytes()); // dim 0 — no elements
2017        buf.extend_from_slice(&4u32.to_le_bytes()); // dim 1
2018        buf.extend_from_slice(&build_fixed_point(4, false, true, 0, 32));
2019
2020        assert_eq!(
2021            Datatype::parse(&buf).unwrap_err(),
2022            FormatError::ZeroSizedDatatype { class: 10 }
2023        );
2024    }
2025
2026    /// The refusal reaches a nested type too: a compound member is parsed through
2027    /// the same entry, so a zero-width member is caught where it is decoded rather
2028    /// than becoming a member whose size no reader can use.
2029    #[test]
2030    fn a_zero_width_compound_member_is_refused() {
2031        let member = build_dt_header(3, 1, [0x01, 0, 0], 0);
2032        let mut buf = build_dt_header(6, 3, [1, 0, 0], 8); // one member
2033        buf.extend_from_slice(b"s\0");
2034        buf.push(0); // byte offset, one byte for a size-8 compound
2035        buf.extend_from_slice(&member);
2036
2037        assert_eq!(
2038            Datatype::parse(&buf).unwrap_err(),
2039            FormatError::ZeroSizedDatatype { class: 3 }
2040        );
2041    }
2042
2043    /// The accessor the rest of the crate uses agrees with `type_size` for an
2044    /// ordinary type. The point of the pair is that one of them carries a proof
2045    /// and the other does not — not that they report different widths.
2046    #[test]
2047    fn element_size_matches_type_size_for_a_type_that_has_one() {
2048        let dt = Datatype::FixedPoint {
2049            size: 4,
2050            byte_order: DatatypeByteOrder::LittleEndian,
2051            signed: true,
2052            bit_offset: 0,
2053            bit_precision: 32,
2054        };
2055        assert_eq!(dt.element_size().unwrap().get(), dt.type_size());
2056    }
2057
2058    /// A caller-built literal never passes through `parse`, so `element_size` is
2059    /// the only thing standing between a degenerate type and the writers. The
2060    /// `Array` case is the one that matters: its width is *computed* from its
2061    /// dimensions, so this cannot be caught by inspecting a stored size field.
2062    #[test]
2063    fn element_size_refuses_a_constructed_array_with_a_zero_dimension() {
2064        let dt = Datatype::Array {
2065            base_type: Box::new(Datatype::FixedPoint {
2066                size: 4,
2067                byte_order: DatatypeByteOrder::LittleEndian,
2068                signed: true,
2069                bit_offset: 0,
2070                bit_precision: 32,
2071            }),
2072            dimensions: vec![0, 4],
2073        };
2074        assert_eq!(dt.type_size(), 0);
2075        assert_eq!(
2076            dt.element_size().unwrap_err(),
2077            FormatError::ZeroSizedDatatype { class: 10 }
2078        );
2079        assert_eq!(
2080            dt.element_size_usize().unwrap_err(),
2081            FormatError::ZeroSizedDatatype { class: 10 }
2082        );
2083    }
2084
2085    /// The class in the error names the type that was refused, not the base type
2086    /// underneath it, so a report points at the message the writer was handed.
2087    #[test]
2088    fn element_size_reports_the_refused_types_own_class() {
2089        let dt = Datatype::Compound {
2090            size: 0,
2091            members: vec![],
2092        };
2093        assert_eq!(
2094            dt.element_size().unwrap_err(),
2095            FormatError::ZeroSizedDatatype { class: 6 }
2096        );
2097    }
2098
2099    #[test]
2100    fn test_bitfield() {
2101        let mut buf = build_dt_header(4, 1, [0, 0, 0], 2); // 16-bit LE bitfield
2102        let mut props = [0u8; 4];
2103        LittleEndian::write_u16(&mut props[0..2], 0);
2104        LittleEndian::write_u16(&mut props[2..4], 16);
2105        buf.extend_from_slice(&props);
2106
2107        let (dt, _) = Datatype::parse(&buf).unwrap();
2108        assert_eq!(
2109            dt,
2110            Datatype::BitField {
2111                size: 2,
2112                byte_order: DatatypeByteOrder::LittleEndian,
2113                bit_offset: 0,
2114                bit_precision: 16,
2115            }
2116        );
2117    }
2118
2119    #[test]
2120    fn test_time() {
2121        let mut buf = build_dt_header(2, 1, [0, 0, 0], 8);
2122        let mut props = [0u8; 2];
2123        LittleEndian::write_u16(&mut props[0..2], 64);
2124        buf.extend_from_slice(&props);
2125
2126        let (dt, consumed) = Datatype::parse(&buf).unwrap();
2127        assert_eq!(consumed, 10);
2128        assert_eq!(
2129            dt,
2130            Datatype::Time {
2131                size: 8,
2132                byte_order: DatatypeByteOrder::LittleEndian,
2133                bit_precision: 64,
2134            }
2135        );
2136    }
2137
2138    #[test]
2139    fn test_time_byte_order_roundtrips() {
2140        // A big-endian time type must serialize and re-parse with its byte order
2141        // preserved (bf0 bit 0), so repack can reproduce it faithfully.
2142        for (be, order) in [
2143            (0u8, DatatypeByteOrder::LittleEndian),
2144            (1u8, DatatypeByteOrder::BigEndian),
2145        ] {
2146            let mut buf = build_dt_header(2, 1, [be, 0, 0], 4);
2147            buf.extend_from_slice(&32u16.to_le_bytes());
2148            let (dt, _) = Datatype::parse(&buf).unwrap();
2149            assert_eq!(
2150                dt,
2151                Datatype::Time {
2152                    size: 4,
2153                    byte_order: order.clone(),
2154                    bit_precision: 32,
2155                }
2156            );
2157            // serialize -> parse must round-trip the byte order.
2158            let (reparsed, _) = Datatype::parse(&dt.serialize()).unwrap();
2159            assert_eq!(reparsed, dt);
2160        }
2161    }
2162
2163    #[test]
2164    fn test_nested_compound_array_enum() {
2165        // Compound containing a single member "data" which is an Array[2] of Enum(i32, 2 values)
2166        // Build the enum first
2167        let mut enum_bytes = build_dt_header(8, 3, [2, 0, 0], 4); // 2 members
2168        enum_bytes.extend_from_slice(&build_fixed_point(4, false, true, 0, 32)); // base i32
2169        enum_bytes.extend_from_slice(b"A\0");
2170        enum_bytes.extend_from_slice(b"B\0");
2171        enum_bytes.extend_from_slice(&0i32.to_le_bytes());
2172        enum_bytes.extend_from_slice(&1i32.to_le_bytes());
2173
2174        // Build array[2] of that enum, version 3
2175        let mut array_bytes = build_dt_header(10, 3, [0, 0, 0], 8); // 2*4=8
2176        array_bytes.push(1); // ndims=1
2177        array_bytes.extend_from_slice(&2u32.to_le_bytes()); // dim[0]=2
2178        array_bytes.extend_from_slice(&enum_bytes);
2179
2180        // Build compound with 1 member, size=8
2181        let mut buf = build_dt_header(6, 3, [1, 0, 0], 8); // 1 member
2182        buf.extend_from_slice(b"data\0");
2183        buf.push(0); // byte_offset = 0 (size=8, so 1 byte offsets)
2184        buf.extend_from_slice(&array_bytes);
2185
2186        let (dt, _) = Datatype::parse(&buf).unwrap();
2187        match dt {
2188            Datatype::Compound { members, .. } => {
2189                assert_eq!(members.len(), 1);
2190                assert_eq!(members[0].name, "data");
2191                match &members[0].datatype {
2192                    Datatype::Array {
2193                        dimensions,
2194                        base_type,
2195                    } => {
2196                        assert_eq!(dimensions, &[2]);
2197                        match base_type.as_ref() {
2198                            Datatype::Enumeration { members, .. } => {
2199                                assert_eq!(members.len(), 2);
2200                                assert_eq!(members[0].name, "A");
2201                                assert_eq!(members[1].name, "B");
2202                            }
2203                            other => panic!("expected Enum, got {other:?}"),
2204                        }
2205                    }
2206                    other => panic!("expected Array, got {other:?}"),
2207                }
2208            }
2209            _ => panic!("expected Compound"),
2210        }
2211    }
2212
2213    #[test]
2214    fn test_error_invalid_class() {
2215        let buf = build_dt_header(13, 1, [0, 0, 0], 4);
2216        let err = Datatype::parse(&buf).unwrap_err();
2217        assert_eq!(err, FormatError::InvalidDatatypeClass(13));
2218    }
2219
2220    #[test]
2221    fn test_error_truncated_data() {
2222        let buf = [0u8; 4]; // too short for header
2223        let err = Datatype::parse(&buf).unwrap_err();
2224        match err {
2225            FormatError::UnexpectedEof { .. } => {}
2226            other => panic!("expected UnexpectedEof, got {other:?}"),
2227        }
2228    }
2229
2230    #[test]
2231    fn test_error_invalid_string_padding() {
2232        let buf = build_dt_header(3, 1, [0x03, 0, 0], 10); // padding=3 invalid
2233        let err = Datatype::parse(&buf).unwrap_err();
2234        assert_eq!(err, FormatError::InvalidStringPadding(3));
2235    }
2236
2237    #[test]
2238    fn test_error_invalid_charset() {
2239        let buf = build_dt_header(3, 1, [0x20, 0, 0], 10); // charset=2 invalid
2240        let err = Datatype::parse(&buf).unwrap_err();
2241        assert_eq!(err, FormatError::InvalidCharacterSet(2));
2242    }
2243
2244    #[test]
2245    fn test_error_invalid_reference_type() {
2246        let buf = build_dt_header(7, 1, [5, 0, 0], 8);
2247        let err = Datatype::parse(&buf).unwrap_err();
2248        assert_eq!(err, FormatError::InvalidReferenceType(5));
2249    }
2250
2251    #[test]
2252    fn serialize_parse_compound_roundtrip() {
2253        let dt = Datatype::Compound {
2254            size: 20,
2255            members: vec![
2256                CompoundMember {
2257                    name: "x".to_string(),
2258                    byte_offset: 0,
2259                    datatype: Datatype::FloatingPoint {
2260                        size: 8,
2261                        byte_order: DatatypeByteOrder::LittleEndian,
2262                        bit_offset: 0,
2263                        bit_precision: 64,
2264                        exponent_location: 52,
2265                        exponent_size: 11,
2266                        mantissa_location: 0,
2267                        mantissa_size: 52,
2268                        exponent_bias: 1023,
2269                    },
2270                },
2271                CompoundMember {
2272                    name: "y".to_string(),
2273                    byte_offset: 8,
2274                    datatype: Datatype::FloatingPoint {
2275                        size: 8,
2276                        byte_order: DatatypeByteOrder::LittleEndian,
2277                        bit_offset: 0,
2278                        bit_precision: 64,
2279                        exponent_location: 52,
2280                        exponent_size: 11,
2281                        mantissa_location: 0,
2282                        mantissa_size: 52,
2283                        exponent_bias: 1023,
2284                    },
2285                },
2286                CompoundMember {
2287                    name: "id".to_string(),
2288                    byte_offset: 16,
2289                    datatype: Datatype::FixedPoint {
2290                        size: 4,
2291                        byte_order: DatatypeByteOrder::LittleEndian,
2292                        signed: true,
2293                        bit_offset: 0,
2294                        bit_precision: 32,
2295                    },
2296                },
2297            ],
2298        };
2299        let bytes = dt.serialize();
2300        let (parsed, _) = Datatype::parse(&bytes).unwrap();
2301        assert_eq!(parsed, dt);
2302    }
2303
2304    #[test]
2305    fn serialize_parse_enum_roundtrip() {
2306        let dt = Datatype::Enumeration {
2307            size: 4,
2308            base_type: Box::new(Datatype::FixedPoint {
2309                size: 4,
2310                byte_order: DatatypeByteOrder::LittleEndian,
2311                signed: true,
2312                bit_offset: 0,
2313                bit_precision: 32,
2314            }),
2315            members: vec![
2316                EnumMember {
2317                    name: "RED".to_string(),
2318                    value: 0i32.to_le_bytes().to_vec(),
2319                },
2320                EnumMember {
2321                    name: "GREEN".to_string(),
2322                    value: 1i32.to_le_bytes().to_vec(),
2323                },
2324                EnumMember {
2325                    name: "BLUE".to_string(),
2326                    value: 2i32.to_le_bytes().to_vec(),
2327                },
2328            ],
2329        };
2330        let bytes = dt.serialize();
2331        let (parsed, _) = Datatype::parse(&bytes).unwrap();
2332        assert_eq!(parsed, dt);
2333    }
2334
2335    /// Fixed-point base type for enum round-trip tests.
2336    fn enum_base_fp(size: u32, be: bool, signed: bool) -> Datatype {
2337        Datatype::FixedPoint {
2338            size,
2339            byte_order: if be {
2340                DatatypeByteOrder::BigEndian
2341            } else {
2342                DatatypeByteOrder::LittleEndian
2343            },
2344            signed,
2345            bit_offset: 0,
2346            #[expect(
2347                clippy::cast_possible_truncation,
2348                reason = "test builds byte-width base types; size*8 is well within u16"
2349            )]
2350            bit_precision: (size * 8) as u16,
2351        }
2352    }
2353
2354    /// Build an enum datatype over `base`, storing each member value truncated to
2355    /// the base width (the value blob is opaque bytes, so any content round-trips).
2356    fn make_enum(base: Datatype, members: &[(&str, i64)]) -> Datatype {
2357        let size = base.type_size();
2358        let width = size as usize;
2359        Datatype::Enumeration {
2360            size,
2361            base_type: Box::new(base),
2362            members: members
2363                .iter()
2364                .map(|(name, v)| EnumMember {
2365                    name: (*name).to_string(),
2366                    value: v.to_le_bytes()[..width].to_vec(),
2367                })
2368                .collect(),
2369        }
2370    }
2371
2372    #[test]
2373    fn serialize_parse_enum_base_type_variety() {
2374        // The i32 base is already covered above; here u8, big-endian i16, and i64
2375        // bases all round-trip through the enum wrapper.
2376        for base in [
2377            enum_base_fp(1, false, false), // u8
2378            enum_base_fp(2, true, true),   // i16 big-endian
2379            enum_base_fp(8, false, true),  // i64
2380        ] {
2381            let dt = make_enum(base.clone(), &[("A", 0), ("B", 1), ("NEG", -1)]);
2382            let bytes = dt.serialize();
2383            let (parsed, consumed) = Datatype::parse(&bytes).unwrap();
2384            assert_eq!(parsed, dt, "round-trip failed for base {base:?}");
2385            assert_eq!(consumed, bytes.len());
2386        }
2387    }
2388
2389    #[test]
2390    fn serialize_parse_enum_large_member_count() {
2391        // More than 256 members exercises the 2-byte member-count field, which is
2392        // split across bf0/bf1 in the datatype message header.
2393        let owned: Vec<(String, i64)> = (0..300).map(|i| (format!("M{i}"), i)).collect();
2394        let members: Vec<(&str, i64)> = owned.iter().map(|(n, v)| (n.as_str(), *v)).collect();
2395        let dt = make_enum(enum_base_fp(4, false, true), &members);
2396        let bytes = dt.serialize();
2397        let (parsed, _) = Datatype::parse(&bytes).unwrap();
2398        assert_eq!(parsed, dt);
2399        match parsed {
2400            Datatype::Enumeration { members, .. } => {
2401                assert_eq!(members.len(), 300);
2402                assert_eq!(members[299].name, "M299");
2403            }
2404            other => panic!("expected Enumeration, got {other:?}"),
2405        }
2406    }
2407
2408    #[test]
2409    fn enum_value_width_is_not_validated_against_base_size() {
2410        // `EnumTypeBuilder::build`/`Datatype::Enumeration` take the element size
2411        // from the base type only, with no check that member value blobs match it.
2412        // A 4-byte value on a 1-byte base therefore serializes in full but parses
2413        // back reading just `base_size` (1) byte per member, silently truncating.
2414        // This documents the current permissiveness; it is NOT a supported
2415        // round-trip, and the assertion guards against a silent change either way.
2416        let dt = Datatype::Enumeration {
2417            size: 1,
2418            base_type: Box::new(enum_base_fp(1, false, false)),
2419            members: vec![EnumMember {
2420                name: "X".to_string(),
2421                value: 5i32.to_le_bytes().to_vec(), // 4 bytes on a 1-byte base
2422            }],
2423        };
2424        let bytes = dt.serialize();
2425        let (parsed, _) = Datatype::parse(&bytes).unwrap();
2426        assert_ne!(
2427            parsed, dt,
2428            "a value wider than the base silently truncates on parse"
2429        );
2430        match parsed {
2431            Datatype::Enumeration { members, .. } => assert_eq!(members[0].value, vec![5]),
2432            other => panic!("expected Enumeration, got {other:?}"),
2433        }
2434    }
2435
2436    #[test]
2437    fn serialize_parse_array_roundtrip() {
2438        let dt = Datatype::Array {
2439            base_type: Box::new(Datatype::FloatingPoint {
2440                size: 8,
2441                byte_order: DatatypeByteOrder::LittleEndian,
2442                bit_offset: 0,
2443                bit_precision: 64,
2444                exponent_location: 52,
2445                exponent_size: 11,
2446                mantissa_location: 0,
2447                mantissa_size: 52,
2448                exponent_bias: 1023,
2449            }),
2450            dimensions: vec![3],
2451        };
2452        let bytes = dt.serialize();
2453        let (parsed, _) = Datatype::parse(&bytes).unwrap();
2454        assert_eq!(parsed, dt);
2455    }
2456
2457    #[test]
2458    fn serialize_parse_time_roundtrip() {
2459        let dt = Datatype::Time {
2460            size: 8,
2461            byte_order: DatatypeByteOrder::LittleEndian,
2462            bit_precision: 64,
2463        };
2464        let bytes = dt.serialize();
2465        let (parsed, consumed) = Datatype::parse(&bytes).unwrap();
2466        assert_eq!(parsed, dt);
2467        assert_eq!(consumed, bytes.len());
2468    }
2469
2470    #[test]
2471    fn serialize_parse_bitfield_roundtrip() {
2472        for byte_order in [
2473            DatatypeByteOrder::LittleEndian,
2474            DatatypeByteOrder::BigEndian,
2475        ] {
2476            let dt = Datatype::BitField {
2477                size: 4,
2478                byte_order,
2479                bit_offset: 3,
2480                bit_precision: 17,
2481            };
2482            let bytes = dt.serialize();
2483            let (parsed, consumed) = Datatype::parse(&bytes).unwrap();
2484            assert_eq!(parsed, dt);
2485            assert_eq!(consumed, bytes.len());
2486        }
2487    }
2488
2489    #[test]
2490    fn serialize_parse_opaque_roundtrip() {
2491        // Tag lengths that do and do not land on an 8-byte boundary, to exercise
2492        // the zero padding both ways.
2493        for tag in [
2494            b"abc".to_vec(),         // 3 bytes -> padded to 8
2495            b"12345678".to_vec(),    // 8 bytes -> no padding
2496            b"sensor-id\0".to_vec(), // 10 bytes -> padded to 16, embedded NUL preserved
2497        ] {
2498            let dt = Datatype::Opaque { size: 16, tag };
2499            let bytes = dt.serialize();
2500            // The property section (after the 8-byte header) must be a multiple
2501            // of 8, matching what the reference library expects.
2502            assert_eq!((bytes.len() - 8) % 8, 0);
2503            let (parsed, consumed) = Datatype::parse(&bytes).unwrap();
2504            assert_eq!(parsed, dt);
2505            assert_eq!(consumed, bytes.len());
2506        }
2507    }
2508
2509    #[test]
2510    fn test_type_size() {
2511        let dt = Datatype::FixedPoint {
2512            size: 4,
2513            byte_order: DatatypeByteOrder::LittleEndian,
2514            signed: true,
2515            bit_offset: 0,
2516            bit_precision: 32,
2517        };
2518        assert_eq!(dt.type_size(), 4);
2519
2520        let dt = Datatype::Array {
2521            base_type: Box::new(Datatype::FixedPoint {
2522                size: 4,
2523                byte_order: DatatypeByteOrder::LittleEndian,
2524                signed: true,
2525                bit_offset: 0,
2526                bit_precision: 32,
2527            }),
2528            dimensions: vec![3, 4],
2529        };
2530        assert_eq!(dt.type_size(), 48);
2531    }
2532}
2533
2534#[cfg(all(test, feature = "std"))]
2535mod display_tests {
2536    use super::*;
2537
2538    #[test]
2539    fn ordinary_numeric_types_read_as_their_rust_names() {
2540        let int = Datatype::FixedPoint {
2541            size: 4,
2542            byte_order: DatatypeByteOrder::LittleEndian,
2543            signed: true,
2544            bit_offset: 0,
2545            bit_precision: 32,
2546        };
2547        assert_eq!(int.to_string(), "i32");
2548
2549        let float = Datatype::FloatingPoint {
2550            size: 8,
2551            byte_order: DatatypeByteOrder::LittleEndian,
2552            bit_offset: 0,
2553            bit_precision: 64,
2554            exponent_location: 52,
2555            exponent_size: 11,
2556            mantissa_location: 0,
2557            mantissa_size: 52,
2558            exponent_bias: 1023,
2559        };
2560        assert_eq!(float.to_string(), "f64");
2561    }
2562
2563    /// Every width a message writes is `size * 8` over an on-disk `u32`, so a
2564    /// crafted size near [`u32::MAX`] overflows a `u32` multiply and panics a
2565    /// debug build (issue #140). [`bit_width`] widens first; this holds each
2566    /// class that calls it to that, rather than reaching one of them through
2567    /// whatever `classify_datatype` happens to route here.
2568    #[test]
2569    fn a_crafted_size_writes_its_width_instead_of_overflowing() {
2570        let bits = u64::from(u32::MAX) * 8;
2571        let cases = [
2572            (
2573                Datatype::FixedPoint {
2574                    size: u32::MAX,
2575                    byte_order: DatatypeByteOrder::LittleEndian,
2576                    signed: true,
2577                    bit_offset: 0,
2578                    bit_precision: 0,
2579                },
2580                format!("i{bits}(bits 0..0)"),
2581            ),
2582            (
2583                Datatype::FloatingPoint {
2584                    size: u32::MAX,
2585                    byte_order: DatatypeByteOrder::LittleEndian,
2586                    bit_offset: 0,
2587                    bit_precision: 0,
2588                    exponent_location: 0,
2589                    exponent_size: 0,
2590                    mantissa_location: 0,
2591                    mantissa_size: 0,
2592                    exponent_bias: 0,
2593                },
2594                format!("f{bits}(bits 0..0)"),
2595            ),
2596            (
2597                Datatype::Time {
2598                    size: u32::MAX,
2599                    byte_order: DatatypeByteOrder::LittleEndian,
2600                    bit_precision: 0,
2601                },
2602                format!("time{bits}(bits 0..0)"),
2603            ),
2604            (
2605                Datatype::BitField {
2606                    size: u32::MAX,
2607                    byte_order: DatatypeByteOrder::LittleEndian,
2608                    bit_offset: 0,
2609                    bit_precision: 0,
2610                },
2611                format!("bitfield{bits}(bits 0..0)"),
2612            ),
2613        ];
2614
2615        for (dtype, expected) in cases {
2616            assert_eq!(dtype.to_string(), expected);
2617        }
2618    }
2619
2620    /// The bit span adds two `u16`s, which is the other place a crafted field
2621    /// could wrap. Both widen, so the end is 131,070 rather than 65,534.
2622    #[test]
2623    fn a_crafted_bit_span_does_not_wrap() {
2624        let dtype = Datatype::FixedPoint {
2625            size: 1,
2626            byte_order: DatatypeByteOrder::LittleEndian,
2627            signed: false,
2628            bit_offset: u16::MAX,
2629            bit_precision: u16::MAX,
2630        };
2631        assert_eq!(dtype.to_string(), "u8(bits 65535..131070)");
2632    }
2633
2634    /// Only what departs from the ordinary is written, since that is what the
2635    /// reader of the message is looking for.
2636    #[test]
2637    fn unusual_fields_are_written_and_ordinary_ones_are_not() {
2638        let big_endian = Datatype::FixedPoint {
2639            size: 2,
2640            byte_order: DatatypeByteOrder::BigEndian,
2641            signed: false,
2642            bit_offset: 0,
2643            bit_precision: 16,
2644        };
2645        assert_eq!(big_endian.to_string(), "u16 be");
2646
2647        let narrow = Datatype::FixedPoint {
2648            size: 4,
2649            byte_order: DatatypeByteOrder::LittleEndian,
2650            signed: true,
2651            bit_offset: 0,
2652            bit_precision: 24,
2653        };
2654        assert_eq!(narrow.to_string(), "i32(bits 0..24)");
2655    }
2656
2657    #[test]
2658    fn nested_types_recurse_through_their_members() {
2659        let compound = Datatype::Compound {
2660            size: 12,
2661            members: vec![
2662                CompoundMember {
2663                    name: "x".into(),
2664                    byte_offset: 0,
2665                    datatype: Datatype::FloatingPoint {
2666                        size: 4,
2667                        byte_order: DatatypeByteOrder::LittleEndian,
2668                        bit_offset: 0,
2669                        bit_precision: 32,
2670                        exponent_location: 23,
2671                        exponent_size: 8,
2672                        mantissa_location: 0,
2673                        mantissa_size: 23,
2674                        exponent_bias: 127,
2675                    },
2676                },
2677                CompoundMember {
2678                    name: "n".into(),
2679                    byte_offset: 4,
2680                    datatype: Datatype::FixedPoint {
2681                        size: 8,
2682                        byte_order: DatatypeByteOrder::LittleEndian,
2683                        signed: true,
2684                        bit_offset: 0,
2685                        bit_precision: 64,
2686                    },
2687                },
2688            ],
2689        };
2690        assert_eq!(compound.to_string(), "compound{x: f32, n: i64}");
2691
2692        let array = Datatype::Array {
2693            base_type: Box::new(Datatype::FixedPoint {
2694                size: 1,
2695                byte_order: DatatypeByteOrder::LittleEndian,
2696                signed: false,
2697                bit_offset: 0,
2698                bit_precision: 8,
2699            }),
2700            dimensions: vec![2, 3],
2701        };
2702        assert_eq!(
2703            array.to_string(),
2704            "array<u8, 2x3>",
2705            "the shape is spelled `2x3`, never a `Debug` slice"
2706        );
2707    }
2708
2709    /// The leaf enums format through `Formatter::pad`, so a caller lining these
2710    /// up in a column gets the width it asked for rather than having it
2711    /// silently dropped.
2712    #[test]
2713    fn a_leaf_enum_honors_the_width_it_is_given() {
2714        assert_eq!(format!("{:>8}", CharacterSet::Ascii), "   ascii");
2715        assert_eq!(format!("{:<8}|", DatatypeByteOrder::BigEndian), "be      |");
2716        assert_eq!(format!("{}", StringPadding::NullPad), "null-pad");
2717    }
2718
2719    #[test]
2720    fn a_string_carries_its_width_charset_and_padding() {
2721        let string = Datatype::String {
2722            size: 16,
2723            padding: StringPadding::NullPad,
2724            charset: CharacterSet::Utf8,
2725        };
2726        assert_eq!(string.to_string(), "string[16] utf8 null-pad");
2727    }
2728
2729    /// The tag is arbitrary file bytes, so it cannot reach a message unescaped.
2730    #[test]
2731    fn an_opaque_tag_is_quoted_and_escaped() {
2732        let opaque = Datatype::Opaque {
2733            size: 4,
2734            tag: b"a\"b\x00".to_vec(),
2735        };
2736        assert_eq!(opaque.to_string(), "opaque[4] \"a\\\"b\\x00\"");
2737    }
2738
2739    /// A member name comes from the file by way of `from_utf8_lossy`, which
2740    /// rejects nothing, so it is escaped for the same reason an opaque tag is.
2741    #[test]
2742    fn a_member_name_cannot_carry_a_control_character_into_a_message() {
2743        let compound = Datatype::Compound {
2744            size: 4,
2745            members: vec![CompoundMember {
2746                name: "a\nb\u{1b}[31m".into(),
2747                byte_offset: 0,
2748                datatype: u32_datatype(),
2749            }],
2750        };
2751        let shown = compound.to_string();
2752        assert!(!shown.chars().any(char::is_control), "{shown}");
2753        assert_eq!(shown, "compound{a\\nb\\u{1b}[31m: u32}");
2754
2755        let enumeration = Datatype::Enumeration {
2756            size: 4,
2757            base_type: Box::new(u32_datatype()),
2758            members: vec![EnumMember {
2759                name: "red\u{0}".into(),
2760                value: vec![0, 0, 0, 0],
2761            }],
2762        };
2763        let shown = enumeration.to_string();
2764        assert!(!shown.chars().any(char::is_control), "{shown}");
2765        assert_eq!(shown, "enum<u32>[red\\0]");
2766    }
2767
2768    /// The member count is an on-disk `u16`, so the list a file can ask for is
2769    /// far longer than a message can carry. Both member-bearing variants elide,
2770    /// so both are checked.
2771    #[test]
2772    fn a_long_member_list_is_elided_and_reports_the_remainder() {
2773        let over_cap = DISPLAY_MAX_MEMBERS + 3;
2774
2775        let compound = Datatype::Compound {
2776            size: (over_cap * 4) as u32,
2777            members: (0..over_cap)
2778                .map(|i| CompoundMember {
2779                    name: format!("m{i}"),
2780                    byte_offset: (i * 4) as u64,
2781                    datatype: u32_datatype(),
2782                })
2783                .collect(),
2784        };
2785        let enumeration = Datatype::Enumeration {
2786            size: 4,
2787            base_type: Box::new(u32_datatype()),
2788            members: (0..over_cap)
2789                .map(|i| EnumMember {
2790                    name: format!("m{i}"),
2791                    value: vec![0, 0, 0, 0],
2792                })
2793                .collect(),
2794        };
2795
2796        for (datatype, close) in [(compound, "}"), (enumeration, "]")] {
2797            let shown = datatype.to_string();
2798            assert!(shown.ends_with(&format!(", … 3 more{close}")), "{shown}");
2799            assert!(shown.contains("m0"), "{shown}");
2800            assert!(
2801                !shown.contains(&format!("m{DISPLAY_MAX_MEMBERS}")),
2802                "{shown}"
2803            );
2804        }
2805    }
2806
2807    /// The boundary: exactly the cap is written whole, with no "0 more".
2808    #[test]
2809    fn a_member_list_at_exactly_the_cap_is_not_elided() {
2810        let members: Vec<_> = (0..DISPLAY_MAX_MEMBERS)
2811            .map(|i| EnumMember {
2812                name: format!("m{i}"),
2813                value: vec![0, 0, 0, 0],
2814            })
2815            .collect();
2816        let shown = Datatype::Enumeration {
2817            size: 4,
2818            base_type: Box::new(u32_datatype()),
2819            members,
2820        }
2821        .to_string();
2822
2823        assert!(!shown.contains('…'), "{shown}");
2824        assert!(
2825            shown.ends_with(&format!("m{}]", DISPLAY_MAX_MEMBERS - 1)),
2826            "{shown}"
2827        );
2828    }
2829
2830    fn u32_datatype() -> Datatype {
2831        Datatype::FixedPoint {
2832            size: 4,
2833            byte_order: DatatypeByteOrder::LittleEndian,
2834            signed: false,
2835            bit_offset: 0,
2836            bit_precision: 32,
2837        }
2838    }
2839}