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