Skip to main content

nros_serdes/
walk.rs

1//! phase-421 W5 — the schema-driven serialization strategy (RFC-0088 D7).
2//!
3//! A provider that declares `impl = "schema"` in its `nros-serdes.toml` writes
4//! ONE implementation and gets every message, with no codegen plugin and no
5//! generated code per type. This module is the machinery that makes that true:
6//! the walk over [`crate::schema::Field`] lives here, once, and a provider
7//! supplies only the primitive encode/decode operations of its own wire.
8//!
9//! # The signature RFC-0088 D7 sketched cannot be implemented
10//!
11//! D7 wrote:
12//!
13//! ```text
14//! fn serialize(msg: *const u8, schema: &'static [Field], out: &mut [u8]) -> …
15//! ```
16//!
17//! That takes the HOST STRUCT as a pointer and reads fields at `Field::offset`.
18//! Measured against the schemas codegen actually emits, it stops at the first
19//! variable-length field, and it does so for three independent reasons:
20//!
21//! * **A `String` field's host type is `heapless::String<N>` and the schema
22//!   does not carry `N`.** `nros generate-rust` emits
23//!   `FieldType::String` — the IDL type — for a member whose Rust storage is a
24//!   fixed-capacity buffer. `BoundedString(n)` is the IDL bound, not the host
25//!   capacity, and the two are different numbers.
26//! * **`heapless::String` / `heapless::Vec` are `repr(Rust)`.** Their field
27//!   order and padding are unspecified, so "the length is at offset 0" is not a
28//!   fact this crate is allowed to assume. `Field::offset` is well defined —
29//!   `offset_of!` works on a `repr(Rust)` struct — but it only gets you to the
30//!   START of the container, and the container's own interior is opaque.
31//! * **A nested type's SIZE is not in the schema.** [`crate::schema::NestedType`]
32//!   carries `type_name` and `fields`; striding an `Array(N, Nested(..))` or a
33//!   sequence of structs needs `size_of` of the element, and the largest
34//!   `offset` in a `repr(Rust)` child does not determine it.
35//!
36//! Extending the schema to carry host layout would change what codegen must
37//! emit for every committed generated message, which is a different change from
38//! this one. So the pivot moves: the value access a schema-driven provider has
39//! today is **the CDR byte stream**, which nano-ros already produces for every
40//! message from generated code. `impl = "schema"` is therefore a TRANSCODER
41//! strategy in v1 — CDR in, foreign wire out, and back — and that is precisely
42//! why the walk belongs here rather than in each provider.
43//!
44//! The cost is the one D7 already accepted: schema-driven is slower than the
45//! per-type serializer we emit for CDR, and `impl = "codegen"` is the answer
46//! when someone hits the wall.
47//!
48//! # What the walk does not cover
49//!
50//! `FieldType::WString` / `FieldType::BoundedWString` reach
51//! [`SchemaError::Unsupported`], because [`crate::cdr::CdrReader`] has no
52//! wide-string primitive to read them WITH — there is no `read_wstring`, in
53//! either direction. No message in `packages/interfaces/*` uses one, so this is
54//! a hole in the CDR codec that the schema walk inherits rather than one it
55//! introduces.
56
57use crate::{
58    cdr::{CdrReader, CdrWriter},
59    error::{DeserError, SerError},
60    schema::{Field, FieldType, Message},
61};
62
63/// What can go wrong in a schema-driven encode or decode.
64///
65/// Deliberately separate from [`SerError`] / [`DeserError`]: those describe the
66/// CDR side of a transcode, and a provider's own wire has failure modes CDR
67/// does not (a truncated foreign buffer, a length prefix that disagrees with
68/// the schema).
69#[derive(Debug, Clone, Copy, PartialEq, Eq)]
70pub enum SchemaError {
71    /// The CDR side failed while reading.
72    CdrRead(DeserError),
73    /// The CDR side failed while writing.
74    CdrWrite(SerError),
75    /// The foreign buffer is too small to hold the encoding.
76    BufferTooSmall,
77    /// The foreign buffer ended in the middle of a value.
78    Truncated,
79    /// The foreign buffer disagrees with the schema — a length prefix past a
80    /// declared bound, a bool that is not 0 or 1, invalid UTF-8.
81    Malformed,
82    /// A schema shape this walk cannot express, with the reason.
83    ///
84    /// Never a silent skip: a field that cannot be walked ends the encode,
85    /// because a partial message on the wire is worse than no message.
86    Unsupported(&'static str),
87}
88
89impl From<DeserError> for SchemaError {
90    fn from(e: DeserError) -> Self {
91        SchemaError::CdrRead(e)
92    }
93}
94
95impl From<SerError> for SchemaError {
96    fn from(e: SerError) -> Self {
97        SchemaError::CdrWrite(e)
98    }
99}
100
101impl core::fmt::Display for SchemaError {
102    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103        match self {
104            SchemaError::CdrRead(e) => write!(f, "cdr read: {e}"),
105            SchemaError::CdrWrite(e) => write!(f, "cdr write: {e}"),
106            SchemaError::BufferTooSmall => write!(f, "output buffer too small"),
107            SchemaError::Truncated => write!(f, "input ended mid-value"),
108            SchemaError::Malformed => write!(f, "input disagrees with the schema"),
109            SchemaError::Unsupported(why) => write!(f, "unsupported schema shape: {why}"),
110        }
111    }
112}
113
114/// The encode half of a schema-driven format: the walk pushes typed values in,
115/// the implementor writes its wire.
116///
117/// Every structural hook has a no-op default, so a flat format like the
118/// reference `packed` provider implements only the primitives. A self-describing
119/// format (one that writes field names, or a per-struct length) overrides the
120/// hooks it needs.
121pub trait SchemaSink {
122    /// Entering a struct — the top-level message, or a nested member.
123    fn struct_begin(&mut self, type_name: &str) -> Result<(), SchemaError> {
124        let _ = type_name;
125        Ok(())
126    }
127    /// Leaving the struct opened by the matching [`Self::struct_begin`].
128    fn struct_end(&mut self) -> Result<(), SchemaError> {
129        Ok(())
130    }
131    /// Entering one field. Carries the whole [`Field`], so a format that writes
132    /// names or types has them without a second lookup.
133    fn field_begin(&mut self, field: &'static Field) -> Result<(), SchemaError> {
134        let _ = field;
135        Ok(())
136    }
137    /// Leaving the field opened by the matching [`Self::field_begin`].
138    fn field_end(&mut self, field: &'static Field) -> Result<(), SchemaError> {
139        let _ = field;
140        Ok(())
141    }
142    /// A fixed-size array of `len` elements is about to be written. No length
143    /// is on the wire unless the format chooses to put one there — `len` comes
144    /// from the schema on both sides.
145    fn array_begin(&mut self, len: usize) -> Result<(), SchemaError> {
146        let _ = len;
147        Ok(())
148    }
149    /// Leaving the array opened by the matching [`Self::array_begin`].
150    fn array_end(&mut self) -> Result<(), SchemaError> {
151        Ok(())
152    }
153    /// A sequence of `len` elements is about to be written. Unlike an array
154    /// this length is DATA, so a format must record it.
155    fn seq_begin(&mut self, len: usize) -> Result<(), SchemaError>;
156    /// Leaving the sequence opened by the matching [`Self::seq_begin`].
157    fn seq_end(&mut self) -> Result<(), SchemaError> {
158        Ok(())
159    }
160
161    /// Write an IDL `boolean`.
162    fn put_bool(&mut self, v: bool) -> Result<(), SchemaError>;
163    /// Write an IDL `octet` / `uint8`.
164    fn put_u8(&mut self, v: u8) -> Result<(), SchemaError>;
165    /// Write an IDL `int8`.
166    fn put_i8(&mut self, v: i8) -> Result<(), SchemaError>;
167    /// Write an IDL `uint16`.
168    fn put_u16(&mut self, v: u16) -> Result<(), SchemaError>;
169    /// Write an IDL `int16`.
170    fn put_i16(&mut self, v: i16) -> Result<(), SchemaError>;
171    /// Write an IDL `uint32`.
172    fn put_u32(&mut self, v: u32) -> Result<(), SchemaError>;
173    /// Write an IDL `int32`.
174    fn put_i32(&mut self, v: i32) -> Result<(), SchemaError>;
175    /// Write an IDL `uint64`.
176    fn put_u64(&mut self, v: u64) -> Result<(), SchemaError>;
177    /// Write an IDL `int64`.
178    fn put_i64(&mut self, v: i64) -> Result<(), SchemaError>;
179    /// Write an IDL `float`.
180    fn put_f32(&mut self, v: f32) -> Result<(), SchemaError>;
181    /// Write an IDL `double`.
182    fn put_f64(&mut self, v: f64) -> Result<(), SchemaError>;
183    /// Write an IDL narrow `string`. The NUL CDR appends is a CDR concern and
184    /// is already stripped — `v` is the payload.
185    fn put_str(&mut self, v: &str) -> Result<(), SchemaError>;
186}
187
188/// The decode half: the walk pulls typed values out, in schema order.
189///
190/// Exactly dual to [`SchemaSink`]. The walk knows what comes next from the
191/// schema, so the implementor never has to parse a type tag it did not write.
192pub trait SchemaSource {
193    /// Entering a struct — the top-level message, or a nested member.
194    fn struct_begin(&mut self, type_name: &str) -> Result<(), SchemaError> {
195        let _ = type_name;
196        Ok(())
197    }
198    /// Leaving the struct opened by the matching [`Self::struct_begin`].
199    fn struct_end(&mut self) -> Result<(), SchemaError> {
200        Ok(())
201    }
202    /// Entering one field.
203    fn field_begin(&mut self, field: &'static Field) -> Result<(), SchemaError> {
204        let _ = field;
205        Ok(())
206    }
207    /// Leaving the field opened by the matching [`Self::field_begin`].
208    fn field_end(&mut self, field: &'static Field) -> Result<(), SchemaError> {
209        let _ = field;
210        Ok(())
211    }
212    /// A fixed-size array of `len` elements follows; `len` is from the schema.
213    fn array_begin(&mut self, len: usize) -> Result<(), SchemaError> {
214        let _ = len;
215        Ok(())
216    }
217    /// Leaving the array opened by the matching [`Self::array_begin`].
218    fn array_end(&mut self) -> Result<(), SchemaError> {
219        Ok(())
220    }
221    /// Read the element count of a sequence off the wire.
222    fn seq_begin(&mut self) -> Result<usize, SchemaError>;
223    /// Leaving the sequence opened by the matching [`Self::seq_begin`].
224    fn seq_end(&mut self) -> Result<(), SchemaError> {
225        Ok(())
226    }
227
228    /// Read an IDL `boolean`.
229    fn take_bool(&mut self) -> Result<bool, SchemaError>;
230    /// Read an IDL `octet` / `uint8`.
231    fn take_u8(&mut self) -> Result<u8, SchemaError>;
232    /// Read an IDL `int8`.
233    fn take_i8(&mut self) -> Result<i8, SchemaError>;
234    /// Read an IDL `uint16`.
235    fn take_u16(&mut self) -> Result<u16, SchemaError>;
236    /// Read an IDL `int16`.
237    fn take_i16(&mut self) -> Result<i16, SchemaError>;
238    /// Read an IDL `uint32`.
239    fn take_u32(&mut self) -> Result<u32, SchemaError>;
240    /// Read an IDL `int32`.
241    fn take_i32(&mut self) -> Result<i32, SchemaError>;
242    /// Read an IDL `uint64`.
243    fn take_u64(&mut self) -> Result<u64, SchemaError>;
244    /// Read an IDL `int64`.
245    fn take_i64(&mut self) -> Result<i64, SchemaError>;
246    /// Read an IDL `float`.
247    fn take_f32(&mut self) -> Result<f32, SchemaError>;
248    /// Read an IDL `double`.
249    fn take_f64(&mut self) -> Result<f64, SchemaError>;
250    /// Read an IDL narrow `string`, borrowed out of the source buffer.
251    fn take_str(&mut self) -> Result<&str, SchemaError>;
252}
253
254/// Walk `schema`, reading CDR and pushing each value into `sink`.
255///
256/// `reader` must be positioned at the start of the struct's members (past the
257/// encapsulation header). The DHEADER handling mirrors a generated `serialize`
258/// exactly: one per struct, top-level AND nested, a no-op under XCDR1 — the
259/// same rule `crate::size` was written against.
260pub fn encode_from_cdr<S: SchemaSink + ?Sized>(
261    reader: &mut CdrReader<'_>,
262    type_name: &str,
263    schema: &'static [Field],
264    sink: &mut S,
265) -> Result<(), SchemaError> {
266    let scope = reader.begin_dheader()?;
267    sink.struct_begin(type_name)?;
268    for field in schema {
269        sink.field_begin(field)?;
270        encode_one(reader, &field.ty, sink)?;
271        sink.field_end(field)?;
272    }
273    sink.struct_end()?;
274    reader.end_dheader(scope)?;
275    Ok(())
276}
277
278fn encode_one<S: SchemaSink + ?Sized>(
279    r: &mut CdrReader<'_>,
280    ty: &'static FieldType,
281    sink: &mut S,
282) -> Result<(), SchemaError> {
283    match ty {
284        FieldType::Bool => sink.put_bool(r.read_bool()?),
285        FieldType::Uint8 => sink.put_u8(r.read_u8()?),
286        FieldType::Int8 => sink.put_i8(r.read_i8()?),
287        FieldType::Uint16 => sink.put_u16(r.read_u16()?),
288        FieldType::Int16 => sink.put_i16(r.read_i16()?),
289        FieldType::Uint32 => sink.put_u32(r.read_u32()?),
290        FieldType::Int32 => sink.put_i32(r.read_i32()?),
291        FieldType::Uint64 => sink.put_u64(r.read_u64()?),
292        FieldType::Int64 => sink.put_i64(r.read_i64()?),
293        FieldType::Float32 => sink.put_f32(r.read_f32()?),
294        FieldType::Float64 => sink.put_f64(r.read_f64()?),
295        FieldType::String => sink.put_str(r.read_string()?),
296        FieldType::BoundedString(n) => {
297            let s = r.read_string()?;
298            // The bound is an IDL fact the peer may have violated; refusing
299            // here is what stops it becoming the provider's problem.
300            if s.len() > *n {
301                return Err(SchemaError::Malformed);
302            }
303            sink.put_str(s)
304        }
305        FieldType::WString | FieldType::BoundedWString(_) => Err(SchemaError::Unsupported(
306            "wstring: CdrReader has no wide-string primitive to transcode from",
307        )),
308        FieldType::Nested(nested) => encode_from_cdr(r, nested.type_name, nested.fields, sink),
309        FieldType::Array(n, inner) => {
310            sink.array_begin(*n)?;
311            for _ in 0..*n {
312                encode_one(r, inner, sink)?;
313            }
314            sink.array_end()
315        }
316        FieldType::Sequence(inner) => {
317            let n = r.read_sequence_len()?;
318            sink.seq_begin(n)?;
319            for _ in 0..n {
320                encode_one(r, inner, sink)?;
321            }
322            sink.seq_end()
323        }
324        FieldType::BoundedSequence(cap, inner) => {
325            let n = r.read_sequence_len()?;
326            if n > *cap {
327                return Err(SchemaError::Malformed);
328            }
329            sink.seq_begin(n)?;
330            for _ in 0..n {
331                encode_one(r, inner, sink)?;
332            }
333            sink.seq_end()
334        }
335    }
336}
337
338/// Walk `schema`, pulling each value from `source` and writing CDR.
339///
340/// The exact inverse of [`encode_from_cdr`], including the per-struct DHEADER,
341/// so a value that survives one survives the pair byte-for-byte.
342pub fn decode_to_cdr<S: SchemaSource + ?Sized>(
343    source: &mut S,
344    type_name: &str,
345    schema: &'static [Field],
346    writer: &mut CdrWriter<'_>,
347) -> Result<(), SchemaError> {
348    let mark = writer.begin_dheader()?;
349    source.struct_begin(type_name)?;
350    for field in schema {
351        source.field_begin(field)?;
352        decode_one(source, &field.ty, writer)?;
353        source.field_end(field)?;
354    }
355    source.struct_end()?;
356    writer.end_dheader(mark)?;
357    Ok(())
358}
359
360fn decode_one<S: SchemaSource + ?Sized>(
361    source: &mut S,
362    ty: &'static FieldType,
363    w: &mut CdrWriter<'_>,
364) -> Result<(), SchemaError> {
365    match ty {
366        FieldType::Bool => w.write_bool(source.take_bool()?)?,
367        FieldType::Uint8 => w.write_u8(source.take_u8()?)?,
368        FieldType::Int8 => w.write_i8(source.take_i8()?)?,
369        FieldType::Uint16 => w.write_u16(source.take_u16()?)?,
370        FieldType::Int16 => w.write_i16(source.take_i16()?)?,
371        FieldType::Uint32 => w.write_u32(source.take_u32()?)?,
372        FieldType::Int32 => w.write_i32(source.take_i32()?)?,
373        FieldType::Uint64 => w.write_u64(source.take_u64()?)?,
374        FieldType::Int64 => w.write_i64(source.take_i64()?)?,
375        FieldType::Float32 => w.write_f32(source.take_f32()?)?,
376        FieldType::Float64 => w.write_f64(source.take_f64()?)?,
377        FieldType::String => {
378            let s = source.take_str()?;
379            w.write_string(s)?;
380        }
381        FieldType::BoundedString(n) => {
382            let s = source.take_str()?;
383            if s.len() > *n {
384                return Err(SchemaError::Malformed);
385            }
386            w.write_string(s)?;
387        }
388        FieldType::WString | FieldType::BoundedWString(_) => {
389            return Err(SchemaError::Unsupported(
390                "wstring: CdrWriter has no wide-string primitive to transcode into",
391            ));
392        }
393        FieldType::Nested(nested) => {
394            decode_to_cdr(source, nested.type_name, nested.fields, w)?;
395        }
396        FieldType::Array(n, inner) => {
397            source.array_begin(*n)?;
398            for _ in 0..*n {
399                decode_one(source, inner, w)?;
400            }
401            source.array_end()?;
402        }
403        FieldType::Sequence(inner) => {
404            let n = source.seq_begin()?;
405            w.write_sequence_len(n)?;
406            for _ in 0..n {
407                decode_one(source, inner, w)?;
408            }
409            source.seq_end()?;
410        }
411        FieldType::BoundedSequence(cap, inner) => {
412            let n = source.seq_begin()?;
413            if n > *cap {
414                return Err(SchemaError::Malformed);
415            }
416            w.write_sequence_len(n)?;
417            for _ in 0..n {
418                decode_one(source, inner, w)?;
419            }
420            source.seq_end()?;
421        }
422    }
423    Ok(())
424}
425
426/// A serialization format implemented once, by walking the schema (RFC-0088 D7).
427///
428/// # How this differs from the D7 sketch, and why
429///
430/// D7 wrote `serialize(msg: *const u8, schema, out)`. The host struct pointer
431/// is not usable — see the module docs for the three measured reasons — so the
432/// message side of both methods is the CDR byte stream instead: a
433/// [`CdrReader`] positioned at the members on the way out, a [`CdrWriter`] on
434/// the way back. Everything else survives: one implementation, every message,
435/// no generated code, the schema as the only description.
436///
437/// The second added parameter is `type_name`. A `&'static [Field]` slice has no
438/// name of its own — [`Message::TYPE_NAME`] is a separate const, and nested
439/// members carry theirs in [`crate::schema::NestedType`] — so without it the
440/// top-level struct would be the one node a self-describing format could not
441/// name. Use [`SchemaSerializer::serialize_message`] to supply both from a type.
442pub trait SchemaSerializer {
443    /// Cross-image identity (RFC-0088 D2). The string, never the number, is
444    /// what crosses an image boundary.
445    const FORMAT_NAME: &'static str;
446
447    /// Image-local discriminant, as a raw `u8` rather than a
448    /// [`crate::format::SerializationFormatId`]: the enum reserves values for
449    /// in-tree formats, and a third-party provider is assigned one by the build
450    /// from the set of formats its image declares. A provider that becomes
451    /// in-tree gains a variant; one that does not still has a number here.
452    const FORMAT_ID: u8;
453
454    /// Encode one message from its CDR form into `out`, returning bytes written.
455    fn serialize(
456        msg: &mut CdrReader<'_>,
457        type_name: &str,
458        schema: &'static [Field],
459        out: &mut [u8],
460    ) -> Result<usize, SchemaError>;
461
462    /// Decode one message from `bytes` into CDR, returning bytes consumed.
463    fn deserialize(
464        bytes: &[u8],
465        type_name: &str,
466        schema: &'static [Field],
467        msg: &mut CdrWriter<'_>,
468    ) -> Result<usize, SchemaError>;
469
470    /// [`Self::serialize`] with the name and schema taken from the type.
471    fn serialize_message<M: Message>(
472        msg: &mut CdrReader<'_>,
473        out: &mut [u8],
474    ) -> Result<usize, SchemaError>
475    where
476        Self: Sized,
477    {
478        Self::serialize(msg, M::TYPE_NAME, M::FIELDS, out)
479    }
480
481    /// [`Self::deserialize`] with the name and schema taken from the type.
482    fn deserialize_message<M: Message>(
483        bytes: &[u8],
484        msg: &mut CdrWriter<'_>,
485    ) -> Result<usize, SchemaError>
486    where
487        Self: Sized,
488    {
489        Self::deserialize(bytes, M::TYPE_NAME, M::FIELDS, msg)
490    }
491}
492
493#[cfg(test)]
494mod tests {
495    use super::*;
496    use crate::schema::NestedType;
497
498    /// A sink that records the walk as text, so the ORDER of the callbacks is
499    /// asserted rather than assumed. It writes no wire at all — the point is
500    /// that a provider sees the structure, not just a flat value stream.
501    #[derive(Default)]
502    struct Trace {
503        out: heapless::String<512>,
504    }
505
506    impl Trace {
507        fn push(&mut self, s: &str) -> Result<(), SchemaError> {
508            self.out
509                .push_str(s)
510                .map_err(|_| SchemaError::BufferTooSmall)
511        }
512    }
513
514    impl SchemaSink for Trace {
515        fn struct_begin(&mut self, type_name: &str) -> Result<(), SchemaError> {
516            self.push("{")?;
517            self.push(type_name)
518        }
519        fn struct_end(&mut self) -> Result<(), SchemaError> {
520            self.push("}")
521        }
522        fn field_begin(&mut self, field: &'static Field) -> Result<(), SchemaError> {
523            self.push(" ")?;
524            self.push(field.name)?;
525            self.push("=")
526        }
527        fn seq_begin(&mut self, len: usize) -> Result<(), SchemaError> {
528            self.push(if len == 0 { "[0" } else { "[n" })
529        }
530        fn seq_end(&mut self) -> Result<(), SchemaError> {
531            self.push("]")
532        }
533        fn put_bool(&mut self, _: bool) -> Result<(), SchemaError> {
534            self.push("b")
535        }
536        fn put_u8(&mut self, _: u8) -> Result<(), SchemaError> {
537            self.push("u8")
538        }
539        fn put_i8(&mut self, _: i8) -> Result<(), SchemaError> {
540            self.push("i8")
541        }
542        fn put_u16(&mut self, _: u16) -> Result<(), SchemaError> {
543            self.push("u16")
544        }
545        fn put_i16(&mut self, _: i16) -> Result<(), SchemaError> {
546            self.push("i16")
547        }
548        fn put_u32(&mut self, _: u32) -> Result<(), SchemaError> {
549            self.push("u32")
550        }
551        fn put_i32(&mut self, _: i32) -> Result<(), SchemaError> {
552            self.push("i32")
553        }
554        fn put_u64(&mut self, _: u64) -> Result<(), SchemaError> {
555            self.push("u64")
556        }
557        fn put_i64(&mut self, _: i64) -> Result<(), SchemaError> {
558            self.push("i64")
559        }
560        fn put_f32(&mut self, _: f32) -> Result<(), SchemaError> {
561            self.push("f32")
562        }
563        fn put_f64(&mut self, _: f64) -> Result<(), SchemaError> {
564            self.push("f64")
565        }
566        fn put_str(&mut self, v: &str) -> Result<(), SchemaError> {
567            self.push("\"")?;
568            self.push(v)?;
569            self.push("\"")
570        }
571    }
572
573    const TIME_FIELDS: &[Field] = &[
574        Field {
575            name: "sec",
576            ty: FieldType::Int32,
577            offset: 0,
578        },
579        Field {
580            name: "nanosec",
581            ty: FieldType::Uint32,
582            offset: 4,
583        },
584    ];
585    const TIME: NestedType = NestedType {
586        type_name: "builtin_interfaces/msg/Time",
587        fields: TIME_FIELDS,
588    };
589    const HEADER_FIELDS: &[Field] = &[
590        Field {
591            name: "stamp",
592            ty: FieldType::Nested(&TIME),
593            offset: 0,
594        },
595        Field {
596            name: "frame_id",
597            ty: FieldType::String,
598            offset: 8,
599        },
600    ];
601
602    fn header_cdr(buf: &mut [u8]) -> usize {
603        let mut w = CdrWriter::new(buf);
604        let dh = w.begin_dheader().unwrap();
605        let inner = w.begin_dheader().unwrap();
606        w.write_i32(7).unwrap();
607        w.write_u32(8).unwrap();
608        w.end_dheader(inner).unwrap();
609        w.write_string("map").unwrap();
610        w.end_dheader(dh).unwrap();
611        w.position()
612    }
613
614    #[test]
615    fn the_walk_visits_nested_structs_in_declaration_order() {
616        let mut buf = [0u8; 64];
617        let len = header_cdr(&mut buf);
618        let mut r = CdrReader::new(&buf[..len]);
619        let mut trace = Trace::default();
620        encode_from_cdr(&mut r, "std_msgs/msg/Header", HEADER_FIELDS, &mut trace).unwrap();
621        assert_eq!(
622            trace.out.as_str(),
623            "{std_msgs/msg/Header stamp={builtin_interfaces/msg/Time sec=i32 nanosec=u32} \
624             frame_id=\"map\"}"
625        );
626    }
627
628    #[test]
629    fn a_wstring_is_refused_by_name_rather_than_skipped() {
630        const WIDE: &[Field] = &[Field {
631            name: "text",
632            ty: FieldType::WString,
633            offset: 0,
634        }];
635        let buf = [0u8; 16];
636        let mut r = CdrReader::new(&buf);
637        let mut trace = Trace::default();
638        let err = encode_from_cdr(&mut r, "t/msg/W", WIDE, &mut trace).unwrap_err();
639        match err {
640            SchemaError::Unsupported(why) => assert!(why.contains("wstring"), "got {why:?}"),
641            other => panic!("expected Unsupported, got {other:?}"),
642        }
643    }
644
645    #[test]
646    fn a_bounded_sequence_past_its_bound_is_malformed_not_accepted() {
647        const ELEM: FieldType = FieldType::Uint8;
648        const FIELDS: &[Field] = &[Field {
649            name: "data",
650            ty: FieldType::BoundedSequence(2, &ELEM),
651            offset: 0,
652        }];
653        let mut buf = [0u8; 32];
654        let len = {
655            let mut w = CdrWriter::new(&mut buf);
656            let dh = w.begin_dheader().unwrap();
657            w.write_sequence_len(5).unwrap();
658            for _ in 0..5 {
659                w.write_u8(1).unwrap();
660            }
661            w.end_dheader(dh).unwrap();
662            w.position()
663        };
664        let mut r = CdrReader::new(&buf[..len]);
665        let mut trace = Trace::default();
666        assert_eq!(
667            encode_from_cdr(&mut r, "t/msg/B", FIELDS, &mut trace),
668            Err(SchemaError::Malformed)
669        );
670    }
671}