Skip to main content

zerodds_amqp_bridge/
types.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! AMQP 1.0 Type System — Spec `amqp-1.0-types`.
5//!
6//! Implementiert die Format-Code-Constructors fuer Primitive-Types
7//! und Variable-Width-Encodings. Compound-Types (list/map) werden mit
8//! Length-Prefix-Validation unterstuetzt; die innere Struktur ist
9//! Caller-Layer (Caller serialisiert Elemente und uebergibt
10//! Element-Count + Element-Bytes).
11
12use alloc::string::String;
13use alloc::vec::Vec;
14use core::fmt;
15
16/// AMQP 1.0 Format Codes — Spec §1.2 Table 1-1.
17///
18/// Wir geben die wichtigsten benannten Konstanten + Kategorie-Helpers.
19pub mod codes {
20    /// `0x40` — null (fixed-0).
21    pub const NULL: u8 = 0x40;
22    /// `0x41` — boolean true (fixed-0).
23    pub const BOOLEAN_TRUE: u8 = 0x41;
24    /// `0x42` — boolean false (fixed-0).
25    pub const BOOLEAN_FALSE: u8 = 0x42;
26    /// `0x56` — boolean (fixed-1, 0x00=false, 0x01=true).
27    pub const BOOLEAN: u8 = 0x56;
28    /// `0x50` — ubyte (fixed-1).
29    pub const UBYTE: u8 = 0x50;
30    /// `0x60` — ushort (fixed-2, BE).
31    pub const USHORT: u8 = 0x60;
32    /// `0x70` — uint (fixed-4, BE).
33    pub const UINT: u8 = 0x70;
34    /// `0x52` — smalluint (fixed-1).
35    pub const SMALLUINT: u8 = 0x52;
36    /// `0x43` — uint0 (fixed-0, value=0).
37    pub const UINT0: u8 = 0x43;
38    /// `0x80` — ulong (fixed-8, BE).
39    pub const ULONG: u8 = 0x80;
40    /// `0x53` — smallulong (fixed-1).
41    pub const SMALLULONG: u8 = 0x53;
42    /// `0x44` — ulong0 (fixed-0, value=0).
43    pub const ULONG0: u8 = 0x44;
44    /// `0x51` — byte (fixed-1, signed).
45    pub const BYTE: u8 = 0x51;
46    /// `0x61` — short (fixed-2, BE signed).
47    pub const SHORT: u8 = 0x61;
48    /// `0x71` — int (fixed-4, BE signed).
49    pub const INT: u8 = 0x71;
50    /// `0x54` — smallint (fixed-1, signed).
51    pub const SMALLINT: u8 = 0x54;
52    /// `0x81` — long (fixed-8, BE signed).
53    pub const LONG: u8 = 0x81;
54    /// `0x55` — smalllong (fixed-1, signed).
55    pub const SMALLLONG: u8 = 0x55;
56    /// `0xA0` — vbin8 (1-byte length + bytes).
57    pub const VBIN8: u8 = 0xA0;
58    /// `0xB0` — vbin32 (4-byte BE length + bytes).
59    pub const VBIN32: u8 = 0xB0;
60    /// `0xA1` — str8-utf8 (1-byte length + UTF-8).
61    pub const STR8: u8 = 0xA1;
62    /// `0xB1` — str32-utf8 (4-byte BE length + UTF-8).
63    pub const STR32: u8 = 0xB1;
64    /// `0xA3` — sym8 (1-byte length + ASCII).
65    pub const SYM8: u8 = 0xA3;
66    /// `0xB3` — sym32 (4-byte BE length + ASCII).
67    pub const SYM32: u8 = 0xB3;
68
69    // ------------------------------------------------------------------
70    //  Floating + Char + Timestamp + UUID + Decimal.
71    // ------------------------------------------------------------------
72    /// `0x72` — float (fixed-4, IEEE 754 binary32 BE).
73    pub const FLOAT: u8 = 0x72;
74    /// `0x82` — double (fixed-8, IEEE 754 binary64 BE).
75    pub const DOUBLE: u8 = 0x82;
76    /// `0x73` — char (fixed-4, UTF-32 BE codepoint).
77    pub const CHAR: u8 = 0x73;
78    /// `0x74` — decimal32 (fixed-4, IEEE 754-2008 BID).
79    pub const DECIMAL32: u8 = 0x74;
80    /// `0x84` — decimal64 (fixed-8, IEEE 754-2008 BID).
81    pub const DECIMAL64: u8 = 0x84;
82    /// `0x94` — decimal128 (fixed-16, IEEE 754-2008 BID).
83    pub const DECIMAL128: u8 = 0x94;
84    /// `0x83` — timestamp (fixed-8, BE signed ms since Unix epoch).
85    pub const TIMESTAMP: u8 = 0x83;
86    /// `0x98` — uuid (fixed-16, RFC 4122).
87    pub const UUID: u8 = 0x98;
88
89    // ------------------------------------------------------------------
90    //  Compound (list, map, array).
91    // ------------------------------------------------------------------
92    /// `0x45` — list0 (fixed-0, count=0).
93    pub const LIST0: u8 = 0x45;
94    /// `0xC0` — list8 (1-byte size + 1-byte count + items).
95    pub const LIST8: u8 = 0xC0;
96    /// `0xD0` — list32 (4-byte BE size + 4-byte BE count + items).
97    pub const LIST32: u8 = 0xD0;
98    /// `0xC1` — map8 (1-byte size + 1-byte count + entries).
99    pub const MAP8: u8 = 0xC1;
100    /// `0xD1` — map32 (4-byte BE size + 4-byte BE count + entries).
101    pub const MAP32: u8 = 0xD1;
102    /// `0xE0` — array8 (1-byte size + 1-byte count + element-constructor + items).
103    pub const ARRAY8: u8 = 0xE0;
104    /// `0xF0` — array32 (4-byte BE size + 4-byte BE count + element-constructor + items).
105    pub const ARRAY32: u8 = 0xF0;
106}
107
108/// Format-Code-Kategorie (Spec §1.2).
109#[derive(Debug, Clone, Copy, PartialEq, Eq)]
110pub enum FormatCode {
111    /// Spec §1.2.1 — fixed-width: 0..=8 octets payload, kategorie 0x4*-0x9*.
112    Fixed(u8),
113    /// Spec §1.2.2 — variable-width: 1- oder 4-byte length + bytes,
114    /// kategorie 0xA*/0xB*.
115    Variable(u8),
116    /// Spec §1.2.3 — compound: 1- oder 4-byte size + count + items,
117    /// kategorie 0xC*/0xD*.
118    Compound(u8),
119    /// Spec §1.2.4 — array: 1- oder 4-byte size + count + element-
120    /// constructor + items, kategorie 0xE*/0xF*.
121    Array(u8),
122}
123
124impl FormatCode {
125    /// Bestimmt Kategorie aus dem Format-Code-Byte (Spec §1.2 Table 1-1
126    /// Subcategory column).
127    #[must_use]
128    pub const fn from_byte(b: u8) -> Self {
129        match b >> 4 {
130            0x4..=0x9 => Self::Fixed(b),
131            0xA | 0xB => Self::Variable(b),
132            0xC | 0xD => Self::Compound(b),
133            0xE | 0xF => Self::Array(b),
134            _ => Self::Fixed(b),
135        }
136    }
137}
138
139/// AMQP-Wert (Subset der Primitive-Types).
140#[derive(Debug, Clone, PartialEq, Eq)]
141pub enum AmqpValue {
142    /// `null`.
143    Null,
144    /// `boolean`.
145    Boolean(bool),
146    /// `ulong` (oder smallulong/ulong0 je nach Wert).
147    Ulong(u64),
148    /// `long`.
149    Long(i64),
150    /// `binary` (max u32::MAX-Bytes).
151    Binary(Vec<u8>),
152    /// `string` (UTF-8).
153    String(String),
154    /// `symbol` (ASCII).
155    Symbol(String),
156}
157
158/// Type-Codec-Fehler.
159#[derive(Debug, Clone, PartialEq, Eq)]
160pub enum TypeError {
161    /// Input bytes truncated.
162    Truncated,
163    /// Unknown / unsupported Format-Code.
164    UnsupportedFormatCode(u8),
165    /// String hat invalides UTF-8.
166    InvalidUtf8,
167    /// Symbol enthaelt non-ASCII (Spec §1.6.21: "Symbols are encoded
168    /// as ASCII").
169    NonAsciiSymbol,
170    /// Length-Praefix > u32::MAX (geht nicht ueber Wire).
171    LengthTooLarge,
172}
173
174impl fmt::Display for TypeError {
175    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
176        match self {
177            Self::Truncated => f.write_str("input truncated"),
178            Self::UnsupportedFormatCode(c) => write!(f, "unsupported format code 0x{c:02X}"),
179            Self::InvalidUtf8 => f.write_str("invalid UTF-8 in str8/str32"),
180            Self::NonAsciiSymbol => f.write_str("non-ASCII byte in symbol"),
181            Self::LengthTooLarge => f.write_str("length exceeds u32::MAX"),
182        }
183    }
184}
185
186#[cfg(feature = "std")]
187impl std::error::Error for TypeError {}
188
189/// Spec §1.6.1 — `null` = 0x40.
190#[must_use]
191pub fn encode_null() -> Vec<u8> {
192    alloc::vec![codes::NULL]
193}
194
195/// Spec §1.6.2 — `boolean`. Wir nutzen die kompakte Form 0x41/0x42
196/// (fixed-0).
197#[must_use]
198pub fn encode_boolean(v: bool) -> Vec<u8> {
199    alloc::vec![if v {
200        codes::BOOLEAN_TRUE
201    } else {
202        codes::BOOLEAN_FALSE
203    }]
204}
205
206/// Spec §1.6.6 — `ulong`. Wir waehlen die kompakteste Form (ulong0
207/// fuer 0, smallulong fuer 1..=255, ulong sonst).
208#[must_use]
209pub fn encode_ulong(v: u64) -> Vec<u8> {
210    if v == 0 {
211        alloc::vec![codes::ULONG0]
212    } else if v <= u64::from(u8::MAX) {
213        let b = (v & 0xFF) as u8;
214        alloc::vec![codes::SMALLULONG, b]
215    } else {
216        let mut out = Vec::with_capacity(9);
217        out.push(codes::ULONG);
218        out.extend_from_slice(&v.to_be_bytes());
219        out
220    }
221}
222
223/// Spec §1.6.10 — `long`. Wir waehlen smalllong fuer -128..=127, long
224/// (full 8-byte) sonst.
225#[must_use]
226pub fn encode_long(v: i64) -> Vec<u8> {
227    if (i64::from(i8::MIN)..=i64::from(i8::MAX)).contains(&v) {
228        let b = (v as i8) as u8;
229        alloc::vec![codes::SMALLLONG, b]
230    } else {
231        let mut out = Vec::with_capacity(9);
232        out.push(codes::LONG);
233        out.extend_from_slice(&v.to_be_bytes());
234        out
235    }
236}
237
238/// Spec §1.6.19 — `binary`. Waehlt vbin8 fuer len <= 255, vbin32 sonst.
239///
240/// # Errors
241/// `LengthTooLarge` wenn `data.len() > u32::MAX`.
242pub fn encode_binary(data: &[u8]) -> Result<Vec<u8>, TypeError> {
243    let len = data.len();
244    if len > u32::MAX as usize {
245        return Err(TypeError::LengthTooLarge);
246    }
247    if len <= u8::MAX as usize {
248        let mut out = Vec::with_capacity(2 + len);
249        out.push(codes::VBIN8);
250        #[allow(clippy::cast_possible_truncation)]
251        out.push(len as u8);
252        out.extend_from_slice(data);
253        Ok(out)
254    } else {
255        let mut out = Vec::with_capacity(5 + len);
256        out.push(codes::VBIN32);
257        #[allow(clippy::cast_possible_truncation)]
258        out.extend_from_slice(&(len as u32).to_be_bytes());
259        out.extend_from_slice(data);
260        Ok(out)
261    }
262}
263
264/// Spec §1.6.20 — `string`. Waehlt str8 fuer len <= 255, str32 sonst.
265///
266/// # Errors
267/// `LengthTooLarge` wenn `s.len() > u32::MAX`.
268pub fn encode_string(s: &str) -> Result<Vec<u8>, TypeError> {
269    let bytes = s.as_bytes();
270    let len = bytes.len();
271    if len > u32::MAX as usize {
272        return Err(TypeError::LengthTooLarge);
273    }
274    if len <= u8::MAX as usize {
275        let mut out = Vec::with_capacity(2 + len);
276        out.push(codes::STR8);
277        #[allow(clippy::cast_possible_truncation)]
278        out.push(len as u8);
279        out.extend_from_slice(bytes);
280        Ok(out)
281    } else {
282        let mut out = Vec::with_capacity(5 + len);
283        out.push(codes::STR32);
284        #[allow(clippy::cast_possible_truncation)]
285        out.extend_from_slice(&(len as u32).to_be_bytes());
286        out.extend_from_slice(bytes);
287        Ok(out)
288    }
289}
290
291/// Spec §1.6.21 — `symbol`. Waehlt sym8 fuer len <= 255, sym32 sonst.
292///
293/// # Errors
294/// * `NonAsciiSymbol` wenn der String non-ASCII enthaelt.
295/// * `LengthTooLarge` wenn `s.len() > u32::MAX`.
296pub fn encode_symbol(s: &str) -> Result<Vec<u8>, TypeError> {
297    if !s.is_ascii() {
298        return Err(TypeError::NonAsciiSymbol);
299    }
300    let bytes = s.as_bytes();
301    let len = bytes.len();
302    if len > u32::MAX as usize {
303        return Err(TypeError::LengthTooLarge);
304    }
305    if len <= u8::MAX as usize {
306        let mut out = Vec::with_capacity(2 + len);
307        out.push(codes::SYM8);
308        #[allow(clippy::cast_possible_truncation)]
309        out.push(len as u8);
310        out.extend_from_slice(bytes);
311        Ok(out)
312    } else {
313        let mut out = Vec::with_capacity(5 + len);
314        out.push(codes::SYM32);
315        #[allow(clippy::cast_possible_truncation)]
316        out.extend_from_slice(&(len as u32).to_be_bytes());
317        out.extend_from_slice(bytes);
318        Ok(out)
319    }
320}
321
322/// Decodiert einen einzigen `AmqpValue` ab `bytes[0]`. Liefert
323/// `Ok((value, consumed_bytes))`.
324///
325/// # Errors
326/// Siehe [`TypeError`].
327pub fn decode_value(bytes: &[u8]) -> Result<(AmqpValue, usize), TypeError> {
328    if bytes.is_empty() {
329        return Err(TypeError::Truncated);
330    }
331    let code = bytes[0];
332    match code {
333        codes::NULL => Ok((AmqpValue::Null, 1)),
334        codes::BOOLEAN_TRUE => Ok((AmqpValue::Boolean(true), 1)),
335        codes::BOOLEAN_FALSE => Ok((AmqpValue::Boolean(false), 1)),
336        codes::BOOLEAN => {
337            if bytes.len() < 2 {
338                return Err(TypeError::Truncated);
339            }
340            Ok((AmqpValue::Boolean(bytes[1] != 0), 2))
341        }
342        codes::ULONG0 => Ok((AmqpValue::Ulong(0), 1)),
343        codes::SMALLULONG => {
344            if bytes.len() < 2 {
345                return Err(TypeError::Truncated);
346            }
347            Ok((AmqpValue::Ulong(u64::from(bytes[1])), 2))
348        }
349        codes::ULONG => {
350            if bytes.len() < 9 {
351                return Err(TypeError::Truncated);
352            }
353            let mut buf = [0u8; 8];
354            buf.copy_from_slice(&bytes[1..9]);
355            Ok((AmqpValue::Ulong(u64::from_be_bytes(buf)), 9))
356        }
357        codes::SMALLLONG => {
358            if bytes.len() < 2 {
359                return Err(TypeError::Truncated);
360            }
361            #[allow(clippy::cast_possible_wrap)]
362            Ok((AmqpValue::Long(i64::from(bytes[1] as i8)), 2))
363        }
364        codes::LONG => {
365            if bytes.len() < 9 {
366                return Err(TypeError::Truncated);
367            }
368            let mut buf = [0u8; 8];
369            buf.copy_from_slice(&bytes[1..9]);
370            Ok((AmqpValue::Long(i64::from_be_bytes(buf)), 9))
371        }
372        codes::VBIN8 => {
373            if bytes.len() < 2 {
374                return Err(TypeError::Truncated);
375            }
376            let len = usize::from(bytes[1]);
377            if bytes.len() < 2 + len {
378                return Err(TypeError::Truncated);
379            }
380            Ok((AmqpValue::Binary(bytes[2..2 + len].to_vec()), 2 + len))
381        }
382        codes::VBIN32 => {
383            if bytes.len() < 5 {
384                return Err(TypeError::Truncated);
385            }
386            let len = u32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]) as usize;
387            if bytes.len() < 5 + len {
388                return Err(TypeError::Truncated);
389            }
390            Ok((AmqpValue::Binary(bytes[5..5 + len].to_vec()), 5 + len))
391        }
392        codes::STR8 => decode_str8(bytes, AmqpValue::String),
393        codes::STR32 => decode_str32(bytes, AmqpValue::String),
394        codes::SYM8 => decode_str8(bytes, AmqpValue::Symbol),
395        codes::SYM32 => decode_str32(bytes, AmqpValue::Symbol),
396        other => Err(TypeError::UnsupportedFormatCode(other)),
397    }
398}
399
400fn decode_str8(
401    bytes: &[u8],
402    wrap: fn(String) -> AmqpValue,
403) -> Result<(AmqpValue, usize), TypeError> {
404    if bytes.len() < 2 {
405        return Err(TypeError::Truncated);
406    }
407    let len = usize::from(bytes[1]);
408    if bytes.len() < 2 + len {
409        return Err(TypeError::Truncated);
410    }
411    let s = core::str::from_utf8(&bytes[2..2 + len])
412        .map_err(|_| TypeError::InvalidUtf8)?
413        .to_owned();
414    Ok((wrap(s), 2 + len))
415}
416
417fn decode_str32(
418    bytes: &[u8],
419    wrap: fn(String) -> AmqpValue,
420) -> Result<(AmqpValue, usize), TypeError> {
421    if bytes.len() < 5 {
422        return Err(TypeError::Truncated);
423    }
424    let len = u32::from_be_bytes([bytes[1], bytes[2], bytes[3], bytes[4]]) as usize;
425    if bytes.len() < 5 + len {
426        return Err(TypeError::Truncated);
427    }
428    let s = core::str::from_utf8(&bytes[5..5 + len])
429        .map_err(|_| TypeError::InvalidUtf8)?
430        .to_owned();
431    Ok((wrap(s), 5 + len))
432}
433
434#[cfg(test)]
435#[allow(clippy::expect_used, clippy::panic)]
436mod tests {
437    use super::*;
438
439    #[test]
440    fn null_encodes_to_single_byte_0x40() {
441        // Spec §1.6.1.
442        assert_eq!(encode_null(), alloc::vec![0x40]);
443    }
444
445    #[test]
446    fn boolean_uses_compact_format_codes() {
447        // Spec §1.6.2 — 0x41 true, 0x42 false.
448        assert_eq!(encode_boolean(true), alloc::vec![0x41]);
449        assert_eq!(encode_boolean(false), alloc::vec![0x42]);
450    }
451
452    #[test]
453    fn ulong_zero_uses_ulong0_format() {
454        // Spec §1.6.6.
455        assert_eq!(encode_ulong(0), alloc::vec![0x44]);
456    }
457
458    #[test]
459    fn ulong_small_uses_smallulong_format() {
460        // Spec §1.6.6 — smallulong = 0x53 + 1 byte.
461        assert_eq!(encode_ulong(255), alloc::vec![0x53, 0xFF]);
462    }
463
464    #[test]
465    fn ulong_large_uses_full_8_byte_format() {
466        // Spec §1.6.6 — ulong = 0x80 + 8 bytes BE.
467        let bytes = encode_ulong(0x1122_3344_5566_7788);
468        assert_eq!(bytes[0], 0x80);
469        assert_eq!(&bytes[1..], &0x1122_3344_5566_7788_u64.to_be_bytes());
470    }
471
472    #[test]
473    fn long_small_uses_smalllong_format() {
474        // Spec §1.6.10 — smalllong = 0x55 + 1 byte (signed).
475        assert_eq!(encode_long(-1), alloc::vec![0x55, 0xFF]);
476        assert_eq!(encode_long(127), alloc::vec![0x55, 0x7F]);
477    }
478
479    #[test]
480    fn long_large_uses_full_8_byte_format() {
481        let bytes = encode_long(i64::MIN);
482        assert_eq!(bytes[0], 0x81);
483    }
484
485    #[test]
486    fn binary_short_uses_vbin8_format() {
487        // Spec §1.6.19 — vbin8 = 0xA0 + 1 byte len.
488        let bytes = encode_binary(&[1, 2, 3]).expect("encode");
489        assert_eq!(bytes, alloc::vec![0xA0, 0x03, 1, 2, 3]);
490    }
491
492    #[test]
493    fn binary_long_uses_vbin32_format() {
494        // Spec §1.6.19 — vbin32 = 0xB0 + 4 byte BE len.
495        let data = alloc::vec![0xAA; 300];
496        let bytes = encode_binary(&data).expect("encode");
497        assert_eq!(bytes[0], 0xB0);
498        assert_eq!(&bytes[1..5], &300u32.to_be_bytes());
499    }
500
501    #[test]
502    fn string_short_uses_str8_format() {
503        // Spec §1.6.20 — str8-utf8 = 0xA1 + 1 byte len + UTF-8.
504        let bytes = encode_string("hi").expect("encode");
505        assert_eq!(bytes, alloc::vec![0xA1, 0x02, b'h', b'i']);
506    }
507
508    #[test]
509    fn string_unicode_round_trip() {
510        let bytes = encode_string("Käfer").expect("encode");
511        let (parsed, consumed) = decode_value(&bytes).expect("decode");
512        assert_eq!(consumed, bytes.len());
513        match parsed {
514            AmqpValue::String(s) => assert_eq!(s, "Käfer"),
515            _ => panic!("expected string"),
516        }
517    }
518
519    #[test]
520    fn symbol_rejects_non_ascii() {
521        // Spec §1.6.21 — Symbols are ASCII.
522        assert_eq!(encode_symbol("Käfer"), Err(TypeError::NonAsciiSymbol));
523    }
524
525    #[test]
526    fn symbol_short_uses_sym8_format() {
527        let bytes = encode_symbol("hello").expect("encode");
528        assert_eq!(bytes[0], 0xA3);
529        assert_eq!(bytes[1], 0x05);
530        assert_eq!(&bytes[2..], b"hello");
531    }
532
533    #[test]
534    fn round_trip_all_primitive_values() {
535        let values = alloc::vec![
536            (encode_null(), AmqpValue::Null),
537            (encode_boolean(true), AmqpValue::Boolean(true)),
538            (encode_boolean(false), AmqpValue::Boolean(false)),
539            (encode_ulong(0), AmqpValue::Ulong(0)),
540            (encode_ulong(42), AmqpValue::Ulong(42)),
541            (
542                encode_ulong(0x1234_5678_9ABC_DEF0),
543                AmqpValue::Ulong(0x1234_5678_9ABC_DEF0)
544            ),
545            (encode_long(-100), AmqpValue::Long(-100)),
546            (encode_long(i64::MIN), AmqpValue::Long(i64::MIN)),
547            (
548                encode_binary(&[1, 2, 3]).expect("ok"),
549                AmqpValue::Binary(alloc::vec![1, 2, 3])
550            ),
551            (
552                encode_binary(&alloc::vec![0u8; 500]).expect("ok"),
553                AmqpValue::Binary(alloc::vec![0u8; 500])
554            ),
555            (
556                encode_string("foo").expect("ok"),
557                AmqpValue::String("foo".into())
558            ),
559            (
560                encode_symbol("bar").expect("ok"),
561                AmqpValue::Symbol("bar".into())
562            ),
563        ];
564        for (bytes, expected) in values {
565            let (parsed, consumed) = decode_value(&bytes).expect("decode");
566            assert_eq!(parsed, expected);
567            assert_eq!(consumed, bytes.len());
568        }
569    }
570
571    #[test]
572    fn unsupported_format_code_yields_error() {
573        // 0xFF ist Reserved/unsupported in unserer Subset-Implementation.
574        assert_eq!(
575            decode_value(&[0xFF]),
576            Err(TypeError::UnsupportedFormatCode(0xFF))
577        );
578    }
579
580    #[test]
581    fn truncated_inputs_yield_error() {
582        assert_eq!(decode_value(&[]), Err(TypeError::Truncated));
583        assert_eq!(decode_value(&[0xA0]), Err(TypeError::Truncated)); // vbin8 ohne len.
584        assert_eq!(decode_value(&[0xA0, 5, 1]), Err(TypeError::Truncated)); // vbin8 truncated body.
585        assert_eq!(decode_value(&[0x80, 0, 0, 0]), Err(TypeError::Truncated)); // ulong truncated.
586    }
587
588    #[test]
589    fn invalid_utf8_in_str_yields_error() {
590        // str8 mit invalid UTF-8 byte 0xFF.
591        assert_eq!(
592            decode_value(&[0xA1, 0x01, 0xFF]),
593            Err(TypeError::InvalidUtf8)
594        );
595    }
596
597    #[test]
598    fn format_code_categorizes_correctly() {
599        // Spec §1.2 Subcategories.
600        assert!(matches!(FormatCode::from_byte(0x40), FormatCode::Fixed(_)));
601        assert!(matches!(FormatCode::from_byte(0x80), FormatCode::Fixed(_)));
602        assert!(matches!(
603            FormatCode::from_byte(0xA0),
604            FormatCode::Variable(_)
605        ));
606        assert!(matches!(
607            FormatCode::from_byte(0xB0),
608            FormatCode::Variable(_)
609        ));
610        assert!(matches!(
611            FormatCode::from_byte(0xC0),
612            FormatCode::Compound(_)
613        ));
614        assert!(matches!(
615            FormatCode::from_byte(0xD0),
616            FormatCode::Compound(_)
617        ));
618        assert!(matches!(FormatCode::from_byte(0xE0), FormatCode::Array(_)));
619        assert!(matches!(FormatCode::from_byte(0xF0), FormatCode::Array(_)));
620    }
621}