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 byteorder::{ByteOrder, LittleEndian};
10
11use crate::error::FormatError;
12
13/// Byte order of numeric data.
14#[derive(Debug, Clone, PartialEq)]
15pub enum DatatypeByteOrder {
16    LittleEndian,
17    BigEndian,
18    Vax,
19}
20
21/// String padding type.
22#[derive(Debug, Clone, PartialEq)]
23pub enum StringPadding {
24    NullTerminate,
25    NullPad,
26    SpacePad,
27}
28
29/// Character set encoding.
30#[derive(Debug, Clone, PartialEq)]
31pub enum CharacterSet {
32    Ascii,
33    Utf8,
34}
35
36/// Reference type.
37#[derive(Debug, Clone, PartialEq)]
38pub enum ReferenceType {
39    Object,
40    DatasetRegion,
41}
42
43/// A member of a compound datatype.
44#[derive(Debug, Clone, PartialEq)]
45pub struct CompoundMember {
46    /// Member name.
47    pub name: String,
48    /// Byte offset within the compound.
49    pub byte_offset: u64,
50    /// Member datatype.
51    pub datatype: Datatype,
52}
53
54/// A member of an enumeration datatype.
55#[derive(Debug, Clone, PartialEq)]
56pub struct EnumMember {
57    /// Member name.
58    pub name: String,
59    /// Raw value bytes (length = base type size).
60    pub value: Vec<u8>,
61}
62
63/// Parsed HDF5 datatype.
64#[derive(Debug, Clone, PartialEq)]
65pub enum Datatype {
66    /// Class 0: Fixed-point (integer) types.
67    FixedPoint {
68        size: u32,
69        byte_order: DatatypeByteOrder,
70        signed: bool,
71        bit_offset: u16,
72        bit_precision: u16,
73    },
74    /// Class 1: Floating-point types.
75    FloatingPoint {
76        size: u32,
77        byte_order: DatatypeByteOrder,
78        bit_offset: u16,
79        bit_precision: u16,
80        exponent_location: u8,
81        exponent_size: u8,
82        mantissa_location: u8,
83        mantissa_size: u8,
84        exponent_bias: u32,
85    },
86    /// Class 2: Time type (rarely used).
87    Time {
88        size: u32,
89        byte_order: DatatypeByteOrder,
90        bit_precision: u16,
91    },
92    /// Class 3: Fixed-length string.
93    String {
94        size: u32,
95        padding: StringPadding,
96        charset: CharacterSet,
97    },
98    /// Class 4: Bit field.
99    BitField {
100        size: u32,
101        byte_order: DatatypeByteOrder,
102        bit_offset: u16,
103        bit_precision: u16,
104    },
105    /// Class 5: Opaque data.
106    Opaque { size: u32, tag: Vec<u8> },
107    /// Class 6: Compound type.
108    Compound {
109        size: u32,
110        members: Vec<CompoundMember>,
111    },
112    /// Class 7: Reference type.
113    Reference { size: u32, ref_type: ReferenceType },
114    /// Class 8: Enumeration type.
115    Enumeration {
116        size: u32,
117        base_type: Box<Datatype>,
118        members: Vec<EnumMember>,
119    },
120    /// Class 9: Variable-length type.
121    VariableLength {
122        is_string: bool,
123        padding: Option<StringPadding>,
124        charset: Option<CharacterSet>,
125        base_type: Box<Datatype>,
126    },
127    /// Class 10: Array type.
128    Array {
129        base_type: Box<Datatype>,
130        dimensions: Vec<u32>,
131    },
132}
133
134fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> {
135    match offset.checked_add(needed) {
136        Some(end) if end <= data.len() => Ok(()),
137        _ => Err(FormatError::UnexpectedEof {
138            expected: offset.saturating_add(needed),
139            available: data.len(),
140        }),
141    }
142}
143
144fn parse_string_padding(val: u8) -> Result<StringPadding, FormatError> {
145    match val {
146        0 => Ok(StringPadding::NullTerminate),
147        1 => Ok(StringPadding::NullPad),
148        2 => Ok(StringPadding::SpacePad),
149        _ => Err(FormatError::InvalidStringPadding(val)),
150    }
151}
152
153fn parse_charset(val: u8) -> Result<CharacterSet, FormatError> {
154    match val {
155        0 => Ok(CharacterSet::Ascii),
156        1 => Ok(CharacterSet::Utf8),
157        _ => Err(FormatError::InvalidCharacterSet(val)),
158    }
159}
160
161/// Read a null-terminated string from `data` starting at `offset`.
162/// Returns (string, bytes_consumed including the null terminator).
163fn read_null_terminated_string(data: &[u8], offset: usize) -> Result<(String, usize), FormatError> {
164    if offset >= data.len() {
165        return Err(FormatError::UnexpectedEof {
166            expected: offset + 1,
167            available: data.len(),
168        });
169    }
170    let remaining = &data[offset..];
171    let null_pos = remaining
172        .iter()
173        .position(|&b| b == 0)
174        .ok_or(FormatError::UnexpectedEof {
175            expected: offset + 1,
176            available: data.len(),
177        })?;
178    let name = String::from_utf8_lossy(&remaining[..null_pos]).into_owned();
179    Ok((name, null_pos + 1))
180}
181
182/// Determine how many bytes are needed to encode `compound_size` as a byte offset (v3).
183fn offset_bytes_for_size(compound_size: u32) -> usize {
184    if compound_size <= 0xFF {
185        1
186    } else if compound_size <= 0xFFFF {
187        2
188    } else {
189        4
190    }
191}
192
193/// Read an unsigned integer of 1, 2, 4, or 8 bytes (LE).
194fn read_uint(data: &[u8], offset: usize, nbytes: usize) -> Result<u64, FormatError> {
195    ensure_len(data, offset, nbytes)?;
196    let slice = &data[offset..offset + nbytes];
197    Ok(match nbytes {
198        1 => slice[0] as u64,
199        2 => LittleEndian::read_u16(slice) as u64,
200        4 => LittleEndian::read_u32(slice) as u64,
201        8 => LittleEndian::read_u64(slice),
202        _ => {
203            return Err(FormatError::UnexpectedEof {
204                expected: offset + nbytes,
205                available: data.len(),
206            });
207        }
208    })
209}
210
211impl Datatype {
212    /// Parse a datatype message from raw bytes.
213    ///
214    /// Returns `(Datatype, bytes_consumed)` for recursive parsing.
215    pub fn parse(data: &[u8]) -> Result<(Datatype, usize), FormatError> {
216        // Minimum header: 4 bytes (class_and_version + 3 bytes bit field) + 4 bytes size = 8
217        ensure_len(data, 0, 8)?;
218
219        let class_and_version = data[0];
220        let class_id = class_and_version & 0x0F;
221        let version = (class_and_version >> 4) & 0x0F;
222
223        // 24-bit class bit field (little-endian)
224        let bf0 = data[1];
225        let bf1 = data[2];
226        let bf2 = data[3];
227        let _bit_field_24 = (bf0 as u32) | ((bf1 as u32) << 8) | ((bf2 as u32) << 16);
228
229        let size = LittleEndian::read_u32(&data[4..8]);
230        let mut pos = 8;
231
232        match class_id {
233            0 => {
234                // Fixed-Point
235                ensure_len(data, pos, 4)?;
236                let byte_order = if bf0 & 0x01 == 0 {
237                    DatatypeByteOrder::LittleEndian
238                } else {
239                    DatatypeByteOrder::BigEndian
240                };
241                let signed = (bf0 >> 3) & 0x01 == 1;
242                let bit_offset = LittleEndian::read_u16(&data[pos..pos + 2]);
243                let bit_precision = LittleEndian::read_u16(&data[pos + 2..pos + 4]);
244                pos += 4;
245                Ok((
246                    Datatype::FixedPoint {
247                        size,
248                        byte_order,
249                        signed,
250                        bit_offset,
251                        bit_precision,
252                    },
253                    pos,
254                ))
255            }
256            1 => {
257                // Floating-Point
258                ensure_len(data, pos, 12)?;
259                let bo_low = bf0 & 0x01;
260                let bo_high = (bf0 >> 6) & 0x01;
261                let byte_order = match (bo_high, bo_low) {
262                    (0, 0) => DatatypeByteOrder::LittleEndian,
263                    (0, 1) => DatatypeByteOrder::BigEndian,
264                    (1, 0) => DatatypeByteOrder::Vax,
265                    (1, 1) => DatatypeByteOrder::Vax,
266                    _ => unreachable!(),
267                };
268                let bit_offset = LittleEndian::read_u16(&data[pos..pos + 2]);
269                let bit_precision = LittleEndian::read_u16(&data[pos + 2..pos + 4]);
270                let exponent_location = data[pos + 4];
271                let exponent_size = data[pos + 5];
272                let mantissa_location = data[pos + 6];
273                let mantissa_size = data[pos + 7];
274                let exponent_bias = LittleEndian::read_u32(&data[pos + 8..pos + 12]);
275                pos += 12;
276                Ok((
277                    Datatype::FloatingPoint {
278                        size,
279                        byte_order,
280                        bit_offset,
281                        bit_precision,
282                        exponent_location,
283                        exponent_size,
284                        mantissa_location,
285                        mantissa_size,
286                        exponent_bias,
287                    },
288                    pos,
289                ))
290            }
291            2 => {
292                // Time
293                ensure_len(data, pos, 2)?;
294                let byte_order = if bf0 & 0x01 == 0 {
295                    DatatypeByteOrder::LittleEndian
296                } else {
297                    DatatypeByteOrder::BigEndian
298                };
299                let bit_precision = LittleEndian::read_u16(&data[pos..pos + 2]);
300                pos += 2;
301                Ok((
302                    Datatype::Time {
303                        size,
304                        byte_order,
305                        bit_precision,
306                    },
307                    pos,
308                ))
309            }
310            3 => {
311                // String
312                let padding_val = bf0 & 0x0F;
313                let charset_val = (bf0 >> 4) & 0x0F;
314                let padding = parse_string_padding(padding_val)?;
315                let charset = parse_charset(charset_val)?;
316                Ok((
317                    Datatype::String {
318                        size,
319                        padding,
320                        charset,
321                    },
322                    pos,
323                ))
324            }
325            4 => {
326                // Bit Field
327                ensure_len(data, pos, 4)?;
328                let byte_order = if bf0 & 0x01 == 0 {
329                    DatatypeByteOrder::LittleEndian
330                } else {
331                    DatatypeByteOrder::BigEndian
332                };
333                let bit_offset = LittleEndian::read_u16(&data[pos..pos + 2]);
334                let bit_precision = LittleEndian::read_u16(&data[pos + 2..pos + 4]);
335                pos += 4;
336                Ok((
337                    Datatype::BitField {
338                        size,
339                        byte_order,
340                        bit_offset,
341                        bit_precision,
342                    },
343                    pos,
344                ))
345            }
346            5 => {
347                // Opaque
348                let tag_len = bf0 as usize;
349                ensure_len(data, pos, tag_len)?;
350                let tag = data[pos..pos + tag_len].to_vec();
351                // Tags are padded to multiple of 8 bytes
352                let padded = (tag_len + 7) & !7;
353                let pos = 8 + padded; // from start of properties
354                Ok((Datatype::Opaque { size, tag }, pos))
355            }
356            6 => {
357                // Compound
358                let num_members = (bf0 as u16) | ((bf1 as u16) << 8);
359                let mut members = Vec::with_capacity(num_members as usize);
360
361                if version == 3 || version == 4 {
362                    let ob = offset_bytes_for_size(size);
363                    for _ in 0..num_members {
364                        let (name, name_len) = read_null_terminated_string(data, pos)?;
365                        pos += name_len;
366                        let byte_offset = read_uint(data, pos, ob)?;
367                        pos += ob;
368                        let (member_dt, consumed) = Datatype::parse(&data[pos..])?;
369                        pos += consumed;
370                        members.push(CompoundMember {
371                            name,
372                            byte_offset,
373                            datatype: member_dt,
374                        });
375                    }
376                } else if version == 1 || version == 2 {
377                    // v1 and v2: the member name is NUL-terminated and padded with
378                    // additional NULs to a multiple of 8 bytes, followed by a
379                    // 4-byte member byte offset. v1 then carries a fixed 28-byte
380                    // dimension block — dimensionality(1) + reserved(3) +
381                    // dimension permutation(4) + reserved(4) + dimension sizes(16)
382                    // — before the member datatype message; v2 drops that block.
383                    for _ in 0..num_members {
384                        let (name, name_len) = read_null_terminated_string(data, pos)?;
385                        let padded = (name_len + 7) & !7;
386                        pos += padded;
387                        ensure_len(data, pos, 4)?;
388                        let byte_offset = LittleEndian::read_u32(&data[pos..pos + 4]) as u64;
389                        pos += 4;
390                        if version == 1 {
391                            ensure_len(data, pos, 28)?;
392                            pos += 28;
393                        }
394                        let (member_dt, consumed) = Datatype::parse(&data[pos..])?;
395                        pos += consumed;
396                        members.push(CompoundMember {
397                            name,
398                            byte_offset,
399                            datatype: member_dt,
400                        });
401                    }
402                } else {
403                    return Err(FormatError::InvalidDatatypeVersion {
404                        class: class_id,
405                        version,
406                    });
407                }
408
409                Ok((Datatype::Compound { size, members }, pos))
410            }
411            7 => {
412                // Reference
413                let ref_type_val = bf0 & 0x0F;
414                let ref_type = match ref_type_val {
415                    0 => ReferenceType::Object,
416                    1 => ReferenceType::DatasetRegion,
417                    _ => return Err(FormatError::InvalidReferenceType(ref_type_val)),
418                };
419                Ok((Datatype::Reference { size, ref_type }, pos))
420            }
421            8 => {
422                // Enumeration
423                let num_members = (bf0 as u16) | ((bf1 as u16) << 8);
424                // Parse base type
425                let (base_type, base_consumed) = Datatype::parse(&data[pos..])?;
426                pos += base_consumed;
427                let base_size = base_type.type_size();
428                let mut members = Vec::with_capacity(num_members as usize);
429                // Enum layout: base_type, then all names (null-terminated), then all values
430                // v1/v2: names are padded to 8-byte boundaries
431                // v3: names are just null-terminated
432                let mut member_names = Vec::with_capacity(num_members as usize);
433                for _ in 0..num_members {
434                    let (name, name_len) = read_null_terminated_string(data, pos)?;
435                    if version < 3 {
436                        let padded = (name_len + 7) & !7;
437                        pos += padded;
438                    } else {
439                        pos += name_len;
440                    }
441                    member_names.push(name);
442                }
443                // Now values
444                for name in &member_names {
445                    ensure_len(data, pos, base_size as usize)?;
446                    let value = data[pos..pos + base_size as usize].to_vec();
447                    pos += base_size as usize;
448                    members.push(EnumMember {
449                        name: name.clone(),
450                        value,
451                    });
452                }
453                Ok((
454                    Datatype::Enumeration {
455                        size,
456                        base_type: Box::new(base_type),
457                        members,
458                    },
459                    pos,
460                ))
461            }
462            9 => {
463                // Variable-Length
464                let vl_type = bf0 & 0x0F;
465                let is_string = vl_type == 1;
466                let padding = if is_string {
467                    let pad_val = (bf0 >> 4) & 0x0F;
468                    Some(parse_string_padding(pad_val)?)
469                } else {
470                    None
471                };
472                let charset = if is_string {
473                    let cs_val = bf1 & 0x0F;
474                    Some(parse_charset(cs_val)?)
475                } else {
476                    None
477                };
478                let (base_type, consumed) = Datatype::parse(&data[pos..])?;
479                pos += consumed;
480                Ok((
481                    Datatype::VariableLength {
482                        is_string,
483                        padding,
484                        charset,
485                        base_type: Box::new(base_type),
486                    },
487                    pos,
488                ))
489            }
490            10 => {
491                // Array
492                if version == 2 {
493                    ensure_len(data, pos, 4)?;
494                    let ndims = data[pos] as usize;
495                    pos += 4; // ndims(1) + reserved(3)
496                    ensure_len(data, pos, ndims * 4 + ndims * 4)?;
497                    let mut dimensions = Vec::with_capacity(ndims);
498                    for _ in 0..ndims {
499                        dimensions.push(LittleEndian::read_u32(&data[pos..pos + 4]));
500                        pos += 4;
501                    }
502                    // skip permutation indices
503                    pos += ndims * 4;
504                    let (base_type, consumed) = Datatype::parse(&data[pos..])?;
505                    pos += consumed;
506                    Ok((
507                        Datatype::Array {
508                            base_type: Box::new(base_type),
509                            dimensions,
510                        },
511                        pos,
512                    ))
513                } else if version == 3 {
514                    ensure_len(data, pos, 1)?;
515                    let ndims = data[pos] as usize;
516                    pos += 1;
517                    ensure_len(data, pos, ndims * 4)?;
518                    let mut dimensions = Vec::with_capacity(ndims);
519                    for _ in 0..ndims {
520                        dimensions.push(LittleEndian::read_u32(&data[pos..pos + 4]));
521                        pos += 4;
522                    }
523                    let (base_type, consumed) = Datatype::parse(&data[pos..])?;
524                    pos += consumed;
525                    Ok((
526                        Datatype::Array {
527                            base_type: Box::new(base_type),
528                            dimensions,
529                        },
530                        pos,
531                    ))
532                } else {
533                    Err(FormatError::InvalidDatatypeVersion {
534                        class: class_id,
535                        version,
536                    })
537                }
538            }
539            11 => {
540                // Complex number — store as compound of two floats internally
541                // Parse like compound with version 3 and 2 members
542                // But actually class 11 has no special properties beyond class 6 compound.
543                // It's just recognized as a separate class. For now parse the 2 members
544                // as compound.
545                let num_members = (bf0 as u16) | ((bf1 as u16) << 8);
546                let mut members = Vec::with_capacity(num_members as usize);
547                let ob = offset_bytes_for_size(size);
548                for _ in 0..num_members {
549                    let (name, name_len) = read_null_terminated_string(data, pos)?;
550                    pos += name_len;
551                    let byte_offset = read_uint(data, pos, ob)?;
552                    pos += ob;
553                    let (member_dt, consumed) = Datatype::parse(&data[pos..])?;
554                    pos += consumed;
555                    members.push(CompoundMember {
556                        name,
557                        byte_offset,
558                        datatype: member_dt,
559                    });
560                }
561                Ok((Datatype::Compound { size, members }, pos))
562            }
563            _ => Err(FormatError::InvalidDatatypeClass(class_id)),
564        }
565    }
566
567    /// Serialize datatype to HDF5 message bytes.
568    pub fn serialize(&self) -> Vec<u8> {
569        match self {
570            Datatype::FixedPoint {
571                size,
572                byte_order,
573                signed,
574                bit_offset,
575                bit_precision,
576            } => {
577                let mut bf0 = 0u8;
578                if matches!(byte_order, DatatypeByteOrder::BigEndian) {
579                    bf0 |= 0x01;
580                }
581                if *signed {
582                    bf0 |= 0x08;
583                }
584                let mut buf = Self::build_header(0, 1, [bf0, 0, 0], *size);
585                buf.extend_from_slice(&bit_offset.to_le_bytes());
586                buf.extend_from_slice(&bit_precision.to_le_bytes());
587                buf
588            }
589            Datatype::FloatingPoint {
590                size,
591                byte_order,
592                bit_offset,
593                bit_precision,
594                exponent_location,
595                exponent_size,
596                mantissa_location,
597                mantissa_size,
598                exponent_bias,
599            } => {
600                let mut bf0 = 0x20u8; // bit 5: sign location bit (standard IEEE 754)
601                match byte_order {
602                    DatatypeByteOrder::BigEndian => {
603                        bf0 |= 0x01;
604                    }
605                    DatatypeByteOrder::Vax => {
606                        bf0 |= 0x40;
607                    }
608                    _ => {}
609                }
610                // bf[1] = sign bit location (bit position of sign in the value)
611                #[expect(
612                    clippy::cast_possible_truncation,
613                    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)"
614                )]
615                let bf1 = (*size * 8 - 1) as u8;
616                let mut buf = Self::build_header(1, 1, [bf0, bf1, 0], *size);
617                buf.extend_from_slice(&bit_offset.to_le_bytes());
618                buf.extend_from_slice(&bit_precision.to_le_bytes());
619                buf.push(*exponent_location);
620                buf.push(*exponent_size);
621                buf.push(*mantissa_location);
622                buf.push(*mantissa_size);
623                buf.extend_from_slice(&exponent_bias.to_le_bytes());
624                buf
625            }
626            Datatype::String {
627                size,
628                padding,
629                charset,
630            } => {
631                let pad_val = match padding {
632                    StringPadding::NullTerminate => 0,
633                    StringPadding::NullPad => 1,
634                    StringPadding::SpacePad => 2,
635                };
636                let cs_val = match charset {
637                    CharacterSet::Ascii => 0,
638                    CharacterSet::Utf8 => 1,
639                };
640                let bf0 = pad_val | (cs_val << 4);
641                Self::build_header(3, 1, [bf0, 0, 0], *size)
642            }
643            Datatype::VariableLength {
644                is_string,
645                padding,
646                charset,
647                base_type,
648            } => {
649                let mut bf0 = if *is_string { 0x01u8 } else { 0x00 };
650                if *is_string && let Some(p) = padding {
651                    let pv = match p {
652                        StringPadding::NullTerminate => 0,
653                        StringPadding::NullPad => 1,
654                        StringPadding::SpacePad => 2,
655                    };
656                    bf0 |= pv << 4;
657                }
658                let bf1 = if *is_string {
659                    charset.as_ref().map_or(0, |c| match c {
660                        CharacterSet::Ascii => 0,
661                        CharacterSet::Utf8 => 1,
662                    })
663                } else {
664                    0
665                };
666                let mut buf = Self::build_header(9, 1, [bf0, bf1, 0], 16);
667                buf.extend_from_slice(&base_type.serialize());
668                buf
669            }
670            Datatype::Compound { size, members } => {
671                #[expect(
672                    clippy::cast_possible_truncation,
673                    reason = "compound member count is written into the 2-byte member-count field of the datatype message"
674                )]
675                let num = members.len() as u16;
676                let bf0 = (num & 0xFF) as u8;
677                let bf1 = ((num >> 8) & 0xFF) as u8;
678                let mut buf = Self::build_header(6, 3, [bf0, bf1, 0], *size);
679                let ob = offset_bytes_for_size(*size);
680                for m in members {
681                    // Null-terminated name
682                    buf.extend_from_slice(m.name.as_bytes());
683                    buf.push(0);
684                    // Byte offset (variable-width)
685                    #[expect(
686                        clippy::cast_possible_truncation,
687                        reason = "ob is the offset-byte width chosen to hold byte_offset, so each arm casts to a width that fits by construction"
688                    )]
689                    match ob {
690                        1 => buf.push(m.byte_offset as u8),
691                        2 => buf.extend_from_slice(&(m.byte_offset as u16).to_le_bytes()),
692                        _ => buf.extend_from_slice(&(m.byte_offset as u32).to_le_bytes()),
693                    }
694                    // Recursively serialize member datatype
695                    buf.extend_from_slice(&m.datatype.serialize());
696                }
697                buf
698            }
699            Datatype::Enumeration {
700                size,
701                base_type,
702                members,
703            } => {
704                #[expect(
705                    clippy::cast_possible_truncation,
706                    reason = "enumeration member count is written into the 2-byte member-count field of the datatype message"
707                )]
708                let num = members.len() as u16;
709                let bf0 = (num & 0xFF) as u8;
710                let bf1 = ((num >> 8) & 0xFF) as u8;
711                let mut buf = Self::build_header(8, 3, [bf0, bf1, 0], *size);
712                // Base type
713                buf.extend_from_slice(&base_type.serialize());
714                // All names (null-terminated)
715                for m in members {
716                    buf.extend_from_slice(m.name.as_bytes());
717                    buf.push(0);
718                }
719                // All values
720                for m in members {
721                    buf.extend_from_slice(&m.value);
722                }
723                buf
724            }
725            Datatype::Array {
726                base_type,
727                dimensions,
728            } => {
729                let mut buf = Self::build_header(10, 3, [0, 0, 0], self.type_size());
730                #[expect(
731                    clippy::cast_possible_truncation,
732                    reason = "array rank is written into the 1-byte dimensionality field; HDF5 caps array rank well below 255"
733                )]
734                buf.push(dimensions.len() as u8);
735                for &d in dimensions {
736                    buf.extend_from_slice(&d.to_le_bytes());
737                }
738                buf.extend_from_slice(&base_type.serialize());
739                buf
740            }
741            Datatype::Reference { size, ref_type } => {
742                let bf0 = match ref_type {
743                    ReferenceType::Object => 0,
744                    ReferenceType::DatasetRegion => 1,
745                };
746                Self::build_header(7, 1, [bf0, 0, 0], *size)
747            }
748            Datatype::Time {
749                size,
750                byte_order,
751                bit_precision,
752            } => {
753                // bf0 bit 0 is the byte order (0 = little-endian, 1 = big-endian).
754                let bf0 = if matches!(byte_order, DatatypeByteOrder::BigEndian) {
755                    0x01u8
756                } else {
757                    0
758                };
759                let mut buf = Self::build_header(2, 1, [bf0, 0, 0], *size);
760                buf.extend_from_slice(&bit_precision.to_le_bytes());
761                buf
762            }
763            Datatype::BitField {
764                size,
765                byte_order,
766                bit_offset,
767                bit_precision,
768            } => {
769                let bf0 = if matches!(byte_order, DatatypeByteOrder::BigEndian) {
770                    0x01u8
771                } else {
772                    0
773                };
774                let mut buf = Self::build_header(4, 1, [bf0, 0, 0], *size);
775                buf.extend_from_slice(&bit_offset.to_le_bytes());
776                buf.extend_from_slice(&bit_precision.to_le_bytes());
777                buf
778            }
779            Datatype::Opaque { size, tag } => {
780                // bf0 carries the ASCII tag length; the tag is padded with zero
781                // bytes to a multiple of 8, mirroring `parse`.
782                #[expect(
783                    clippy::cast_possible_truncation,
784                    reason = "opaque tag length is written into the 1-byte tag-length bit field (bf0)"
785                )]
786                let bf0 = tag.len() as u8;
787                let mut buf = Self::build_header(5, 1, [bf0, 0, 0], *size);
788                buf.extend_from_slice(tag);
789                let padded = (tag.len() + 7) & !7;
790                buf.resize(buf.len() + (padded - tag.len()), 0);
791                buf
792            }
793        }
794    }
795
796    fn build_header(class: u8, version: u8, bf: [u8; 3], size: u32) -> Vec<u8> {
797        let mut buf = vec![0u8; 8];
798        buf[0] = (class & 0x0F) | ((version & 0x0F) << 4);
799        buf[1] = bf[0];
800        buf[2] = bf[1];
801        buf[3] = bf[2];
802        buf[4..8].copy_from_slice(&size.to_le_bytes());
803        buf
804    }
805
806    /// Return the size in bytes of one element of this type.
807    pub fn type_size(&self) -> u32 {
808        match self {
809            Datatype::FixedPoint { size, .. } => *size,
810            Datatype::FloatingPoint { size, .. } => *size,
811            Datatype::Time { size, .. } => *size,
812            Datatype::String { size, .. } => *size,
813            Datatype::BitField { size, .. } => *size,
814            Datatype::Opaque { size, .. } => *size,
815            Datatype::Compound { size, .. } => *size,
816            Datatype::Reference { size, .. } => *size,
817            Datatype::Enumeration { size, .. } => *size,
818            Datatype::VariableLength { .. } => 16, // typically pointer + length
819            Datatype::Array {
820                base_type,
821                dimensions,
822            } => {
823                let elem_count: u32 = dimensions
824                    .iter()
825                    .copied()
826                    .fold(1u32, |a, b| a.saturating_mul(b));
827                base_type.type_size().saturating_mul(elem_count)
828            }
829        }
830    }
831}
832
833/// Build a datatype header (8 bytes) for testing.
834#[cfg(test)]
835fn build_dt_header(class: u8, version: u8, bf: [u8; 3], size: u32) -> Vec<u8> {
836    let mut buf = vec![0u8; 8];
837    buf[0] = (class & 0x0F) | ((version & 0x0F) << 4);
838    buf[1] = bf[0];
839    buf[2] = bf[1];
840    buf[3] = bf[2];
841    LittleEndian::write_u32(&mut buf[4..8], size);
842    buf
843}
844
845#[cfg(test)]
846mod tests {
847    use super::*;
848
849    // Helper to build a fixed-point datatype message
850    fn build_fixed_point(
851        size: u32,
852        be: bool,
853        signed: bool,
854        bit_offset: u16,
855        bit_precision: u16,
856    ) -> Vec<u8> {
857        let bf0 = if be { 0x01 } else { 0x00 } | if signed { 0x08 } else { 0x00 };
858        let mut buf = build_dt_header(0, 1, [bf0, 0, 0], size);
859        let mut props = [0u8; 4];
860        LittleEndian::write_u16(&mut props[0..2], bit_offset);
861        LittleEndian::write_u16(&mut props[2..4], bit_precision);
862        buf.extend_from_slice(&props);
863        buf
864    }
865
866    // Helper to build a floating-point datatype message
867    fn build_float(
868        size: u32,
869        exp_loc: u8,
870        exp_size: u8,
871        mant_loc: u8,
872        mant_size: u8,
873        exp_bias: u32,
874    ) -> Vec<u8> {
875        // LE byte order: bo_low=0, bo_high=0
876        let bf0 = 0x00u8;
877        let bf1 = 0x00u8;
878        // mantissa norm = 2 (MSB not stored) in bits 24-31... wait, that's bf2
879        let bf2 = 0x02u8; // norm = 2
880        let mut buf = build_dt_header(1, 1, [bf0, bf1, bf2], size);
881        let mut props = [0u8; 12];
882        LittleEndian::write_u16(&mut props[0..2], 0); // bit_offset
883        LittleEndian::write_u16(&mut props[2..4], (size * 8) as u16); // bit_precision
884        props[4] = exp_loc;
885        props[5] = exp_size;
886        props[6] = mant_loc;
887        props[7] = mant_size;
888        LittleEndian::write_u32(&mut props[8..12], exp_bias);
889        buf.extend_from_slice(&props);
890        buf
891    }
892
893    #[test]
894    fn test_fixed_point_u8() {
895        let data = build_fixed_point(1, false, false, 0, 8);
896        let (dt, consumed) = Datatype::parse(&data).unwrap();
897        assert_eq!(consumed, 12);
898        assert_eq!(
899            dt,
900            Datatype::FixedPoint {
901                size: 1,
902                byte_order: DatatypeByteOrder::LittleEndian,
903                signed: false,
904                bit_offset: 0,
905                bit_precision: 8,
906            }
907        );
908    }
909
910    #[test]
911    fn test_fixed_point_i16_le() {
912        let data = build_fixed_point(2, false, true, 0, 16);
913        let (dt, _) = Datatype::parse(&data).unwrap();
914        assert_eq!(
915            dt,
916            Datatype::FixedPoint {
917                size: 2,
918                byte_order: DatatypeByteOrder::LittleEndian,
919                signed: true,
920                bit_offset: 0,
921                bit_precision: 16,
922            }
923        );
924    }
925
926    #[test]
927    fn test_fixed_point_u32_be() {
928        let data = build_fixed_point(4, true, false, 0, 32);
929        let (dt, _) = Datatype::parse(&data).unwrap();
930        match &dt {
931            Datatype::FixedPoint {
932                byte_order,
933                signed,
934                size,
935                ..
936            } => {
937                assert_eq!(*byte_order, DatatypeByteOrder::BigEndian);
938                assert!(!signed);
939                assert_eq!(*size, 4);
940            }
941            _ => panic!("expected FixedPoint"),
942        }
943    }
944
945    #[test]
946    fn test_fixed_point_i64_le() {
947        let data = build_fixed_point(8, false, true, 0, 64);
948        let (dt, _) = Datatype::parse(&data).unwrap();
949        assert_eq!(
950            dt,
951            Datatype::FixedPoint {
952                size: 8,
953                byte_order: DatatypeByteOrder::LittleEndian,
954                signed: true,
955                bit_offset: 0,
956                bit_precision: 64,
957            }
958        );
959    }
960
961    #[test]
962    fn test_float_f32_le() {
963        // IEEE 754 f32: exp=8 bits at bit 23, mant=23 bits at bit 0, bias=127
964        let data = build_float(4, 23, 8, 0, 23, 127);
965        let (dt, consumed) = Datatype::parse(&data).unwrap();
966        assert_eq!(consumed, 20);
967        assert_eq!(
968            dt,
969            Datatype::FloatingPoint {
970                size: 4,
971                byte_order: DatatypeByteOrder::LittleEndian,
972                bit_offset: 0,
973                bit_precision: 32,
974                exponent_location: 23,
975                exponent_size: 8,
976                mantissa_location: 0,
977                mantissa_size: 23,
978                exponent_bias: 127,
979            }
980        );
981    }
982
983    #[test]
984    fn test_float_f64_le() {
985        let data = build_float(8, 52, 11, 0, 52, 1023);
986        let (dt, _) = Datatype::parse(&data).unwrap();
987        assert_eq!(
988            dt,
989            Datatype::FloatingPoint {
990                size: 8,
991                byte_order: DatatypeByteOrder::LittleEndian,
992                bit_offset: 0,
993                bit_precision: 64,
994                exponent_location: 52,
995                exponent_size: 11,
996                mantissa_location: 0,
997                mantissa_size: 52,
998                exponent_bias: 1023,
999            }
1000        );
1001    }
1002
1003    #[test]
1004    fn test_string_null_terminated_ascii() {
1005        let buf = build_dt_header(3, 1, [0x00, 0, 0], 10); // padding=0(nullterm), charset=0(ascii)
1006        let (dt, consumed) = Datatype::parse(&buf).unwrap();
1007        assert_eq!(consumed, 8);
1008        assert_eq!(
1009            dt,
1010            Datatype::String {
1011                size: 10,
1012                padding: StringPadding::NullTerminate,
1013                charset: CharacterSet::Ascii,
1014            }
1015        );
1016    }
1017
1018    #[test]
1019    fn test_string_space_padded_utf8() {
1020        // padding=2(space pad), charset=1(utf8) → bf0 = 0x12
1021        let buf = build_dt_header(3, 1, [0x12, 0, 0], 32);
1022        let (dt, _) = Datatype::parse(&buf).unwrap();
1023        assert_eq!(
1024            dt,
1025            Datatype::String {
1026                size: 32,
1027                padding: StringPadding::SpacePad,
1028                charset: CharacterSet::Utf8,
1029            }
1030        );
1031    }
1032
1033    #[test]
1034    fn test_opaque() {
1035        // tag_len = 4, tag = "BLOB"
1036        let mut buf = build_dt_header(5, 1, [4, 0, 0], 64);
1037        buf.extend_from_slice(b"BLOB");
1038        // Pad to 8 bytes
1039        buf.extend_from_slice(&[0, 0, 0, 0]);
1040        let (dt, consumed) = Datatype::parse(&buf).unwrap();
1041        assert_eq!(consumed, 16); // 8 header + 8 padded tag
1042        assert_eq!(
1043            dt,
1044            Datatype::Opaque {
1045                size: 64,
1046                tag: b"BLOB".to_vec(),
1047            }
1048        );
1049    }
1050
1051    #[test]
1052    fn test_compound_v3_two_members() {
1053        // Compound with size=12, 2 members: "x" u32 at offset 0, "y" f64 at offset 4
1054        // Size=12, so offset_bytes=1
1055        let mut buf = build_dt_header(6, 3, [2, 0, 0], 12); // 2 members
1056        // Member "x": name "x\0", offset=0, then u32 LE datatype
1057        buf.extend_from_slice(b"x\0");
1058        buf.push(0); // byte_offset = 0
1059        buf.extend_from_slice(&build_fixed_point(4, false, false, 0, 32));
1060        // Member "y": name "y\0", offset=4, then f64 LE datatype
1061        buf.extend_from_slice(b"y\0");
1062        buf.push(4); // byte_offset = 4
1063        buf.extend_from_slice(&build_float(8, 52, 11, 0, 52, 1023));
1064
1065        let (dt, _) = Datatype::parse(&buf).unwrap();
1066        match dt {
1067            Datatype::Compound { size, members } => {
1068                assert_eq!(size, 12);
1069                assert_eq!(members.len(), 2);
1070                assert_eq!(members[0].name, "x");
1071                assert_eq!(members[0].byte_offset, 0);
1072                assert_eq!(members[1].name, "y");
1073                assert_eq!(members[1].byte_offset, 4);
1074                match &members[0].datatype {
1075                    Datatype::FixedPoint {
1076                        size: 4,
1077                        signed: false,
1078                        ..
1079                    } => {}
1080                    other => panic!("expected u32, got {other:?}"),
1081                }
1082                match &members[1].datatype {
1083                    Datatype::FloatingPoint { size: 8, .. } => {}
1084                    other => panic!("expected f64, got {other:?}"),
1085                }
1086            }
1087            _ => panic!("expected Compound"),
1088        }
1089    }
1090
1091    #[test]
1092    fn test_compound_v1_complex_matlab_layout() {
1093        // MATLAB stores a complex value as a version-1 compound of two f64
1094        // members named "real" and "imag" at offsets 0 and 8. v1 members pad
1095        // the NUL-terminated name to a multiple of 8 bytes and carry a fixed
1096        // 28-byte dimension block — dimensionality(1) + reserved(3) +
1097        // dimension permutation(4) + reserved(4) + dimension sizes(16) —
1098        // between the byte offset and the member datatype message. Regression
1099        // test for a stride bug that skipped only 24 bytes (omitting the second
1100        // reserved field) and so misread every real-MATLAB complex compound.
1101        let mut buf = build_dt_header(6, 1, [2, 0, 0], 16); // v1, 2 members, size 16
1102        for (name, offset) in [(&b"real\0\0\0\0"[..], 0u32), (&b"imag\0\0\0\0"[..], 8)] {
1103            buf.extend_from_slice(name); // NUL-terminated, padded to 8
1104            let mut off = [0u8; 4];
1105            LittleEndian::write_u32(&mut off, offset);
1106            buf.extend_from_slice(&off);
1107            buf.extend_from_slice(&[0u8; 28]); // v1 dimension block
1108            buf.extend_from_slice(&build_float(8, 52, 11, 0, 52, 1023));
1109        }
1110
1111        let (dt, _) = Datatype::parse(&buf).unwrap();
1112        match dt {
1113            Datatype::Compound { size, members } => {
1114                assert_eq!(size, 16);
1115                assert_eq!(members.len(), 2);
1116                assert_eq!(members[0].name, "real");
1117                assert_eq!(members[0].byte_offset, 0);
1118                assert_eq!(members[1].name, "imag");
1119                assert_eq!(members[1].byte_offset, 8);
1120                for m in &members {
1121                    assert!(
1122                        matches!(m.datatype, Datatype::FloatingPoint { size: 8, .. }),
1123                        "expected f64 member, got {:?}",
1124                        m.datatype
1125                    );
1126                }
1127            }
1128            _ => panic!("expected Compound"),
1129        }
1130    }
1131
1132    #[test]
1133    fn test_reference_object() {
1134        let buf = build_dt_header(7, 1, [0, 0, 0], 8);
1135        let (dt, _) = Datatype::parse(&buf).unwrap();
1136        assert_eq!(
1137            dt,
1138            Datatype::Reference {
1139                size: 8,
1140                ref_type: ReferenceType::Object,
1141            }
1142        );
1143    }
1144
1145    #[test]
1146    fn test_reference_region() {
1147        let buf = build_dt_header(7, 1, [1, 0, 0], 12);
1148        let (dt, _) = Datatype::parse(&buf).unwrap();
1149        assert_eq!(
1150            dt,
1151            Datatype::Reference {
1152                size: 12,
1153                ref_type: ReferenceType::DatasetRegion,
1154            }
1155        );
1156    }
1157
1158    #[test]
1159    fn test_enumeration() {
1160        // Enum with base type i32 LE, 3 members
1161        let mut buf = build_dt_header(8, 3, [3, 0, 0], 4); // 3 members
1162        // Base type: i32 LE
1163        buf.extend_from_slice(&build_fixed_point(4, false, true, 0, 32));
1164        // Names: "RED\0", "GREEN\0", "BLUE\0"
1165        buf.extend_from_slice(b"RED\0");
1166        buf.extend_from_slice(b"GREEN\0");
1167        buf.extend_from_slice(b"BLUE\0");
1168        // Values: 0, 1, 2 (as i32 LE)
1169        buf.extend_from_slice(&0i32.to_le_bytes());
1170        buf.extend_from_slice(&1i32.to_le_bytes());
1171        buf.extend_from_slice(&2i32.to_le_bytes());
1172
1173        let (dt, _) = Datatype::parse(&buf).unwrap();
1174        match dt {
1175            Datatype::Enumeration {
1176                size,
1177                base_type,
1178                members,
1179            } => {
1180                assert_eq!(size, 4);
1181                assert_eq!(members.len(), 3);
1182                assert_eq!(members[0].name, "RED");
1183                assert_eq!(members[0].value, 0i32.to_le_bytes().to_vec());
1184                assert_eq!(members[1].name, "GREEN");
1185                assert_eq!(members[1].value, 1i32.to_le_bytes().to_vec());
1186                assert_eq!(members[2].name, "BLUE");
1187                assert_eq!(members[2].value, 2i32.to_le_bytes().to_vec());
1188                match *base_type {
1189                    Datatype::FixedPoint {
1190                        signed: true,
1191                        size: 4,
1192                        ..
1193                    } => {}
1194                    other => panic!("expected i32, got {other:?}"),
1195                }
1196            }
1197            _ => panic!("expected Enumeration"),
1198        }
1199    }
1200
1201    #[test]
1202    fn test_variable_length_string_utf8() {
1203        // VL string: type=1, padding=0(null term), charset=1(utf8)
1204        // bf0: bits 0-3 = 1 (string), bits 4-7 = 0 (null term) → 0x01
1205        // bf1: bits 0-3 = 1 (utf8) → 0x01
1206        let mut buf = build_dt_header(9, 1, [0x01, 0x01, 0], 16);
1207        // Base type: u8 (class 0, unsigned, size 1)
1208        buf.extend_from_slice(&build_fixed_point(1, false, false, 0, 8));
1209
1210        let (dt, _) = Datatype::parse(&buf).unwrap();
1211        match dt {
1212            Datatype::VariableLength {
1213                is_string,
1214                padding,
1215                charset,
1216                base_type,
1217            } => {
1218                assert!(is_string);
1219                assert_eq!(padding, Some(StringPadding::NullTerminate));
1220                assert_eq!(charset, Some(CharacterSet::Utf8));
1221                assert_eq!(base_type.type_size(), 1);
1222            }
1223            _ => panic!("expected VariableLength"),
1224        }
1225    }
1226
1227    #[test]
1228    fn test_variable_length_sequence_f32() {
1229        // VL sequence: type=0
1230        // bf0 = 0x00
1231        let mut buf = build_dt_header(9, 1, [0x00, 0x00, 0], 16);
1232        // Base type: f32 LE
1233        buf.extend_from_slice(&build_float(4, 23, 8, 0, 23, 127));
1234
1235        let (dt, _) = Datatype::parse(&buf).unwrap();
1236        match dt {
1237            Datatype::VariableLength {
1238                is_string,
1239                padding,
1240                charset,
1241                base_type,
1242            } => {
1243                assert!(!is_string);
1244                assert_eq!(padding, None);
1245                assert_eq!(charset, None);
1246                assert_eq!(base_type.type_size(), 4);
1247            }
1248            _ => panic!("expected VariableLength"),
1249        }
1250    }
1251
1252    #[test]
1253    fn test_array_2d() {
1254        // Array [3][4] of i32 LE, version 3
1255        let mut buf = build_dt_header(10, 3, [0, 0, 0], 48); // 3*4*4=48
1256        buf.push(2); // ndims=2
1257        buf.extend_from_slice(&3u32.to_le_bytes()); // dim 0
1258        buf.extend_from_slice(&4u32.to_le_bytes()); // dim 1
1259        // Base type: i32 LE
1260        buf.extend_from_slice(&build_fixed_point(4, false, true, 0, 32));
1261
1262        let (dt, _) = Datatype::parse(&buf).unwrap();
1263        match dt {
1264            Datatype::Array {
1265                base_type,
1266                dimensions,
1267            } => {
1268                assert_eq!(dimensions, vec![3, 4]);
1269                match *base_type {
1270                    Datatype::FixedPoint {
1271                        size: 4,
1272                        signed: true,
1273                        ..
1274                    } => {}
1275                    other => panic!("expected i32, got {other:?}"),
1276                }
1277            }
1278            _ => panic!("expected Array"),
1279        }
1280    }
1281
1282    #[test]
1283    fn test_bitfield() {
1284        let mut buf = build_dt_header(4, 1, [0, 0, 0], 2); // 16-bit LE bitfield
1285        let mut props = [0u8; 4];
1286        LittleEndian::write_u16(&mut props[0..2], 0);
1287        LittleEndian::write_u16(&mut props[2..4], 16);
1288        buf.extend_from_slice(&props);
1289
1290        let (dt, _) = Datatype::parse(&buf).unwrap();
1291        assert_eq!(
1292            dt,
1293            Datatype::BitField {
1294                size: 2,
1295                byte_order: DatatypeByteOrder::LittleEndian,
1296                bit_offset: 0,
1297                bit_precision: 16,
1298            }
1299        );
1300    }
1301
1302    #[test]
1303    fn test_time() {
1304        let mut buf = build_dt_header(2, 1, [0, 0, 0], 8);
1305        let mut props = [0u8; 2];
1306        LittleEndian::write_u16(&mut props[0..2], 64);
1307        buf.extend_from_slice(&props);
1308
1309        let (dt, consumed) = Datatype::parse(&buf).unwrap();
1310        assert_eq!(consumed, 10);
1311        assert_eq!(
1312            dt,
1313            Datatype::Time {
1314                size: 8,
1315                byte_order: DatatypeByteOrder::LittleEndian,
1316                bit_precision: 64,
1317            }
1318        );
1319    }
1320
1321    #[test]
1322    fn test_time_byte_order_roundtrips() {
1323        // A big-endian time type must serialize and re-parse with its byte order
1324        // preserved (bf0 bit 0), so repack can reproduce it faithfully.
1325        for (be, order) in [
1326            (0u8, DatatypeByteOrder::LittleEndian),
1327            (1u8, DatatypeByteOrder::BigEndian),
1328        ] {
1329            let mut buf = build_dt_header(2, 1, [be, 0, 0], 4);
1330            buf.extend_from_slice(&32u16.to_le_bytes());
1331            let (dt, _) = Datatype::parse(&buf).unwrap();
1332            assert_eq!(
1333                dt,
1334                Datatype::Time {
1335                    size: 4,
1336                    byte_order: order.clone(),
1337                    bit_precision: 32,
1338                }
1339            );
1340            // serialize -> parse must round-trip the byte order.
1341            let (reparsed, _) = Datatype::parse(&dt.serialize()).unwrap();
1342            assert_eq!(reparsed, dt);
1343        }
1344    }
1345
1346    #[test]
1347    fn test_nested_compound_array_enum() {
1348        // Compound containing a single member "data" which is an Array[2] of Enum(i32, 2 values)
1349        // Build the enum first
1350        let mut enum_bytes = build_dt_header(8, 3, [2, 0, 0], 4); // 2 members
1351        enum_bytes.extend_from_slice(&build_fixed_point(4, false, true, 0, 32)); // base i32
1352        enum_bytes.extend_from_slice(b"A\0");
1353        enum_bytes.extend_from_slice(b"B\0");
1354        enum_bytes.extend_from_slice(&0i32.to_le_bytes());
1355        enum_bytes.extend_from_slice(&1i32.to_le_bytes());
1356
1357        // Build array[2] of that enum, version 3
1358        let mut array_bytes = build_dt_header(10, 3, [0, 0, 0], 8); // 2*4=8
1359        array_bytes.push(1); // ndims=1
1360        array_bytes.extend_from_slice(&2u32.to_le_bytes()); // dim[0]=2
1361        array_bytes.extend_from_slice(&enum_bytes);
1362
1363        // Build compound with 1 member, size=8
1364        let mut buf = build_dt_header(6, 3, [1, 0, 0], 8); // 1 member
1365        buf.extend_from_slice(b"data\0");
1366        buf.push(0); // byte_offset = 0 (size=8, so 1 byte offsets)
1367        buf.extend_from_slice(&array_bytes);
1368
1369        let (dt, _) = Datatype::parse(&buf).unwrap();
1370        match dt {
1371            Datatype::Compound { members, .. } => {
1372                assert_eq!(members.len(), 1);
1373                assert_eq!(members[0].name, "data");
1374                match &members[0].datatype {
1375                    Datatype::Array {
1376                        dimensions,
1377                        base_type,
1378                    } => {
1379                        assert_eq!(dimensions, &[2]);
1380                        match base_type.as_ref() {
1381                            Datatype::Enumeration { members, .. } => {
1382                                assert_eq!(members.len(), 2);
1383                                assert_eq!(members[0].name, "A");
1384                                assert_eq!(members[1].name, "B");
1385                            }
1386                            other => panic!("expected Enum, got {other:?}"),
1387                        }
1388                    }
1389                    other => panic!("expected Array, got {other:?}"),
1390                }
1391            }
1392            _ => panic!("expected Compound"),
1393        }
1394    }
1395
1396    #[test]
1397    fn test_error_invalid_class() {
1398        let buf = build_dt_header(13, 1, [0, 0, 0], 4);
1399        let err = Datatype::parse(&buf).unwrap_err();
1400        assert_eq!(err, FormatError::InvalidDatatypeClass(13));
1401    }
1402
1403    #[test]
1404    fn test_error_truncated_data() {
1405        let buf = [0u8; 4]; // too short for header
1406        let err = Datatype::parse(&buf).unwrap_err();
1407        match err {
1408            FormatError::UnexpectedEof { .. } => {}
1409            other => panic!("expected UnexpectedEof, got {other:?}"),
1410        }
1411    }
1412
1413    #[test]
1414    fn test_error_invalid_string_padding() {
1415        let buf = build_dt_header(3, 1, [0x03, 0, 0], 10); // padding=3 invalid
1416        let err = Datatype::parse(&buf).unwrap_err();
1417        assert_eq!(err, FormatError::InvalidStringPadding(3));
1418    }
1419
1420    #[test]
1421    fn test_error_invalid_charset() {
1422        let buf = build_dt_header(3, 1, [0x20, 0, 0], 10); // charset=2 invalid
1423        let err = Datatype::parse(&buf).unwrap_err();
1424        assert_eq!(err, FormatError::InvalidCharacterSet(2));
1425    }
1426
1427    #[test]
1428    fn test_error_invalid_reference_type() {
1429        let buf = build_dt_header(7, 1, [5, 0, 0], 8);
1430        let err = Datatype::parse(&buf).unwrap_err();
1431        assert_eq!(err, FormatError::InvalidReferenceType(5));
1432    }
1433
1434    #[test]
1435    fn serialize_parse_compound_roundtrip() {
1436        let dt = Datatype::Compound {
1437            size: 20,
1438            members: vec![
1439                CompoundMember {
1440                    name: "x".to_string(),
1441                    byte_offset: 0,
1442                    datatype: Datatype::FloatingPoint {
1443                        size: 8,
1444                        byte_order: DatatypeByteOrder::LittleEndian,
1445                        bit_offset: 0,
1446                        bit_precision: 64,
1447                        exponent_location: 52,
1448                        exponent_size: 11,
1449                        mantissa_location: 0,
1450                        mantissa_size: 52,
1451                        exponent_bias: 1023,
1452                    },
1453                },
1454                CompoundMember {
1455                    name: "y".to_string(),
1456                    byte_offset: 8,
1457                    datatype: Datatype::FloatingPoint {
1458                        size: 8,
1459                        byte_order: DatatypeByteOrder::LittleEndian,
1460                        bit_offset: 0,
1461                        bit_precision: 64,
1462                        exponent_location: 52,
1463                        exponent_size: 11,
1464                        mantissa_location: 0,
1465                        mantissa_size: 52,
1466                        exponent_bias: 1023,
1467                    },
1468                },
1469                CompoundMember {
1470                    name: "id".to_string(),
1471                    byte_offset: 16,
1472                    datatype: Datatype::FixedPoint {
1473                        size: 4,
1474                        byte_order: DatatypeByteOrder::LittleEndian,
1475                        signed: true,
1476                        bit_offset: 0,
1477                        bit_precision: 32,
1478                    },
1479                },
1480            ],
1481        };
1482        let bytes = dt.serialize();
1483        let (parsed, _) = Datatype::parse(&bytes).unwrap();
1484        assert_eq!(parsed, dt);
1485    }
1486
1487    #[test]
1488    fn serialize_parse_enum_roundtrip() {
1489        let dt = Datatype::Enumeration {
1490            size: 4,
1491            base_type: Box::new(Datatype::FixedPoint {
1492                size: 4,
1493                byte_order: DatatypeByteOrder::LittleEndian,
1494                signed: true,
1495                bit_offset: 0,
1496                bit_precision: 32,
1497            }),
1498            members: vec![
1499                EnumMember {
1500                    name: "RED".to_string(),
1501                    value: 0i32.to_le_bytes().to_vec(),
1502                },
1503                EnumMember {
1504                    name: "GREEN".to_string(),
1505                    value: 1i32.to_le_bytes().to_vec(),
1506                },
1507                EnumMember {
1508                    name: "BLUE".to_string(),
1509                    value: 2i32.to_le_bytes().to_vec(),
1510                },
1511            ],
1512        };
1513        let bytes = dt.serialize();
1514        let (parsed, _) = Datatype::parse(&bytes).unwrap();
1515        assert_eq!(parsed, dt);
1516    }
1517
1518    /// Fixed-point base type for enum round-trip tests.
1519    fn enum_base_fp(size: u32, be: bool, signed: bool) -> Datatype {
1520        Datatype::FixedPoint {
1521            size,
1522            byte_order: if be {
1523                DatatypeByteOrder::BigEndian
1524            } else {
1525                DatatypeByteOrder::LittleEndian
1526            },
1527            signed,
1528            bit_offset: 0,
1529            #[expect(
1530                clippy::cast_possible_truncation,
1531                reason = "test builds byte-width base types; size*8 is well within u16"
1532            )]
1533            bit_precision: (size * 8) as u16,
1534        }
1535    }
1536
1537    /// Build an enum datatype over `base`, storing each member value truncated to
1538    /// the base width (the value blob is opaque bytes, so any content round-trips).
1539    fn make_enum(base: Datatype, members: &[(&str, i64)]) -> Datatype {
1540        let size = base.type_size();
1541        let width = size as usize;
1542        Datatype::Enumeration {
1543            size,
1544            base_type: Box::new(base),
1545            members: members
1546                .iter()
1547                .map(|(name, v)| EnumMember {
1548                    name: (*name).to_string(),
1549                    value: v.to_le_bytes()[..width].to_vec(),
1550                })
1551                .collect(),
1552        }
1553    }
1554
1555    #[test]
1556    fn serialize_parse_enum_base_type_variety() {
1557        // The i32 base is already covered above; here u8, big-endian i16, and i64
1558        // bases all round-trip through the enum wrapper.
1559        for base in [
1560            enum_base_fp(1, false, false), // u8
1561            enum_base_fp(2, true, true),   // i16 big-endian
1562            enum_base_fp(8, false, true),  // i64
1563        ] {
1564            let dt = make_enum(base.clone(), &[("A", 0), ("B", 1), ("NEG", -1)]);
1565            let bytes = dt.serialize();
1566            let (parsed, consumed) = Datatype::parse(&bytes).unwrap();
1567            assert_eq!(parsed, dt, "round-trip failed for base {base:?}");
1568            assert_eq!(consumed, bytes.len());
1569        }
1570    }
1571
1572    #[test]
1573    fn serialize_parse_enum_large_member_count() {
1574        // More than 256 members exercises the 2-byte member-count field, which is
1575        // split across bf0/bf1 in the datatype message header.
1576        let owned: Vec<(String, i64)> = (0..300).map(|i| (format!("M{i}"), i)).collect();
1577        let members: Vec<(&str, i64)> = owned.iter().map(|(n, v)| (n.as_str(), *v)).collect();
1578        let dt = make_enum(enum_base_fp(4, false, true), &members);
1579        let bytes = dt.serialize();
1580        let (parsed, _) = Datatype::parse(&bytes).unwrap();
1581        assert_eq!(parsed, dt);
1582        match parsed {
1583            Datatype::Enumeration { members, .. } => {
1584                assert_eq!(members.len(), 300);
1585                assert_eq!(members[299].name, "M299");
1586            }
1587            other => panic!("expected Enumeration, got {other:?}"),
1588        }
1589    }
1590
1591    #[test]
1592    fn enum_value_width_is_not_validated_against_base_size() {
1593        // `EnumTypeBuilder::build`/`Datatype::Enumeration` take the element size
1594        // from the base type only, with no check that member value blobs match it.
1595        // A 4-byte value on a 1-byte base therefore serializes in full but parses
1596        // back reading just `base_size` (1) byte per member, silently truncating.
1597        // This documents the current permissiveness; it is NOT a supported
1598        // round-trip, and the assertion guards against a silent change either way.
1599        let dt = Datatype::Enumeration {
1600            size: 1,
1601            base_type: Box::new(enum_base_fp(1, false, false)),
1602            members: vec![EnumMember {
1603                name: "X".to_string(),
1604                value: 5i32.to_le_bytes().to_vec(), // 4 bytes on a 1-byte base
1605            }],
1606        };
1607        let bytes = dt.serialize();
1608        let (parsed, _) = Datatype::parse(&bytes).unwrap();
1609        assert_ne!(
1610            parsed, dt,
1611            "a value wider than the base silently truncates on parse"
1612        );
1613        match parsed {
1614            Datatype::Enumeration { members, .. } => assert_eq!(members[0].value, vec![5]),
1615            other => panic!("expected Enumeration, got {other:?}"),
1616        }
1617    }
1618
1619    #[test]
1620    fn serialize_parse_array_roundtrip() {
1621        let dt = Datatype::Array {
1622            base_type: Box::new(Datatype::FloatingPoint {
1623                size: 8,
1624                byte_order: DatatypeByteOrder::LittleEndian,
1625                bit_offset: 0,
1626                bit_precision: 64,
1627                exponent_location: 52,
1628                exponent_size: 11,
1629                mantissa_location: 0,
1630                mantissa_size: 52,
1631                exponent_bias: 1023,
1632            }),
1633            dimensions: vec![3],
1634        };
1635        let bytes = dt.serialize();
1636        let (parsed, _) = Datatype::parse(&bytes).unwrap();
1637        assert_eq!(parsed, dt);
1638    }
1639
1640    #[test]
1641    fn serialize_parse_time_roundtrip() {
1642        let dt = Datatype::Time {
1643            size: 8,
1644            byte_order: DatatypeByteOrder::LittleEndian,
1645            bit_precision: 64,
1646        };
1647        let bytes = dt.serialize();
1648        let (parsed, consumed) = Datatype::parse(&bytes).unwrap();
1649        assert_eq!(parsed, dt);
1650        assert_eq!(consumed, bytes.len());
1651    }
1652
1653    #[test]
1654    fn serialize_parse_bitfield_roundtrip() {
1655        for byte_order in [
1656            DatatypeByteOrder::LittleEndian,
1657            DatatypeByteOrder::BigEndian,
1658        ] {
1659            let dt = Datatype::BitField {
1660                size: 4,
1661                byte_order,
1662                bit_offset: 3,
1663                bit_precision: 17,
1664            };
1665            let bytes = dt.serialize();
1666            let (parsed, consumed) = Datatype::parse(&bytes).unwrap();
1667            assert_eq!(parsed, dt);
1668            assert_eq!(consumed, bytes.len());
1669        }
1670    }
1671
1672    #[test]
1673    fn serialize_parse_opaque_roundtrip() {
1674        // Tag lengths that do and do not land on an 8-byte boundary, to exercise
1675        // the zero padding both ways.
1676        for tag in [
1677            b"abc".to_vec(),         // 3 bytes -> padded to 8
1678            b"12345678".to_vec(),    // 8 bytes -> no padding
1679            b"sensor-id\0".to_vec(), // 10 bytes -> padded to 16, embedded NUL preserved
1680        ] {
1681            let dt = Datatype::Opaque { size: 16, tag };
1682            let bytes = dt.serialize();
1683            // The property section (after the 8-byte header) must be a multiple
1684            // of 8, matching what the reference library expects.
1685            assert_eq!((bytes.len() - 8) % 8, 0);
1686            let (parsed, consumed) = Datatype::parse(&bytes).unwrap();
1687            assert_eq!(parsed, dt);
1688            assert_eq!(consumed, bytes.len());
1689        }
1690    }
1691
1692    #[test]
1693    fn test_type_size() {
1694        let dt = Datatype::FixedPoint {
1695            size: 4,
1696            byte_order: DatatypeByteOrder::LittleEndian,
1697            signed: true,
1698            bit_offset: 0,
1699            bit_precision: 32,
1700        };
1701        assert_eq!(dt.type_size(), 4);
1702
1703        let dt = Datatype::Array {
1704            base_type: Box::new(Datatype::FixedPoint {
1705                size: 4,
1706                byte_order: DatatypeByteOrder::LittleEndian,
1707                signed: true,
1708                bit_offset: 0,
1709                bit_precision: 32,
1710            }),
1711            dimensions: vec![3, 4],
1712        };
1713        assert_eq!(dt.type_size(), 48);
1714    }
1715}