Skip to main content

hdf5_reader/messages/
datatype.rs

1//! HDF5 Datatype message (type 0x0003).
2//!
3//! The datatype message describes the type of each element in a dataset.
4//! The first 4 bytes encode class (bits 0-3), version (bits 4-7), and
5//! class-specific bit flags (bits 8-31). The remaining bytes carry
6//! class-specific properties.
7//!
8//! Supported classes:
9//! - 0: Fixed-point (integer)
10//! - 1: Floating-point
11//! - 2: Time (treated as opaque)
12//! - 3: String
13//! - 4: Bitfield
14//! - 5: Opaque
15//! - 6: Compound
16//! - 7: Reference
17//! - 8: Enum
18//! - 9: Variable-length
19//! - 10: Array
20
21use crate::error::{ByteOrder, Error, Result};
22use crate::io::Cursor;
23
24// ---------------------------------------------------------------------------
25// Public types
26// ---------------------------------------------------------------------------
27
28/// How a string's length is determined.
29#[derive(Debug, Clone, Copy, PartialEq, Eq)]
30pub enum StringSize {
31    /// Fixed-length, padded to `n` bytes.
32    Fixed(u32),
33    /// Variable-length (stored as a global-heap reference).
34    Variable,
35}
36
37/// String character encoding.
38#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum StringEncoding {
40    Ascii,
41    Utf8,
42}
43
44/// String padding type.
45#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum StringPadding {
47    NullTerminate,
48    NullPad,
49    SpacePad,
50}
51
52/// A field within a compound datatype.
53#[derive(Debug, Clone)]
54pub struct CompoundField {
55    pub name: String,
56    pub byte_offset: u32,
57    pub datatype: Datatype,
58}
59
60/// A member of an enumeration.
61#[derive(Debug, Clone)]
62pub struct EnumMember {
63    pub name: String,
64    pub value: Vec<u8>,
65}
66
67/// HDF5 reference type.
68#[derive(Debug, Clone, Copy, PartialEq, Eq)]
69pub enum ReferenceType {
70    /// Object reference (8 bytes in HDF5 1.8+).
71    Object,
72    /// Dataset region reference (12 bytes).
73    DatasetRegion,
74}
75
76/// Describes the element type of a dataset or attribute.
77#[derive(Debug, Clone)]
78pub enum Datatype {
79    /// Integer (class 0).
80    FixedPoint {
81        size: u8,
82        signed: bool,
83        byte_order: ByteOrder,
84    },
85    /// IEEE 754 float (class 1).
86    FloatingPoint { size: u8, byte_order: ByteOrder },
87    /// Character string (class 3).
88    String {
89        size: StringSize,
90        encoding: StringEncoding,
91        padding: StringPadding,
92    },
93    /// Compound / struct (class 6).
94    Compound {
95        size: u32,
96        fields: Vec<CompoundField>,
97    },
98    /// Fixed-size array of a base type (class 10).
99    Array { base: Box<Datatype>, dims: Vec<u64> },
100    /// Enumeration (class 8).
101    Enum {
102        base: Box<Datatype>,
103        members: Vec<EnumMember>,
104    },
105    /// Variable-length sequence or string (class 9).
106    VarLen { base: Box<Datatype> },
107    /// Opaque blob (class 5).
108    Opaque { size: u32, tag: String },
109    /// Object or region reference (class 7).
110    Reference { ref_type: ReferenceType, size: u8 },
111    /// Bitfield (class 4).
112    Bitfield { size: u8, byte_order: ByteOrder },
113}
114
115/// Wrapper returned by the message parser, pairing the decoded datatype
116/// with the total element size from the message header.
117#[derive(Debug, Clone)]
118pub struct DatatypeMessage {
119    pub datatype: Datatype,
120    /// Element size in bytes (from the 4-byte class/version word).
121    pub size: u32,
122}
123
124// ---------------------------------------------------------------------------
125// Parsing
126// ---------------------------------------------------------------------------
127
128/// Parse a datatype message starting at the current cursor position.
129///
130/// The `msg_size` is the total number of bytes allocated for this message
131/// (used to skip any trailing padding).
132pub fn parse(cursor: &mut Cursor<'_>, msg_size: usize) -> Result<DatatypeMessage> {
133    let start = cursor.position();
134    let (dt, size) = parse_datatype_description(cursor)?;
135
136    let consumed = (cursor.position() - start) as usize;
137    if consumed < msg_size {
138        cursor.skip(msg_size - consumed)?;
139    }
140
141    Ok(DatatypeMessage { datatype: dt, size })
142}
143
144/// Parse a single datatype description (the 4-byte header + properties).
145///
146/// This is also called recursively for compound members, arrays, enums, etc.
147pub fn parse_datatype_description(cursor: &mut Cursor<'_>) -> Result<(Datatype, u32)> {
148    let class_and_flags = cursor.read_u32_le()?;
149    let class = (class_and_flags & 0x0F) as u8;
150    let version = ((class_and_flags >> 4) & 0x0F) as u8;
151    let class_flags = class_and_flags >> 8; // upper 24 bits
152    let size = cursor.read_u32_le()?;
153
154    let dt = match class {
155        0 => parse_fixed_point(cursor, class_flags, size)?,
156        1 => parse_floating_point(cursor, class_flags, size)?,
157        2 => parse_time(cursor, size)?,
158        3 => parse_string(class_flags, size)?,
159        4 => parse_bitfield(cursor, class_flags, size)?,
160        5 => parse_opaque(cursor, class_flags, size)?,
161        6 => parse_compound(cursor, class_flags, size, version)?,
162        7 => parse_reference(class_flags, size)?,
163        8 => parse_enum(cursor, class_flags, size)?,
164        9 => parse_varlen(cursor, class_flags, size)?,
165        10 => parse_array(cursor, size, version)?,
166        c => return Err(Error::UnsupportedDatatypeClass(c)),
167    };
168
169    Ok((dt, size))
170}
171
172// ---------------------------------------------------------------------------
173// Class 0: Fixed-point (integer)
174// ---------------------------------------------------------------------------
175
176fn parse_fixed_point(cursor: &mut Cursor<'_>, flags: u32, size: u32) -> Result<Datatype> {
177    // Bit 0 of class flags: byte order (0 = LE, 1 = BE)
178    let byte_order = if (flags & 0x01) != 0 {
179        ByteOrder::BigEndian
180    } else {
181        ByteOrder::LittleEndian
182    };
183    // Bit 3: signed (0 = unsigned, 1 = signed)
184    let signed = (flags & 0x08) != 0;
185
186    // Properties: bit offset (u16) + bit precision (u16)
187    let _bit_offset = cursor.read_u16_le()?;
188    let _bit_precision = cursor.read_u16_le()?;
189
190    Ok(Datatype::FixedPoint {
191        size: size as u8,
192        signed,
193        byte_order,
194    })
195}
196
197// ---------------------------------------------------------------------------
198// Class 1: Floating-point
199// ---------------------------------------------------------------------------
200
201fn parse_floating_point(cursor: &mut Cursor<'_>, flags: u32, size: u32) -> Result<Datatype> {
202    // Byte order: bit 0 low-order, bit 6 high-order
203    //   00 = LE, 01 = BE, 10 = VAX (treated as LE for our purposes)
204    let bo_lo = flags & 0x01;
205    let bo_hi = (flags >> 6) & 0x01;
206    let byte_order = match (bo_hi, bo_lo) {
207        (0, 0) => ByteOrder::LittleEndian,
208        (0, 1) => ByteOrder::BigEndian,
209        // VAX order — map to little endian (close enough for decoding)
210        _ => ByteOrder::LittleEndian,
211    };
212
213    // Properties: 12 bytes
214    // bit offset (u16), bit precision (u16), exponent location (u8),
215    // exponent size (u8), mantissa location (u8), mantissa size (u8),
216    // exponent bias (u32)
217    let _bit_offset = cursor.read_u16_le()?;
218    let _bit_precision = cursor.read_u16_le()?;
219    let _exp_location = cursor.read_u8()?;
220    let _exp_size = cursor.read_u8()?;
221    let _mant_location = cursor.read_u8()?;
222    let _mant_size = cursor.read_u8()?;
223    let _exp_bias = cursor.read_u32_le()?;
224
225    Ok(Datatype::FloatingPoint {
226        size: size as u8,
227        byte_order,
228    })
229}
230
231// ---------------------------------------------------------------------------
232// Class 2: Time (rarely used, treat as opaque)
233// ---------------------------------------------------------------------------
234
235fn parse_time(cursor: &mut Cursor<'_>, size: u32) -> Result<Datatype> {
236    // Properties: bit precision (u16)
237    let _bit_precision = cursor.read_u16_le()?;
238    Ok(Datatype::Opaque {
239        size,
240        tag: "HDF5_TIME".to_string(),
241    })
242}
243
244// ---------------------------------------------------------------------------
245// Class 3: String
246// ---------------------------------------------------------------------------
247
248fn parse_string(flags: u32, size: u32) -> Result<Datatype> {
249    // Bits 0-3: padding type
250    let padding = match flags & 0x0F {
251        0 => StringPadding::NullTerminate,
252        1 => StringPadding::NullPad,
253        2 => StringPadding::SpacePad,
254        _ => StringPadding::NullTerminate,
255    };
256
257    // Bits 4-7: character set
258    let encoding = match (flags >> 4) & 0x0F {
259        0 => StringEncoding::Ascii,
260        1 => StringEncoding::Utf8,
261        _ => StringEncoding::Ascii,
262    };
263
264    // No additional property bytes for string class.
265
266    let string_size = if size == 0 {
267        // Size 0 can indicate variable-length when used with vlen wrapper,
268        // but for the string class itself we treat it as Variable.
269        StringSize::Variable
270    } else {
271        StringSize::Fixed(size)
272    };
273
274    Ok(Datatype::String {
275        size: string_size,
276        encoding,
277        padding,
278    })
279}
280
281// ---------------------------------------------------------------------------
282// Class 4: Bitfield
283// ---------------------------------------------------------------------------
284
285fn parse_bitfield(cursor: &mut Cursor<'_>, flags: u32, size: u32) -> Result<Datatype> {
286    let byte_order = if (flags & 0x01) != 0 {
287        ByteOrder::BigEndian
288    } else {
289        ByteOrder::LittleEndian
290    };
291
292    // Properties: bit offset (u16) + bit precision (u16)
293    let _bit_offset = cursor.read_u16_le()?;
294    let _bit_precision = cursor.read_u16_le()?;
295
296    Ok(Datatype::Bitfield {
297        size: size as u8,
298        byte_order,
299    })
300}
301
302// ---------------------------------------------------------------------------
303// Class 5: Opaque
304// ---------------------------------------------------------------------------
305
306fn parse_opaque(cursor: &mut Cursor<'_>, flags: u32, size: u32) -> Result<Datatype> {
307    // The class flags encode the length of the tag (in the lower bits).
308    let tag_len = (flags & 0xFF) as usize;
309
310    let tag = if tag_len > 0 {
311        let tag_bytes = cursor.read_bytes(tag_len)?;
312        // Trim trailing nulls
313        let end = tag_bytes.iter().rposition(|&b| b != 0).map_or(0, |i| i + 1);
314        String::from_utf8_lossy(&tag_bytes[..end]).into_owned()
315    } else {
316        String::new()
317    };
318
319    // Pad to 8-byte alignment
320    let padded = (tag_len + 7) & !7;
321    if padded > tag_len {
322        cursor.skip(padded - tag_len)?;
323    }
324
325    Ok(Datatype::Opaque { size, tag })
326}
327
328// ---------------------------------------------------------------------------
329// Class 6: Compound
330// ---------------------------------------------------------------------------
331
332fn parse_compound(cursor: &mut Cursor<'_>, flags: u32, size: u32, version: u8) -> Result<Datatype> {
333    // Lower 16 bits of class flags = number of members
334    let n_members = (flags & 0xFFFF) as usize;
335    let byte_offset_size = compound_member_offset_size(size);
336
337    let mut fields = Vec::with_capacity(n_members);
338
339    for _ in 0..n_members {
340        let name = cursor.read_null_terminated_string()?;
341
342        if version < 3 {
343            // V1/V2: name is padded to 8-byte boundary (relative to start of name)
344            // The null terminator is included in the count. We already read
345            // through the null terminator via read_null_terminated_string.
346            // Pad the position to 8-byte alignment.
347            cursor.align(8)?;
348        }
349
350        let byte_offset = if version == 1 {
351            // V1: byte offset is `size of offsets` (4 bytes)
352            cursor.read_u32_le()?
353        } else if version >= 3 {
354            cursor.read_uvar(byte_offset_size)? as u32
355        } else {
356            // V2/V3: byte offset is 4 bytes
357            cursor.read_u32_le()?
358        };
359
360        if version == 1 {
361            // V1: dimensionality (1 byte), reserved (3 bytes), dim perm (4 bytes),
362            // reserved (4 bytes), dim sizes (4 * 4 = 16 bytes)
363            let _dimensionality = cursor.read_u8()?;
364            cursor.skip(3)?; // reserved
365            cursor.skip(4)?; // dimension permutation
366            cursor.skip(4)?; // reserved
367            cursor.skip(16)?; // 4 dimension sizes (each u32)
368        }
369
370        let (member_dt, _member_size) = parse_datatype_description(cursor)?;
371
372        fields.push(CompoundField {
373            name,
374            byte_offset,
375            datatype: member_dt,
376        });
377    }
378
379    Ok(Datatype::Compound { size, fields })
380}
381
382fn compound_member_offset_size(size: u32) -> usize {
383    match size {
384        0..=0xFF => 1,
385        0x100..=0xFFFF => 2,
386        0x1_0000..=0xFF_FFFF => 3,
387        _ => 4,
388    }
389}
390
391// ---------------------------------------------------------------------------
392// Class 7: Reference
393// ---------------------------------------------------------------------------
394
395fn parse_reference(flags: u32, size: u32) -> Result<Datatype> {
396    // Bit 0-3: reference type (0 = object, 1 = dataset region)
397    let ref_type = match flags & 0x0F {
398        0 => ReferenceType::Object,
399        1 => ReferenceType::DatasetRegion,
400        _ => ReferenceType::Object,
401    };
402
403    // No property bytes for reference class.
404
405    Ok(Datatype::Reference {
406        ref_type,
407        size: size as u8,
408    })
409}
410
411// ---------------------------------------------------------------------------
412// Class 8: Enum
413// ---------------------------------------------------------------------------
414
415fn parse_enum(cursor: &mut Cursor<'_>, flags: u32, size: u32) -> Result<Datatype> {
416    let n_members = (flags & 0xFFFF) as usize;
417
418    // Base type
419    let (base_dt, _base_size) = parse_datatype_description(cursor)?;
420
421    // Member names (null-terminated)
422    let mut names = Vec::with_capacity(n_members);
423    for _ in 0..n_members {
424        names.push(cursor.read_null_terminated_string()?);
425    }
426
427    // Member values (each is `size` bytes, matching the base type size)
428    let member_value_size = size as usize;
429    let mut members = Vec::with_capacity(n_members);
430    for name in names {
431        let value = cursor.read_bytes(member_value_size)?.to_vec();
432        members.push(EnumMember { name, value });
433    }
434
435    Ok(Datatype::Enum {
436        base: Box::new(base_dt),
437        members,
438    })
439}
440
441// ---------------------------------------------------------------------------
442// Class 9: Variable-length
443// ---------------------------------------------------------------------------
444
445fn parse_varlen(cursor: &mut Cursor<'_>, flags: u32, _size: u32) -> Result<Datatype> {
446    // Bits 0-3: type (0 = sequence, 1 = string)
447    let _vlen_type = flags & 0x0F;
448    // Bits 4-7: padding type (for strings)
449    let _padding = (flags >> 4) & 0x0F;
450    // Bits 8-11: character set (for strings)
451    let _charset = (flags >> 8) & 0x0F;
452
453    // Base type follows
454    let (base_dt, _base_size) = parse_datatype_description(cursor)?;
455
456    Ok(Datatype::VarLen {
457        base: Box::new(base_dt),
458    })
459}
460
461// ---------------------------------------------------------------------------
462// Class 10: Array
463// ---------------------------------------------------------------------------
464
465fn parse_array(cursor: &mut Cursor<'_>, _size: u32, version: u8) -> Result<Datatype> {
466    let rank = cursor.read_u8()? as usize;
467
468    if version < 3 {
469        // Version 1/2: 3 reserved bytes after rank
470        cursor.skip(3)?;
471    }
472
473    let mut dims = Vec::with_capacity(rank);
474    for _ in 0..rank {
475        dims.push(cursor.read_u32_le()? as u64);
476    }
477
478    if version < 3 {
479        // Version 1: permutation indices (rank * u32) — skip them
480        cursor.skip(rank * 4)?;
481    }
482
483    // Base type
484    let (base_dt, _base_size) = parse_datatype_description(cursor)?;
485
486    Ok(Datatype::Array {
487        base: Box::new(base_dt),
488        dims,
489    })
490}
491
492// ---------------------------------------------------------------------------
493// Tests
494// ---------------------------------------------------------------------------
495
496#[cfg(test)]
497mod tests {
498    use super::*;
499
500    /// Build the 4-byte class+version+flags word.
501    fn class_word(class: u8, version: u8, flags: u32) -> u32 {
502        (class as u32) | ((version as u32) << 4) | (flags << 8)
503    }
504
505    #[test]
506    fn test_parse_u32_le() {
507        let mut data = Vec::new();
508        // class=0 (fixed-point), version=1, flags: bit0=0 (LE), bit3=0 (unsigned)
509        data.extend_from_slice(&class_word(0, 1, 0x00).to_le_bytes());
510        // size = 4
511        data.extend_from_slice(&4u32.to_le_bytes());
512        // properties: bit offset=0, bit precision=32
513        data.extend_from_slice(&0u16.to_le_bytes());
514        data.extend_from_slice(&32u16.to_le_bytes());
515
516        let mut cursor = Cursor::new(&data);
517        let msg = parse(&mut cursor, data.len()).unwrap();
518        assert_eq!(msg.size, 4);
519        match &msg.datatype {
520            Datatype::FixedPoint {
521                size,
522                signed,
523                byte_order,
524            } => {
525                assert_eq!(*size, 4);
526                assert!(!*signed);
527                assert_eq!(*byte_order, ByteOrder::LittleEndian);
528            }
529            other => panic!("expected FixedPoint, got {:?}", other),
530        }
531    }
532
533    #[test]
534    fn test_parse_i64_be() {
535        let mut data = Vec::new();
536        // class=0 (fixed-point), version=1, flags: bit0=1 (BE), bit3=1 (signed)
537        data.extend_from_slice(&class_word(0, 1, 0x09).to_le_bytes());
538        // size = 8
539        data.extend_from_slice(&8u32.to_le_bytes());
540        // properties: bit offset=0, bit precision=64
541        data.extend_from_slice(&0u16.to_le_bytes());
542        data.extend_from_slice(&64u16.to_le_bytes());
543
544        let mut cursor = Cursor::new(&data);
545        let msg = parse(&mut cursor, data.len()).unwrap();
546        assert_eq!(msg.size, 8);
547        match &msg.datatype {
548            Datatype::FixedPoint {
549                size,
550                signed,
551                byte_order,
552            } => {
553                assert_eq!(*size, 8);
554                assert!(*signed);
555                assert_eq!(*byte_order, ByteOrder::BigEndian);
556            }
557            other => panic!("expected FixedPoint, got {:?}", other),
558        }
559    }
560
561    #[test]
562    fn test_parse_f32_le() {
563        let mut data = Vec::new();
564        // class=1 (float), version=1, flags: bit0=0 (LE), bit6=0
565        data.extend_from_slice(&class_word(1, 1, 0x20).to_le_bytes());
566        // size = 4
567        data.extend_from_slice(&4u32.to_le_bytes());
568        // properties: bit offset=0, bit precision=32
569        data.extend_from_slice(&0u16.to_le_bytes());
570        data.extend_from_slice(&32u16.to_le_bytes());
571        // exp location=23, exp size=8
572        data.push(23);
573        data.push(8);
574        // mant location=0, mant size=23
575        data.push(0);
576        data.push(23);
577        // exp bias=127
578        data.extend_from_slice(&127u32.to_le_bytes());
579
580        let mut cursor = Cursor::new(&data);
581        let msg = parse(&mut cursor, data.len()).unwrap();
582        assert_eq!(msg.size, 4);
583        match &msg.datatype {
584            Datatype::FloatingPoint { size, byte_order } => {
585                assert_eq!(*size, 4);
586                assert_eq!(*byte_order, ByteOrder::LittleEndian);
587            }
588            other => panic!("expected FloatingPoint, got {:?}", other),
589        }
590    }
591
592    #[test]
593    fn test_parse_f64_be() {
594        let mut data = Vec::new();
595        // class=1 (float), version=1, flags: bit0=1 (BE), bit6=0
596        data.extend_from_slice(&class_word(1, 1, 0x01).to_le_bytes());
597        // size = 8
598        data.extend_from_slice(&8u32.to_le_bytes());
599        // properties
600        data.extend_from_slice(&0u16.to_le_bytes());
601        data.extend_from_slice(&64u16.to_le_bytes());
602        data.push(52);
603        data.push(11);
604        data.push(0);
605        data.push(52);
606        data.extend_from_slice(&1023u32.to_le_bytes());
607
608        let mut cursor = Cursor::new(&data);
609        let msg = parse(&mut cursor, data.len()).unwrap();
610        assert_eq!(msg.size, 8);
611        match &msg.datatype {
612            Datatype::FloatingPoint { size, byte_order } => {
613                assert_eq!(*size, 8);
614                assert_eq!(*byte_order, ByteOrder::BigEndian);
615            }
616            other => panic!("expected FloatingPoint, got {:?}", other),
617        }
618    }
619
620    #[test]
621    fn test_parse_string_fixed_ascii() {
622        let mut data = Vec::new();
623        // class=3 (string), version=1, flags: padding=0 (null-term), charset=0 (ascii)
624        data.extend_from_slice(&class_word(3, 1, 0x00).to_le_bytes());
625        // size = 32
626        data.extend_from_slice(&32u32.to_le_bytes());
627        // No property bytes for strings.
628
629        let mut cursor = Cursor::new(&data);
630        let msg = parse(&mut cursor, data.len()).unwrap();
631        assert_eq!(msg.size, 32);
632        match &msg.datatype {
633            Datatype::String {
634                size,
635                encoding,
636                padding,
637            } => {
638                assert_eq!(*size, StringSize::Fixed(32));
639                assert_eq!(*encoding, StringEncoding::Ascii);
640                assert_eq!(*padding, StringPadding::NullTerminate);
641            }
642            other => panic!("expected String, got {:?}", other),
643        }
644    }
645
646    #[test]
647    fn test_parse_string_utf8_space_pad() {
648        let mut data = Vec::new();
649        // class=3, version=1, flags: padding=2 (space-pad), charset=1 (utf8)
650        // padding bits 0-3 = 2, charset bits 4-7 = 1
651        let flags: u32 = 0x02 | (0x01 << 4);
652        data.extend_from_slice(&class_word(3, 1, flags).to_le_bytes());
653        data.extend_from_slice(&16u32.to_le_bytes());
654
655        let mut cursor = Cursor::new(&data);
656        let msg = parse(&mut cursor, data.len()).unwrap();
657        match &msg.datatype {
658            Datatype::String {
659                size,
660                encoding,
661                padding,
662            } => {
663                assert_eq!(*size, StringSize::Fixed(16));
664                assert_eq!(*encoding, StringEncoding::Utf8);
665                assert_eq!(*padding, StringPadding::SpacePad);
666            }
667            other => panic!("expected String, got {:?}", other),
668        }
669    }
670
671    #[test]
672    fn test_parse_reference_object() {
673        let mut data = Vec::new();
674        // class=7, version=1, flags: ref_type=0 (object)
675        data.extend_from_slice(&class_word(7, 1, 0x00).to_le_bytes());
676        data.extend_from_slice(&8u32.to_le_bytes());
677
678        let mut cursor = Cursor::new(&data);
679        let msg = parse(&mut cursor, data.len()).unwrap();
680        match &msg.datatype {
681            Datatype::Reference { ref_type, size } => {
682                assert_eq!(*ref_type, ReferenceType::Object);
683                assert_eq!(*size, 8);
684            }
685            other => panic!("expected Reference, got {:?}", other),
686        }
687    }
688
689    #[test]
690    fn test_parse_reference_region() {
691        let mut data = Vec::new();
692        // class=7, version=1, flags: ref_type=1 (dataset region)
693        data.extend_from_slice(&class_word(7, 1, 0x01).to_le_bytes());
694        data.extend_from_slice(&12u32.to_le_bytes());
695
696        let mut cursor = Cursor::new(&data);
697        let msg = parse(&mut cursor, data.len()).unwrap();
698        match &msg.datatype {
699            Datatype::Reference { ref_type, size } => {
700                assert_eq!(*ref_type, ReferenceType::DatasetRegion);
701                assert_eq!(*size, 12);
702            }
703            other => panic!("expected Reference, got {:?}", other),
704        }
705    }
706
707    #[test]
708    fn test_parse_compound_v3_variable_member_offsets() {
709        let mut data = Vec::new();
710        data.extend_from_slice(&class_word(6, 3, 2).to_le_bytes());
711        data.extend_from_slice(&16u32.to_le_bytes());
712
713        data.extend_from_slice(b"dataset\0");
714        data.push(0x00);
715        data.extend_from_slice(&class_word(7, 1, 0x00).to_le_bytes());
716        data.extend_from_slice(&8u32.to_le_bytes());
717
718        data.extend_from_slice(b"dimension\0");
719        data.push(0x08);
720        data.extend_from_slice(&class_word(0, 1, 0x00).to_le_bytes());
721        data.extend_from_slice(&4u32.to_le_bytes());
722        data.extend_from_slice(&0u16.to_le_bytes());
723        data.extend_from_slice(&32u16.to_le_bytes());
724
725        let mut cursor = Cursor::new(&data);
726        let msg = parse(&mut cursor, data.len()).unwrap();
727        match &msg.datatype {
728            Datatype::Compound { size, fields } => {
729                assert_eq!(*size, 16);
730                assert_eq!(fields.len(), 2);
731                assert_eq!(fields[0].name, "dataset");
732                assert_eq!(fields[0].byte_offset, 0);
733                assert_eq!(fields[1].name, "dimension");
734                assert_eq!(fields[1].byte_offset, 8);
735            }
736            other => panic!("expected Compound, got {:?}", other),
737        }
738    }
739
740    #[test]
741    fn test_parse_enum_u8() {
742        let mut data = Vec::new();
743        // class=8 (enum), version=3, flags: n_members=2
744        data.extend_from_slice(&class_word(8, 3, 2).to_le_bytes());
745        // size = 1
746        data.extend_from_slice(&1u32.to_le_bytes());
747
748        // Base type: u8 (class=0, version=1, flags=0, size=1, props: offset=0 precision=8)
749        data.extend_from_slice(&class_word(0, 1, 0).to_le_bytes());
750        data.extend_from_slice(&1u32.to_le_bytes());
751        data.extend_from_slice(&0u16.to_le_bytes());
752        data.extend_from_slice(&8u16.to_le_bytes());
753
754        // Member names
755        data.extend_from_slice(b"OFF\0");
756        data.extend_from_slice(b"ON\0");
757
758        // Member values (1 byte each, matching size=1)
759        data.push(0x00);
760        data.push(0x01);
761
762        let mut cursor = Cursor::new(&data);
763        let msg = parse(&mut cursor, data.len()).unwrap();
764        match &msg.datatype {
765            Datatype::Enum { base, members } => {
766                assert!(matches!(
767                    base.as_ref(),
768                    Datatype::FixedPoint {
769                        size: 1,
770                        signed: false,
771                        ..
772                    }
773                ));
774                assert_eq!(members.len(), 2);
775                assert_eq!(members[0].name, "OFF");
776                assert_eq!(members[0].value, vec![0x00]);
777                assert_eq!(members[1].name, "ON");
778                assert_eq!(members[1].value, vec![0x01]);
779            }
780            other => panic!("expected Enum, got {:?}", other),
781        }
782    }
783
784    #[test]
785    fn test_parse_bitfield() {
786        let mut data = Vec::new();
787        // class=4 (bitfield), version=1, flags: bit0=0 (LE)
788        data.extend_from_slice(&class_word(4, 1, 0x00).to_le_bytes());
789        data.extend_from_slice(&2u32.to_le_bytes());
790        // properties: bit offset=0, bit precision=16
791        data.extend_from_slice(&0u16.to_le_bytes());
792        data.extend_from_slice(&16u16.to_le_bytes());
793
794        let mut cursor = Cursor::new(&data);
795        let msg = parse(&mut cursor, data.len()).unwrap();
796        match &msg.datatype {
797            Datatype::Bitfield { size, byte_order } => {
798                assert_eq!(*size, 2);
799                assert_eq!(*byte_order, ByteOrder::LittleEndian);
800            }
801            other => panic!("expected Bitfield, got {:?}", other),
802        }
803    }
804
805    #[test]
806    fn test_unsupported_class() {
807        let mut data = Vec::new();
808        // class=15 (invalid), version=1, flags=0
809        data.extend_from_slice(&class_word(15, 1, 0).to_le_bytes());
810        data.extend_from_slice(&0u32.to_le_bytes());
811
812        let mut cursor = Cursor::new(&data);
813        assert!(parse(&mut cursor, data.len()).is_err());
814    }
815}