Skip to main content

zerodds_types/type_identifier/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! XTypes 1.3 TypeIdentifier (Spec §7.3.4.2).
4//!
5//! TypeIdentifier is a CDR union with an `octet` discriminator. It
6//! identifies a type either **directly** (primitives, plain
7//! collections) or **indirectly** via a 14-byte SHA-256 hash of the
8//! serialized TypeObject (EK_MINIMAL / EK_COMPLETE).
9//!
10//! # Wire-Encoding (XCDR2 LE)
11//!
12//! ```text
13//! TypeIdentifier {
14//!     octet _d;            // 1 byte discriminator
15//!     // body depending on _d:
16//!     // TK_NONE..TK_CHAR16:          no body
17//!     // TI_STRING8_SMALL/LE_SMALL:   { octet bound; }
18//!     // TI_STRING8_LARGE/LE_LARGE:   { uint32 bound; }  // 4-byte aligned
19//!     // TI_PLAIN_SEQUENCE_SMALL:     { PlainCollectionHeader; octet bound; @external TypeIdentifier elem; }
20//!     // TI_PLAIN_SEQUENCE_LARGE:     { PlainCollectionHeader; uint32 bound; @external TypeIdentifier elem; }
21//!     // TI_PLAIN_ARRAY_SMALL:        { PlainCollectionHeader; seq<octet,5> dims; @external TypeIdentifier elem; }
22//!     // TI_PLAIN_ARRAY_LARGE:        { PlainCollectionHeader; seq<uint32,5> dims; @external TypeIdentifier elem; }
23//!     // TI_PLAIN_MAP_SMALL:          { PlainCollectionHeader; octet bound;
24//!     //                                @external TypeIdentifier elem;
25//!     //                                CollectionElementFlag key_flags;
26//!     //                                @external TypeIdentifier key; }
27//!     // TI_PLAIN_MAP_LARGE:          { ... uint32 bound ... }
28//!     // TI_STRONGLY_CONNECTED_COMPONENT: { SCC_ID scc_id; }
29//!     // EK_MINIMAL / EK_COMPLETE:    { octet hash[14]; }
30//! }
31//! ```
32
33pub mod kinds;
34
35use alloc::boxed::Box;
36use alloc::vec::Vec;
37
38use zerodds_cdr::{BufferReader, BufferWriter, DecodeError, EncodeError, Endianness};
39
40use self::kinds::{
41    EK_COMPLETE, EK_MINIMAL, EQUIVALENCE_HASH_LEN, TI_PLAIN_ARRAY_LARGE, TI_PLAIN_ARRAY_SMALL,
42    TI_PLAIN_MAP_LARGE, TI_PLAIN_MAP_SMALL, TI_PLAIN_SEQUENCE_LARGE, TI_PLAIN_SEQUENCE_SMALL,
43    TI_STRING8_LARGE, TI_STRING8_SMALL, TI_STRING16_LARGE, TI_STRING16_SMALL,
44    TI_STRONGLY_CONNECTED_COMPONENT, TK_BOOLEAN, TK_BYTE, TK_CHAR8, TK_CHAR16, TK_FLOAT32,
45    TK_FLOAT64, TK_FLOAT128, TK_INT8, TK_INT16, TK_INT32, TK_INT64, TK_NONE, TK_UINT8, TK_UINT16,
46    TK_UINT32, TK_UINT64,
47};
48
49/// 14-byte SHA256 hash of a serialized TypeObject.
50///
51/// Spec §7.3.1.2: the hash is formed from the **first 14 bytes** of the
52/// SHA-256 over the XCDR2-serialized TypeObject.
53#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
54pub struct EquivalenceHash(pub [u8; EQUIVALENCE_HASH_LEN]);
55
56impl EquivalenceHash {
57    /// Null hash (placeholder).
58    pub const ZERO: Self = Self([0; EQUIVALENCE_HASH_LEN]);
59}
60
61/// Collection element flags (§7.3.4.7.1). A 16-bit bitmask with only one
62/// relevant bit (TRY_CONSTRUCT) for TypeIdentifier.
63#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub struct CollectionElementFlag(pub u16);
65
66impl CollectionElementFlag {
67    /// TryConstruct = DISCARD (the default TryConstructKind, §7.3.4.5), set on
68    /// the element of every plain collection by Cyclone + FastDDS
69    /// (byte-verified). Bit position is shared with the member-flag types.
70    pub const TRY_CONSTRUCT1: u16 = 1 << 0;
71
72    /// The element flags as emitted on the wire: a bare DISCARD.
73    #[must_use]
74    pub const fn discard() -> Self {
75        Self(Self::TRY_CONSTRUCT1)
76    }
77}
78
79/// Equivalence kind of the element of a PlainCollection (§7.3.4.7.1).
80///
81/// EK_MINIMAL = the TI is strongly-hashed minimal, EK_COMPLETE =
82/// complete, EK_BOTH = identical for both, `None` = the element is
83/// itself primitive or plain (no strong hash).
84#[derive(Debug, Clone, Copy, PartialEq, Eq)]
85pub enum EquivalenceKind {
86    /// The element is a primitive or otherwise plain — no hash.
87    None,
88    /// The element TypeIdentifier is EK_MINIMAL.
89    Minimal,
90    /// The element TypeIdentifier is EK_COMPLETE.
91    Complete,
92    /// Minimal + complete are the same (e.g. for fully primitive types).
93    Both,
94}
95
96impl EquivalenceKind {
97    /// Encoded as `octet` (§7.3.4.7.1 EquivalenceKind).
98    #[must_use]
99    pub const fn to_u8(self) -> u8 {
100        match self {
101            Self::None => 0,
102            Self::Minimal => EK_MINIMAL,
103            Self::Complete => EK_COMPLETE,
104            Self::Both => 0xF3,
105        }
106    }
107
108    /// Decoder.
109    #[must_use]
110    pub const fn from_u8(v: u8) -> Self {
111        match v {
112            EK_MINIMAL => Self::Minimal,
113            EK_COMPLETE => Self::Complete,
114            0xF3 => Self::Both,
115            _ => Self::None,
116        }
117    }
118}
119
120/// Header for plain collections (§7.3.4.7.1).
121#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
122pub struct PlainCollectionHeader {
123    /// Equivalence kind of the element TypeIdentifier.
124    pub equiv_kind: u8,
125    /// Try-construct flags.
126    pub element_flags: CollectionElementFlag,
127}
128
129impl PlainCollectionHeader {
130    /// Builds the header the way the vendors do (byte-verified against
131    /// Cyclone + FastDDS): the given `equiv_kind` plus a bare DISCARD in the
132    /// element flags. `equiv_kind` is `EK_BOTH` (0xF3) for a fully-descriptive
133    /// element (primitive/string/nested-plain), or `EK_MINIMAL`/`EK_COMPLETE`
134    /// when the element is a strong hash.
135    #[must_use]
136    pub const fn for_element(equiv_kind: u8) -> Self {
137        Self {
138            equiv_kind,
139            element_flags: CollectionElementFlag::discard(),
140        }
141    }
142}
143
144/// Identifies a strongly-connected-component ID for recursive types.
145#[derive(Debug, Clone, Copy, PartialEq, Eq)]
146pub struct StronglyConnectedComponentId {
147    /// 14-byte SHA256 of the SCC.
148    pub hash: EquivalenceHash,
149    /// Component index within the SCC.
150    pub scc_length: i32,
151    /// Index of the type in the SCC.
152    pub scc_index: i32,
153}
154
155/// TypeIdentifier — XTypes §7.3.4.2.
156///
157/// Identifies primitive and plain types directly, composite types
158/// (struct, union, etc.) indirectly (via a 14-byte hash). Plain
159/// collections can recursively contain a TypeIdentifier, hence `Box`
160/// for the nested variants.
161#[derive(Debug, Clone, PartialEq, Eq, Default)]
162#[non_exhaustive]
163pub enum TypeIdentifier {
164    /// TK_NONE — spec sentinel for "no TypeIdentifier known".
165    #[default]
166    None,
167    /// Primitive without a body — the discriminator carries all info.
168    Primitive(PrimitiveKind),
169    /// `string<Bound>` with 8-bit characters, Bound <= 255 bytes.
170    String8Small {
171        /// Max length (0 = unbounded).
172        bound: u8,
173    },
174    /// `string<Bound>` with Bound > 255.
175    String8Large {
176        /// Max length.
177        bound: u32,
178    },
179    /// `wstring<Bound>` with Bound <= 255 chars.
180    String16Small {
181        /// Max length.
182        bound: u8,
183    },
184    /// `wstring<Bound>`.
185    String16Large {
186        /// Max length.
187        bound: u32,
188    },
189    /// `sequence<T, N>` with N <= 255.
190    PlainSequenceSmall {
191        /// Header (EquivKind of the element + flags).
192        header: PlainCollectionHeader,
193        /// Maximum bound (0 = unbounded).
194        bound: u8,
195        /// Element TypeIdentifier.
196        element: Box<TypeIdentifier>,
197    },
198    /// `sequence<T, N>` with N > 255.
199    PlainSequenceLarge {
200        /// Header.
201        header: PlainCollectionHeader,
202        /// Max bound.
203        bound: u32,
204        /// Element TypeIdentifier.
205        element: Box<TypeIdentifier>,
206    },
207    /// `T[D1, D2, ...]` with all dimensions <= 255.
208    PlainArraySmall {
209        /// Header.
210        header: PlainCollectionHeader,
211        /// Array dimensions (max 2^20 total per §7.3.4.7).
212        array_bounds: Vec<u8>,
213        /// Element TypeIdentifier.
214        element: Box<TypeIdentifier>,
215    },
216    /// `T[D1, D2, ...]` with at least one dimension > 255.
217    PlainArrayLarge {
218        /// Header.
219        header: PlainCollectionHeader,
220        /// Array dimensions.
221        array_bounds: Vec<u32>,
222        /// Element TypeIdentifier.
223        element: Box<TypeIdentifier>,
224    },
225    /// `map<K, V, N>` with N <= 255.
226    PlainMapSmall {
227        /// Header.
228        header: PlainCollectionHeader,
229        /// Max size.
230        bound: u8,
231        /// Value TypeIdentifier.
232        element: Box<TypeIdentifier>,
233        /// Key flags.
234        key_flags: CollectionElementFlag,
235        /// Key TypeIdentifier.
236        key: Box<TypeIdentifier>,
237    },
238    /// `map<K, V, N>` with N > 255.
239    PlainMapLarge {
240        /// Header.
241        header: PlainCollectionHeader,
242        /// Max size.
243        bound: u32,
244        /// Value TypeIdentifier.
245        element: Box<TypeIdentifier>,
246        /// Key flags.
247        key_flags: CollectionElementFlag,
248        /// Key TypeIdentifier.
249        key: Box<TypeIdentifier>,
250    },
251    /// Strongly connected component (recursive types) — §7.3.4.9.
252    StronglyConnectedComponent(StronglyConnectedComponentId),
253    /// 14-byte hash of the MinimalTypeObject.
254    EquivalenceHashMinimal(EquivalenceHash),
255    /// 14-byte hash of the CompleteTypeObject.
256    EquivalenceHashComplete(EquivalenceHash),
257    /// Unknown/unsupported discriminator (forward-compat).
258    Unknown(u8),
259}
260
261/// Primitive kind (no body in the TypeIdentifier, only the discriminator).
262#[derive(Debug, Clone, Copy, PartialEq, Eq)]
263pub enum PrimitiveKind {
264    /// `bool`.
265    Boolean,
266    /// `octet` / `byte`.
267    Byte,
268    /// `int8`.
269    Int8,
270    /// `int16`.
271    Int16,
272    /// `int32`.
273    Int32,
274    /// `int64`.
275    Int64,
276    /// `uint8`.
277    UInt8,
278    /// `uint16`.
279    UInt16,
280    /// `uint32`.
281    UInt32,
282    /// `uint64`.
283    UInt64,
284    /// `float32`.
285    Float32,
286    /// `float64`.
287    Float64,
288    /// `float128`.
289    Float128,
290    /// `char` (8-bit).
291    Char8,
292    /// `wchar` (16-bit).
293    Char16,
294}
295
296impl PrimitiveKind {
297    /// Diskriminator-Byte im TypeIdentifier.
298    #[must_use]
299    pub const fn to_u8(self) -> u8 {
300        match self {
301            Self::Boolean => TK_BOOLEAN,
302            Self::Byte => TK_BYTE,
303            Self::Int8 => TK_INT8,
304            Self::Int16 => TK_INT16,
305            Self::Int32 => TK_INT32,
306            Self::Int64 => TK_INT64,
307            Self::UInt8 => TK_UINT8,
308            Self::UInt16 => TK_UINT16,
309            Self::UInt32 => TK_UINT32,
310            Self::UInt64 => TK_UINT64,
311            Self::Float32 => TK_FLOAT32,
312            Self::Float64 => TK_FLOAT64,
313            Self::Float128 => TK_FLOAT128,
314            Self::Char8 => TK_CHAR8,
315            Self::Char16 => TK_CHAR16,
316        }
317    }
318
319    /// Attempts to read a primitive kind from a discriminator byte.
320    #[must_use]
321    pub const fn from_u8(v: u8) -> Option<Self> {
322        Some(match v {
323            TK_BOOLEAN => Self::Boolean,
324            TK_BYTE => Self::Byte,
325            TK_INT8 => Self::Int8,
326            TK_INT16 => Self::Int16,
327            TK_INT32 => Self::Int32,
328            TK_INT64 => Self::Int64,
329            TK_UINT8 => Self::UInt8,
330            TK_UINT16 => Self::UInt16,
331            TK_UINT32 => Self::UInt32,
332            TK_UINT64 => Self::UInt64,
333            TK_FLOAT32 => Self::Float32,
334            TK_FLOAT64 => Self::Float64,
335            TK_FLOAT128 => Self::Float128,
336            TK_CHAR8 => Self::Char8,
337            TK_CHAR16 => Self::Char16,
338            _ => return None,
339        })
340    }
341}
342
343// ============================================================================
344// Wire-Encoding
345// ============================================================================
346
347impl TypeIdentifier {
348    /// Encoded as XCDR2 little-endian bytes (TypeIdentifier without an
349    /// encapsulation header — that is provided by the caller, e.g. the
350    /// TYPE_INFORMATION PID or the TypeLookup RPC).
351    ///
352    /// # Errors
353    /// `EncodeError` on buffer overflow (very unlikely for normal types;
354    /// the 2^32 limit for large kinds).
355    pub fn encode_into(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
356        let d = self.discriminator();
357        w.write_u8(d)?;
358        match self {
359            Self::None | Self::Primitive(_) | Self::Unknown(_) => Ok(()),
360            Self::String8Small { bound } | Self::String16Small { bound } => w.write_u8(*bound),
361            Self::String8Large { bound } | Self::String16Large { bound } => w.write_u32(*bound),
362            Self::PlainSequenceSmall {
363                header,
364                bound,
365                element,
366            } => {
367                encode_collection_header(w, *header)?;
368                w.write_u8(*bound)?;
369                element.encode_into(w)
370            }
371            Self::PlainSequenceLarge {
372                header,
373                bound,
374                element,
375            } => {
376                encode_collection_header(w, *header)?;
377                w.write_u32(*bound)?;
378                element.encode_into(w)
379            }
380            Self::PlainArraySmall {
381                header,
382                array_bounds,
383                element,
384            } => {
385                encode_collection_header(w, *header)?;
386                // sequence<octet> — 4-byte length + bytes (XCDR2 LE).
387                let len = u32::try_from(array_bounds.len()).map_err(|_| {
388                    EncodeError::ValueOutOfRange {
389                        message: "plain array dimensions length exceeds u32::MAX",
390                    }
391                })?;
392                w.write_u32(len)?;
393                w.write_bytes(array_bounds)?;
394                element.encode_into(w)
395            }
396            Self::PlainArrayLarge {
397                header,
398                array_bounds,
399                element,
400            } => {
401                encode_collection_header(w, *header)?;
402                let len = u32::try_from(array_bounds.len()).map_err(|_| {
403                    EncodeError::ValueOutOfRange {
404                        message: "plain array dimensions length exceeds u32::MAX",
405                    }
406                })?;
407                w.write_u32(len)?;
408                for dim in array_bounds {
409                    w.write_u32(*dim)?;
410                }
411                element.encode_into(w)
412            }
413            Self::PlainMapSmall {
414                header,
415                bound,
416                element,
417                key_flags,
418                key,
419            } => {
420                encode_collection_header(w, *header)?;
421                w.write_u8(*bound)?;
422                element.encode_into(w)?;
423                w.write_u16(key_flags.0)?;
424                key.encode_into(w)
425            }
426            Self::PlainMapLarge {
427                header,
428                bound,
429                element,
430                key_flags,
431                key,
432            } => {
433                encode_collection_header(w, *header)?;
434                w.write_u32(*bound)?;
435                element.encode_into(w)?;
436                w.write_u16(key_flags.0)?;
437                key.encode_into(w)
438            }
439            Self::StronglyConnectedComponent(scc) => {
440                // Spec §7.3.4.9: scc_length >= 0, 0 <= scc_index < scc_length.
441                // Prevent negative values + index OoB (would otherwise
442                // leak as a huge u32 through two's complement).
443                if scc.scc_length < 0 || scc.scc_index < 0 || scc.scc_index >= scc.scc_length {
444                    return Err(EncodeError::ValueOutOfRange {
445                        message: "SCC scc_length/scc_index invalid",
446                    });
447                }
448                w.write_bytes(&scc.hash.0)?;
449                w.write_u32(scc.scc_length as u32)?;
450                w.write_u32(scc.scc_index as u32)
451            }
452            Self::EquivalenceHashMinimal(h) | Self::EquivalenceHashComplete(h) => {
453                w.write_bytes(&h.0)
454            }
455        }
456    }
457
458    /// Maximum recursion depth when wire-decoding a nested
459    /// `TypeIdentifier` (plain collections can reference recursively).
460    /// Protects against stack overflow on pathological datagrams.
461    pub const MAX_DECODE_DEPTH: usize = 16;
462
463    /// Decode from XCDR2 little-endian bytes. The reader must be
464    /// positioned at the discriminator. Internal recursion is capped
465    /// (see [`Self::MAX_DECODE_DEPTH`]).
466    ///
467    /// # Errors
468    /// `DecodeError` on buffer underflow, an inconsistent length field
469    /// or when the maximum recursion depth is exceeded.
470    pub fn decode_from(r: &mut BufferReader<'_>) -> Result<Self, DecodeError> {
471        Self::decode_with_depth(r, 0)
472    }
473
474    fn decode_with_depth(r: &mut BufferReader<'_>, depth: usize) -> Result<Self, DecodeError> {
475        if depth >= Self::MAX_DECODE_DEPTH {
476            return Err(DecodeError::LengthExceeded {
477                announced: Self::MAX_DECODE_DEPTH,
478                remaining: 0,
479                offset: r.position(),
480            });
481        }
482        let d = r.read_u8()?;
483        Ok(match d {
484            TK_NONE => Self::None,
485            TK_BOOLEAN | TK_BYTE | TK_INT8 | TK_INT16 | TK_INT32 | TK_INT64 | TK_UINT8
486            | TK_UINT16 | TK_UINT32 | TK_UINT64 | TK_FLOAT32 | TK_FLOAT64 | TK_FLOAT128
487            | TK_CHAR8 | TK_CHAR16 => {
488                // Primitive discriminator → no body. The outer match arm
489                // ensures from_u8 returns Some(...) — if someone extends
490                // the list and forgets from_u8, d falls into the
491                // `other =>` path as Unknown(d).
492                match PrimitiveKind::from_u8(d) {
493                    Some(p) => Self::Primitive(p),
494                    None => Self::Unknown(d),
495                }
496            }
497            TI_STRING8_SMALL => Self::String8Small {
498                bound: r.read_u8()?,
499            },
500            TI_STRING8_LARGE => Self::String8Large {
501                bound: r.read_u32()?,
502            },
503            TI_STRING16_SMALL => Self::String16Small {
504                bound: r.read_u8()?,
505            },
506            TI_STRING16_LARGE => Self::String16Large {
507                bound: r.read_u32()?,
508            },
509            TI_PLAIN_SEQUENCE_SMALL => {
510                let header = decode_collection_header(r)?;
511                let bound = r.read_u8()?;
512                let element = Box::new(Self::decode_with_depth(r, depth + 1)?);
513                Self::PlainSequenceSmall {
514                    header,
515                    bound,
516                    element,
517                }
518            }
519            TI_PLAIN_SEQUENCE_LARGE => {
520                let header = decode_collection_header(r)?;
521                let bound = r.read_u32()?;
522                let element = Box::new(Self::decode_with_depth(r, depth + 1)?);
523                Self::PlainSequenceLarge {
524                    header,
525                    bound,
526                    element,
527                }
528            }
529            TI_PLAIN_ARRAY_SMALL => {
530                let header = decode_collection_header(r)?;
531                let n = r.read_u32()? as usize;
532                let array_bounds = r.read_bytes(n)?.to_vec();
533                let element = Box::new(Self::decode_with_depth(r, depth + 1)?);
534                Self::PlainArraySmall {
535                    header,
536                    array_bounds,
537                    element,
538                }
539            }
540            TI_PLAIN_ARRAY_LARGE => {
541                let header = decode_collection_header(r)?;
542                let n = r.read_u32()? as usize;
543                let cap = crate::type_object::common::safe_capacity(n, 4, r.remaining());
544                let mut array_bounds = Vec::with_capacity(cap);
545                for _ in 0..n {
546                    array_bounds.push(r.read_u32()?);
547                }
548                let element = Box::new(Self::decode_with_depth(r, depth + 1)?);
549                Self::PlainArrayLarge {
550                    header,
551                    array_bounds,
552                    element,
553                }
554            }
555            TI_PLAIN_MAP_SMALL => {
556                let header = decode_collection_header(r)?;
557                let bound = r.read_u8()?;
558                let element = Box::new(Self::decode_with_depth(r, depth + 1)?);
559                let key_flags = CollectionElementFlag(r.read_u16()?);
560                let key = Box::new(Self::decode_with_depth(r, depth + 1)?);
561                Self::PlainMapSmall {
562                    header,
563                    bound,
564                    element,
565                    key_flags,
566                    key,
567                }
568            }
569            TI_PLAIN_MAP_LARGE => {
570                let header = decode_collection_header(r)?;
571                let bound = r.read_u32()?;
572                let element = Box::new(Self::decode_with_depth(r, depth + 1)?);
573                let key_flags = CollectionElementFlag(r.read_u16()?);
574                let key = Box::new(Self::decode_with_depth(r, depth + 1)?);
575                Self::PlainMapLarge {
576                    header,
577                    bound,
578                    element,
579                    key_flags,
580                    key,
581                }
582            }
583            TI_STRONGLY_CONNECTED_COMPONENT => {
584                let hash_bytes = r.read_bytes(EQUIVALENCE_HASH_LEN)?;
585                let Ok(h): Result<[u8; EQUIVALENCE_HASH_LEN], _> = hash_bytes.try_into() else {
586                    return Err(DecodeError::UnexpectedEof {
587                        needed: EQUIVALENCE_HASH_LEN,
588                        offset: 0,
589                    });
590                };
591                let scc_length = r.read_u32()? as i32;
592                let scc_index = r.read_u32()? as i32;
593                Self::StronglyConnectedComponent(StronglyConnectedComponentId {
594                    hash: EquivalenceHash(h),
595                    scc_length,
596                    scc_index,
597                })
598            }
599            EK_MINIMAL | EK_COMPLETE => {
600                let hash_bytes = r.read_bytes(EQUIVALENCE_HASH_LEN)?;
601                let Ok(h): Result<[u8; EQUIVALENCE_HASH_LEN], _> = hash_bytes.try_into() else {
602                    return Err(DecodeError::UnexpectedEof {
603                        needed: EQUIVALENCE_HASH_LEN,
604                        offset: 0,
605                    });
606                };
607                if d == EK_MINIMAL {
608                    Self::EquivalenceHashMinimal(EquivalenceHash(h))
609                } else {
610                    Self::EquivalenceHashComplete(EquivalenceHash(h))
611                }
612            }
613            other => Self::Unknown(other),
614        })
615    }
616
617    /// The discriminator byte for this TypeIdentifier.
618    #[must_use]
619    pub const fn discriminator(&self) -> u8 {
620        match self {
621            Self::None => TK_NONE,
622            Self::Primitive(p) => p.to_u8(),
623            Self::String8Small { .. } => TI_STRING8_SMALL,
624            Self::String8Large { .. } => TI_STRING8_LARGE,
625            Self::String16Small { .. } => TI_STRING16_SMALL,
626            Self::String16Large { .. } => TI_STRING16_LARGE,
627            Self::PlainSequenceSmall { .. } => TI_PLAIN_SEQUENCE_SMALL,
628            Self::PlainSequenceLarge { .. } => TI_PLAIN_SEQUENCE_LARGE,
629            Self::PlainArraySmall { .. } => TI_PLAIN_ARRAY_SMALL,
630            Self::PlainArrayLarge { .. } => TI_PLAIN_ARRAY_LARGE,
631            Self::PlainMapSmall { .. } => TI_PLAIN_MAP_SMALL,
632            Self::PlainMapLarge { .. } => TI_PLAIN_MAP_LARGE,
633            Self::StronglyConnectedComponent(_) => TI_STRONGLY_CONNECTED_COMPONENT,
634            Self::EquivalenceHashMinimal(_) => EK_MINIMAL,
635            Self::EquivalenceHashComplete(_) => EK_COMPLETE,
636            Self::Unknown(d) => *d,
637        }
638    }
639
640    /// Kurzform: encode in neuen BufferWriter, LE.
641    ///
642    /// # Errors
643    /// `EncodeError` on overflow.
644    pub fn to_bytes_le(&self) -> Result<Vec<u8>, EncodeError> {
645        let mut w = BufferWriter::new(Endianness::Little);
646        self.encode_into(&mut w)?;
647        Ok(w.into_bytes())
648    }
649
650    /// Short form: decode from LE bytes.
651    ///
652    /// # Errors
653    /// `DecodeError`.
654    pub fn from_bytes_le(bytes: &[u8]) -> Result<Self, DecodeError> {
655        let mut r = BufferReader::new(bytes, Endianness::Little);
656        Self::decode_from(&mut r)
657    }
658}
659
660fn encode_collection_header(
661    w: &mut BufferWriter,
662    header: PlainCollectionHeader,
663) -> Result<(), EncodeError> {
664    w.write_u8(header.equiv_kind)?;
665    w.write_u16(header.element_flags.0)
666}
667
668fn decode_collection_header(
669    r: &mut BufferReader<'_>,
670) -> Result<PlainCollectionHeader, DecodeError> {
671    let equiv_kind = r.read_u8()?;
672    let element_flags = CollectionElementFlag(r.read_u16()?);
673    Ok(PlainCollectionHeader {
674        equiv_kind,
675        element_flags,
676    })
677}
678
679// ============================================================================
680// Tests
681// ============================================================================
682
683#[cfg(test)]
684#[allow(clippy::unwrap_used, clippy::panic)]
685mod tests {
686    use super::*;
687
688    fn roundtrip(ti: TypeIdentifier) {
689        let bytes = ti.to_bytes_le().unwrap();
690        let decoded = TypeIdentifier::from_bytes_le(&bytes).unwrap();
691        assert_eq!(ti, decoded);
692    }
693
694    #[test]
695    fn primitive_none_roundtrips() {
696        roundtrip(TypeIdentifier::None);
697    }
698
699    #[test]
700    fn all_primitives_roundtrip() {
701        for p in [
702            PrimitiveKind::Boolean,
703            PrimitiveKind::Byte,
704            PrimitiveKind::Int8,
705            PrimitiveKind::Int16,
706            PrimitiveKind::Int32,
707            PrimitiveKind::Int64,
708            PrimitiveKind::UInt8,
709            PrimitiveKind::UInt16,
710            PrimitiveKind::UInt32,
711            PrimitiveKind::UInt64,
712            PrimitiveKind::Float32,
713            PrimitiveKind::Float64,
714            PrimitiveKind::Float128,
715            PrimitiveKind::Char8,
716            PrimitiveKind::Char16,
717        ] {
718            roundtrip(TypeIdentifier::Primitive(p));
719        }
720    }
721
722    #[test]
723    fn primitive_int32_discriminator_is_spec_value() {
724        let ti = TypeIdentifier::Primitive(PrimitiveKind::Int32);
725        let bytes = ti.to_bytes_le().unwrap();
726        assert_eq!(bytes, [TK_INT32]);
727    }
728
729    #[test]
730    fn string8_small_roundtrips() {
731        roundtrip(TypeIdentifier::String8Small { bound: 64 });
732        roundtrip(TypeIdentifier::String8Small { bound: 0 }); // unbounded
733    }
734
735    #[test]
736    fn string8_large_roundtrips() {
737        roundtrip(TypeIdentifier::String8Large { bound: 65_536 });
738    }
739
740    #[test]
741    fn string16_small_and_large_roundtrip() {
742        roundtrip(TypeIdentifier::String16Small { bound: 32 });
743        roundtrip(TypeIdentifier::String16Large { bound: 100_000 });
744    }
745
746    #[test]
747    fn plain_sequence_of_int32_roundtrips() {
748        let ti = TypeIdentifier::PlainSequenceSmall {
749            header: PlainCollectionHeader {
750                equiv_kind: 0,
751                element_flags: CollectionElementFlag(0),
752            },
753            bound: 10,
754            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
755        };
756        roundtrip(ti);
757    }
758
759    #[test]
760    fn plain_sequence_large_of_string_roundtrips() {
761        let ti = TypeIdentifier::PlainSequenceLarge {
762            header: PlainCollectionHeader {
763                equiv_kind: 0,
764                element_flags: CollectionElementFlag(0),
765            },
766            bound: 1_000_000,
767            element: Box::new(TypeIdentifier::String8Small { bound: 255 }),
768        };
769        roundtrip(ti);
770    }
771
772    #[test]
773    fn plain_array_small_3d_roundtrips() {
774        let ti = TypeIdentifier::PlainArraySmall {
775            header: PlainCollectionHeader::default(),
776            array_bounds: alloc::vec![3, 4, 5],
777            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Float64)),
778        };
779        roundtrip(ti);
780    }
781
782    #[test]
783    fn plain_array_large_roundtrips() {
784        let ti = TypeIdentifier::PlainArrayLarge {
785            header: PlainCollectionHeader::default(),
786            array_bounds: alloc::vec![1_000, 2_000],
787            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Byte)),
788        };
789        roundtrip(ti);
790    }
791
792    #[test]
793    fn plain_map_small_roundtrips() {
794        let ti = TypeIdentifier::PlainMapSmall {
795            header: PlainCollectionHeader::default(),
796            bound: 100,
797            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int64)),
798            key_flags: CollectionElementFlag(0),
799            key: Box::new(TypeIdentifier::String8Small { bound: 64 }),
800        };
801        roundtrip(ti);
802    }
803
804    #[test]
805    fn equivalence_hash_minimal_roundtrips() {
806        let hash = EquivalenceHash([
807            0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e,
808        ]);
809        roundtrip(TypeIdentifier::EquivalenceHashMinimal(hash));
810        roundtrip(TypeIdentifier::EquivalenceHashComplete(hash));
811    }
812
813    #[test]
814    fn equivalence_hash_wire_is_discriminator_plus_14_bytes() {
815        let hash = EquivalenceHash([0xAA; 14]);
816        let bytes = TypeIdentifier::EquivalenceHashMinimal(hash)
817            .to_bytes_le()
818            .unwrap();
819        assert_eq!(bytes.len(), 15);
820        assert_eq!(bytes[0], EK_MINIMAL);
821        assert_eq!(&bytes[1..], &[0xAA; 14]);
822    }
823
824    #[test]
825    fn nested_sequence_of_sequence_roundtrips() {
826        let inner = TypeIdentifier::PlainSequenceSmall {
827            header: PlainCollectionHeader::default(),
828            bound: 5,
829            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int16)),
830        };
831        let outer = TypeIdentifier::PlainSequenceSmall {
832            header: PlainCollectionHeader::default(),
833            bound: 3,
834            element: Box::new(inner),
835        };
836        roundtrip(outer);
837    }
838
839    #[test]
840    fn strongly_connected_component_roundtrips() {
841        let scc = TypeIdentifier::StronglyConnectedComponent(StronglyConnectedComponentId {
842            hash: EquivalenceHash([0x11; 14]),
843            scc_length: 5,
844            scc_index: 2,
845        });
846        roundtrip(scc);
847    }
848
849    #[test]
850    fn scc_encode_rejects_negative_values() {
851        let scc = TypeIdentifier::StronglyConnectedComponent(StronglyConnectedComponentId {
852            hash: EquivalenceHash::ZERO,
853            scc_length: -1,
854            scc_index: 0,
855        });
856        assert!(scc.to_bytes_le().is_err());
857    }
858
859    #[test]
860    fn scc_encode_rejects_index_out_of_bounds() {
861        let scc = TypeIdentifier::StronglyConnectedComponent(StronglyConnectedComponentId {
862            hash: EquivalenceHash::ZERO,
863            scc_length: 3,
864            scc_index: 3, // must be < 3
865        });
866        assert!(scc.to_bytes_le().is_err());
867    }
868
869    #[test]
870    fn max_decode_depth_constant_is_reasonable() {
871        // DoS guard: recursion when wire-decoding a nested
872        // `TypeIdentifier` is bounded. The cap must be > 4 (for
873        // realistic nested sequences) and < 64 (DoS protection).
874        const _ASSERT_MIN: usize = TypeIdentifier::MAX_DECODE_DEPTH - 4;
875        const _ASSERT_MAX: usize = 64 - TypeIdentifier::MAX_DECODE_DEPTH;
876    }
877
878    #[test]
879    fn deeply_nested_but_bounded_sequence_decodes_ok() {
880        // 3-deep sequence<sequence<sequence<int32>>> — within the cap.
881        let l1 = TypeIdentifier::PlainSequenceSmall {
882            header: PlainCollectionHeader::default(),
883            bound: 5,
884            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
885        };
886        let l2 = TypeIdentifier::PlainSequenceSmall {
887            header: PlainCollectionHeader::default(),
888            bound: 5,
889            element: Box::new(l1),
890        };
891        let l3 = TypeIdentifier::PlainSequenceSmall {
892            header: PlainCollectionHeader::default(),
893            bound: 5,
894            element: Box::new(l2),
895        };
896        roundtrip(l3);
897    }
898
899    #[test]
900    fn unknown_discriminator_preserved_in_decode() {
901        let bytes = [0xC7];
902        let decoded = TypeIdentifier::from_bytes_le(&bytes).unwrap();
903        assert_eq!(decoded, TypeIdentifier::Unknown(0xC7));
904    }
905
906    // ---- Additional edge-case roundtrips --------------------------------
907
908    #[test]
909    fn plain_sequence_large_with_u32_max_bound_roundtrips() {
910        let ti = TypeIdentifier::PlainSequenceLarge {
911            header: PlainCollectionHeader::default(),
912            bound: u32::MAX,
913            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Byte)),
914        };
915        roundtrip(ti);
916    }
917
918    #[test]
919    fn plain_array_large_with_many_dimensions_roundtrips() {
920        let ti = TypeIdentifier::PlainArrayLarge {
921            header: PlainCollectionHeader::default(),
922            array_bounds: alloc::vec![
923                1_000, 2_000, 3_000, 4_000, 5_000, 6_000, 7_000, 8_000, 9_000, 10_000, 11_000,
924                12_000,
925            ],
926            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Float32)),
927        };
928        roundtrip(ti);
929    }
930
931    #[test]
932    fn plain_array_small_with_single_dimension_roundtrips() {
933        let ti = TypeIdentifier::PlainArraySmall {
934            header: PlainCollectionHeader::default(),
935            array_bounds: alloc::vec![250],
936            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
937        };
938        roundtrip(ti);
939    }
940
941    #[test]
942    fn plain_map_large_with_nested_map_value_roundtrips() {
943        let inner = TypeIdentifier::PlainMapSmall {
944            header: PlainCollectionHeader::default(),
945            bound: 10,
946            element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
947            key_flags: CollectionElementFlag(0),
948            key: Box::new(TypeIdentifier::String8Small { bound: 8 }),
949        };
950        let outer = TypeIdentifier::PlainMapLarge {
951            header: PlainCollectionHeader::default(),
952            bound: 5_000,
953            element: Box::new(inner),
954            key_flags: CollectionElementFlag(0),
955            key: Box::new(TypeIdentifier::String8Small { bound: 16 }),
956        };
957        roundtrip(outer);
958    }
959
960    #[test]
961    fn strongly_connected_component_large_scc_length_and_index() {
962        let scc = TypeIdentifier::StronglyConnectedComponent(StronglyConnectedComponentId {
963            hash: EquivalenceHash([0x5A; 14]),
964            scc_length: i32::MAX,
965            scc_index: i32::MAX - 1,
966        });
967        roundtrip(scc);
968    }
969
970    #[test]
971    fn unknown_discriminators_cover_multiple_bytes() {
972        // Deliberately discriminator bytes not assigned to any
973        // TK_/TI_/EK_. 0x01 (TK_BOOLEAN), 0x70ff (TI_STRING8_*) etc. are
974        // avoided. 0x12-0x1F, 0x40, 0x50, 0xC7, 0xFE, 0xFF are free.
975        for d in [0x12_u8, 0x1F, 0x40, 0x50, 0xC7, 0xFE, 0xFF] {
976            let decoded = TypeIdentifier::from_bytes_le(&[d]).unwrap();
977            assert_eq!(decoded, TypeIdentifier::Unknown(d));
978            // Encode should round-trip back to the same discriminator byte.
979            let re = decoded.to_bytes_le().unwrap();
980            assert_eq!(re, alloc::vec![d]);
981        }
982    }
983
984    #[test]
985    fn encode_first_byte_is_always_discriminator() {
986        let samples: alloc::vec::Vec<TypeIdentifier> = alloc::vec![
987            TypeIdentifier::None,
988            TypeIdentifier::Primitive(PrimitiveKind::Int32),
989            TypeIdentifier::String8Small { bound: 8 },
990            TypeIdentifier::String8Large { bound: 10_000 },
991            TypeIdentifier::String16Small { bound: 8 },
992            TypeIdentifier::String16Large { bound: 10_000 },
993            TypeIdentifier::PlainSequenceSmall {
994                header: PlainCollectionHeader::default(),
995                bound: 1,
996                element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
997            },
998            TypeIdentifier::PlainSequenceLarge {
999                header: PlainCollectionHeader::default(),
1000                bound: 300,
1001                element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
1002            },
1003            TypeIdentifier::PlainArraySmall {
1004                header: PlainCollectionHeader::default(),
1005                array_bounds: alloc::vec![2, 3],
1006                element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
1007            },
1008            TypeIdentifier::PlainArrayLarge {
1009                header: PlainCollectionHeader::default(),
1010                array_bounds: alloc::vec![500, 500],
1011                element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
1012            },
1013            TypeIdentifier::PlainMapSmall {
1014                header: PlainCollectionHeader::default(),
1015                bound: 1,
1016                element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
1017                key_flags: CollectionElementFlag(0),
1018                key: Box::new(TypeIdentifier::String8Small { bound: 1 }),
1019            },
1020            TypeIdentifier::PlainMapLarge {
1021                header: PlainCollectionHeader::default(),
1022                bound: 1_000,
1023                element: Box::new(TypeIdentifier::Primitive(PrimitiveKind::Int32)),
1024                key_flags: CollectionElementFlag(0),
1025                key: Box::new(TypeIdentifier::String8Small { bound: 1 }),
1026            },
1027            TypeIdentifier::StronglyConnectedComponent(StronglyConnectedComponentId {
1028                hash: EquivalenceHash([0; 14]),
1029                scc_length: 1,
1030                scc_index: 0,
1031            }),
1032            TypeIdentifier::EquivalenceHashMinimal(EquivalenceHash([0x11; 14])),
1033            TypeIdentifier::EquivalenceHashComplete(EquivalenceHash([0x22; 14])),
1034            TypeIdentifier::Unknown(0x77),
1035        ];
1036        for ti in samples {
1037            let bytes = ti.to_bytes_le().unwrap();
1038            assert!(!bytes.is_empty());
1039            assert_eq!(bytes[0], ti.discriminator());
1040        }
1041    }
1042
1043    #[test]
1044    fn primitive_kind_from_u8_rejects_unknown() {
1045        assert!(PrimitiveKind::from_u8(0xC7).is_none());
1046        assert!(PrimitiveKind::from_u8(0x00).is_none() || PrimitiveKind::from_u8(0x00).is_some());
1047    }
1048
1049    #[test]
1050    fn primitive_kind_roundtrip_via_u8() {
1051        for p in [
1052            PrimitiveKind::Boolean,
1053            PrimitiveKind::Byte,
1054            PrimitiveKind::Int8,
1055            PrimitiveKind::Int16,
1056            PrimitiveKind::Int32,
1057            PrimitiveKind::Int64,
1058            PrimitiveKind::UInt8,
1059            PrimitiveKind::UInt16,
1060            PrimitiveKind::UInt32,
1061            PrimitiveKind::UInt64,
1062            PrimitiveKind::Float32,
1063            PrimitiveKind::Float64,
1064            PrimitiveKind::Float128,
1065            PrimitiveKind::Char8,
1066            PrimitiveKind::Char16,
1067        ] {
1068            assert_eq!(PrimitiveKind::from_u8(p.to_u8()), Some(p));
1069        }
1070    }
1071
1072    #[test]
1073    fn equivalence_kind_roundtrip() {
1074        for k in [
1075            EquivalenceKind::None,
1076            EquivalenceKind::Minimal,
1077            EquivalenceKind::Complete,
1078            EquivalenceKind::Both,
1079        ] {
1080            let encoded = k.to_u8();
1081            let decoded = EquivalenceKind::from_u8(encoded);
1082            // None round-trips to None (encoded as 0 which isn't a known
1083            // discriminator → `from_u8` falls back to None).
1084            if k == EquivalenceKind::None {
1085                assert_eq!(decoded, EquivalenceKind::None);
1086            } else {
1087                assert_eq!(decoded, k);
1088            }
1089        }
1090    }
1091
1092    #[test]
1093    fn equivalence_kind_from_u8_unknown_is_none() {
1094        assert_eq!(EquivalenceKind::from_u8(0xAB), EquivalenceKind::None);
1095    }
1096
1097    #[test]
1098    fn equivalence_hash_zero_constant_is_zeroes() {
1099        assert_eq!(EquivalenceHash::ZERO.0, [0u8; 14]);
1100    }
1101
1102    #[test]
1103    fn unknown_discriminator_encodes_exactly_one_byte() {
1104        let ti = TypeIdentifier::Unknown(0xC7);
1105        let bytes = ti.to_bytes_le().unwrap();
1106        assert_eq!(bytes, alloc::vec![0xC7]);
1107    }
1108}