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;
10
11use byteorder::{ByteOrder, LittleEndian};
12
13use crate::display::{DISPLAY_MAX_MEMBERS, Dims, EscapedName, QuotedBytes, write_elided};
14use crate::error::FormatError;
15
16/// Byte order of numeric data.
17#[derive(Debug, Clone, PartialEq)]
18pub enum DatatypeByteOrder {
19    LittleEndian,
20    BigEndian,
21    Vax,
22}
23
24/// String padding type.
25#[derive(Debug, Clone, PartialEq)]
26pub enum StringPadding {
27    NullTerminate,
28    NullPad,
29    SpacePad,
30}
31
32/// Character set encoding.
33#[derive(Debug, Clone, PartialEq)]
34pub enum CharacterSet {
35    Ascii,
36    Utf8,
37}
38
39/// Reference type.
40///
41/// Non-exhaustive: the format has gained reference kinds since (HDF5 1.12 added
42/// attribute references), so match with a `_` arm.
43#[derive(Debug, Clone, PartialEq)]
44#[non_exhaustive]
45pub enum ReferenceType {
46    Object,
47    DatasetRegion,
48}
49
50/// A member of a compound datatype.
51///
52/// Non-exhaustive: parsed from a datatype message, and
53/// [`CompoundTypeBuilder`](crate::CompoundTypeBuilder) builds one over an
54/// arbitrary offset and member datatype, so nothing needs to construct this
55/// directly.
56#[derive(Debug, Clone, PartialEq)]
57#[non_exhaustive]
58pub struct CompoundMember {
59    /// Member name.
60    pub name: String,
61    /// Byte offset within the compound.
62    pub byte_offset: u64,
63    /// Member datatype.
64    pub datatype: Datatype,
65}
66
67/// A member of an enumeration datatype.
68///
69/// Non-exhaustive: parsed from a datatype message, and
70/// [`EnumTypeBuilder`](crate::EnumTypeBuilder) builds one over any integer base
71/// type (`with_base` plus `raw_value`), so nothing needs to construct this
72/// directly.
73#[derive(Debug, Clone, PartialEq)]
74#[non_exhaustive]
75pub struct EnumMember {
76    /// Member name.
77    pub name: String,
78    /// Raw value bytes (length = base type size).
79    pub value: Vec<u8>,
80}
81
82/// Parsed HDF5 datatype.
83///
84/// Non-exhaustive: the format's class set is not closed (HDF5 1.14.6 added a
85/// complex-number class), so match with a `_` arm. Only the *class* set is
86/// sealed — the variants stay open, so an exotic type this crate has no
87/// constructor for can still be built as a literal, and surfacing a format field
88/// this crate currently discards (a fixed-point type's padding bits, say) would
89/// still be a breaking change.
90#[derive(Debug, Clone, PartialEq)]
91#[non_exhaustive]
92pub enum Datatype {
93    /// Class 0: Fixed-point (integer) types.
94    FixedPoint {
95        size: u32,
96        byte_order: DatatypeByteOrder,
97        signed: bool,
98        bit_offset: u16,
99        bit_precision: u16,
100    },
101    /// Class 1: Floating-point types.
102    FloatingPoint {
103        size: u32,
104        byte_order: DatatypeByteOrder,
105        bit_offset: u16,
106        bit_precision: u16,
107        exponent_location: u8,
108        exponent_size: u8,
109        mantissa_location: u8,
110        mantissa_size: u8,
111        exponent_bias: u32,
112    },
113    /// Class 2: Time type (rarely used).
114    Time {
115        size: u32,
116        byte_order: DatatypeByteOrder,
117        bit_precision: u16,
118    },
119    /// Class 3: Fixed-length string.
120    String {
121        size: u32,
122        padding: StringPadding,
123        charset: CharacterSet,
124    },
125    /// Class 4: Bit field.
126    BitField {
127        size: u32,
128        byte_order: DatatypeByteOrder,
129        bit_offset: u16,
130        bit_precision: u16,
131    },
132    /// Class 5: Opaque data.
133    Opaque { size: u32, tag: Vec<u8> },
134    /// Class 6: Compound type.
135    Compound {
136        size: u32,
137        members: Vec<CompoundMember>,
138    },
139    /// Class 7: Reference type.
140    Reference { size: u32, ref_type: ReferenceType },
141    /// Class 8: Enumeration type.
142    Enumeration {
143        size: u32,
144        base_type: Box<Datatype>,
145        members: Vec<EnumMember>,
146    },
147    /// Class 9: Variable-length type.
148    VariableLength {
149        is_string: bool,
150        padding: Option<StringPadding>,
151        charset: Option<CharacterSet>,
152        base_type: Box<Datatype>,
153    },
154    /// Class 10: Array type.
155    Array {
156        base_type: Box<Datatype>,
157        dimensions: Vec<u32>,
158    },
159}
160
161// ---- Display ----
162//
163// These types land in error messages, so `Display` is the short form: the width
164// and class, plus the fields that depart from the ordinary — a big-endian order,
165// a bit span narrower than the type. A string always names its charset and
166// padding, ordinary or not, because they decide how its bytes read. `Debug`
167// keeps the full record.
168
169impl fmt::Display for DatatypeByteOrder {
170    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
171        f.pad(match self {
172            Self::LittleEndian => "le",
173            Self::BigEndian => "be",
174            Self::Vax => "vax",
175        })
176    }
177}
178
179impl fmt::Display for StringPadding {
180    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
181        f.pad(match self {
182            Self::NullTerminate => "null-term",
183            Self::NullPad => "null-pad",
184            Self::SpacePad => "space-pad",
185        })
186    }
187}
188
189impl fmt::Display for CharacterSet {
190    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
191        f.pad(match self {
192            Self::Ascii => "ascii",
193            Self::Utf8 => "utf8",
194        })
195    }
196}
197
198impl fmt::Display for ReferenceType {
199    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
200        f.pad(match self {
201            Self::Object => "object_ref",
202            Self::DatasetRegion => "region_ref",
203        })
204    }
205}
206
207/// The width in bits of a `size`-byte type.
208///
209/// Widens first: `size` is an on-disk `u32`, so a crafted size near [`u32::MAX`]
210/// would overflow a `u32` multiply (issue #140).
211fn bit_width(size: u32) -> u64 {
212    u64::from(size) * 8
213}
214
215/// The bit span, written only when it is narrower than the whole type.
216fn write_bit_span(
217    f: &mut fmt::Formatter<'_>,
218    size: u32,
219    bit_offset: u16,
220    bit_precision: u16,
221) -> fmt::Result {
222    if bit_offset != 0 || u64::from(bit_precision) != bit_width(size) {
223        let end = u64::from(bit_offset) + u64::from(bit_precision);
224        write!(f, "(bits {bit_offset}..{end})")?;
225    }
226    Ok(())
227}
228
229/// The byte order, written only when it is not little-endian.
230fn write_byte_order(f: &mut fmt::Formatter<'_>, byte_order: &DatatypeByteOrder) -> fmt::Result {
231    if *byte_order != DatatypeByteOrder::LittleEndian {
232        write!(f, " {byte_order}")?;
233    }
234    Ok(())
235}
236
237impl fmt::Display for Datatype {
238    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
239        match self {
240            Self::FixedPoint {
241                size,
242                byte_order,
243                signed,
244                bit_offset,
245                bit_precision,
246            } => {
247                let sign = if *signed { 'i' } else { 'u' };
248                write!(f, "{sign}{}", bit_width(*size))?;
249                write_bit_span(f, *size, *bit_offset, *bit_precision)?;
250                write_byte_order(f, byte_order)
251            }
252            Self::FloatingPoint {
253                size,
254                byte_order,
255                bit_offset,
256                bit_precision,
257                ..
258            } => {
259                write!(f, "f{}", bit_width(*size))?;
260                write_bit_span(f, *size, *bit_offset, *bit_precision)?;
261                write_byte_order(f, byte_order)
262            }
263            Self::Time {
264                size,
265                byte_order,
266                bit_precision,
267            } => {
268                write!(f, "time{}", bit_width(*size))?;
269                write_bit_span(f, *size, 0, *bit_precision)?;
270                write_byte_order(f, byte_order)
271            }
272            Self::String {
273                size,
274                padding,
275                charset,
276            } => write!(f, "string[{size}] {charset} {padding}"),
277            Self::BitField {
278                size,
279                byte_order,
280                bit_offset,
281                bit_precision,
282            } => {
283                write!(f, "bitfield{}", bit_width(*size))?;
284                write_bit_span(f, *size, *bit_offset, *bit_precision)?;
285                write_byte_order(f, byte_order)
286            }
287            Self::Opaque { size, tag } => {
288                write!(f, "opaque[{size}]")?;
289                if !tag.is_empty() {
290                    write!(f, " {}", QuotedBytes(tag))?;
291                }
292                Ok(())
293            }
294            Self::Compound { members, .. } => {
295                f.write_str("compound{")?;
296                for (i, member) in members.iter().take(DISPLAY_MAX_MEMBERS).enumerate() {
297                    if i > 0 {
298                        f.write_str(", ")?;
299                    }
300                    write!(f, "{}: {}", EscapedName(&member.name), member.datatype)?;
301                }
302                write_elided(f, members.len().saturating_sub(DISPLAY_MAX_MEMBERS))?;
303                f.write_str("}")
304            }
305            Self::Reference { ref_type, .. } => write!(f, "{ref_type}"),
306            Self::Enumeration {
307                base_type, members, ..
308            } => {
309                write!(f, "enum<{base_type}>[")?;
310                for (i, member) in members.iter().take(DISPLAY_MAX_MEMBERS).enumerate() {
311                    if i > 0 {
312                        f.write_str(", ")?;
313                    }
314                    write!(f, "{}", EscapedName(&member.name))?;
315                }
316                write_elided(f, members.len().saturating_sub(DISPLAY_MAX_MEMBERS))?;
317                f.write_str("]")
318            }
319            Self::VariableLength {
320                is_string,
321                charset,
322                base_type,
323                ..
324            } => {
325                if *is_string {
326                    f.write_str("vlen_string")?;
327                    if let Some(charset) = charset {
328                        write!(f, " {charset}")?;
329                    }
330                    Ok(())
331                } else {
332                    write!(f, "vlen<{base_type}>")
333                }
334            }
335            Self::Array {
336                base_type,
337                dimensions,
338            } => write!(f, "array<{base_type}, {}>", Dims(dimensions)),
339        }
340    }
341}
342
343fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> {
344    match offset.checked_add(needed) {
345        Some(end) if end <= data.len() => Ok(()),
346        _ => Err(FormatError::UnexpectedEof {
347            expected: offset.saturating_add(needed),
348            available: data.len(),
349        }),
350    }
351}
352
353fn parse_string_padding(val: u8) -> Result<StringPadding, FormatError> {
354    match val {
355        0 => Ok(StringPadding::NullTerminate),
356        1 => Ok(StringPadding::NullPad),
357        2 => Ok(StringPadding::SpacePad),
358        _ => Err(FormatError::InvalidStringPadding(val)),
359    }
360}
361
362fn parse_charset(val: u8) -> Result<CharacterSet, FormatError> {
363    match val {
364        0 => Ok(CharacterSet::Ascii),
365        1 => Ok(CharacterSet::Utf8),
366        _ => Err(FormatError::InvalidCharacterSet(val)),
367    }
368}
369
370/// Read a null-terminated string from `data` starting at `offset`.
371/// Returns (string, bytes_consumed including the null terminator).
372fn read_null_terminated_string(data: &[u8], offset: usize) -> Result<(String, usize), FormatError> {
373    if offset >= data.len() {
374        return Err(FormatError::UnexpectedEof {
375            expected: offset + 1,
376            available: data.len(),
377        });
378    }
379    let remaining = &data[offset..];
380    let null_pos = remaining
381        .iter()
382        .position(|&b| b == 0)
383        .ok_or(FormatError::UnexpectedEof {
384            expected: offset + 1,
385            available: data.len(),
386        })?;
387    let name = String::from_utf8_lossy(&remaining[..null_pos]).into_owned();
388    Ok((name, null_pos + 1))
389}
390
391/// Determine how many bytes are needed to encode `compound_size` as a byte offset (v3).
392fn offset_bytes_for_size(compound_size: u32) -> usize {
393    if compound_size <= 0xFF {
394        1
395    } else if compound_size <= 0xFFFF {
396        2
397    } else {
398        4
399    }
400}
401
402/// Read an unsigned integer of 1, 2, 4, or 8 bytes (LE).
403fn read_uint(data: &[u8], offset: usize, nbytes: usize) -> Result<u64, FormatError> {
404    ensure_len(data, offset, nbytes)?;
405    let slice = &data[offset..offset + nbytes];
406    Ok(match nbytes {
407        1 => slice[0] as u64,
408        2 => LittleEndian::read_u16(slice) as u64,
409        4 => LittleEndian::read_u32(slice) as u64,
410        8 => LittleEndian::read_u64(slice),
411        _ => {
412            return Err(FormatError::UnexpectedEof {
413                expected: offset + nbytes,
414                available: data.len(),
415            });
416        }
417    })
418}
419
420impl Datatype {
421    /// Parse a datatype message from raw bytes.
422    ///
423    /// Returns `(Datatype, bytes_consumed)` for recursive parsing.
424    ///
425    /// Crate-internal: no public API hands out datatype-message bytes to feed it.
426    /// Read a dataset's type with [`Dataset::datatype`](crate::Dataset::datatype).
427    pub(crate) fn parse(data: &[u8]) -> Result<(Datatype, usize), FormatError> {
428        // Minimum header: 4 bytes (class_and_version + 3 bytes bit field) + 4 bytes size = 8
429        ensure_len(data, 0, 8)?;
430
431        let class_and_version = data[0];
432        let class_id = class_and_version & 0x0F;
433        let version = (class_and_version >> 4) & 0x0F;
434
435        // 24-bit class bit field (little-endian)
436        let bf0 = data[1];
437        let bf1 = data[2];
438        let bf2 = data[3];
439        let _bit_field_24 = (bf0 as u32) | ((bf1 as u32) << 8) | ((bf2 as u32) << 16);
440
441        let size = LittleEndian::read_u32(&data[4..8]);
442        let mut pos = 8;
443
444        match class_id {
445            0 => {
446                // Fixed-Point
447                ensure_len(data, pos, 4)?;
448                let byte_order = if bf0 & 0x01 == 0 {
449                    DatatypeByteOrder::LittleEndian
450                } else {
451                    DatatypeByteOrder::BigEndian
452                };
453                let signed = (bf0 >> 3) & 0x01 == 1;
454                let bit_offset = LittleEndian::read_u16(&data[pos..pos + 2]);
455                let bit_precision = LittleEndian::read_u16(&data[pos + 2..pos + 4]);
456                pos += 4;
457                Ok((
458                    Datatype::FixedPoint {
459                        size,
460                        byte_order,
461                        signed,
462                        bit_offset,
463                        bit_precision,
464                    },
465                    pos,
466                ))
467            }
468            1 => {
469                // Floating-Point
470                ensure_len(data, pos, 12)?;
471                let bo_low = bf0 & 0x01;
472                let bo_high = (bf0 >> 6) & 0x01;
473                let byte_order = match (bo_high, bo_low) {
474                    (0, 0) => DatatypeByteOrder::LittleEndian,
475                    (0, 1) => DatatypeByteOrder::BigEndian,
476                    (1, 0) => DatatypeByteOrder::Vax,
477                    (1, 1) => DatatypeByteOrder::Vax,
478                    _ => unreachable!(),
479                };
480                let bit_offset = LittleEndian::read_u16(&data[pos..pos + 2]);
481                let bit_precision = LittleEndian::read_u16(&data[pos + 2..pos + 4]);
482                let exponent_location = data[pos + 4];
483                let exponent_size = data[pos + 5];
484                let mantissa_location = data[pos + 6];
485                let mantissa_size = data[pos + 7];
486                let exponent_bias = LittleEndian::read_u32(&data[pos + 8..pos + 12]);
487                pos += 12;
488                Ok((
489                    Datatype::FloatingPoint {
490                        size,
491                        byte_order,
492                        bit_offset,
493                        bit_precision,
494                        exponent_location,
495                        exponent_size,
496                        mantissa_location,
497                        mantissa_size,
498                        exponent_bias,
499                    },
500                    pos,
501                ))
502            }
503            2 => {
504                // Time
505                ensure_len(data, pos, 2)?;
506                let byte_order = if bf0 & 0x01 == 0 {
507                    DatatypeByteOrder::LittleEndian
508                } else {
509                    DatatypeByteOrder::BigEndian
510                };
511                let bit_precision = LittleEndian::read_u16(&data[pos..pos + 2]);
512                pos += 2;
513                Ok((
514                    Datatype::Time {
515                        size,
516                        byte_order,
517                        bit_precision,
518                    },
519                    pos,
520                ))
521            }
522            3 => {
523                // String
524                let padding_val = bf0 & 0x0F;
525                let charset_val = (bf0 >> 4) & 0x0F;
526                let padding = parse_string_padding(padding_val)?;
527                let charset = parse_charset(charset_val)?;
528                Ok((
529                    Datatype::String {
530                        size,
531                        padding,
532                        charset,
533                    },
534                    pos,
535                ))
536            }
537            4 => {
538                // Bit Field
539                ensure_len(data, pos, 4)?;
540                let byte_order = if bf0 & 0x01 == 0 {
541                    DatatypeByteOrder::LittleEndian
542                } else {
543                    DatatypeByteOrder::BigEndian
544                };
545                let bit_offset = LittleEndian::read_u16(&data[pos..pos + 2]);
546                let bit_precision = LittleEndian::read_u16(&data[pos + 2..pos + 4]);
547                pos += 4;
548                Ok((
549                    Datatype::BitField {
550                        size,
551                        byte_order,
552                        bit_offset,
553                        bit_precision,
554                    },
555                    pos,
556                ))
557            }
558            5 => {
559                // Opaque
560                let tag_len = bf0 as usize;
561                ensure_len(data, pos, tag_len)?;
562                let tag = data[pos..pos + tag_len].to_vec();
563                // Tags are padded to multiple of 8 bytes
564                let padded = (tag_len + 7) & !7;
565                let pos = 8 + padded; // from start of properties
566                Ok((Datatype::Opaque { size, tag }, pos))
567            }
568            6 => {
569                // Compound
570                let num_members = (bf0 as u16) | ((bf1 as u16) << 8);
571                let mut members = Vec::with_capacity(num_members as usize);
572
573                if version == 3 || version == 4 {
574                    let ob = offset_bytes_for_size(size);
575                    for _ in 0..num_members {
576                        let (name, name_len) = read_null_terminated_string(data, pos)?;
577                        pos += name_len;
578                        let byte_offset = read_uint(data, pos, ob)?;
579                        pos += ob;
580                        let (member_dt, consumed) = Datatype::parse(&data[pos..])?;
581                        pos += consumed;
582                        members.push(CompoundMember {
583                            name,
584                            byte_offset,
585                            datatype: member_dt,
586                        });
587                    }
588                } else if version == 1 || version == 2 {
589                    // v1 and v2: the member name is NUL-terminated and padded with
590                    // additional NULs to a multiple of 8 bytes, followed by a
591                    // 4-byte member byte offset. v1 then carries a fixed 28-byte
592                    // dimension block — dimensionality(1) + reserved(3) +
593                    // dimension permutation(4) + reserved(4) + dimension sizes(16)
594                    // — before the member datatype message; v2 drops that block.
595                    for _ in 0..num_members {
596                        let (name, name_len) = read_null_terminated_string(data, pos)?;
597                        let padded = (name_len + 7) & !7;
598                        pos += padded;
599                        ensure_len(data, pos, 4)?;
600                        let byte_offset = LittleEndian::read_u32(&data[pos..pos + 4]) as u64;
601                        pos += 4;
602                        if version == 1 {
603                            ensure_len(data, pos, 28)?;
604                            pos += 28;
605                        }
606                        let (member_dt, consumed) = Datatype::parse(&data[pos..])?;
607                        pos += consumed;
608                        members.push(CompoundMember {
609                            name,
610                            byte_offset,
611                            datatype: member_dt,
612                        });
613                    }
614                } else {
615                    return Err(FormatError::InvalidDatatypeVersion {
616                        class: class_id,
617                        version,
618                    });
619                }
620
621                Ok((Datatype::Compound { size, members }, pos))
622            }
623            7 => {
624                // Reference
625                let ref_type_val = bf0 & 0x0F;
626                let ref_type = match ref_type_val {
627                    0 => ReferenceType::Object,
628                    1 => ReferenceType::DatasetRegion,
629                    _ => return Err(FormatError::InvalidReferenceType(ref_type_val)),
630                };
631                Ok((Datatype::Reference { size, ref_type }, pos))
632            }
633            8 => {
634                // Enumeration
635                let num_members = (bf0 as u16) | ((bf1 as u16) << 8);
636                // Parse base type
637                let (base_type, base_consumed) = Datatype::parse(&data[pos..])?;
638                pos += base_consumed;
639                let base_size = base_type.type_size();
640                let mut members = Vec::with_capacity(num_members as usize);
641                // Enum layout: base_type, then all names (null-terminated), then all values
642                // v1/v2: names are padded to 8-byte boundaries
643                // v3: names are just null-terminated
644                let mut member_names = Vec::with_capacity(num_members as usize);
645                for _ in 0..num_members {
646                    let (name, name_len) = read_null_terminated_string(data, pos)?;
647                    if version < 3 {
648                        let padded = (name_len + 7) & !7;
649                        pos += padded;
650                    } else {
651                        pos += name_len;
652                    }
653                    member_names.push(name);
654                }
655                // Now values
656                for name in &member_names {
657                    ensure_len(data, pos, base_size as usize)?;
658                    let value = data[pos..pos + base_size as usize].to_vec();
659                    pos += base_size as usize;
660                    members.push(EnumMember {
661                        name: name.clone(),
662                        value,
663                    });
664                }
665                Ok((
666                    Datatype::Enumeration {
667                        size,
668                        base_type: Box::new(base_type),
669                        members,
670                    },
671                    pos,
672                ))
673            }
674            9 => {
675                // Variable-Length
676                let vl_type = bf0 & 0x0F;
677                let is_string = vl_type == 1;
678                let padding = if is_string {
679                    let pad_val = (bf0 >> 4) & 0x0F;
680                    Some(parse_string_padding(pad_val)?)
681                } else {
682                    None
683                };
684                let charset = if is_string {
685                    let cs_val = bf1 & 0x0F;
686                    Some(parse_charset(cs_val)?)
687                } else {
688                    None
689                };
690                let (base_type, consumed) = Datatype::parse(&data[pos..])?;
691                pos += consumed;
692                Ok((
693                    Datatype::VariableLength {
694                        is_string,
695                        padding,
696                        charset,
697                        base_type: Box::new(base_type),
698                    },
699                    pos,
700                ))
701            }
702            10 => {
703                // Array
704                if version == 2 {
705                    ensure_len(data, pos, 4)?;
706                    let ndims = data[pos] as usize;
707                    pos += 4; // ndims(1) + reserved(3)
708                    ensure_len(data, pos, ndims * 4 + ndims * 4)?;
709                    let mut dimensions = Vec::with_capacity(ndims);
710                    for _ in 0..ndims {
711                        dimensions.push(LittleEndian::read_u32(&data[pos..pos + 4]));
712                        pos += 4;
713                    }
714                    // skip permutation indices
715                    pos += ndims * 4;
716                    let (base_type, consumed) = Datatype::parse(&data[pos..])?;
717                    pos += consumed;
718                    Ok((
719                        Datatype::Array {
720                            base_type: Box::new(base_type),
721                            dimensions,
722                        },
723                        pos,
724                    ))
725                } else if version == 3 {
726                    ensure_len(data, pos, 1)?;
727                    let ndims = data[pos] as usize;
728                    pos += 1;
729                    ensure_len(data, pos, ndims * 4)?;
730                    let mut dimensions = Vec::with_capacity(ndims);
731                    for _ in 0..ndims {
732                        dimensions.push(LittleEndian::read_u32(&data[pos..pos + 4]));
733                        pos += 4;
734                    }
735                    let (base_type, consumed) = Datatype::parse(&data[pos..])?;
736                    pos += consumed;
737                    Ok((
738                        Datatype::Array {
739                            base_type: Box::new(base_type),
740                            dimensions,
741                        },
742                        pos,
743                    ))
744                } else {
745                    Err(FormatError::InvalidDatatypeVersion {
746                        class: class_id,
747                        version,
748                    })
749                }
750            }
751            11 => {
752                // Complex number — store as compound of two floats internally
753                // Parse like compound with version 3 and 2 members
754                // But actually class 11 has no special properties beyond class 6 compound.
755                // It's just recognized as a separate class. For now parse the 2 members
756                // as compound.
757                let num_members = (bf0 as u16) | ((bf1 as u16) << 8);
758                let mut members = Vec::with_capacity(num_members as usize);
759                let ob = offset_bytes_for_size(size);
760                for _ in 0..num_members {
761                    let (name, name_len) = read_null_terminated_string(data, pos)?;
762                    pos += name_len;
763                    let byte_offset = read_uint(data, pos, ob)?;
764                    pos += ob;
765                    let (member_dt, consumed) = Datatype::parse(&data[pos..])?;
766                    pos += consumed;
767                    members.push(CompoundMember {
768                        name,
769                        byte_offset,
770                        datatype: member_dt,
771                    });
772                }
773                Ok((Datatype::Compound { size, members }, pos))
774            }
775            _ => Err(FormatError::InvalidDatatypeClass(class_id)),
776        }
777    }
778
779    /// Serialize datatype to HDF5 message bytes.
780    ///
781    /// Crate-internal: hand a `Datatype` to
782    /// [`DatasetBuilder::with_dtype`](crate::DatasetBuilder::with_dtype) and the
783    /// writer encodes it. Widening this again is additive if a caller ever needs
784    /// the raw encoding.
785    pub(crate) fn serialize(&self) -> Vec<u8> {
786        match self {
787            Datatype::FixedPoint {
788                size,
789                byte_order,
790                signed,
791                bit_offset,
792                bit_precision,
793            } => {
794                let mut bf0 = 0u8;
795                if matches!(byte_order, DatatypeByteOrder::BigEndian) {
796                    bf0 |= 0x01;
797                }
798                if *signed {
799                    bf0 |= 0x08;
800                }
801                let mut buf = Self::build_header(0, 1, [bf0, 0, 0], *size);
802                buf.extend_from_slice(&bit_offset.to_le_bytes());
803                buf.extend_from_slice(&bit_precision.to_le_bytes());
804                buf
805            }
806            Datatype::FloatingPoint {
807                size,
808                byte_order,
809                bit_offset,
810                bit_precision,
811                exponent_location,
812                exponent_size,
813                mantissa_location,
814                mantissa_size,
815                exponent_bias,
816            } => {
817                let mut bf0 = 0x20u8; // bit 5: sign location bit (standard IEEE 754)
818                match byte_order {
819                    DatatypeByteOrder::BigEndian => {
820                        bf0 |= 0x01;
821                    }
822                    DatatypeByteOrder::Vax => {
823                        bf0 |= 0x40;
824                    }
825                    _ => {}
826                }
827                // bf[1] = sign bit location (bit position of sign in the value)
828                #[expect(
829                    clippy::cast_possible_truncation,
830                    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)"
831                )]
832                let bf1 = (*size * 8 - 1) as u8;
833                let mut buf = Self::build_header(1, 1, [bf0, bf1, 0], *size);
834                buf.extend_from_slice(&bit_offset.to_le_bytes());
835                buf.extend_from_slice(&bit_precision.to_le_bytes());
836                buf.push(*exponent_location);
837                buf.push(*exponent_size);
838                buf.push(*mantissa_location);
839                buf.push(*mantissa_size);
840                buf.extend_from_slice(&exponent_bias.to_le_bytes());
841                buf
842            }
843            Datatype::String {
844                size,
845                padding,
846                charset,
847            } => {
848                let pad_val = match padding {
849                    StringPadding::NullTerminate => 0,
850                    StringPadding::NullPad => 1,
851                    StringPadding::SpacePad => 2,
852                };
853                let cs_val = match charset {
854                    CharacterSet::Ascii => 0,
855                    CharacterSet::Utf8 => 1,
856                };
857                let bf0 = pad_val | (cs_val << 4);
858                Self::build_header(3, 1, [bf0, 0, 0], *size)
859            }
860            Datatype::VariableLength {
861                is_string,
862                padding,
863                charset,
864                base_type,
865            } => {
866                let mut bf0 = if *is_string { 0x01u8 } else { 0x00 };
867                if *is_string && let Some(p) = padding {
868                    let pv = match p {
869                        StringPadding::NullTerminate => 0,
870                        StringPadding::NullPad => 1,
871                        StringPadding::SpacePad => 2,
872                    };
873                    bf0 |= pv << 4;
874                }
875                let bf1 = if *is_string {
876                    charset.as_ref().map_or(0, |c| match c {
877                        CharacterSet::Ascii => 0,
878                        CharacterSet::Utf8 => 1,
879                    })
880                } else {
881                    0
882                };
883                let mut buf = Self::build_header(9, 1, [bf0, bf1, 0], 16);
884                buf.extend_from_slice(&base_type.serialize());
885                buf
886            }
887            Datatype::Compound { size, members } => {
888                #[expect(
889                    clippy::cast_possible_truncation,
890                    reason = "compound member count is written into the 2-byte member-count field of the datatype message"
891                )]
892                let num = members.len() as u16;
893                let bf0 = (num & 0xFF) as u8;
894                let bf1 = ((num >> 8) & 0xFF) as u8;
895                let mut buf = Self::build_header(6, 3, [bf0, bf1, 0], *size);
896                let ob = offset_bytes_for_size(*size);
897                for m in members {
898                    // Null-terminated name
899                    buf.extend_from_slice(m.name.as_bytes());
900                    buf.push(0);
901                    // Byte offset (variable-width)
902                    #[expect(
903                        clippy::cast_possible_truncation,
904                        reason = "ob is the offset-byte width chosen to hold byte_offset, so each arm casts to a width that fits by construction"
905                    )]
906                    match ob {
907                        1 => buf.push(m.byte_offset as u8),
908                        2 => buf.extend_from_slice(&(m.byte_offset as u16).to_le_bytes()),
909                        _ => buf.extend_from_slice(&(m.byte_offset as u32).to_le_bytes()),
910                    }
911                    // Recursively serialize member datatype
912                    buf.extend_from_slice(&m.datatype.serialize());
913                }
914                buf
915            }
916            Datatype::Enumeration {
917                size,
918                base_type,
919                members,
920            } => {
921                #[expect(
922                    clippy::cast_possible_truncation,
923                    reason = "enumeration member count is written into the 2-byte member-count field of the datatype message"
924                )]
925                let num = members.len() as u16;
926                let bf0 = (num & 0xFF) as u8;
927                let bf1 = ((num >> 8) & 0xFF) as u8;
928                let mut buf = Self::build_header(8, 3, [bf0, bf1, 0], *size);
929                // Base type
930                buf.extend_from_slice(&base_type.serialize());
931                // All names (null-terminated)
932                for m in members {
933                    buf.extend_from_slice(m.name.as_bytes());
934                    buf.push(0);
935                }
936                // All values
937                for m in members {
938                    buf.extend_from_slice(&m.value);
939                }
940                buf
941            }
942            Datatype::Array {
943                base_type,
944                dimensions,
945            } => {
946                let mut buf = Self::build_header(10, 3, [0, 0, 0], self.type_size());
947                #[expect(
948                    clippy::cast_possible_truncation,
949                    reason = "array rank is written into the 1-byte dimensionality field; HDF5 caps array rank well below 255"
950                )]
951                buf.push(dimensions.len() as u8);
952                for &d in dimensions {
953                    buf.extend_from_slice(&d.to_le_bytes());
954                }
955                buf.extend_from_slice(&base_type.serialize());
956                buf
957            }
958            Datatype::Reference { size, ref_type } => {
959                let bf0 = match ref_type {
960                    ReferenceType::Object => 0,
961                    ReferenceType::DatasetRegion => 1,
962                };
963                Self::build_header(7, 1, [bf0, 0, 0], *size)
964            }
965            Datatype::Time {
966                size,
967                byte_order,
968                bit_precision,
969            } => {
970                // bf0 bit 0 is the byte order (0 = little-endian, 1 = big-endian).
971                let bf0 = if matches!(byte_order, DatatypeByteOrder::BigEndian) {
972                    0x01u8
973                } else {
974                    0
975                };
976                let mut buf = Self::build_header(2, 1, [bf0, 0, 0], *size);
977                buf.extend_from_slice(&bit_precision.to_le_bytes());
978                buf
979            }
980            Datatype::BitField {
981                size,
982                byte_order,
983                bit_offset,
984                bit_precision,
985            } => {
986                let bf0 = if matches!(byte_order, DatatypeByteOrder::BigEndian) {
987                    0x01u8
988                } else {
989                    0
990                };
991                let mut buf = Self::build_header(4, 1, [bf0, 0, 0], *size);
992                buf.extend_from_slice(&bit_offset.to_le_bytes());
993                buf.extend_from_slice(&bit_precision.to_le_bytes());
994                buf
995            }
996            Datatype::Opaque { size, tag } => {
997                // bf0 carries the ASCII tag length; the tag is padded with zero
998                // bytes to a multiple of 8, mirroring `parse`.
999                #[expect(
1000                    clippy::cast_possible_truncation,
1001                    reason = "opaque tag length is written into the 1-byte tag-length bit field (bf0)"
1002                )]
1003                let bf0 = tag.len() as u8;
1004                let mut buf = Self::build_header(5, 1, [bf0, 0, 0], *size);
1005                buf.extend_from_slice(tag);
1006                let padded = (tag.len() + 7) & !7;
1007                buf.resize(buf.len() + (padded - tag.len()), 0);
1008                buf
1009            }
1010        }
1011    }
1012
1013    fn build_header(class: u8, version: u8, bf: [u8; 3], size: u32) -> Vec<u8> {
1014        let mut buf = vec![0u8; 8];
1015        buf[0] = (class & 0x0F) | ((version & 0x0F) << 4);
1016        buf[1] = bf[0];
1017        buf[2] = bf[1];
1018        buf[3] = bf[2];
1019        buf[4..8].copy_from_slice(&size.to_le_bytes());
1020        buf
1021    }
1022
1023    /// Return the size in bytes of one element of this type.
1024    pub fn type_size(&self) -> u32 {
1025        match self {
1026            Datatype::FixedPoint { size, .. } => *size,
1027            Datatype::FloatingPoint { size, .. } => *size,
1028            Datatype::Time { size, .. } => *size,
1029            Datatype::String { size, .. } => *size,
1030            Datatype::BitField { size, .. } => *size,
1031            Datatype::Opaque { size, .. } => *size,
1032            Datatype::Compound { size, .. } => *size,
1033            Datatype::Reference { size, .. } => *size,
1034            Datatype::Enumeration { size, .. } => *size,
1035            Datatype::VariableLength { .. } => 16, // typically pointer + length
1036            Datatype::Array {
1037                base_type,
1038                dimensions,
1039            } => {
1040                let elem_count: u32 = dimensions
1041                    .iter()
1042                    .copied()
1043                    .fold(1u32, |a, b| a.saturating_mul(b));
1044                base_type.type_size().saturating_mul(elem_count)
1045            }
1046        }
1047    }
1048}
1049
1050/// Build a datatype header (8 bytes) for testing.
1051#[cfg(test)]
1052fn build_dt_header(class: u8, version: u8, bf: [u8; 3], size: u32) -> Vec<u8> {
1053    let mut buf = vec![0u8; 8];
1054    buf[0] = (class & 0x0F) | ((version & 0x0F) << 4);
1055    buf[1] = bf[0];
1056    buf[2] = bf[1];
1057    buf[3] = bf[2];
1058    LittleEndian::write_u32(&mut buf[4..8], size);
1059    buf
1060}
1061
1062#[cfg(test)]
1063mod tests {
1064    use super::*;
1065
1066    // Helper to build a fixed-point datatype message
1067    fn build_fixed_point(
1068        size: u32,
1069        be: bool,
1070        signed: bool,
1071        bit_offset: u16,
1072        bit_precision: u16,
1073    ) -> Vec<u8> {
1074        let bf0 = if be { 0x01 } else { 0x00 } | if signed { 0x08 } else { 0x00 };
1075        let mut buf = build_dt_header(0, 1, [bf0, 0, 0], size);
1076        let mut props = [0u8; 4];
1077        LittleEndian::write_u16(&mut props[0..2], bit_offset);
1078        LittleEndian::write_u16(&mut props[2..4], bit_precision);
1079        buf.extend_from_slice(&props);
1080        buf
1081    }
1082
1083    // Helper to build a floating-point datatype message
1084    fn build_float(
1085        size: u32,
1086        exp_loc: u8,
1087        exp_size: u8,
1088        mant_loc: u8,
1089        mant_size: u8,
1090        exp_bias: u32,
1091    ) -> Vec<u8> {
1092        // LE byte order: bo_low=0, bo_high=0
1093        let bf0 = 0x00u8;
1094        let bf1 = 0x00u8;
1095        // mantissa norm = 2 (MSB not stored) in bits 24-31... wait, that's bf2
1096        let bf2 = 0x02u8; // norm = 2
1097        let mut buf = build_dt_header(1, 1, [bf0, bf1, bf2], size);
1098        let mut props = [0u8; 12];
1099        LittleEndian::write_u16(&mut props[0..2], 0); // bit_offset
1100        LittleEndian::write_u16(&mut props[2..4], (size * 8) as u16); // bit_precision
1101        props[4] = exp_loc;
1102        props[5] = exp_size;
1103        props[6] = mant_loc;
1104        props[7] = mant_size;
1105        LittleEndian::write_u32(&mut props[8..12], exp_bias);
1106        buf.extend_from_slice(&props);
1107        buf
1108    }
1109
1110    #[test]
1111    fn test_fixed_point_u8() {
1112        let data = build_fixed_point(1, false, false, 0, 8);
1113        let (dt, consumed) = Datatype::parse(&data).unwrap();
1114        assert_eq!(consumed, 12);
1115        assert_eq!(
1116            dt,
1117            Datatype::FixedPoint {
1118                size: 1,
1119                byte_order: DatatypeByteOrder::LittleEndian,
1120                signed: false,
1121                bit_offset: 0,
1122                bit_precision: 8,
1123            }
1124        );
1125    }
1126
1127    #[test]
1128    fn test_fixed_point_i16_le() {
1129        let data = build_fixed_point(2, false, true, 0, 16);
1130        let (dt, _) = Datatype::parse(&data).unwrap();
1131        assert_eq!(
1132            dt,
1133            Datatype::FixedPoint {
1134                size: 2,
1135                byte_order: DatatypeByteOrder::LittleEndian,
1136                signed: true,
1137                bit_offset: 0,
1138                bit_precision: 16,
1139            }
1140        );
1141    }
1142
1143    #[test]
1144    fn test_fixed_point_u32_be() {
1145        let data = build_fixed_point(4, true, false, 0, 32);
1146        let (dt, _) = Datatype::parse(&data).unwrap();
1147        match &dt {
1148            Datatype::FixedPoint {
1149                byte_order,
1150                signed,
1151                size,
1152                ..
1153            } => {
1154                assert_eq!(*byte_order, DatatypeByteOrder::BigEndian);
1155                assert!(!signed);
1156                assert_eq!(*size, 4);
1157            }
1158            _ => panic!("expected FixedPoint"),
1159        }
1160    }
1161
1162    #[test]
1163    fn test_fixed_point_i64_le() {
1164        let data = build_fixed_point(8, false, true, 0, 64);
1165        let (dt, _) = Datatype::parse(&data).unwrap();
1166        assert_eq!(
1167            dt,
1168            Datatype::FixedPoint {
1169                size: 8,
1170                byte_order: DatatypeByteOrder::LittleEndian,
1171                signed: true,
1172                bit_offset: 0,
1173                bit_precision: 64,
1174            }
1175        );
1176    }
1177
1178    #[test]
1179    fn test_float_f32_le() {
1180        // IEEE 754 f32: exp=8 bits at bit 23, mant=23 bits at bit 0, bias=127
1181        let data = build_float(4, 23, 8, 0, 23, 127);
1182        let (dt, consumed) = Datatype::parse(&data).unwrap();
1183        assert_eq!(consumed, 20);
1184        assert_eq!(
1185            dt,
1186            Datatype::FloatingPoint {
1187                size: 4,
1188                byte_order: DatatypeByteOrder::LittleEndian,
1189                bit_offset: 0,
1190                bit_precision: 32,
1191                exponent_location: 23,
1192                exponent_size: 8,
1193                mantissa_location: 0,
1194                mantissa_size: 23,
1195                exponent_bias: 127,
1196            }
1197        );
1198    }
1199
1200    #[test]
1201    fn test_float_f64_le() {
1202        let data = build_float(8, 52, 11, 0, 52, 1023);
1203        let (dt, _) = Datatype::parse(&data).unwrap();
1204        assert_eq!(
1205            dt,
1206            Datatype::FloatingPoint {
1207                size: 8,
1208                byte_order: DatatypeByteOrder::LittleEndian,
1209                bit_offset: 0,
1210                bit_precision: 64,
1211                exponent_location: 52,
1212                exponent_size: 11,
1213                mantissa_location: 0,
1214                mantissa_size: 52,
1215                exponent_bias: 1023,
1216            }
1217        );
1218    }
1219
1220    #[test]
1221    fn test_string_null_terminated_ascii() {
1222        let buf = build_dt_header(3, 1, [0x00, 0, 0], 10); // padding=0(nullterm), charset=0(ascii)
1223        let (dt, consumed) = Datatype::parse(&buf).unwrap();
1224        assert_eq!(consumed, 8);
1225        assert_eq!(
1226            dt,
1227            Datatype::String {
1228                size: 10,
1229                padding: StringPadding::NullTerminate,
1230                charset: CharacterSet::Ascii,
1231            }
1232        );
1233    }
1234
1235    #[test]
1236    fn test_string_space_padded_utf8() {
1237        // padding=2(space pad), charset=1(utf8) → bf0 = 0x12
1238        let buf = build_dt_header(3, 1, [0x12, 0, 0], 32);
1239        let (dt, _) = Datatype::parse(&buf).unwrap();
1240        assert_eq!(
1241            dt,
1242            Datatype::String {
1243                size: 32,
1244                padding: StringPadding::SpacePad,
1245                charset: CharacterSet::Utf8,
1246            }
1247        );
1248    }
1249
1250    #[test]
1251    fn test_opaque() {
1252        // tag_len = 4, tag = "BLOB"
1253        let mut buf = build_dt_header(5, 1, [4, 0, 0], 64);
1254        buf.extend_from_slice(b"BLOB");
1255        // Pad to 8 bytes
1256        buf.extend_from_slice(&[0, 0, 0, 0]);
1257        let (dt, consumed) = Datatype::parse(&buf).unwrap();
1258        assert_eq!(consumed, 16); // 8 header + 8 padded tag
1259        assert_eq!(
1260            dt,
1261            Datatype::Opaque {
1262                size: 64,
1263                tag: b"BLOB".to_vec(),
1264            }
1265        );
1266    }
1267
1268    #[test]
1269    fn test_compound_v3_two_members() {
1270        // Compound with size=12, 2 members: "x" u32 at offset 0, "y" f64 at offset 4
1271        // Size=12, so offset_bytes=1
1272        let mut buf = build_dt_header(6, 3, [2, 0, 0], 12); // 2 members
1273        // Member "x": name "x\0", offset=0, then u32 LE datatype
1274        buf.extend_from_slice(b"x\0");
1275        buf.push(0); // byte_offset = 0
1276        buf.extend_from_slice(&build_fixed_point(4, false, false, 0, 32));
1277        // Member "y": name "y\0", offset=4, then f64 LE datatype
1278        buf.extend_from_slice(b"y\0");
1279        buf.push(4); // byte_offset = 4
1280        buf.extend_from_slice(&build_float(8, 52, 11, 0, 52, 1023));
1281
1282        let (dt, _) = Datatype::parse(&buf).unwrap();
1283        match dt {
1284            Datatype::Compound { size, members } => {
1285                assert_eq!(size, 12);
1286                assert_eq!(members.len(), 2);
1287                assert_eq!(members[0].name, "x");
1288                assert_eq!(members[0].byte_offset, 0);
1289                assert_eq!(members[1].name, "y");
1290                assert_eq!(members[1].byte_offset, 4);
1291                match &members[0].datatype {
1292                    Datatype::FixedPoint {
1293                        size: 4,
1294                        signed: false,
1295                        ..
1296                    } => {}
1297                    other => panic!("expected u32, got {other:?}"),
1298                }
1299                match &members[1].datatype {
1300                    Datatype::FloatingPoint { size: 8, .. } => {}
1301                    other => panic!("expected f64, got {other:?}"),
1302                }
1303            }
1304            _ => panic!("expected Compound"),
1305        }
1306    }
1307
1308    #[test]
1309    fn test_compound_v1_complex_matlab_layout() {
1310        // MATLAB stores a complex value as a version-1 compound of two f64
1311        // members named "real" and "imag" at offsets 0 and 8. v1 members pad
1312        // the NUL-terminated name to a multiple of 8 bytes and carry a fixed
1313        // 28-byte dimension block — dimensionality(1) + reserved(3) +
1314        // dimension permutation(4) + reserved(4) + dimension sizes(16) —
1315        // between the byte offset and the member datatype message. Regression
1316        // test for a stride bug that skipped only 24 bytes (omitting the second
1317        // reserved field) and so misread every real-MATLAB complex compound.
1318        let mut buf = build_dt_header(6, 1, [2, 0, 0], 16); // v1, 2 members, size 16
1319        for (name, offset) in [(&b"real\0\0\0\0"[..], 0u32), (&b"imag\0\0\0\0"[..], 8)] {
1320            buf.extend_from_slice(name); // NUL-terminated, padded to 8
1321            let mut off = [0u8; 4];
1322            LittleEndian::write_u32(&mut off, offset);
1323            buf.extend_from_slice(&off);
1324            buf.extend_from_slice(&[0u8; 28]); // v1 dimension block
1325            buf.extend_from_slice(&build_float(8, 52, 11, 0, 52, 1023));
1326        }
1327
1328        let (dt, _) = Datatype::parse(&buf).unwrap();
1329        match dt {
1330            Datatype::Compound { size, members } => {
1331                assert_eq!(size, 16);
1332                assert_eq!(members.len(), 2);
1333                assert_eq!(members[0].name, "real");
1334                assert_eq!(members[0].byte_offset, 0);
1335                assert_eq!(members[1].name, "imag");
1336                assert_eq!(members[1].byte_offset, 8);
1337                for m in &members {
1338                    assert!(
1339                        matches!(m.datatype, Datatype::FloatingPoint { size: 8, .. }),
1340                        "expected f64 member, got {:?}",
1341                        m.datatype
1342                    );
1343                }
1344            }
1345            _ => panic!("expected Compound"),
1346        }
1347    }
1348
1349    #[test]
1350    fn test_reference_object() {
1351        let buf = build_dt_header(7, 1, [0, 0, 0], 8);
1352        let (dt, _) = Datatype::parse(&buf).unwrap();
1353        assert_eq!(
1354            dt,
1355            Datatype::Reference {
1356                size: 8,
1357                ref_type: ReferenceType::Object,
1358            }
1359        );
1360    }
1361
1362    #[test]
1363    fn test_reference_region() {
1364        let buf = build_dt_header(7, 1, [1, 0, 0], 12);
1365        let (dt, _) = Datatype::parse(&buf).unwrap();
1366        assert_eq!(
1367            dt,
1368            Datatype::Reference {
1369                size: 12,
1370                ref_type: ReferenceType::DatasetRegion,
1371            }
1372        );
1373    }
1374
1375    #[test]
1376    fn test_enumeration() {
1377        // Enum with base type i32 LE, 3 members
1378        let mut buf = build_dt_header(8, 3, [3, 0, 0], 4); // 3 members
1379        // Base type: i32 LE
1380        buf.extend_from_slice(&build_fixed_point(4, false, true, 0, 32));
1381        // Names: "RED\0", "GREEN\0", "BLUE\0"
1382        buf.extend_from_slice(b"RED\0");
1383        buf.extend_from_slice(b"GREEN\0");
1384        buf.extend_from_slice(b"BLUE\0");
1385        // Values: 0, 1, 2 (as i32 LE)
1386        buf.extend_from_slice(&0i32.to_le_bytes());
1387        buf.extend_from_slice(&1i32.to_le_bytes());
1388        buf.extend_from_slice(&2i32.to_le_bytes());
1389
1390        let (dt, _) = Datatype::parse(&buf).unwrap();
1391        match dt {
1392            Datatype::Enumeration {
1393                size,
1394                base_type,
1395                members,
1396            } => {
1397                assert_eq!(size, 4);
1398                assert_eq!(members.len(), 3);
1399                assert_eq!(members[0].name, "RED");
1400                assert_eq!(members[0].value, 0i32.to_le_bytes().to_vec());
1401                assert_eq!(members[1].name, "GREEN");
1402                assert_eq!(members[1].value, 1i32.to_le_bytes().to_vec());
1403                assert_eq!(members[2].name, "BLUE");
1404                assert_eq!(members[2].value, 2i32.to_le_bytes().to_vec());
1405                match *base_type {
1406                    Datatype::FixedPoint {
1407                        signed: true,
1408                        size: 4,
1409                        ..
1410                    } => {}
1411                    other => panic!("expected i32, got {other:?}"),
1412                }
1413            }
1414            _ => panic!("expected Enumeration"),
1415        }
1416    }
1417
1418    #[test]
1419    fn test_variable_length_string_utf8() {
1420        // VL string: type=1, padding=0(null term), charset=1(utf8)
1421        // bf0: bits 0-3 = 1 (string), bits 4-7 = 0 (null term) → 0x01
1422        // bf1: bits 0-3 = 1 (utf8) → 0x01
1423        let mut buf = build_dt_header(9, 1, [0x01, 0x01, 0], 16);
1424        // Base type: u8 (class 0, unsigned, size 1)
1425        buf.extend_from_slice(&build_fixed_point(1, false, false, 0, 8));
1426
1427        let (dt, _) = Datatype::parse(&buf).unwrap();
1428        match dt {
1429            Datatype::VariableLength {
1430                is_string,
1431                padding,
1432                charset,
1433                base_type,
1434            } => {
1435                assert!(is_string);
1436                assert_eq!(padding, Some(StringPadding::NullTerminate));
1437                assert_eq!(charset, Some(CharacterSet::Utf8));
1438                assert_eq!(base_type.type_size(), 1);
1439            }
1440            _ => panic!("expected VariableLength"),
1441        }
1442    }
1443
1444    #[test]
1445    fn test_variable_length_sequence_f32() {
1446        // VL sequence: type=0
1447        // bf0 = 0x00
1448        let mut buf = build_dt_header(9, 1, [0x00, 0x00, 0], 16);
1449        // Base type: f32 LE
1450        buf.extend_from_slice(&build_float(4, 23, 8, 0, 23, 127));
1451
1452        let (dt, _) = Datatype::parse(&buf).unwrap();
1453        match dt {
1454            Datatype::VariableLength {
1455                is_string,
1456                padding,
1457                charset,
1458                base_type,
1459            } => {
1460                assert!(!is_string);
1461                assert_eq!(padding, None);
1462                assert_eq!(charset, None);
1463                assert_eq!(base_type.type_size(), 4);
1464            }
1465            _ => panic!("expected VariableLength"),
1466        }
1467    }
1468
1469    #[test]
1470    fn test_array_2d() {
1471        // Array [3][4] of i32 LE, version 3
1472        let mut buf = build_dt_header(10, 3, [0, 0, 0], 48); // 3*4*4=48
1473        buf.push(2); // ndims=2
1474        buf.extend_from_slice(&3u32.to_le_bytes()); // dim 0
1475        buf.extend_from_slice(&4u32.to_le_bytes()); // dim 1
1476        // Base type: i32 LE
1477        buf.extend_from_slice(&build_fixed_point(4, false, true, 0, 32));
1478
1479        let (dt, _) = Datatype::parse(&buf).unwrap();
1480        match dt {
1481            Datatype::Array {
1482                base_type,
1483                dimensions,
1484            } => {
1485                assert_eq!(dimensions, vec![3, 4]);
1486                match *base_type {
1487                    Datatype::FixedPoint {
1488                        size: 4,
1489                        signed: true,
1490                        ..
1491                    } => {}
1492                    other => panic!("expected i32, got {other:?}"),
1493                }
1494            }
1495            _ => panic!("expected Array"),
1496        }
1497    }
1498
1499    #[test]
1500    fn test_bitfield() {
1501        let mut buf = build_dt_header(4, 1, [0, 0, 0], 2); // 16-bit LE bitfield
1502        let mut props = [0u8; 4];
1503        LittleEndian::write_u16(&mut props[0..2], 0);
1504        LittleEndian::write_u16(&mut props[2..4], 16);
1505        buf.extend_from_slice(&props);
1506
1507        let (dt, _) = Datatype::parse(&buf).unwrap();
1508        assert_eq!(
1509            dt,
1510            Datatype::BitField {
1511                size: 2,
1512                byte_order: DatatypeByteOrder::LittleEndian,
1513                bit_offset: 0,
1514                bit_precision: 16,
1515            }
1516        );
1517    }
1518
1519    #[test]
1520    fn test_time() {
1521        let mut buf = build_dt_header(2, 1, [0, 0, 0], 8);
1522        let mut props = [0u8; 2];
1523        LittleEndian::write_u16(&mut props[0..2], 64);
1524        buf.extend_from_slice(&props);
1525
1526        let (dt, consumed) = Datatype::parse(&buf).unwrap();
1527        assert_eq!(consumed, 10);
1528        assert_eq!(
1529            dt,
1530            Datatype::Time {
1531                size: 8,
1532                byte_order: DatatypeByteOrder::LittleEndian,
1533                bit_precision: 64,
1534            }
1535        );
1536    }
1537
1538    #[test]
1539    fn test_time_byte_order_roundtrips() {
1540        // A big-endian time type must serialize and re-parse with its byte order
1541        // preserved (bf0 bit 0), so repack can reproduce it faithfully.
1542        for (be, order) in [
1543            (0u8, DatatypeByteOrder::LittleEndian),
1544            (1u8, DatatypeByteOrder::BigEndian),
1545        ] {
1546            let mut buf = build_dt_header(2, 1, [be, 0, 0], 4);
1547            buf.extend_from_slice(&32u16.to_le_bytes());
1548            let (dt, _) = Datatype::parse(&buf).unwrap();
1549            assert_eq!(
1550                dt,
1551                Datatype::Time {
1552                    size: 4,
1553                    byte_order: order.clone(),
1554                    bit_precision: 32,
1555                }
1556            );
1557            // serialize -> parse must round-trip the byte order.
1558            let (reparsed, _) = Datatype::parse(&dt.serialize()).unwrap();
1559            assert_eq!(reparsed, dt);
1560        }
1561    }
1562
1563    #[test]
1564    fn test_nested_compound_array_enum() {
1565        // Compound containing a single member "data" which is an Array[2] of Enum(i32, 2 values)
1566        // Build the enum first
1567        let mut enum_bytes = build_dt_header(8, 3, [2, 0, 0], 4); // 2 members
1568        enum_bytes.extend_from_slice(&build_fixed_point(4, false, true, 0, 32)); // base i32
1569        enum_bytes.extend_from_slice(b"A\0");
1570        enum_bytes.extend_from_slice(b"B\0");
1571        enum_bytes.extend_from_slice(&0i32.to_le_bytes());
1572        enum_bytes.extend_from_slice(&1i32.to_le_bytes());
1573
1574        // Build array[2] of that enum, version 3
1575        let mut array_bytes = build_dt_header(10, 3, [0, 0, 0], 8); // 2*4=8
1576        array_bytes.push(1); // ndims=1
1577        array_bytes.extend_from_slice(&2u32.to_le_bytes()); // dim[0]=2
1578        array_bytes.extend_from_slice(&enum_bytes);
1579
1580        // Build compound with 1 member, size=8
1581        let mut buf = build_dt_header(6, 3, [1, 0, 0], 8); // 1 member
1582        buf.extend_from_slice(b"data\0");
1583        buf.push(0); // byte_offset = 0 (size=8, so 1 byte offsets)
1584        buf.extend_from_slice(&array_bytes);
1585
1586        let (dt, _) = Datatype::parse(&buf).unwrap();
1587        match dt {
1588            Datatype::Compound { members, .. } => {
1589                assert_eq!(members.len(), 1);
1590                assert_eq!(members[0].name, "data");
1591                match &members[0].datatype {
1592                    Datatype::Array {
1593                        dimensions,
1594                        base_type,
1595                    } => {
1596                        assert_eq!(dimensions, &[2]);
1597                        match base_type.as_ref() {
1598                            Datatype::Enumeration { members, .. } => {
1599                                assert_eq!(members.len(), 2);
1600                                assert_eq!(members[0].name, "A");
1601                                assert_eq!(members[1].name, "B");
1602                            }
1603                            other => panic!("expected Enum, got {other:?}"),
1604                        }
1605                    }
1606                    other => panic!("expected Array, got {other:?}"),
1607                }
1608            }
1609            _ => panic!("expected Compound"),
1610        }
1611    }
1612
1613    #[test]
1614    fn test_error_invalid_class() {
1615        let buf = build_dt_header(13, 1, [0, 0, 0], 4);
1616        let err = Datatype::parse(&buf).unwrap_err();
1617        assert_eq!(err, FormatError::InvalidDatatypeClass(13));
1618    }
1619
1620    #[test]
1621    fn test_error_truncated_data() {
1622        let buf = [0u8; 4]; // too short for header
1623        let err = Datatype::parse(&buf).unwrap_err();
1624        match err {
1625            FormatError::UnexpectedEof { .. } => {}
1626            other => panic!("expected UnexpectedEof, got {other:?}"),
1627        }
1628    }
1629
1630    #[test]
1631    fn test_error_invalid_string_padding() {
1632        let buf = build_dt_header(3, 1, [0x03, 0, 0], 10); // padding=3 invalid
1633        let err = Datatype::parse(&buf).unwrap_err();
1634        assert_eq!(err, FormatError::InvalidStringPadding(3));
1635    }
1636
1637    #[test]
1638    fn test_error_invalid_charset() {
1639        let buf = build_dt_header(3, 1, [0x20, 0, 0], 10); // charset=2 invalid
1640        let err = Datatype::parse(&buf).unwrap_err();
1641        assert_eq!(err, FormatError::InvalidCharacterSet(2));
1642    }
1643
1644    #[test]
1645    fn test_error_invalid_reference_type() {
1646        let buf = build_dt_header(7, 1, [5, 0, 0], 8);
1647        let err = Datatype::parse(&buf).unwrap_err();
1648        assert_eq!(err, FormatError::InvalidReferenceType(5));
1649    }
1650
1651    #[test]
1652    fn serialize_parse_compound_roundtrip() {
1653        let dt = Datatype::Compound {
1654            size: 20,
1655            members: vec![
1656                CompoundMember {
1657                    name: "x".to_string(),
1658                    byte_offset: 0,
1659                    datatype: Datatype::FloatingPoint {
1660                        size: 8,
1661                        byte_order: DatatypeByteOrder::LittleEndian,
1662                        bit_offset: 0,
1663                        bit_precision: 64,
1664                        exponent_location: 52,
1665                        exponent_size: 11,
1666                        mantissa_location: 0,
1667                        mantissa_size: 52,
1668                        exponent_bias: 1023,
1669                    },
1670                },
1671                CompoundMember {
1672                    name: "y".to_string(),
1673                    byte_offset: 8,
1674                    datatype: Datatype::FloatingPoint {
1675                        size: 8,
1676                        byte_order: DatatypeByteOrder::LittleEndian,
1677                        bit_offset: 0,
1678                        bit_precision: 64,
1679                        exponent_location: 52,
1680                        exponent_size: 11,
1681                        mantissa_location: 0,
1682                        mantissa_size: 52,
1683                        exponent_bias: 1023,
1684                    },
1685                },
1686                CompoundMember {
1687                    name: "id".to_string(),
1688                    byte_offset: 16,
1689                    datatype: Datatype::FixedPoint {
1690                        size: 4,
1691                        byte_order: DatatypeByteOrder::LittleEndian,
1692                        signed: true,
1693                        bit_offset: 0,
1694                        bit_precision: 32,
1695                    },
1696                },
1697            ],
1698        };
1699        let bytes = dt.serialize();
1700        let (parsed, _) = Datatype::parse(&bytes).unwrap();
1701        assert_eq!(parsed, dt);
1702    }
1703
1704    #[test]
1705    fn serialize_parse_enum_roundtrip() {
1706        let dt = Datatype::Enumeration {
1707            size: 4,
1708            base_type: Box::new(Datatype::FixedPoint {
1709                size: 4,
1710                byte_order: DatatypeByteOrder::LittleEndian,
1711                signed: true,
1712                bit_offset: 0,
1713                bit_precision: 32,
1714            }),
1715            members: vec![
1716                EnumMember {
1717                    name: "RED".to_string(),
1718                    value: 0i32.to_le_bytes().to_vec(),
1719                },
1720                EnumMember {
1721                    name: "GREEN".to_string(),
1722                    value: 1i32.to_le_bytes().to_vec(),
1723                },
1724                EnumMember {
1725                    name: "BLUE".to_string(),
1726                    value: 2i32.to_le_bytes().to_vec(),
1727                },
1728            ],
1729        };
1730        let bytes = dt.serialize();
1731        let (parsed, _) = Datatype::parse(&bytes).unwrap();
1732        assert_eq!(parsed, dt);
1733    }
1734
1735    /// Fixed-point base type for enum round-trip tests.
1736    fn enum_base_fp(size: u32, be: bool, signed: bool) -> Datatype {
1737        Datatype::FixedPoint {
1738            size,
1739            byte_order: if be {
1740                DatatypeByteOrder::BigEndian
1741            } else {
1742                DatatypeByteOrder::LittleEndian
1743            },
1744            signed,
1745            bit_offset: 0,
1746            #[expect(
1747                clippy::cast_possible_truncation,
1748                reason = "test builds byte-width base types; size*8 is well within u16"
1749            )]
1750            bit_precision: (size * 8) as u16,
1751        }
1752    }
1753
1754    /// Build an enum datatype over `base`, storing each member value truncated to
1755    /// the base width (the value blob is opaque bytes, so any content round-trips).
1756    fn make_enum(base: Datatype, members: &[(&str, i64)]) -> Datatype {
1757        let size = base.type_size();
1758        let width = size as usize;
1759        Datatype::Enumeration {
1760            size,
1761            base_type: Box::new(base),
1762            members: members
1763                .iter()
1764                .map(|(name, v)| EnumMember {
1765                    name: (*name).to_string(),
1766                    value: v.to_le_bytes()[..width].to_vec(),
1767                })
1768                .collect(),
1769        }
1770    }
1771
1772    #[test]
1773    fn serialize_parse_enum_base_type_variety() {
1774        // The i32 base is already covered above; here u8, big-endian i16, and i64
1775        // bases all round-trip through the enum wrapper.
1776        for base in [
1777            enum_base_fp(1, false, false), // u8
1778            enum_base_fp(2, true, true),   // i16 big-endian
1779            enum_base_fp(8, false, true),  // i64
1780        ] {
1781            let dt = make_enum(base.clone(), &[("A", 0), ("B", 1), ("NEG", -1)]);
1782            let bytes = dt.serialize();
1783            let (parsed, consumed) = Datatype::parse(&bytes).unwrap();
1784            assert_eq!(parsed, dt, "round-trip failed for base {base:?}");
1785            assert_eq!(consumed, bytes.len());
1786        }
1787    }
1788
1789    #[test]
1790    fn serialize_parse_enum_large_member_count() {
1791        // More than 256 members exercises the 2-byte member-count field, which is
1792        // split across bf0/bf1 in the datatype message header.
1793        let owned: Vec<(String, i64)> = (0..300).map(|i| (format!("M{i}"), i)).collect();
1794        let members: Vec<(&str, i64)> = owned.iter().map(|(n, v)| (n.as_str(), *v)).collect();
1795        let dt = make_enum(enum_base_fp(4, false, true), &members);
1796        let bytes = dt.serialize();
1797        let (parsed, _) = Datatype::parse(&bytes).unwrap();
1798        assert_eq!(parsed, dt);
1799        match parsed {
1800            Datatype::Enumeration { members, .. } => {
1801                assert_eq!(members.len(), 300);
1802                assert_eq!(members[299].name, "M299");
1803            }
1804            other => panic!("expected Enumeration, got {other:?}"),
1805        }
1806    }
1807
1808    #[test]
1809    fn enum_value_width_is_not_validated_against_base_size() {
1810        // `EnumTypeBuilder::build`/`Datatype::Enumeration` take the element size
1811        // from the base type only, with no check that member value blobs match it.
1812        // A 4-byte value on a 1-byte base therefore serializes in full but parses
1813        // back reading just `base_size` (1) byte per member, silently truncating.
1814        // This documents the current permissiveness; it is NOT a supported
1815        // round-trip, and the assertion guards against a silent change either way.
1816        let dt = Datatype::Enumeration {
1817            size: 1,
1818            base_type: Box::new(enum_base_fp(1, false, false)),
1819            members: vec![EnumMember {
1820                name: "X".to_string(),
1821                value: 5i32.to_le_bytes().to_vec(), // 4 bytes on a 1-byte base
1822            }],
1823        };
1824        let bytes = dt.serialize();
1825        let (parsed, _) = Datatype::parse(&bytes).unwrap();
1826        assert_ne!(
1827            parsed, dt,
1828            "a value wider than the base silently truncates on parse"
1829        );
1830        match parsed {
1831            Datatype::Enumeration { members, .. } => assert_eq!(members[0].value, vec![5]),
1832            other => panic!("expected Enumeration, got {other:?}"),
1833        }
1834    }
1835
1836    #[test]
1837    fn serialize_parse_array_roundtrip() {
1838        let dt = Datatype::Array {
1839            base_type: Box::new(Datatype::FloatingPoint {
1840                size: 8,
1841                byte_order: DatatypeByteOrder::LittleEndian,
1842                bit_offset: 0,
1843                bit_precision: 64,
1844                exponent_location: 52,
1845                exponent_size: 11,
1846                mantissa_location: 0,
1847                mantissa_size: 52,
1848                exponent_bias: 1023,
1849            }),
1850            dimensions: vec![3],
1851        };
1852        let bytes = dt.serialize();
1853        let (parsed, _) = Datatype::parse(&bytes).unwrap();
1854        assert_eq!(parsed, dt);
1855    }
1856
1857    #[test]
1858    fn serialize_parse_time_roundtrip() {
1859        let dt = Datatype::Time {
1860            size: 8,
1861            byte_order: DatatypeByteOrder::LittleEndian,
1862            bit_precision: 64,
1863        };
1864        let bytes = dt.serialize();
1865        let (parsed, consumed) = Datatype::parse(&bytes).unwrap();
1866        assert_eq!(parsed, dt);
1867        assert_eq!(consumed, bytes.len());
1868    }
1869
1870    #[test]
1871    fn serialize_parse_bitfield_roundtrip() {
1872        for byte_order in [
1873            DatatypeByteOrder::LittleEndian,
1874            DatatypeByteOrder::BigEndian,
1875        ] {
1876            let dt = Datatype::BitField {
1877                size: 4,
1878                byte_order,
1879                bit_offset: 3,
1880                bit_precision: 17,
1881            };
1882            let bytes = dt.serialize();
1883            let (parsed, consumed) = Datatype::parse(&bytes).unwrap();
1884            assert_eq!(parsed, dt);
1885            assert_eq!(consumed, bytes.len());
1886        }
1887    }
1888
1889    #[test]
1890    fn serialize_parse_opaque_roundtrip() {
1891        // Tag lengths that do and do not land on an 8-byte boundary, to exercise
1892        // the zero padding both ways.
1893        for tag in [
1894            b"abc".to_vec(),         // 3 bytes -> padded to 8
1895            b"12345678".to_vec(),    // 8 bytes -> no padding
1896            b"sensor-id\0".to_vec(), // 10 bytes -> padded to 16, embedded NUL preserved
1897        ] {
1898            let dt = Datatype::Opaque { size: 16, tag };
1899            let bytes = dt.serialize();
1900            // The property section (after the 8-byte header) must be a multiple
1901            // of 8, matching what the reference library expects.
1902            assert_eq!((bytes.len() - 8) % 8, 0);
1903            let (parsed, consumed) = Datatype::parse(&bytes).unwrap();
1904            assert_eq!(parsed, dt);
1905            assert_eq!(consumed, bytes.len());
1906        }
1907    }
1908
1909    #[test]
1910    fn test_type_size() {
1911        let dt = Datatype::FixedPoint {
1912            size: 4,
1913            byte_order: DatatypeByteOrder::LittleEndian,
1914            signed: true,
1915            bit_offset: 0,
1916            bit_precision: 32,
1917        };
1918        assert_eq!(dt.type_size(), 4);
1919
1920        let dt = Datatype::Array {
1921            base_type: Box::new(Datatype::FixedPoint {
1922                size: 4,
1923                byte_order: DatatypeByteOrder::LittleEndian,
1924                signed: true,
1925                bit_offset: 0,
1926                bit_precision: 32,
1927            }),
1928            dimensions: vec![3, 4],
1929        };
1930        assert_eq!(dt.type_size(), 48);
1931    }
1932}
1933
1934#[cfg(all(test, feature = "std"))]
1935mod display_tests {
1936    use super::*;
1937
1938    #[test]
1939    fn ordinary_numeric_types_read_as_their_rust_names() {
1940        let int = Datatype::FixedPoint {
1941            size: 4,
1942            byte_order: DatatypeByteOrder::LittleEndian,
1943            signed: true,
1944            bit_offset: 0,
1945            bit_precision: 32,
1946        };
1947        assert_eq!(int.to_string(), "i32");
1948
1949        let float = Datatype::FloatingPoint {
1950            size: 8,
1951            byte_order: DatatypeByteOrder::LittleEndian,
1952            bit_offset: 0,
1953            bit_precision: 64,
1954            exponent_location: 52,
1955            exponent_size: 11,
1956            mantissa_location: 0,
1957            mantissa_size: 52,
1958            exponent_bias: 1023,
1959        };
1960        assert_eq!(float.to_string(), "f64");
1961    }
1962
1963    /// Every width a message writes is `size * 8` over an on-disk `u32`, so a
1964    /// crafted size near [`u32::MAX`] overflows a `u32` multiply and panics a
1965    /// debug build (issue #140). [`bit_width`] widens first; this holds each
1966    /// class that calls it to that, rather than reaching one of them through
1967    /// whatever `classify_datatype` happens to route here.
1968    #[test]
1969    fn a_crafted_size_writes_its_width_instead_of_overflowing() {
1970        let bits = u64::from(u32::MAX) * 8;
1971        let cases = [
1972            (
1973                Datatype::FixedPoint {
1974                    size: u32::MAX,
1975                    byte_order: DatatypeByteOrder::LittleEndian,
1976                    signed: true,
1977                    bit_offset: 0,
1978                    bit_precision: 0,
1979                },
1980                format!("i{bits}(bits 0..0)"),
1981            ),
1982            (
1983                Datatype::FloatingPoint {
1984                    size: u32::MAX,
1985                    byte_order: DatatypeByteOrder::LittleEndian,
1986                    bit_offset: 0,
1987                    bit_precision: 0,
1988                    exponent_location: 0,
1989                    exponent_size: 0,
1990                    mantissa_location: 0,
1991                    mantissa_size: 0,
1992                    exponent_bias: 0,
1993                },
1994                format!("f{bits}(bits 0..0)"),
1995            ),
1996            (
1997                Datatype::Time {
1998                    size: u32::MAX,
1999                    byte_order: DatatypeByteOrder::LittleEndian,
2000                    bit_precision: 0,
2001                },
2002                format!("time{bits}(bits 0..0)"),
2003            ),
2004            (
2005                Datatype::BitField {
2006                    size: u32::MAX,
2007                    byte_order: DatatypeByteOrder::LittleEndian,
2008                    bit_offset: 0,
2009                    bit_precision: 0,
2010                },
2011                format!("bitfield{bits}(bits 0..0)"),
2012            ),
2013        ];
2014
2015        for (dtype, expected) in cases {
2016            assert_eq!(dtype.to_string(), expected);
2017        }
2018    }
2019
2020    /// The bit span adds two `u16`s, which is the other place a crafted field
2021    /// could wrap. Both widen, so the end is 131,070 rather than 65,534.
2022    #[test]
2023    fn a_crafted_bit_span_does_not_wrap() {
2024        let dtype = Datatype::FixedPoint {
2025            size: 1,
2026            byte_order: DatatypeByteOrder::LittleEndian,
2027            signed: false,
2028            bit_offset: u16::MAX,
2029            bit_precision: u16::MAX,
2030        };
2031        assert_eq!(dtype.to_string(), "u8(bits 65535..131070)");
2032    }
2033
2034    /// Only what departs from the ordinary is written, since that is what the
2035    /// reader of the message is looking for.
2036    #[test]
2037    fn unusual_fields_are_written_and_ordinary_ones_are_not() {
2038        let big_endian = Datatype::FixedPoint {
2039            size: 2,
2040            byte_order: DatatypeByteOrder::BigEndian,
2041            signed: false,
2042            bit_offset: 0,
2043            bit_precision: 16,
2044        };
2045        assert_eq!(big_endian.to_string(), "u16 be");
2046
2047        let narrow = Datatype::FixedPoint {
2048            size: 4,
2049            byte_order: DatatypeByteOrder::LittleEndian,
2050            signed: true,
2051            bit_offset: 0,
2052            bit_precision: 24,
2053        };
2054        assert_eq!(narrow.to_string(), "i32(bits 0..24)");
2055    }
2056
2057    #[test]
2058    fn nested_types_recurse_through_their_members() {
2059        let compound = Datatype::Compound {
2060            size: 12,
2061            members: vec![
2062                CompoundMember {
2063                    name: "x".into(),
2064                    byte_offset: 0,
2065                    datatype: Datatype::FloatingPoint {
2066                        size: 4,
2067                        byte_order: DatatypeByteOrder::LittleEndian,
2068                        bit_offset: 0,
2069                        bit_precision: 32,
2070                        exponent_location: 23,
2071                        exponent_size: 8,
2072                        mantissa_location: 0,
2073                        mantissa_size: 23,
2074                        exponent_bias: 127,
2075                    },
2076                },
2077                CompoundMember {
2078                    name: "n".into(),
2079                    byte_offset: 4,
2080                    datatype: Datatype::FixedPoint {
2081                        size: 8,
2082                        byte_order: DatatypeByteOrder::LittleEndian,
2083                        signed: true,
2084                        bit_offset: 0,
2085                        bit_precision: 64,
2086                    },
2087                },
2088            ],
2089        };
2090        assert_eq!(compound.to_string(), "compound{x: f32, n: i64}");
2091
2092        let array = Datatype::Array {
2093            base_type: Box::new(Datatype::FixedPoint {
2094                size: 1,
2095                byte_order: DatatypeByteOrder::LittleEndian,
2096                signed: false,
2097                bit_offset: 0,
2098                bit_precision: 8,
2099            }),
2100            dimensions: vec![2, 3],
2101        };
2102        assert_eq!(
2103            array.to_string(),
2104            "array<u8, 2x3>",
2105            "the shape is spelled `2x3`, never a `Debug` slice"
2106        );
2107    }
2108
2109    /// The leaf enums format through `Formatter::pad`, so a caller lining these
2110    /// up in a column gets the width it asked for rather than having it
2111    /// silently dropped.
2112    #[test]
2113    fn a_leaf_enum_honors_the_width_it_is_given() {
2114        assert_eq!(format!("{:>8}", CharacterSet::Ascii), "   ascii");
2115        assert_eq!(format!("{:<8}|", DatatypeByteOrder::BigEndian), "be      |");
2116        assert_eq!(format!("{}", StringPadding::NullPad), "null-pad");
2117    }
2118
2119    #[test]
2120    fn a_string_carries_its_width_charset_and_padding() {
2121        let string = Datatype::String {
2122            size: 16,
2123            padding: StringPadding::NullPad,
2124            charset: CharacterSet::Utf8,
2125        };
2126        assert_eq!(string.to_string(), "string[16] utf8 null-pad");
2127    }
2128
2129    /// The tag is arbitrary file bytes, so it cannot reach a message unescaped.
2130    #[test]
2131    fn an_opaque_tag_is_quoted_and_escaped() {
2132        let opaque = Datatype::Opaque {
2133            size: 4,
2134            tag: b"a\"b\x00".to_vec(),
2135        };
2136        assert_eq!(opaque.to_string(), "opaque[4] \"a\\\"b\\x00\"");
2137    }
2138
2139    /// A member name comes from the file by way of `from_utf8_lossy`, which
2140    /// rejects nothing, so it is escaped for the same reason an opaque tag is.
2141    #[test]
2142    fn a_member_name_cannot_carry_a_control_character_into_a_message() {
2143        let compound = Datatype::Compound {
2144            size: 4,
2145            members: vec![CompoundMember {
2146                name: "a\nb\u{1b}[31m".into(),
2147                byte_offset: 0,
2148                datatype: u32_datatype(),
2149            }],
2150        };
2151        let shown = compound.to_string();
2152        assert!(!shown.chars().any(char::is_control), "{shown}");
2153        assert_eq!(shown, "compound{a\\nb\\u{1b}[31m: u32}");
2154
2155        let enumeration = Datatype::Enumeration {
2156            size: 4,
2157            base_type: Box::new(u32_datatype()),
2158            members: vec![EnumMember {
2159                name: "red\u{0}".into(),
2160                value: vec![0, 0, 0, 0],
2161            }],
2162        };
2163        let shown = enumeration.to_string();
2164        assert!(!shown.chars().any(char::is_control), "{shown}");
2165        assert_eq!(shown, "enum<u32>[red\\0]");
2166    }
2167
2168    /// The member count is an on-disk `u16`, so the list a file can ask for is
2169    /// far longer than a message can carry. Both member-bearing variants elide,
2170    /// so both are checked.
2171    #[test]
2172    fn a_long_member_list_is_elided_and_reports_the_remainder() {
2173        let over_cap = DISPLAY_MAX_MEMBERS + 3;
2174
2175        let compound = Datatype::Compound {
2176            size: (over_cap * 4) as u32,
2177            members: (0..over_cap)
2178                .map(|i| CompoundMember {
2179                    name: format!("m{i}"),
2180                    byte_offset: (i * 4) as u64,
2181                    datatype: u32_datatype(),
2182                })
2183                .collect(),
2184        };
2185        let enumeration = Datatype::Enumeration {
2186            size: 4,
2187            base_type: Box::new(u32_datatype()),
2188            members: (0..over_cap)
2189                .map(|i| EnumMember {
2190                    name: format!("m{i}"),
2191                    value: vec![0, 0, 0, 0],
2192                })
2193                .collect(),
2194        };
2195
2196        for (datatype, close) in [(compound, "}"), (enumeration, "]")] {
2197            let shown = datatype.to_string();
2198            assert!(shown.ends_with(&format!(", … 3 more{close}")), "{shown}");
2199            assert!(shown.contains("m0"), "{shown}");
2200            assert!(
2201                !shown.contains(&format!("m{DISPLAY_MAX_MEMBERS}")),
2202                "{shown}"
2203            );
2204        }
2205    }
2206
2207    /// The boundary: exactly the cap is written whole, with no "0 more".
2208    #[test]
2209    fn a_member_list_at_exactly_the_cap_is_not_elided() {
2210        let members: Vec<_> = (0..DISPLAY_MAX_MEMBERS)
2211            .map(|i| EnumMember {
2212                name: format!("m{i}"),
2213                value: vec![0, 0, 0, 0],
2214            })
2215            .collect();
2216        let shown = Datatype::Enumeration {
2217            size: 4,
2218            base_type: Box::new(u32_datatype()),
2219            members,
2220        }
2221        .to_string();
2222
2223        assert!(!shown.contains('…'), "{shown}");
2224        assert!(
2225            shown.ends_with(&format!("m{}]", DISPLAY_MAX_MEMBERS - 1)),
2226            "{shown}"
2227        );
2228    }
2229
2230    fn u32_datatype() -> Datatype {
2231        Datatype::FixedPoint {
2232            size: 4,
2233            byte_order: DatatypeByteOrder::LittleEndian,
2234            signed: false,
2235            bit_offset: 0,
2236            bit_precision: 32,
2237        }
2238    }
2239}