Skip to main content

rustyhdf5_format/
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        bit_precision: u16,
90    },
91    /// Class 3: Fixed-length string.
92    String {
93        size: u32,
94        padding: StringPadding,
95        charset: CharacterSet,
96    },
97    /// Class 4: Bit field.
98    BitField {
99        size: u32,
100        byte_order: DatatypeByteOrder,
101        bit_offset: u16,
102        bit_precision: u16,
103    },
104    /// Class 5: Opaque data.
105    Opaque {
106        size: u32,
107        tag: Vec<u8>,
108    },
109    /// Class 6: Compound type.
110    Compound {
111        size: u32,
112        members: Vec<CompoundMember>,
113    },
114    /// Class 7: Reference type.
115    Reference {
116        size: u32,
117        ref_type: ReferenceType,
118    },
119    /// Class 8: Enumeration type.
120    Enumeration {
121        size: u32,
122        base_type: Box<Datatype>,
123        members: Vec<EnumMember>,
124    },
125    /// Class 9: Variable-length type.
126    VariableLength {
127        is_string: bool,
128        padding: Option<StringPadding>,
129        charset: Option<CharacterSet>,
130        base_type: Box<Datatype>,
131    },
132    /// Class 10: Array type.
133    Array {
134        base_type: Box<Datatype>,
135        dimensions: Vec<u32>,
136    },
137}
138
139fn ensure_len(data: &[u8], offset: usize, needed: usize) -> Result<(), FormatError> {
140    match offset.checked_add(needed) {
141        Some(end) if end <= data.len() => Ok(()),
142        _ => Err(FormatError::UnexpectedEof {
143            expected: offset.saturating_add(needed),
144            available: data.len(),
145        }),
146    }
147}
148
149fn parse_string_padding(val: u8) -> Result<StringPadding, FormatError> {
150    match val {
151        0 => Ok(StringPadding::NullTerminate),
152        1 => Ok(StringPadding::NullPad),
153        2 => Ok(StringPadding::SpacePad),
154        _ => Err(FormatError::InvalidStringPadding(val)),
155    }
156}
157
158fn parse_charset(val: u8) -> Result<CharacterSet, FormatError> {
159    match val {
160        0 => Ok(CharacterSet::Ascii),
161        1 => Ok(CharacterSet::Utf8),
162        _ => Err(FormatError::InvalidCharacterSet(val)),
163    }
164}
165
166/// Read a null-terminated string from `data` starting at `offset`.
167/// Returns (string, bytes_consumed including the null terminator).
168fn read_null_terminated_string(data: &[u8], offset: usize) -> Result<(String, usize), FormatError> {
169    if offset >= data.len() {
170        return Err(FormatError::UnexpectedEof {
171            expected: offset + 1,
172            available: data.len(),
173        });
174    }
175    let remaining = &data[offset..];
176    let null_pos = remaining
177        .iter()
178        .position(|&b| b == 0)
179        .ok_or(FormatError::UnexpectedEof {
180            expected: offset + 1,
181            available: data.len(),
182        })?;
183    let name = String::from_utf8_lossy(&remaining[..null_pos]).into_owned();
184    Ok((name, null_pos + 1))
185}
186
187/// Determine how many bytes are needed to encode `compound_size` as a byte offset (v3).
188fn offset_bytes_for_size(compound_size: u32) -> usize {
189    if compound_size <= 0xFF {
190        1
191    } else if compound_size <= 0xFFFF {
192        2
193    } else {
194        4
195    }
196}
197
198/// Read an unsigned integer of 1, 2, 4, or 8 bytes (LE).
199fn read_uint(data: &[u8], offset: usize, nbytes: usize) -> Result<u64, FormatError> {
200    ensure_len(data, offset, nbytes)?;
201    let slice = &data[offset..offset + nbytes];
202    Ok(match nbytes {
203        1 => slice[0] as u64,
204        2 => LittleEndian::read_u16(slice) as u64,
205        4 => LittleEndian::read_u32(slice) as u64,
206        8 => LittleEndian::read_u64(slice),
207        _ => {
208            return Err(FormatError::UnexpectedEof {
209                expected: offset + nbytes,
210                available: data.len(),
211            });
212        }
213    })
214}
215
216impl Datatype {
217    /// Parse a datatype message from raw bytes.
218    ///
219    /// Returns `(Datatype, bytes_consumed)` for recursive parsing.
220    pub fn parse(data: &[u8]) -> Result<(Datatype, usize), FormatError> {
221        // Minimum header: 4 bytes (class_and_version + 3 bytes bit field) + 4 bytes size = 8
222        ensure_len(data, 0, 8)?;
223
224        let class_and_version = data[0];
225        let class_id = class_and_version & 0x0F;
226        let version = (class_and_version >> 4) & 0x0F;
227
228        // 24-bit class bit field (little-endian)
229        let bf0 = data[1];
230        let bf1 = data[2];
231        let bf2 = data[3];
232        let _bit_field_24 = (bf0 as u32) | ((bf1 as u32) << 8) | ((bf2 as u32) << 16);
233
234        let size = LittleEndian::read_u32(&data[4..8]);
235        let mut pos = 8;
236
237        match class_id {
238            0 => {
239                // Fixed-Point
240                ensure_len(data, pos, 4)?;
241                let byte_order = if bf0 & 0x01 == 0 {
242                    DatatypeByteOrder::LittleEndian
243                } else {
244                    DatatypeByteOrder::BigEndian
245                };
246                let signed = (bf0 >> 3) & 0x01 == 1;
247                let bit_offset = LittleEndian::read_u16(&data[pos..pos + 2]);
248                let bit_precision = LittleEndian::read_u16(&data[pos + 2..pos + 4]);
249                pos += 4;
250                Ok((
251                    Datatype::FixedPoint {
252                        size,
253                        byte_order,
254                        signed,
255                        bit_offset,
256                        bit_precision,
257                    },
258                    pos,
259                ))
260            }
261            1 => {
262                // Floating-Point
263                ensure_len(data, pos, 12)?;
264                let bo_low = bf0 & 0x01;
265                let bo_high = (bf0 >> 6) & 0x01;
266                let byte_order = match (bo_high, bo_low) {
267                    (0, 0) => DatatypeByteOrder::LittleEndian,
268                    (0, 1) => DatatypeByteOrder::BigEndian,
269                    (1, 0) => DatatypeByteOrder::Vax,
270                    (1, 1) => DatatypeByteOrder::Vax,
271                    _ => unreachable!(),
272                };
273                let bit_offset = LittleEndian::read_u16(&data[pos..pos + 2]);
274                let bit_precision = LittleEndian::read_u16(&data[pos + 2..pos + 4]);
275                let exponent_location = data[pos + 4];
276                let exponent_size = data[pos + 5];
277                let mantissa_location = data[pos + 6];
278                let mantissa_size = data[pos + 7];
279                let exponent_bias = LittleEndian::read_u32(&data[pos + 8..pos + 12]);
280                pos += 12;
281                Ok((
282                    Datatype::FloatingPoint {
283                        size,
284                        byte_order,
285                        bit_offset,
286                        bit_precision,
287                        exponent_location,
288                        exponent_size,
289                        mantissa_location,
290                        mantissa_size,
291                        exponent_bias,
292                    },
293                    pos,
294                ))
295            }
296            2 => {
297                // Time
298                ensure_len(data, pos, 2)?;
299                let bit_precision = LittleEndian::read_u16(&data[pos..pos + 2]);
300                pos += 2;
301                Ok((Datatype::Time { size, bit_precision }, pos))
302            }
303            3 => {
304                // String
305                let padding_val = bf0 & 0x0F;
306                let charset_val = (bf0 >> 4) & 0x0F;
307                let padding = parse_string_padding(padding_val)?;
308                let charset = parse_charset(charset_val)?;
309                Ok((Datatype::String { size, padding, charset }, pos))
310            }
311            4 => {
312                // Bit Field
313                ensure_len(data, pos, 4)?;
314                let byte_order = if bf0 & 0x01 == 0 {
315                    DatatypeByteOrder::LittleEndian
316                } else {
317                    DatatypeByteOrder::BigEndian
318                };
319                let bit_offset = LittleEndian::read_u16(&data[pos..pos + 2]);
320                let bit_precision = LittleEndian::read_u16(&data[pos + 2..pos + 4]);
321                pos += 4;
322                Ok((
323                    Datatype::BitField {
324                        size,
325                        byte_order,
326                        bit_offset,
327                        bit_precision,
328                    },
329                    pos,
330                ))
331            }
332            5 => {
333                // Opaque
334                let tag_len = bf0 as usize;
335                ensure_len(data, pos, tag_len)?;
336                let tag = data[pos..pos + tag_len].to_vec();
337                // Tags are padded to multiple of 8 bytes
338                let padded = (tag_len + 7) & !7;
339                let pos = 8 + padded; // from start of properties
340                Ok((Datatype::Opaque { size, tag }, pos))
341            }
342            6 => {
343                // Compound
344                let num_members = (bf0 as u16) | ((bf1 as u16) << 8);
345                let mut members = Vec::with_capacity(num_members as usize);
346
347                if version == 3 || version == 4 {
348                    let ob = offset_bytes_for_size(size);
349                    for _ in 0..num_members {
350                        let (name, name_len) = read_null_terminated_string(data, pos)?;
351                        pos += name_len;
352                        let byte_offset = read_uint(data, pos, ob)?;
353                        pos += ob;
354                        let (member_dt, consumed) = Datatype::parse(&data[pos..])?;
355                        pos += consumed;
356                        members.push(CompoundMember {
357                            name,
358                            byte_offset,
359                            datatype: member_dt,
360                        });
361                    }
362                } else if version == 1 || version == 2 {
363                    // v1/v2: name, offset(4), dimensionality(1), reserved(3), dim_perm(4),
364                    //         reserved_dims(up to 4*4=16), member datatype
365                    for _ in 0..num_members {
366                        let (name, name_len) = read_null_terminated_string(data, pos)?;
367                        pos += name_len;
368                        // v1: names padded to 8-byte boundary
369                        if version == 1 {
370                            let total_name_bytes = name_len;
371                            let padded = (total_name_bytes + 7) & !7;
372                            pos = pos - name_len + padded;
373                        }
374                        ensure_len(data, pos, 4)?;
375                        let byte_offset = LittleEndian::read_u32(&data[pos..pos + 4]) as u64;
376                        pos += 4;
377                        // dimensionality(1) + reserved(3) + dim_perm(4) + 4 dim slots(16) = 24
378                        ensure_len(data, pos, 24)?;
379                        pos += 24;
380                        let (member_dt, consumed) = Datatype::parse(&data[pos..])?;
381                        pos += consumed;
382                        members.push(CompoundMember {
383                            name,
384                            byte_offset,
385                            datatype: member_dt,
386                        });
387                    }
388                } else {
389                    return Err(FormatError::InvalidDatatypeVersion {
390                        class: class_id,
391                        version,
392                    });
393                }
394
395                Ok((Datatype::Compound { size, members }, pos))
396            }
397            7 => {
398                // Reference
399                let ref_type_val = bf0 & 0x0F;
400                let ref_type = match ref_type_val {
401                    0 => ReferenceType::Object,
402                    1 => ReferenceType::DatasetRegion,
403                    _ => return Err(FormatError::InvalidReferenceType(ref_type_val)),
404                };
405                Ok((Datatype::Reference { size, ref_type }, pos))
406            }
407            8 => {
408                // Enumeration
409                let num_members = (bf0 as u16) | ((bf1 as u16) << 8);
410                // Parse base type
411                let (base_type, base_consumed) = Datatype::parse(&data[pos..])?;
412                pos += base_consumed;
413                let base_size = base_type.type_size();
414                let mut members = Vec::with_capacity(num_members as usize);
415                // Enum layout: base_type, then all names (null-terminated), then all values
416                // v1/v2: names are padded to 8-byte boundaries
417                // v3: names are just null-terminated
418                let mut member_names = Vec::with_capacity(num_members as usize);
419                for _ in 0..num_members {
420                    let (name, name_len) = read_null_terminated_string(data, pos)?;
421                    if version < 3 {
422                        let padded = (name_len + 7) & !7;
423                        pos += padded;
424                    } else {
425                        pos += name_len;
426                    }
427                    member_names.push(name);
428                }
429                // Now values
430                for name in &member_names {
431                    ensure_len(data, pos, base_size as usize)?;
432                    let value = data[pos..pos + base_size as usize].to_vec();
433                    pos += base_size as usize;
434                    members.push(EnumMember {
435                        name: name.clone(),
436                        value,
437                    });
438                }
439                Ok((
440                    Datatype::Enumeration {
441                        size,
442                        base_type: Box::new(base_type),
443                        members,
444                    },
445                    pos,
446                ))
447            }
448            9 => {
449                // Variable-Length
450                let vl_type = bf0 & 0x0F;
451                let is_string = vl_type == 1;
452                let padding = if is_string {
453                    let pad_val = (bf0 >> 4) & 0x0F;
454                    Some(parse_string_padding(pad_val)?)
455                } else {
456                    None
457                };
458                let charset = if is_string {
459                    let cs_val = bf1 & 0x0F;
460                    Some(parse_charset(cs_val)?)
461                } else {
462                    None
463                };
464                let (base_type, consumed) = Datatype::parse(&data[pos..])?;
465                pos += consumed;
466                Ok((
467                    Datatype::VariableLength {
468                        is_string,
469                        padding,
470                        charset,
471                        base_type: Box::new(base_type),
472                    },
473                    pos,
474                ))
475            }
476            10 => {
477                // Array
478                if version == 2 {
479                    ensure_len(data, pos, 4)?;
480                    let ndims = data[pos] as usize;
481                    pos += 4; // ndims(1) + reserved(3)
482                    ensure_len(data, pos, ndims * 4 + ndims * 4)?;
483                    let mut dimensions = Vec::with_capacity(ndims);
484                    for _ in 0..ndims {
485                        dimensions.push(LittleEndian::read_u32(&data[pos..pos + 4]));
486                        pos += 4;
487                    }
488                    // skip permutation indices
489                    pos += ndims * 4;
490                    let (base_type, consumed) = Datatype::parse(&data[pos..])?;
491                    pos += consumed;
492                    Ok((
493                        Datatype::Array {
494                            base_type: Box::new(base_type),
495                            dimensions,
496                        },
497                        pos,
498                    ))
499                } else if version == 3 {
500                    ensure_len(data, pos, 1)?;
501                    let ndims = data[pos] as usize;
502                    pos += 1;
503                    ensure_len(data, pos, ndims * 4)?;
504                    let mut dimensions = Vec::with_capacity(ndims);
505                    for _ in 0..ndims {
506                        dimensions.push(LittleEndian::read_u32(&data[pos..pos + 4]));
507                        pos += 4;
508                    }
509                    let (base_type, consumed) = Datatype::parse(&data[pos..])?;
510                    pos += consumed;
511                    Ok((
512                        Datatype::Array {
513                            base_type: Box::new(base_type),
514                            dimensions,
515                        },
516                        pos,
517                    ))
518                } else {
519                    Err(FormatError::InvalidDatatypeVersion {
520                        class: class_id,
521                        version,
522                    })
523                }
524            }
525            11 => {
526                // Complex number — store as compound of two floats internally
527                // Parse like compound with version 3 and 2 members
528                // But actually class 11 has no special properties beyond class 6 compound.
529                // It's just recognized as a separate class. For now parse the 2 members
530                // as compound.
531                let num_members = (bf0 as u16) | ((bf1 as u16) << 8);
532                let mut members = Vec::with_capacity(num_members as usize);
533                let ob = offset_bytes_for_size(size);
534                for _ in 0..num_members {
535                    let (name, name_len) = read_null_terminated_string(data, pos)?;
536                    pos += name_len;
537                    let byte_offset = read_uint(data, pos, ob)?;
538                    pos += ob;
539                    let (member_dt, consumed) = Datatype::parse(&data[pos..])?;
540                    pos += consumed;
541                    members.push(CompoundMember {
542                        name,
543                        byte_offset,
544                        datatype: member_dt,
545                    });
546                }
547                Ok((Datatype::Compound { size, members }, pos))
548            }
549            _ => Err(FormatError::InvalidDatatypeClass(class_id)),
550        }
551    }
552
553    /// Serialize datatype to HDF5 message bytes.
554    pub fn serialize(&self) -> Vec<u8> {
555        match self {
556            Datatype::FixedPoint { size, byte_order, signed, bit_offset, bit_precision } => {
557                let mut bf0 = 0u8;
558                if matches!(byte_order, DatatypeByteOrder::BigEndian) { bf0 |= 0x01; }
559                if *signed { bf0 |= 0x08; }
560                let mut buf = Self::build_header(0, 1, [bf0, 0, 0], *size);
561                buf.extend_from_slice(&bit_offset.to_le_bytes());
562                buf.extend_from_slice(&bit_precision.to_le_bytes());
563                buf
564            }
565            Datatype::FloatingPoint { size, byte_order, bit_offset, bit_precision,
566                exponent_location, exponent_size, mantissa_location, mantissa_size, exponent_bias } => {
567                let mut bf0 = 0x20u8; // bit 5: sign location bit (standard IEEE 754)
568                match byte_order {
569                    DatatypeByteOrder::BigEndian => { bf0 |= 0x01; }
570                    DatatypeByteOrder::Vax => { bf0 |= 0x40; }
571                    _ => {}
572                }
573                // bf[1] bits 0-1: mantissa normalization = 2 (MSB not stored, IEEE 754)
574                let bf1 = 0x3fu8; // matching what h5py generates
575                let mut buf = Self::build_header(1, 1, [bf0, bf1, 0], *size);
576                buf.extend_from_slice(&bit_offset.to_le_bytes());
577                buf.extend_from_slice(&bit_precision.to_le_bytes());
578                buf.push(*exponent_location);
579                buf.push(*exponent_size);
580                buf.push(*mantissa_location);
581                buf.push(*mantissa_size);
582                buf.extend_from_slice(&exponent_bias.to_le_bytes());
583                buf
584            }
585            Datatype::String { size, padding, charset } => {
586                let pad_val = match padding {
587                    StringPadding::NullTerminate => 0,
588                    StringPadding::NullPad => 1,
589                    StringPadding::SpacePad => 2,
590                };
591                let cs_val = match charset {
592                    CharacterSet::Ascii => 0,
593                    CharacterSet::Utf8 => 1,
594                };
595                let bf0 = pad_val | (cs_val << 4);
596                Self::build_header(3, 1, [bf0, 0, 0], *size)
597            }
598            Datatype::VariableLength { is_string, padding, charset, base_type } => {
599                let mut bf0 = if *is_string { 0x01u8 } else { 0x00 };
600                if *is_string {
601                    if let Some(p) = padding {
602                        let pv = match p {
603                            StringPadding::NullTerminate => 0,
604                            StringPadding::NullPad => 1,
605                            StringPadding::SpacePad => 2,
606                        };
607                        bf0 |= pv << 4;
608                    }
609                }
610                let bf1 = if *is_string {
611                    charset.as_ref().map_or(0, |c| match c {
612                        CharacterSet::Ascii => 0,
613                        CharacterSet::Utf8 => 1,
614                    })
615                } else { 0 };
616                let mut buf = Self::build_header(9, 1, [bf0, bf1, 0], 16);
617                buf.extend_from_slice(&base_type.serialize());
618                buf
619            }
620            Datatype::Compound { size, members } => {
621                let num = members.len() as u16;
622                let bf0 = (num & 0xFF) as u8;
623                let bf1 = ((num >> 8) & 0xFF) as u8;
624                let mut buf = Self::build_header(6, 3, [bf0, bf1, 0], *size);
625                let ob = offset_bytes_for_size(*size);
626                for m in members {
627                    // Null-terminated name
628                    buf.extend_from_slice(m.name.as_bytes());
629                    buf.push(0);
630                    // Byte offset (variable-width)
631                    match ob {
632                        1 => buf.push(m.byte_offset as u8),
633                        2 => buf.extend_from_slice(&(m.byte_offset as u16).to_le_bytes()),
634                        _ => buf.extend_from_slice(&(m.byte_offset as u32).to_le_bytes()),
635                    }
636                    // Recursively serialize member datatype
637                    buf.extend_from_slice(&m.datatype.serialize());
638                }
639                buf
640            }
641            Datatype::Enumeration { size, base_type, members } => {
642                let num = members.len() as u16;
643                let bf0 = (num & 0xFF) as u8;
644                let bf1 = ((num >> 8) & 0xFF) as u8;
645                let mut buf = Self::build_header(8, 3, [bf0, bf1, 0], *size);
646                // Base type
647                buf.extend_from_slice(&base_type.serialize());
648                // All names (null-terminated)
649                for m in members {
650                    buf.extend_from_slice(m.name.as_bytes());
651                    buf.push(0);
652                }
653                // All values
654                for m in members {
655                    buf.extend_from_slice(&m.value);
656                }
657                buf
658            }
659            Datatype::Array { base_type, dimensions } => {
660                let mut buf = Self::build_header(10, 3, [0, 0, 0], self.type_size());
661                buf.push(dimensions.len() as u8);
662                for &d in dimensions {
663                    buf.extend_from_slice(&d.to_le_bytes());
664                }
665                buf.extend_from_slice(&base_type.serialize());
666                buf
667            }
668            _ => {
669                Vec::new()
670            }
671        }
672    }
673
674    fn build_header(class: u8, version: u8, bf: [u8; 3], size: u32) -> Vec<u8> {
675        let mut buf = vec![0u8; 8];
676        buf[0] = (class & 0x0F) | ((version & 0x0F) << 4);
677        buf[1] = bf[0];
678        buf[2] = bf[1];
679        buf[3] = bf[2];
680        buf[4..8].copy_from_slice(&size.to_le_bytes());
681        buf
682    }
683
684    /// Return the size in bytes of one element of this type.
685    pub fn type_size(&self) -> u32 {
686        match self {
687            Datatype::FixedPoint { size, .. } => *size,
688            Datatype::FloatingPoint { size, .. } => *size,
689            Datatype::Time { size, .. } => *size,
690            Datatype::String { size, .. } => *size,
691            Datatype::BitField { size, .. } => *size,
692            Datatype::Opaque { size, .. } => *size,
693            Datatype::Compound { size, .. } => *size,
694            Datatype::Reference { size, .. } => *size,
695            Datatype::Enumeration { size, .. } => *size,
696            Datatype::VariableLength { .. } => 16, // typically pointer + length
697            Datatype::Array { base_type, dimensions } => {
698                let elem_count: u32 = dimensions.iter().copied()
699                    .fold(1u32, |a, b| a.saturating_mul(b));
700                base_type.type_size().saturating_mul(elem_count)
701            }
702        }
703    }
704}
705
706/// Build a datatype header (8 bytes) for testing.
707#[cfg(test)]
708fn build_dt_header(class: u8, version: u8, bf: [u8; 3], size: u32) -> Vec<u8> {
709    let mut buf = vec![0u8; 8];
710    buf[0] = (class & 0x0F) | ((version & 0x0F) << 4);
711    buf[1] = bf[0];
712    buf[2] = bf[1];
713    buf[3] = bf[2];
714    LittleEndian::write_u32(&mut buf[4..8], size);
715    buf
716}
717
718#[cfg(test)]
719mod tests {
720    use super::*;
721
722    // Helper to build a fixed-point datatype message
723    fn build_fixed_point(size: u32, be: bool, signed: bool, bit_offset: u16, bit_precision: u16) -> Vec<u8> {
724        let bf0 = if be { 0x01 } else { 0x00 } | if signed { 0x08 } else { 0x00 };
725        let mut buf = build_dt_header(0, 1, [bf0, 0, 0], size);
726        let mut props = [0u8; 4];
727        LittleEndian::write_u16(&mut props[0..2], bit_offset);
728        LittleEndian::write_u16(&mut props[2..4], bit_precision);
729        buf.extend_from_slice(&props);
730        buf
731    }
732
733    // Helper to build a floating-point datatype message
734    fn build_float(size: u32, exp_loc: u8, exp_size: u8, mant_loc: u8, mant_size: u8, exp_bias: u32) -> Vec<u8> {
735        // LE byte order: bo_low=0, bo_high=0
736        let bf0 = 0x00u8;
737        let bf1 = 0x00u8;
738        // mantissa norm = 2 (MSB not stored) in bits 24-31... wait, that's bf2
739        let bf2 = 0x02u8; // norm = 2
740        let mut buf = build_dt_header(1, 1, [bf0, bf1, bf2], size);
741        let mut props = [0u8; 12];
742        LittleEndian::write_u16(&mut props[0..2], 0); // bit_offset
743        LittleEndian::write_u16(&mut props[2..4], (size * 8) as u16); // bit_precision
744        props[4] = exp_loc;
745        props[5] = exp_size;
746        props[6] = mant_loc;
747        props[7] = mant_size;
748        LittleEndian::write_u32(&mut props[8..12], exp_bias);
749        buf.extend_from_slice(&props);
750        buf
751    }
752
753    #[test]
754    fn test_fixed_point_u8() {
755        let data = build_fixed_point(1, false, false, 0, 8);
756        let (dt, consumed) = Datatype::parse(&data).unwrap();
757        assert_eq!(consumed, 12);
758        assert_eq!(dt, Datatype::FixedPoint {
759            size: 1,
760            byte_order: DatatypeByteOrder::LittleEndian,
761            signed: false,
762            bit_offset: 0,
763            bit_precision: 8,
764        });
765    }
766
767    #[test]
768    fn test_fixed_point_i16_le() {
769        let data = build_fixed_point(2, false, true, 0, 16);
770        let (dt, _) = Datatype::parse(&data).unwrap();
771        assert_eq!(dt, Datatype::FixedPoint {
772            size: 2,
773            byte_order: DatatypeByteOrder::LittleEndian,
774            signed: true,
775            bit_offset: 0,
776            bit_precision: 16,
777        });
778    }
779
780    #[test]
781    fn test_fixed_point_u32_be() {
782        let data = build_fixed_point(4, true, false, 0, 32);
783        let (dt, _) = Datatype::parse(&data).unwrap();
784        match &dt {
785            Datatype::FixedPoint { byte_order, signed, size, .. } => {
786                assert_eq!(*byte_order, DatatypeByteOrder::BigEndian);
787                assert!(!signed);
788                assert_eq!(*size, 4);
789            }
790            _ => panic!("expected FixedPoint"),
791        }
792    }
793
794    #[test]
795    fn test_fixed_point_i64_le() {
796        let data = build_fixed_point(8, false, true, 0, 64);
797        let (dt, _) = Datatype::parse(&data).unwrap();
798        assert_eq!(dt, Datatype::FixedPoint {
799            size: 8,
800            byte_order: DatatypeByteOrder::LittleEndian,
801            signed: true,
802            bit_offset: 0,
803            bit_precision: 64,
804        });
805    }
806
807    #[test]
808    fn test_float_f32_le() {
809        // IEEE 754 f32: exp=8 bits at bit 23, mant=23 bits at bit 0, bias=127
810        let data = build_float(4, 23, 8, 0, 23, 127);
811        let (dt, consumed) = Datatype::parse(&data).unwrap();
812        assert_eq!(consumed, 20);
813        assert_eq!(dt, Datatype::FloatingPoint {
814            size: 4,
815            byte_order: DatatypeByteOrder::LittleEndian,
816            bit_offset: 0,
817            bit_precision: 32,
818            exponent_location: 23,
819            exponent_size: 8,
820            mantissa_location: 0,
821            mantissa_size: 23,
822            exponent_bias: 127,
823        });
824    }
825
826    #[test]
827    fn test_float_f64_le() {
828        let data = build_float(8, 52, 11, 0, 52, 1023);
829        let (dt, _) = Datatype::parse(&data).unwrap();
830        assert_eq!(dt, Datatype::FloatingPoint {
831            size: 8,
832            byte_order: DatatypeByteOrder::LittleEndian,
833            bit_offset: 0,
834            bit_precision: 64,
835            exponent_location: 52,
836            exponent_size: 11,
837            mantissa_location: 0,
838            mantissa_size: 52,
839            exponent_bias: 1023,
840        });
841    }
842
843    #[test]
844    fn test_string_null_terminated_ascii() {
845        let buf = build_dt_header(3, 1, [0x00, 0, 0], 10); // padding=0(nullterm), charset=0(ascii)
846        let (dt, consumed) = Datatype::parse(&buf).unwrap();
847        assert_eq!(consumed, 8);
848        assert_eq!(dt, Datatype::String {
849            size: 10,
850            padding: StringPadding::NullTerminate,
851            charset: CharacterSet::Ascii,
852        });
853    }
854
855    #[test]
856    fn test_string_space_padded_utf8() {
857        // padding=2(space pad), charset=1(utf8) → bf0 = 0x12
858        let buf = build_dt_header(3, 1, [0x12, 0, 0], 32);
859        let (dt, _) = Datatype::parse(&buf).unwrap();
860        assert_eq!(dt, Datatype::String {
861            size: 32,
862            padding: StringPadding::SpacePad,
863            charset: CharacterSet::Utf8,
864        });
865    }
866
867    #[test]
868    fn test_opaque() {
869        // tag_len = 4, tag = "BLOB"
870        let mut buf = build_dt_header(5, 1, [4, 0, 0], 64);
871        buf.extend_from_slice(b"BLOB");
872        // Pad to 8 bytes
873        buf.extend_from_slice(&[0, 0, 0, 0]);
874        let (dt, consumed) = Datatype::parse(&buf).unwrap();
875        assert_eq!(consumed, 16); // 8 header + 8 padded tag
876        assert_eq!(dt, Datatype::Opaque {
877            size: 64,
878            tag: b"BLOB".to_vec(),
879        });
880    }
881
882    #[test]
883    fn test_compound_v3_two_members() {
884        // Compound with size=12, 2 members: "x" u32 at offset 0, "y" f64 at offset 4
885        // Size=12, so offset_bytes=1
886        let mut buf = build_dt_header(6, 3, [2, 0, 0], 12); // 2 members
887        // Member "x": name "x\0", offset=0, then u32 LE datatype
888        buf.extend_from_slice(b"x\0");
889        buf.push(0); // byte_offset = 0
890        buf.extend_from_slice(&build_fixed_point(4, false, false, 0, 32));
891        // Member "y": name "y\0", offset=4, then f64 LE datatype
892        buf.extend_from_slice(b"y\0");
893        buf.push(4); // byte_offset = 4
894        buf.extend_from_slice(&build_float(8, 52, 11, 0, 52, 1023));
895
896        let (dt, _) = Datatype::parse(&buf).unwrap();
897        match dt {
898            Datatype::Compound { size, members } => {
899                assert_eq!(size, 12);
900                assert_eq!(members.len(), 2);
901                assert_eq!(members[0].name, "x");
902                assert_eq!(members[0].byte_offset, 0);
903                assert_eq!(members[1].name, "y");
904                assert_eq!(members[1].byte_offset, 4);
905                match &members[0].datatype {
906                    Datatype::FixedPoint { size: 4, signed: false, .. } => {}
907                    other => panic!("expected u32, got {other:?}"),
908                }
909                match &members[1].datatype {
910                    Datatype::FloatingPoint { size: 8, .. } => {}
911                    other => panic!("expected f64, got {other:?}"),
912                }
913            }
914            _ => panic!("expected Compound"),
915        }
916    }
917
918    #[test]
919    fn test_reference_object() {
920        let buf = build_dt_header(7, 1, [0, 0, 0], 8);
921        let (dt, _) = Datatype::parse(&buf).unwrap();
922        assert_eq!(dt, Datatype::Reference {
923            size: 8,
924            ref_type: ReferenceType::Object,
925        });
926    }
927
928    #[test]
929    fn test_reference_region() {
930        let buf = build_dt_header(7, 1, [1, 0, 0], 12);
931        let (dt, _) = Datatype::parse(&buf).unwrap();
932        assert_eq!(dt, Datatype::Reference {
933            size: 12,
934            ref_type: ReferenceType::DatasetRegion,
935        });
936    }
937
938    #[test]
939    fn test_enumeration() {
940        // Enum with base type i32 LE, 3 members
941        let mut buf = build_dt_header(8, 3, [3, 0, 0], 4); // 3 members
942        // Base type: i32 LE
943        buf.extend_from_slice(&build_fixed_point(4, false, true, 0, 32));
944        // Names: "RED\0", "GREEN\0", "BLUE\0"
945        buf.extend_from_slice(b"RED\0");
946        buf.extend_from_slice(b"GREEN\0");
947        buf.extend_from_slice(b"BLUE\0");
948        // Values: 0, 1, 2 (as i32 LE)
949        buf.extend_from_slice(&0i32.to_le_bytes());
950        buf.extend_from_slice(&1i32.to_le_bytes());
951        buf.extend_from_slice(&2i32.to_le_bytes());
952
953        let (dt, _) = Datatype::parse(&buf).unwrap();
954        match dt {
955            Datatype::Enumeration { size, base_type, members } => {
956                assert_eq!(size, 4);
957                assert_eq!(members.len(), 3);
958                assert_eq!(members[0].name, "RED");
959                assert_eq!(members[0].value, 0i32.to_le_bytes().to_vec());
960                assert_eq!(members[1].name, "GREEN");
961                assert_eq!(members[1].value, 1i32.to_le_bytes().to_vec());
962                assert_eq!(members[2].name, "BLUE");
963                assert_eq!(members[2].value, 2i32.to_le_bytes().to_vec());
964                match *base_type {
965                    Datatype::FixedPoint { signed: true, size: 4, .. } => {}
966                    other => panic!("expected i32, got {other:?}"),
967                }
968            }
969            _ => panic!("expected Enumeration"),
970        }
971    }
972
973    #[test]
974    fn test_variable_length_string_utf8() {
975        // VL string: type=1, padding=0(null term), charset=1(utf8)
976        // bf0: bits 0-3 = 1 (string), bits 4-7 = 0 (null term) → 0x01
977        // bf1: bits 0-3 = 1 (utf8) → 0x01
978        let mut buf = build_dt_header(9, 1, [0x01, 0x01, 0], 16);
979        // Base type: u8 (class 0, unsigned, size 1)
980        buf.extend_from_slice(&build_fixed_point(1, false, false, 0, 8));
981
982        let (dt, _) = Datatype::parse(&buf).unwrap();
983        match dt {
984            Datatype::VariableLength { is_string, padding, charset, base_type } => {
985                assert!(is_string);
986                assert_eq!(padding, Some(StringPadding::NullTerminate));
987                assert_eq!(charset, Some(CharacterSet::Utf8));
988                assert_eq!(base_type.type_size(), 1);
989            }
990            _ => panic!("expected VariableLength"),
991        }
992    }
993
994    #[test]
995    fn test_variable_length_sequence_f32() {
996        // VL sequence: type=0
997        // bf0 = 0x00
998        let mut buf = build_dt_header(9, 1, [0x00, 0x00, 0], 16);
999        // Base type: f32 LE
1000        buf.extend_from_slice(&build_float(4, 23, 8, 0, 23, 127));
1001
1002        let (dt, _) = Datatype::parse(&buf).unwrap();
1003        match dt {
1004            Datatype::VariableLength { is_string, padding, charset, base_type } => {
1005                assert!(!is_string);
1006                assert_eq!(padding, None);
1007                assert_eq!(charset, None);
1008                assert_eq!(base_type.type_size(), 4);
1009            }
1010            _ => panic!("expected VariableLength"),
1011        }
1012    }
1013
1014    #[test]
1015    fn test_array_2d() {
1016        // Array [3][4] of i32 LE, version 3
1017        let mut buf = build_dt_header(10, 3, [0, 0, 0], 48); // 3*4*4=48
1018        buf.push(2); // ndims=2
1019        buf.extend_from_slice(&3u32.to_le_bytes()); // dim 0
1020        buf.extend_from_slice(&4u32.to_le_bytes()); // dim 1
1021        // Base type: i32 LE
1022        buf.extend_from_slice(&build_fixed_point(4, false, true, 0, 32));
1023
1024        let (dt, _) = Datatype::parse(&buf).unwrap();
1025        match dt {
1026            Datatype::Array { base_type, dimensions } => {
1027                assert_eq!(dimensions, vec![3, 4]);
1028                match *base_type {
1029                    Datatype::FixedPoint { size: 4, signed: true, .. } => {}
1030                    other => panic!("expected i32, got {other:?}"),
1031                }
1032            }
1033            _ => panic!("expected Array"),
1034        }
1035    }
1036
1037    #[test]
1038    fn test_bitfield() {
1039        let mut buf = build_dt_header(4, 1, [0, 0, 0], 2); // 16-bit LE bitfield
1040        let mut props = [0u8; 4];
1041        LittleEndian::write_u16(&mut props[0..2], 0);
1042        LittleEndian::write_u16(&mut props[2..4], 16);
1043        buf.extend_from_slice(&props);
1044
1045        let (dt, _) = Datatype::parse(&buf).unwrap();
1046        assert_eq!(dt, Datatype::BitField {
1047            size: 2,
1048            byte_order: DatatypeByteOrder::LittleEndian,
1049            bit_offset: 0,
1050            bit_precision: 16,
1051        });
1052    }
1053
1054    #[test]
1055    fn test_time() {
1056        let mut buf = build_dt_header(2, 1, [0, 0, 0], 8);
1057        let mut props = [0u8; 2];
1058        LittleEndian::write_u16(&mut props[0..2], 64);
1059        buf.extend_from_slice(&props);
1060
1061        let (dt, consumed) = Datatype::parse(&buf).unwrap();
1062        assert_eq!(consumed, 10);
1063        assert_eq!(dt, Datatype::Time {
1064            size: 8,
1065            bit_precision: 64,
1066        });
1067    }
1068
1069    #[test]
1070    fn test_nested_compound_array_enum() {
1071        // Compound containing a single member "data" which is an Array[2] of Enum(i32, 2 values)
1072        // Build the enum first
1073        let mut enum_bytes = build_dt_header(8, 3, [2, 0, 0], 4); // 2 members
1074        enum_bytes.extend_from_slice(&build_fixed_point(4, false, true, 0, 32)); // base i32
1075        enum_bytes.extend_from_slice(b"A\0");
1076        enum_bytes.extend_from_slice(b"B\0");
1077        enum_bytes.extend_from_slice(&0i32.to_le_bytes());
1078        enum_bytes.extend_from_slice(&1i32.to_le_bytes());
1079
1080        // Build array[2] of that enum, version 3
1081        let mut array_bytes = build_dt_header(10, 3, [0, 0, 0], 8); // 2*4=8
1082        array_bytes.push(1); // ndims=1
1083        array_bytes.extend_from_slice(&2u32.to_le_bytes()); // dim[0]=2
1084        array_bytes.extend_from_slice(&enum_bytes);
1085
1086        // Build compound with 1 member, size=8
1087        let mut buf = build_dt_header(6, 3, [1, 0, 0], 8); // 1 member
1088        buf.extend_from_slice(b"data\0");
1089        buf.push(0); // byte_offset = 0 (size=8, so 1 byte offsets)
1090        buf.extend_from_slice(&array_bytes);
1091
1092        let (dt, _) = Datatype::parse(&buf).unwrap();
1093        match dt {
1094            Datatype::Compound { members, .. } => {
1095                assert_eq!(members.len(), 1);
1096                assert_eq!(members[0].name, "data");
1097                match &members[0].datatype {
1098                    Datatype::Array { dimensions, base_type } => {
1099                        assert_eq!(dimensions, &[2]);
1100                        match base_type.as_ref() {
1101                            Datatype::Enumeration { members, .. } => {
1102                                assert_eq!(members.len(), 2);
1103                                assert_eq!(members[0].name, "A");
1104                                assert_eq!(members[1].name, "B");
1105                            }
1106                            other => panic!("expected Enum, got {other:?}"),
1107                        }
1108                    }
1109                    other => panic!("expected Array, got {other:?}"),
1110                }
1111            }
1112            _ => panic!("expected Compound"),
1113        }
1114    }
1115
1116    #[test]
1117    fn test_error_invalid_class() {
1118        let buf = build_dt_header(13, 1, [0, 0, 0], 4);
1119        let err = Datatype::parse(&buf).unwrap_err();
1120        assert_eq!(err, FormatError::InvalidDatatypeClass(13));
1121    }
1122
1123    #[test]
1124    fn test_error_truncated_data() {
1125        let buf = [0u8; 4]; // too short for header
1126        let err = Datatype::parse(&buf).unwrap_err();
1127        match err {
1128            FormatError::UnexpectedEof { .. } => {}
1129            other => panic!("expected UnexpectedEof, got {other:?}"),
1130        }
1131    }
1132
1133    #[test]
1134    fn test_error_invalid_string_padding() {
1135        let buf = build_dt_header(3, 1, [0x03, 0, 0], 10); // padding=3 invalid
1136        let err = Datatype::parse(&buf).unwrap_err();
1137        assert_eq!(err, FormatError::InvalidStringPadding(3));
1138    }
1139
1140    #[test]
1141    fn test_error_invalid_charset() {
1142        let buf = build_dt_header(3, 1, [0x20, 0, 0], 10); // charset=2 invalid
1143        let err = Datatype::parse(&buf).unwrap_err();
1144        assert_eq!(err, FormatError::InvalidCharacterSet(2));
1145    }
1146
1147    #[test]
1148    fn test_error_invalid_reference_type() {
1149        let buf = build_dt_header(7, 1, [5, 0, 0], 8);
1150        let err = Datatype::parse(&buf).unwrap_err();
1151        assert_eq!(err, FormatError::InvalidReferenceType(5));
1152    }
1153
1154    #[test]
1155    fn serialize_parse_compound_roundtrip() {
1156        let dt = Datatype::Compound {
1157            size: 20,
1158            members: vec![
1159                CompoundMember {
1160                    name: "x".to_string(),
1161                    byte_offset: 0,
1162                    datatype: Datatype::FloatingPoint {
1163                        size: 8, byte_order: DatatypeByteOrder::LittleEndian,
1164                        bit_offset: 0, bit_precision: 64,
1165                        exponent_location: 52, exponent_size: 11,
1166                        mantissa_location: 0, mantissa_size: 52, exponent_bias: 1023,
1167                    },
1168                },
1169                CompoundMember {
1170                    name: "y".to_string(),
1171                    byte_offset: 8,
1172                    datatype: Datatype::FloatingPoint {
1173                        size: 8, byte_order: DatatypeByteOrder::LittleEndian,
1174                        bit_offset: 0, bit_precision: 64,
1175                        exponent_location: 52, exponent_size: 11,
1176                        mantissa_location: 0, mantissa_size: 52, exponent_bias: 1023,
1177                    },
1178                },
1179                CompoundMember {
1180                    name: "id".to_string(),
1181                    byte_offset: 16,
1182                    datatype: Datatype::FixedPoint {
1183                        size: 4, byte_order: DatatypeByteOrder::LittleEndian,
1184                        signed: true, bit_offset: 0, bit_precision: 32,
1185                    },
1186                },
1187            ],
1188        };
1189        let bytes = dt.serialize();
1190        let (parsed, _) = Datatype::parse(&bytes).unwrap();
1191        assert_eq!(parsed, dt);
1192    }
1193
1194    #[test]
1195    fn serialize_parse_enum_roundtrip() {
1196        let dt = Datatype::Enumeration {
1197            size: 4,
1198            base_type: Box::new(Datatype::FixedPoint {
1199                size: 4, byte_order: DatatypeByteOrder::LittleEndian,
1200                signed: true, bit_offset: 0, bit_precision: 32,
1201            }),
1202            members: vec![
1203                EnumMember { name: "RED".to_string(), value: 0i32.to_le_bytes().to_vec() },
1204                EnumMember { name: "GREEN".to_string(), value: 1i32.to_le_bytes().to_vec() },
1205                EnumMember { name: "BLUE".to_string(), value: 2i32.to_le_bytes().to_vec() },
1206            ],
1207        };
1208        let bytes = dt.serialize();
1209        let (parsed, _) = Datatype::parse(&bytes).unwrap();
1210        assert_eq!(parsed, dt);
1211    }
1212
1213    #[test]
1214    fn serialize_parse_array_roundtrip() {
1215        let dt = Datatype::Array {
1216            base_type: Box::new(Datatype::FloatingPoint {
1217                size: 8, byte_order: DatatypeByteOrder::LittleEndian,
1218                bit_offset: 0, bit_precision: 64,
1219                exponent_location: 52, exponent_size: 11,
1220                mantissa_location: 0, mantissa_size: 52, exponent_bias: 1023,
1221            }),
1222            dimensions: vec![3],
1223        };
1224        let bytes = dt.serialize();
1225        let (parsed, _) = Datatype::parse(&bytes).unwrap();
1226        assert_eq!(parsed, dt);
1227    }
1228
1229    #[test]
1230    fn test_type_size() {
1231        let dt = Datatype::FixedPoint {
1232            size: 4, byte_order: DatatypeByteOrder::LittleEndian,
1233            signed: true, bit_offset: 0, bit_precision: 32,
1234        };
1235        assert_eq!(dt.type_size(), 4);
1236
1237        let dt = Datatype::Array {
1238            base_type: Box::new(Datatype::FixedPoint {
1239                size: 4, byte_order: DatatypeByteOrder::LittleEndian,
1240                signed: true, bit_offset: 0, bit_precision: 32,
1241            }),
1242            dimensions: vec![3, 4],
1243        };
1244        assert_eq!(dt.type_size(), 48);
1245    }
1246}