Skip to main content

zerodds_corba_rust/
value_wire.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! CORBA `valuetype` wire (§15.3.4) — the self-describing, **shared**
5//! object encoding. A value instance is written as `value_tag` + RepositoryId +
6//! state members; the same instance (Rc identity) a second time as an
7//! **indirection** (value sharing, §15.3.4) — so object graphs with
8//! multiple references are preserved.
9//!
10//! ## value_tag (§15.3.4.1) — bit layout
11//!
12//! Verified against JacORB `CDRInputStream` (the reference implementation
13//! against which cross-ORB testing is done):
14//! * `0x00000000` — null value.
15//! * `0xffffffff` — indirection, followed by a `long` offset relative to
16//!   the position of the offset field (backwards to the target `value_tag`).
17//! * `0x7fffff00 .. 0x7fffffff` — a value follows; flags in the low byte:
18//!   * Bit 0 (`0x01`) — codebase URL present.
19//!   * Bit 3 (`0x08`) — chunked encoding.
20//!   * Bits 1-2 (`0x06`) — type info: `0x00` none / `0x02` single RepositoryId /
21//!     `0x06` list of RepositoryIds.
22//!
23//! **Scope of this engine**: non-chunked + chunked encoding, single/list
24//! RepositoryId, value sharing (DAG), truncation, **nested chunked
25//! values** + multi-level end-tags, and **codebase URL resolution** via a
26//! [`CodebaseResolver`].
27
28extern crate alloc;
29use alloc::boxed::Box;
30use alloc::collections::BTreeMap;
31use alloc::rc::Rc;
32use alloc::string::String;
33use core::any::Any;
34
35use zerodds_cdr::{BufferReader, BufferWriter, DecodeError, EncodeError};
36
37use crate::runtime::ValueBase;
38
39/// `value_tag` base (§15.3.4.1).
40const VALUE_TAG_BASE: u32 = 0x7fff_ff00;
41/// Bit 0: codebase URL present.
42const VT_FLAG_CODEBASE: u32 = 0x0000_0001;
43/// Bit 3: chunked encoding.
44const VT_FLAG_CHUNKED: u32 = 0x0000_0008;
45/// Bits 1-2: RepositoryId type info.
46const VT_REPO_MASK: u32 = 0x0000_0006;
47const VT_REPO_NONE: u32 = 0x0000_0000;
48const VT_REPO_SINGLE: u32 = 0x0000_0002;
49const VT_REPO_LIST: u32 = 0x0000_0006;
50/// null value.
51const VALUE_NULL: u32 = 0x0000_0000;
52/// Indirection marker.
53const VALUE_INDIRECTION: u32 = 0xffff_ffff;
54
55fn enc_err(message: &'static str) -> EncodeError {
56    EncodeError::ValueOutOfRange { message }
57}
58fn dec_err(kind: &'static str) -> DecodeError {
59    DecodeError::InvalidEnum { kind, value: 0 }
60}
61
62/// A concrete valuetype that marshals over the §15.3.4 wire. Generated
63/// code implements this in addition to [`ValueBase`].
64pub trait ValueMarshal: ValueBase {
65    /// Writes ONLY the state members (declaration order), without `value_tag`.
66    ///
67    /// # Errors
68    /// CDR encode error.
69    fn marshal_state(&self, w: &mut BufferWriter) -> Result<(), EncodeError>;
70}
71
72/// Writes valuetype instances with **value sharing**: the same `Rc` instance is
73/// encoded as an indirection on the second write (one object, not two copies).
74#[derive(Default)]
75pub struct ValueWriter {
76    /// `Rc` pointer identity → stream position of the corresponding `value_tag`.
77    seen: BTreeMap<usize, usize>,
78}
79
80impl ValueWriter {
81    /// Fresh writer (empty sharing table).
82    #[must_use]
83    pub fn new() -> Self {
84        Self::default()
85    }
86
87    /// Writes an optional valuetype: `None` → null; known instance →
88    /// indirection; otherwise `value_tag`(single RepositoryId) + RepositoryId + state.
89    ///
90    /// # Errors
91    /// CDR encode error / offset overflow.
92    pub fn write(
93        &mut self,
94        w: &mut BufferWriter,
95        value: Option<&Rc<dyn ValueMarshal>>,
96    ) -> Result<(), EncodeError> {
97        let Some(v) = value else {
98            w.write_u32(VALUE_NULL)?;
99            return Ok(());
100        };
101        w.align(4);
102        let tag_pos = w.position();
103        let id = Rc::as_ptr(v).cast::<()>() as usize;
104        if let Some(&prior) = self.seen.get(&id) {
105            // Value sharing: indirection backwards to the first value_tag.
106            w.write_u32(VALUE_INDIRECTION)?;
107            // Offset relative to the position of the offset field (now = tag_pos + 4).
108            let offset = i32::try_from(prior as i64 - w.position() as i64)
109                .map_err(|_| enc_err("value indirection offset overflow"))?;
110            w.write_u32(offset as u32)?;
111            return Ok(());
112        }
113        self.seen.insert(id, tag_pos);
114        w.write_u32(VALUE_TAG_BASE | VT_REPO_SINGLE)?; // 0x7fffff02
115        w.write_string(v.repository_id())?;
116        v.marshal_state(w)
117    }
118
119    /// Like [`write`](Self::write), but with a **codebase URL** (§15.3.4.1, bit 0):
120    /// `value_tag` 0x7fffff03, then the codebase URL, then RepositoryId + state.
121    /// The reader can use the URL via a [`CodebaseResolver`] for factory
122    /// resolution.
123    ///
124    /// # Errors
125    /// CDR encode error / offset overflow.
126    pub fn write_with_codebase(
127        &mut self,
128        w: &mut BufferWriter,
129        value: Option<&Rc<dyn ValueMarshal>>,
130        codebase: &str,
131    ) -> Result<(), EncodeError> {
132        let Some(v) = value else {
133            w.write_u32(VALUE_NULL)?;
134            return Ok(());
135        };
136        w.align(4);
137        let tag_pos = w.position();
138        let id = Rc::as_ptr(v).cast::<()>() as usize;
139        if let Some(&prior) = self.seen.get(&id) {
140            w.write_u32(VALUE_INDIRECTION)?;
141            let offset = i32::try_from(prior as i64 - w.position() as i64)
142                .map_err(|_| enc_err("value indirection offset overflow"))?;
143            w.write_u32(offset as u32)?;
144            return Ok(());
145        }
146        self.seen.insert(id, tag_pos);
147        w.write_u32(VALUE_TAG_BASE | VT_FLAG_CODEBASE | VT_REPO_SINGLE)?; // 0x7fffff03
148        w.write_string(codebase)?;
149        w.write_string(v.repository_id())?;
150        v.marshal_state(w)
151    }
152
153    /// Writes a valuetype **chunked** with a RepositoryId list (§15.3.4.3) —
154    /// the format that makes truncatable valuetypes interoperable over the wire
155    /// (a foreign ORB can truncate to a base). `base_ids` are the base
156    /// RepositoryIds (most-derived → base order); the state lies in one
157    /// chunk, terminated with end-tag `-1`. Byte-identical to JacORB 3.9.
158    ///
159    /// # Errors
160    /// CDR encode error / offset or length overflow.
161    pub fn write_chunked(
162        &mut self,
163        w: &mut BufferWriter,
164        value: Option<&Rc<dyn ValueMarshal>>,
165        base_ids: &[&str],
166    ) -> Result<(), EncodeError> {
167        let Some(v) = value else {
168            w.write_u32(VALUE_NULL)?;
169            return Ok(());
170        };
171        w.align(4);
172        let tag_pos = w.position();
173        let id = Rc::as_ptr(v).cast::<()>() as usize;
174        if let Some(&prior) = self.seen.get(&id) {
175            w.write_u32(VALUE_INDIRECTION)?;
176            let offset = i32::try_from(prior as i64 - w.position() as i64)
177                .map_err(|_| enc_err("value indirection offset overflow"))?;
178            w.write_u32(offset as u32)?;
179            return Ok(());
180        }
181        self.seen.insert(id, tag_pos);
182
183        // value_tag: chunked + RepositoryId list (0x7fffff0e).
184        w.write_u32(VALUE_TAG_BASE | VT_FLAG_CHUNKED | VT_REPO_LIST)?;
185        let count = u32::try_from(1 + base_ids.len())
186            .map_err(|_| enc_err("value RepositoryId list too long"))?;
187        w.write_u32(count)?;
188        w.write_string(v.repository_id())?;
189        for b in base_ids {
190            w.write_string(b)?;
191        }
192
193        // Chunk: determine the size up front in an align-origin temp buffer, so that
194        // the CDR alignment of the state (including 8-byte) exactly matches the stream offset.
195        w.align(4);
196        let state_offset = w.position() + 4; // the chunk_size long occupies 4 bytes
197        let mut tmp = BufferWriter::new(w.endianness()).with_align_origin(state_offset);
198        v.marshal_state(&mut tmp)?;
199        let state = tmp.into_bytes();
200        let size = i32::try_from(state.len()).map_err(|_| enc_err("chunk size overflow"))?;
201        w.write_u32(size as u32)?;
202        w.write_bytes(&state)?;
203
204        // End-tag -1 (outermost value).
205        w.align(4);
206        w.write_u32((-1i32) as u32)
207    }
208
209    /// Writes a **multi-chunk** valuetype tree (§15.3.4.3): a chunked value
210    /// whose body carries further chunked values at chunk boundaries, each
211    /// self-delimited by its own end-tag `-level`. This is the encoder
212    /// counterpart to the decoder's nested-chunk handling: where
213    /// [`write_chunked`](Self::write_chunked) emits a single flat state chunk
214    /// plus an end-tag, this emits the leading state chunk followed by the
215    /// node's nested values — the wire a base-only reader truncates over
216    /// (consuming the derived/nested tail). A leaf node (no nested children)
217    /// is byte-identical to [`write_chunked`](Self::write_chunked).
218    ///
219    /// # Errors
220    /// CDR encode error / offset or length overflow.
221    pub fn write_chunked_tree(
222        &mut self,
223        w: &mut BufferWriter,
224        node: &ChunkedNode<'_>,
225    ) -> Result<(), EncodeError> {
226        self.write_chunked_node(w, node, 1)
227    }
228
229    /// Recursive worker for [`write_chunked_tree`](Self::write_chunked_tree).
230    /// `level` is the 1-based nesting depth used for this value's end-tag.
231    ///
232    /// zerodds-lint: recursion-depth 64 (Valuetype-Chunk-Nesting; bounded by GIOP value chunking)
233    fn write_chunked_node(
234        &mut self,
235        w: &mut BufferWriter,
236        node: &ChunkedNode<'_>,
237        level: u32,
238    ) -> Result<(), EncodeError> {
239        w.align(4);
240        let tag_pos = w.position();
241        let id = Rc::as_ptr(node.value).cast::<()>() as usize;
242        if let Some(&prior) = self.seen.get(&id) {
243            w.write_u32(VALUE_INDIRECTION)?;
244            let offset = i32::try_from(prior as i64 - w.position() as i64)
245                .map_err(|_| enc_err("value indirection offset overflow"))?;
246            w.write_u32(offset as u32)?;
247            return Ok(());
248        }
249        self.seen.insert(id, tag_pos);
250
251        // value_tag: chunked + RepositoryId list.
252        w.write_u32(VALUE_TAG_BASE | VT_FLAG_CHUNKED | VT_REPO_LIST)?;
253        let count = u32::try_from(1 + node.base_ids.len())
254            .map_err(|_| enc_err("value RepositoryId list too long"))?;
255        w.write_u32(count)?;
256        w.write_string(node.value.repository_id())?;
257        for b in node.base_ids {
258            w.write_string(b)?;
259        }
260
261        // Leading data chunk: the node's own state.
262        w.align(4);
263        let state_offset = w.position() + 4;
264        let mut tmp = BufferWriter::new(w.endianness()).with_align_origin(state_offset);
265        node.value.marshal_state(&mut tmp)?;
266        let state = tmp.into_bytes();
267        let size = i32::try_from(state.len()).map_err(|_| enc_err("chunk size overflow"))?;
268        w.write_u32(size as u32)?;
269        w.write_bytes(&state)?;
270
271        // Nested chunked values follow at chunk boundaries (derived tail),
272        // each self-delimited by its own end-tag at the deeper level.
273        for child in node.nested {
274            self.write_chunked_node(w, child, level + 1)?;
275        }
276
277        // This value's own end-tag (-level).
278        w.align(4);
279        w.write_u32((-(level as i32)) as u32)
280    }
281}
282
283/// A node in a chunked valuetype tree (§15.3.4.3) for **multi-chunk** encoding
284/// via [`ValueWriter::write_chunked_tree`]: a value, its base RepositoryIds
285/// (most-derived → base, written in the `value_tag` list), and nested chunked
286/// values appended after its state at chunk boundaries. Lets a chunked value
287/// carry further chunked values (e.g. a truncatable derived tail) instead of a
288/// single flat state chunk.
289pub struct ChunkedNode<'a> {
290    /// The value whose leading state forms the first data chunk.
291    pub value: &'a Rc<dyn ValueMarshal>,
292    /// Base RepositoryIds (most-derived → base) for the `value_tag` list.
293    pub base_ids: &'a [&'a str],
294    /// Nested chunked values, appended after the state at chunk boundaries.
295    pub nested: &'a [ChunkedNode<'a>],
296}
297
298/// Factory closure: reads the state members of a valuetype and returns a
299/// typed instance as `Rc<dyn Any>` (the caller downcasts to the concrete type).
300pub type ValueCtor = Box<dyn Fn(&mut BufferReader<'_>) -> Result<Rc<dyn Any>, DecodeError>>;
301
302/// Codebase resolver: returns — based on the codebase URL carried over the
303/// wire (§15.3.4.1, value_tag bit 0) and the RepositoryId — a
304/// factory for a valuetype whose factory is not statically registered.
305/// This way the codebase URL is **taken into account in factory resolution**
306/// (instead of just being skipped) — the Rust counterpart to the CORBA codebase download.
307pub type CodebaseResolver = Box<dyn Fn(&str, &str) -> Option<ValueCtor>>;
308
309/// A resolved factory: either borrowed from the static registry or
310/// freshly supplied (owned) by the [`CodebaseResolver`].
311enum CtorRef<'a> {
312    Borrowed(&'a ValueCtor),
313    Owned(ValueCtor),
314}
315
316impl CtorRef<'_> {
317    fn call(&self, r: &mut BufferReader<'_>) -> Result<Rc<dyn Any>, DecodeError> {
318        match self {
319            CtorRef::Borrowed(c) => c(r),
320            CtorRef::Owned(c) => c(r),
321        }
322    }
323}
324
325/// RepositoryId → state-reader factory. Generated code registers a factory per
326/// valuetype; an optional [`CodebaseResolver`] resolves unknown RepositoryIds
327/// via the codebase URL.
328#[derive(Default)]
329pub struct ValueRegistry {
330    ctors: BTreeMap<String, ValueCtor>,
331    codebase_resolver: Option<CodebaseResolver>,
332}
333
334impl ValueRegistry {
335    /// Empty registry.
336    #[must_use]
337    pub fn new() -> Self {
338        Self::default()
339    }
340
341    /// Registers a state-reader factory for a RepositoryId.
342    pub fn register(&mut self, repo_id: impl Into<String>, ctor: ValueCtor) {
343        self.ctors.insert(repo_id.into(), ctor);
344    }
345
346    /// Sets the codebase resolver: for RepositoryIds without a static factory it
347    /// is queried with `(codebase_url, repo_id)`.
348    pub fn set_codebase_resolver(&mut self, resolver: CodebaseResolver) {
349        self.codebase_resolver = Some(resolver);
350    }
351
352    /// Resolves a factory for `repo_id`: first statically registered, otherwise —
353    /// if a codebase URL is present — via the [`CodebaseResolver`].
354    fn ctor_for(&self, repo_id: &str, codebase: Option<&str>) -> Option<CtorRef<'_>> {
355        if let Some(c) = self.ctors.get(repo_id) {
356            return Some(CtorRef::Borrowed(c));
357        }
358        if let (Some(cb), Some(res)) = (codebase, self.codebase_resolver.as_ref()) {
359            if let Some(c) = res(cb, repo_id) {
360                return Some(CtorRef::Owned(c));
361            }
362        }
363        None
364    }
365}
366
367/// Reads valuetype instances with **indirection resolution** (value sharing): an
368/// indirection yields the same `Rc` instance as the referenced value_tag.
369#[derive(Default)]
370pub struct ValueReader {
371    /// `value_tag` stream position → already-read instance.
372    cache: BTreeMap<usize, Rc<dyn Any>>,
373}
374
375impl ValueReader {
376    /// Fresh reader.
377    #[must_use]
378    pub fn new() -> Self {
379        Self::default()
380    }
381
382    /// Reads an optional valuetype. `base` = absolute stream offset of
383    /// reader byte 0 (for the indirection position arithmetic; top-level `0`).
384    ///
385    /// # Errors
386    /// Unknown RepositoryId, unresolvable/forward indirection,
387    /// (yet) unsupported chunked encoding, CDR decode error.
388    pub fn read(
389        &mut self,
390        r: &mut BufferReader<'_>,
391        base: usize,
392        reg: &ValueRegistry,
393    ) -> Result<Option<Rc<dyn Any>>, DecodeError> {
394        r.align(4)?;
395        let tag_pos = base + r.position();
396        let tag = r.read_u32()?;
397
398        if tag == VALUE_NULL {
399            return Ok(None);
400        }
401        if tag == VALUE_INDIRECTION {
402            let off_field = base + r.position();
403            let offset = r.read_u32()? as i32;
404            if offset >= 0 {
405                return Err(dec_err("value indirection: offset must be negative"));
406            }
407            let target = usize::try_from(off_field as i64 + i64::from(offset))
408                .map_err(|_| dec_err("value indirection: target before stream start"))?;
409            return self
410                .cache
411                .get(&target)
412                .cloned()
413                .map(Some)
414                .ok_or_else(|| dec_err("value indirection: unresolved target"));
415        }
416        if tag < VALUE_TAG_BASE {
417            return Err(dec_err("invalid value_tag"));
418        }
419        let chunked = tag & VT_FLAG_CHUNKED != 0;
420        // codebase URL (bit 0): read it in + remember it for factory resolution.
421        let codebase: Option<String> = if tag & VT_FLAG_CODEBASE != 0 {
422            Some(r.read_string()?)
423        } else {
424            None
425        };
426        // RepositoryId list: 1 entry for single, n for list (most-derived first,
427        // then bases — for truncation).
428        let ids: alloc::vec::Vec<String> = match tag & VT_REPO_MASK {
429            VT_REPO_SINGLE => alloc::vec![r.read_string()?],
430            VT_REPO_LIST => {
431                let n = r.read_u32()? as usize;
432                let mut ids = alloc::vec::Vec::with_capacity(n.min(16));
433                for _ in 0..n {
434                    ids.push(r.read_string()?);
435                }
436                if ids.is_empty() {
437                    return Err(dec_err("empty value RepositoryId list"));
438                }
439                ids
440            }
441            VT_REPO_NONE => return Err(dec_err("value without type info unsupported")),
442            _ => return Err(dec_err("invalid value_tag repo-id flags")),
443        };
444
445        let v = if chunked {
446            read_chunked_state(r, &ids, reg, codebase.as_deref())?
447        } else {
448            // Non-chunked: the most-derived type (ids[0]) must be known
449            // (without chunk sizes no truncation is possible) — possibly resolved
450            // via the codebase URL.
451            let ctor = reg
452                .ctor_for(&ids[0], codebase.as_deref())
453                .ok_or_else(|| dec_err("no ValueFactory for RepositoryId"))?;
454            ctor.call(r)?
455        };
456        self.cache.insert(tag_pos, Rc::clone(&v));
457        Ok(Some(v))
458    }
459}
460
461/// Reads the **chunked** state of a valuetype (§15.3.4.3) with **truncation**:
462/// the first RepositoryId of the list that is known in `reg` (possibly resolved
463/// via `codebase`) is instantiated; the derived state remainder is skipped via
464/// the chunk sizes up to the end-tag.
465///
466/// Handles **nested chunked values** (own `value_tag` at a chunk boundary)
467/// recursively, **multi-level end-tags** (one end-tag `-k` with
468/// `k ≤ 1` also closes this value), and **indirections** in the chunk stream.
469fn read_chunked_state(
470    r: &mut BufferReader<'_>,
471    ids: &[String],
472    reg: &ValueRegistry,
473    codebase: Option<&str>,
474) -> Result<Rc<dyn Any>, DecodeError> {
475    let ctor = ids
476        .iter()
477        .find_map(|id| reg.ctor_for(id, codebase))
478        .ok_or_else(|| dec_err("no ValueFactory for any RepositoryId in chunked value"))?;
479
480    let mut value: Option<Rc<dyn Any>> = None;
481    loop {
482        r.align(4)?;
483        let marker = r.read_u32()? as i32;
484        if marker < 0 {
485            break; // end-tag (-nesting_level) → end of this (level-1) value
486        }
487        let marker_u = marker as u32;
488        if marker_u == VALUE_INDIRECTION {
489            // Shared value in the chunk stream: 8-byte indirection, skipped here.
490            let _offset = r.read_u32()?;
491            continue;
492        }
493        if marker_u >= VALUE_TAG_BASE {
494            // Nested value (in the derived tail or as a member that this
495            // truncation path does not decode) → consume recursively.
496            let closed_level = skip_value_from_tag(r, 2, marker_u)?;
497            if closed_level <= 1 {
498                break; // shared end-tag also closed this value
499            }
500            continue;
501        }
502        let chunk_size = marker as usize;
503        let pos_before = r.position();
504        if value.is_none() {
505            // First data chunk carries the (base) state of the matched type.
506            let v = ctor.call(r)?;
507            let consumed = r.position() - pos_before;
508            if consumed > chunk_size {
509                return Err(dec_err("chunked value: ctor over-read the chunk"));
510            }
511            if consumed < chunk_size {
512                // Truncation: skip the derived state remainder of this chunk.
513                let _ = r.read_bytes(chunk_size - consumed)?;
514            }
515            value = Some(v);
516        } else {
517            // Already truncated → skip further data chunks.
518            let _ = r.read_bytes(chunk_size)?;
519        }
520    }
521    value.ok_or_else(|| dec_err("chunked value produced no state"))
522}
523
524/// Consumes (skips) a nested value whose `value_tag`
525/// (`tag`) has already been read, at nesting `depth`. Reads the
526/// codebase URL + RepositoryId(s) and — when chunked — the body.
527///
528/// Returns the end-tag level `k` that closed this (and possibly outer) values:
529/// `k == depth` closes exactly this value, `k < depth` additionally closes
530/// the `k..depth-1` enclosing values (shared end-tag, §15.3.4.3).
531///
532/// zerodds-lint: recursion-depth 64 (Valuetype-Chunk-Nesting; bounded by GIOP value chunking)
533fn skip_value_from_tag(r: &mut BufferReader<'_>, depth: u32, tag: u32) -> Result<u32, DecodeError> {
534    if tag & VT_FLAG_CODEBASE != 0 {
535        let _ = r.read_string()?;
536    }
537    match tag & VT_REPO_MASK {
538        VT_REPO_SINGLE => {
539            let _ = r.read_string()?;
540        }
541        VT_REPO_LIST => {
542            let n = r.read_u32()? as usize;
543            for _ in 0..n {
544                let _ = r.read_string()?;
545            }
546        }
547        _ => return Err(dec_err("nested value without type info")),
548    }
549    if tag & VT_FLAG_CHUNKED == 0 {
550        // Non-chunked nested values carry no lengths → not skippable without a
551        // factory (does not occur in the chunked context per spec).
552        return Err(dec_err("non-chunked nested value cannot be skipped"));
553    }
554    skip_chunked_body(r, depth)
555}
556
557/// Skips the body of a chunked value at `depth` (data chunks,
558/// nested values, indirections) up to the closing end-tag. Returns
559/// the end-tag level that closed it (see [`skip_value_from_tag`]).
560///
561/// zerodds-lint: recursion-depth 64 (Valuetype-Chunk-Nesting; bounded by GIOP value chunking)
562fn skip_chunked_body(r: &mut BufferReader<'_>, depth: u32) -> Result<u32, DecodeError> {
563    loop {
564        r.align(4)?;
565        let marker = r.read_u32()? as i32;
566        if marker < 0 {
567            return Ok((-marker) as u32);
568        }
569        let marker_u = marker as u32;
570        if marker_u == VALUE_INDIRECTION {
571            let _offset = r.read_u32()?;
572            continue;
573        }
574        if marker_u >= VALUE_TAG_BASE {
575            let closed_level = skip_value_from_tag(r, depth + 1, marker_u)?;
576            if closed_level <= depth {
577                // Shared end-tag also closed this (and possibly outer) values.
578                return Ok(closed_level);
579            }
580            continue;
581        }
582        let _ = r.read_bytes(marker as usize)?;
583    }
584}
585
586#[cfg(test)]
587#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
588mod tests {
589    use super::*;
590    use zerodds_cdr::{CdrDecode, CdrEncode, Endianness};
591
592    // Test valuetype: `valuetype Point { public long x; public long y; };`
593    #[derive(Debug, PartialEq, Eq)]
594    struct Point {
595        x: i32,
596        y: i32,
597    }
598    impl ValueBase for Point {
599        fn repository_id(&self) -> &str {
600            "IDL:Geo/Point:1.0"
601        }
602    }
603    impl ValueMarshal for Point {
604        fn marshal_state(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
605            self.x.encode(w)?;
606            self.y.encode(w)
607        }
608    }
609    fn point_registry() -> ValueRegistry {
610        let mut reg = ValueRegistry::new();
611        reg.register(
612            "IDL:Geo/Point:1.0",
613            Box::new(|r: &mut BufferReader<'_>| {
614                let x = i32::decode(r)?;
615                let y = i32::decode(r)?;
616                Ok(Rc::new(Point { x, y }) as Rc<dyn Any>)
617            }),
618        );
619        reg
620    }
621
622    // valuetype with JacORB RepositoryId for the cross-ORB capture comparison.
623    #[derive(Debug, PartialEq, Eq)]
624    struct JPoint {
625        x: i32,
626        y: i32,
627    }
628    impl ValueBase for JPoint {
629        fn repository_id(&self) -> &str {
630            "IDL:Point:1.0"
631        }
632    }
633    impl ValueMarshal for JPoint {
634        fn marshal_state(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
635            self.x.encode(w)?;
636            self.y.encode(w)
637        }
638    }
639
640    /// Cross-ORB conformance: ZeroDDS must marshal byte-identically to JacORB 3.9
641    /// (the reference ORB). Golden vector from a real
642    /// `org.omg.CORBA_2_3.portable.OutputStream.write_value` capture on the Linux test host:
643    /// `Point(42,-7)`, big-endian. value_tag 0x7fffff02 + CDR string "IDL:Point:1.0\0"
644    /// + align(4) pad + long(42) + long(-7).
645    #[test]
646    fn jacorb_capture_byte_identical() {
647        let mut w = BufferWriter::new(Endianness::Big);
648        let mut vw = ValueWriter::new();
649        let p: Rc<dyn ValueMarshal> = Rc::new(JPoint { x: 42, y: -7 });
650        vw.write(&mut w, Some(&p)).unwrap();
651        let bytes = w.into_bytes();
652        let hex: String = bytes.iter().map(|b| format!("{b:02x}")).collect();
653        assert_eq!(
654            hex, "7fffff020000000e49444c3a506f696e743a312e300000000000002afffffff9",
655            "ZeroDDS-Valuetype-Wire weicht von JacORB-Capture ab"
656        );
657    }
658
659    // Truncatable hierarchy: valuetype Derived : truncatable Base.
660    // Base { long id; }; Derived { + string extra; } — state base-first.
661    #[derive(Debug, PartialEq, Eq)]
662    struct Base {
663        id: i32,
664    }
665    impl ValueBase for Base {
666        fn repository_id(&self) -> &str {
667            "IDL:Base:1.0"
668        }
669    }
670    impl ValueMarshal for Base {
671        fn marshal_state(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
672            self.id.encode(w)
673        }
674    }
675    #[derive(Debug, PartialEq, Eq)]
676    struct Derived {
677        id: i32,
678        extra: alloc::string::String,
679    }
680    impl ValueBase for Derived {
681        fn repository_id(&self) -> &str {
682            "IDL:Derived:1.0"
683        }
684    }
685    impl ValueMarshal for Derived {
686        fn marshal_state(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
687            self.id.encode(w)?; // base state first (§15.3.4.3)
688            self.extra.encode(w)
689        }
690    }
691    fn base_ctor(reg: &mut ValueRegistry) {
692        reg.register(
693            "IDL:Base:1.0",
694            Box::new(|r: &mut BufferReader<'_>| {
695                Ok(Rc::new(Base {
696                    id: i32::decode(r)?,
697                }) as Rc<dyn Any>)
698            }),
699        );
700    }
701    fn derived_ctor(reg: &mut ValueRegistry) {
702        reg.register(
703            "IDL:Derived:1.0",
704            Box::new(|r: &mut BufferReader<'_>| {
705                let id = i32::decode(r)?;
706                let extra = alloc::string::String::decode(r)?;
707                Ok(Rc::new(Derived { id, extra }) as Rc<dyn Any>)
708            }),
709        );
710    }
711
712    // Golden vector: JacORB 3.9 write_value(Derived(42,"hi"), "IDL:Derived:1.0"),
713    // truncatable → chunked + repo-id list (capture from the Linux test host, 68 bytes, BE).
714    const JACORB_CHUNKED: &str = "7fffff0e000000020000001049444c3a446572697665643a312e30000000000d49444c3a426173653a312e30000000000000000b0000002a0000000368690000ffffffff";
715
716    #[test]
717    fn chunked_encode_byte_identical_to_jacorb() {
718        let mut w = BufferWriter::new(Endianness::Big);
719        let mut vw = ValueWriter::new();
720        let d: Rc<dyn ValueMarshal> = Rc::new(Derived {
721            id: 42,
722            extra: "hi".into(),
723        });
724        vw.write_chunked(&mut w, Some(&d), &["IDL:Base:1.0"])
725            .unwrap();
726        let hex: alloc::string::String = w
727            .into_bytes()
728            .iter()
729            .map(|b| alloc::format!("{b:02x}"))
730            .collect();
731        assert_eq!(
732            hex, JACORB_CHUNKED,
733            "chunked-Wire weicht von JacORB-Capture ab"
734        );
735    }
736
737    fn jacorb_chunked_bytes() -> alloc::vec::Vec<u8> {
738        (0..JACORB_CHUNKED.len() / 2)
739            .map(|i| u8::from_str_radix(&JACORB_CHUNKED[2 * i..2 * i + 2], 16).unwrap())
740            .collect()
741    }
742
743    #[test]
744    fn chunked_decode_full_when_derived_known() {
745        let bytes = jacorb_chunked_bytes();
746        let mut reg = ValueRegistry::new();
747        derived_ctor(&mut reg);
748        base_ctor(&mut reg);
749        let mut r = BufferReader::new(&bytes, Endianness::Big);
750        let v = ValueReader::new().read(&mut r, 0, &reg).unwrap().unwrap();
751        assert_eq!(
752            *v.downcast_ref::<Derived>().unwrap(),
753            Derived {
754                id: 42,
755                extra: "hi".into()
756            }
757        );
758    }
759
760    #[test]
761    fn chunked_decode_truncates_to_base() {
762        // Only Base known → most-derived (Derived) is skipped, truncation.
763        let bytes = jacorb_chunked_bytes();
764        let mut reg = ValueRegistry::new();
765        base_ctor(&mut reg);
766        let mut r = BufferReader::new(&bytes, Endianness::Big);
767        let v = ValueReader::new().read(&mut r, 0, &reg).unwrap().unwrap();
768        assert_eq!(*v.downcast_ref::<Base>().unwrap(), Base { id: 42 });
769    }
770
771    #[test]
772    fn chunked_roundtrip_zerodds_to_zerodds() {
773        for e in [Endianness::Big, Endianness::Little] {
774            let mut w = BufferWriter::new(e);
775            let d: Rc<dyn ValueMarshal> = Rc::new(Derived {
776                id: 7,
777                extra: "xyz".into(),
778            });
779            ValueWriter::new()
780                .write_chunked(&mut w, Some(&d), &["IDL:Base:1.0"])
781                .unwrap();
782            let bytes = w.into_bytes();
783            let mut reg = ValueRegistry::new();
784            derived_ctor(&mut reg);
785            base_ctor(&mut reg);
786            let mut r = BufferReader::new(&bytes, e);
787            let v = ValueReader::new().read(&mut r, 0, &reg).unwrap().unwrap();
788            assert_eq!(
789                *v.downcast_ref::<Derived>().unwrap(),
790                Derived {
791                    id: 7,
792                    extra: "xyz".into()
793                }
794            );
795        }
796    }
797
798    #[test]
799    fn single_value_and_null_roundtrip() {
800        for e in [Endianness::Big, Endianness::Little] {
801            let mut w = BufferWriter::new(e);
802            let mut vw = ValueWriter::new();
803            let p: Rc<dyn ValueMarshal> = Rc::new(Point { x: 3, y: 7 });
804            vw.write(&mut w, Some(&p)).unwrap();
805            vw.write(&mut w, None).unwrap(); // null
806            let bytes = w.into_bytes();
807            // value_tag = 0x7fffff02 (single repo-id).
808            let mut r = BufferReader::new(&bytes, e);
809            let mut vr = ValueReader::new();
810            let reg = point_registry();
811            let v = vr.read(&mut r, 0, &reg).unwrap().expect("non-null");
812            let pt = v.downcast_ref::<Point>().expect("Point");
813            assert_eq!(*pt, Point { x: 3, y: 7 });
814            assert!(vr.read(&mut r, 0, &reg).unwrap().is_none()); // null
815        }
816    }
817
818    #[test]
819    fn value_sharing_indirection() {
820        // The same Rc instance written twice → the second is an indirection.
821        // On reading, both must resolve to the SAME Rc instance.
822        for e in [Endianness::Big, Endianness::Little] {
823            let p: Rc<dyn ValueMarshal> = Rc::new(Point { x: 1, y: 2 });
824            let mut w = BufferWriter::new(e);
825            let mut vw = ValueWriter::new();
826            vw.write(&mut w, Some(&p)).unwrap();
827            vw.write(&mut w, Some(&p)).unwrap(); // same instance → indirection
828            let bytes = w.into_bytes();
829
830            let mut r = BufferReader::new(&bytes, e);
831            let mut vr = ValueReader::new();
832            let reg = point_registry();
833            let a = vr.read(&mut r, 0, &reg).unwrap().unwrap();
834            let b = vr.read(&mut r, 0, &reg).unwrap().unwrap();
835            assert!(
836                Rc::ptr_eq(&a, &b),
837                "value sharing: both refs = one instance"
838            );
839            assert_eq!(*a.downcast_ref::<Point>().unwrap(), Point { x: 1, y: 2 });
840        }
841    }
842
843    #[test]
844    fn distinct_values_are_not_shared() {
845        let p1: Rc<dyn ValueMarshal> = Rc::new(Point { x: 1, y: 1 });
846        let p2: Rc<dyn ValueMarshal> = Rc::new(Point { x: 2, y: 2 });
847        let mut w = BufferWriter::new(Endianness::Big);
848        let mut vw = ValueWriter::new();
849        vw.write(&mut w, Some(&p1)).unwrap();
850        vw.write(&mut w, Some(&p2)).unwrap();
851        let bytes = w.into_bytes();
852        let mut r = BufferReader::new(&bytes, Endianness::Big);
853        let mut vr = ValueReader::new();
854        let reg = point_registry();
855        let a = vr.read(&mut r, 0, &reg).unwrap().unwrap();
856        let b = vr.read(&mut r, 0, &reg).unwrap().unwrap();
857        assert!(!Rc::ptr_eq(&a, &b));
858        assert_eq!(*a.downcast_ref::<Point>().unwrap(), Point { x: 1, y: 1 });
859        assert_eq!(*b.downcast_ref::<Point>().unwrap(), Point { x: 2, y: 2 });
860    }
861
862    #[test]
863    fn value_tag_is_single_repo_id_on_wire() {
864        let p: Rc<dyn ValueMarshal> = Rc::new(Point { x: 0, y: 0 });
865        let mut w = BufferWriter::new(Endianness::Big);
866        ValueWriter::new().write(&mut w, Some(&p)).unwrap();
867        let bytes = w.into_bytes();
868        // Erste 4 Byte = value_tag 0x7fffff02 (BE).
869        assert_eq!(&bytes[0..4], &[0x7f, 0xff, 0xff, 0x02]);
870    }
871
872    #[test]
873    fn forward_indirection_rejected() {
874        let mut w = BufferWriter::new(Endianness::Big);
875        w.write_u32(VALUE_INDIRECTION).unwrap();
876        w.write_u32(4).unwrap(); // positive offset
877        let bytes = w.into_bytes();
878        let mut r = BufferReader::new(&bytes, Endianness::Big);
879        assert!(
880            ValueReader::new()
881                .read(&mut r, 0, &ValueRegistry::new())
882                .is_err()
883        );
884    }
885
886    // ---- §4.4 codebase-URL ----------------------------------------------------
887
888    #[test]
889    fn codebase_value_tag_and_roundtrip() {
890        let mut w = BufferWriter::new(Endianness::Big);
891        let p: Rc<dyn ValueMarshal> = Rc::new(Point { x: 5, y: 9 });
892        ValueWriter::new()
893            .write_with_codebase(&mut w, Some(&p), "file:///stubs.jar")
894            .unwrap();
895        let bytes = w.into_bytes();
896        // value_tag 0x7fffff03 (single repo-id + codebase flag).
897        assert_eq!(&bytes[0..4], &[0x7f, 0xff, 0xff, 0x03]);
898        // Reads back correctly with a normal registry (RepositoryId known).
899        let mut r = BufferReader::new(&bytes, Endianness::Big);
900        let reg = point_registry();
901        let v = ValueReader::new().read(&mut r, 0, &reg).unwrap().unwrap();
902        assert_eq!(*v.downcast_ref::<Point>().unwrap(), Point { x: 5, y: 9 });
903    }
904
905    #[test]
906    fn codebase_resolver_supplies_missing_factory() {
907        let mut w = BufferWriter::new(Endianness::Big);
908        let p: Rc<dyn ValueMarshal> = Rc::new(Point { x: 1, y: 2 });
909        ValueWriter::new()
910            .write_with_codebase(&mut w, Some(&p), "ior://factory-host/Point")
911            .unwrap();
912        let bytes = w.into_bytes();
913
914        // Registry WITHOUT a Point factory, but WITH a codebase resolver that
915        // supplies it based on the codebase URL.
916        let mut reg = ValueRegistry::new();
917        reg.set_codebase_resolver(Box::new(|codebase: &str, repo_id: &str| {
918            if codebase.contains("factory-host") && repo_id == "IDL:Geo/Point:1.0" {
919                Some(Box::new(|r: &mut BufferReader<'_>| {
920                    let x = i32::decode(r)?;
921                    let y = i32::decode(r)?;
922                    Ok(Rc::new(Point { x, y }) as Rc<dyn Any>)
923                }) as ValueCtor)
924            } else {
925                None
926            }
927        }));
928
929        let mut r = BufferReader::new(&bytes, Endianness::Big);
930        let v = ValueReader::new().read(&mut r, 0, &reg).unwrap().unwrap();
931        assert_eq!(*v.downcast_ref::<Point>().unwrap(), Point { x: 1, y: 2 });
932
933        // Without a resolver: the same bytes → error (no factory).
934        let mut r2 = BufferReader::new(&bytes, Endianness::Big);
935        assert!(
936            ValueReader::new()
937                .read(&mut r2, 0, &ValueRegistry::new())
938                .is_err()
939        );
940    }
941
942    // ---- §4.4 nested chunked values + multi-level end-tags ------------
943
944    /// Builds a chunked `Derived` value (repo-ids [Derived, Base]) whose
945    /// derived tail contains a **nested chunked value** (`IDL:Inner:1.0`),
946    /// terminated with its own end-tag (-2), then the outer end-tag (-1).
947    fn chunked_with_nested_tail(e: Endianness) -> alloc::vec::Vec<u8> {
948        let mut w = BufferWriter::new(e);
949        w.write_u32(VALUE_TAG_BASE | VT_FLAG_CHUNKED | VT_REPO_LIST)
950            .unwrap(); // 0x7fffff0e
951        w.write_u32(2).unwrap();
952        w.write_string("IDL:Derived:1.0").unwrap();
953        w.write_string("IDL:Base:1.0").unwrap();
954        // Data chunk 1: Base state (long id = 42).
955        w.align(4);
956        w.write_u32(4).unwrap();
957        w.write_u32(42).unwrap();
958        // Nested chunked value in the derived tail.
959        w.write_u32(VALUE_TAG_BASE | VT_FLAG_CHUNKED | VT_REPO_LIST)
960            .unwrap();
961        w.write_u32(1).unwrap();
962        w.write_string("IDL:Inner:1.0").unwrap();
963        w.align(4);
964        w.write_u32(4).unwrap(); // nested chunk size
965        w.write_u32(0x0bad_cafe).unwrap(); // nested state
966        w.align(4);
967        w.write_u32((-2i32) as u32).unwrap(); // nested end-tag (depth 2)
968        // Outer end-tag (depth 1).
969        w.align(4);
970        w.write_u32((-1i32) as u32).unwrap();
971        w.into_bytes()
972    }
973
974    #[test]
975    fn nested_chunked_value_in_tail_is_consumed_on_truncation() {
976        for e in [Endianness::Big, Endianness::Little] {
977            let bytes = chunked_with_nested_tail(e);
978            // Only Base known → truncation to Base; the nested value in the
979            // tail must be consumed correctly (no error).
980            let mut reg = ValueRegistry::new();
981            base_ctor(&mut reg);
982            let mut r = BufferReader::new(&bytes, e);
983            let v = ValueReader::new().read(&mut r, 0, &reg).unwrap().unwrap();
984            assert_eq!(*v.downcast_ref::<Base>().unwrap(), Base { id: 42 });
985            // Entire value stream consumed (position at the end).
986            assert_eq!(r.position(), bytes.len());
987        }
988    }
989
990    #[test]
991    fn shared_end_tag_closes_nested_and_outer() {
992        // Multi-level end-tag: ONE end-tag -1 closes the nested value
993        // (depth 2) AND the outer one (depth 1) together (§15.3.4.3).
994        let e = Endianness::Big;
995        let mut w = BufferWriter::new(e);
996        w.write_u32(VALUE_TAG_BASE | VT_FLAG_CHUNKED | VT_REPO_LIST)
997            .unwrap();
998        w.write_u32(2).unwrap();
999        w.write_string("IDL:Derived:1.0").unwrap();
1000        w.write_string("IDL:Base:1.0").unwrap();
1001        w.align(4);
1002        w.write_u32(4).unwrap();
1003        w.write_u32(42).unwrap(); // Base.id
1004        // Nested value …
1005        w.write_u32(VALUE_TAG_BASE | VT_FLAG_CHUNKED | VT_REPO_LIST)
1006            .unwrap();
1007        w.write_u32(1).unwrap();
1008        w.write_string("IDL:Inner:1.0").unwrap();
1009        w.align(4);
1010        w.write_u32(4).unwrap();
1011        w.write_u32(0x0bad_cafe).unwrap();
1012        // … shared end-tag -1 closes nested (2) AND outer (1).
1013        w.align(4);
1014        w.write_u32((-1i32) as u32).unwrap();
1015        let bytes = w.into_bytes();
1016
1017        let mut reg = ValueRegistry::new();
1018        base_ctor(&mut reg);
1019        let mut r = BufferReader::new(&bytes, e);
1020        let v = ValueReader::new().read(&mut r, 0, &reg).unwrap().unwrap();
1021        assert_eq!(*v.downcast_ref::<Base>().unwrap(), Base { id: 42 });
1022        assert_eq!(r.position(), bytes.len());
1023    }
1024
1025    // ---- multi-chunk ENCODE (write_chunked_tree) ----------------------------
1026
1027    /// A value whose `value_tag` lists `IDL:Derived:1.0` + a base, but whose
1028    /// leading state is just the 4-byte base `id` (the derived tail lives in
1029    /// nested chunks). Mirrors the `chunked_with_nested_tail` hand-built wire.
1030    #[derive(Debug)]
1031    struct OuterBaseState {
1032        id: i32,
1033    }
1034    impl ValueBase for OuterBaseState {
1035        fn repository_id(&self) -> &str {
1036            "IDL:Derived:1.0"
1037        }
1038    }
1039    impl ValueMarshal for OuterBaseState {
1040        fn marshal_state(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
1041            self.id.encode(w)
1042        }
1043    }
1044
1045    /// A nested chunked value (repo `IDL:Inner:1.0`) holding one 4-byte word.
1046    #[derive(Debug)]
1047    struct InnerVal {
1048        word: u32,
1049    }
1050    impl ValueBase for InnerVal {
1051        fn repository_id(&self) -> &str {
1052            "IDL:Inner:1.0"
1053        }
1054    }
1055    impl ValueMarshal for InnerVal {
1056        fn marshal_state(&self, w: &mut BufferWriter) -> Result<(), EncodeError> {
1057            w.write_u32(self.word)
1058        }
1059    }
1060
1061    #[test]
1062    fn chunked_tree_leaf_is_byte_identical_to_write_chunked() {
1063        // A node with no nested children must produce exactly the same wire as
1064        // the single-chunk write_chunked (regression guard for the shared path).
1065        for e in [Endianness::Big, Endianness::Little] {
1066            let d: Rc<dyn ValueMarshal> = Rc::new(Derived {
1067                id: 42,
1068                extra: "hi".into(),
1069            });
1070
1071            let mut w1 = BufferWriter::new(e);
1072            ValueWriter::new()
1073                .write_chunked(&mut w1, Some(&d), &["IDL:Base:1.0"])
1074                .unwrap();
1075
1076            let mut w2 = BufferWriter::new(e);
1077            let node = ChunkedNode {
1078                value: &d,
1079                base_ids: &["IDL:Base:1.0"],
1080                nested: &[],
1081            };
1082            ValueWriter::new()
1083                .write_chunked_tree(&mut w2, &node)
1084                .unwrap();
1085
1086            assert_eq!(
1087                w1.into_bytes(),
1088                w2.into_bytes(),
1089                "leaf tree != write_chunked"
1090            );
1091        }
1092    }
1093
1094    #[test]
1095    fn chunked_tree_with_nested_matches_handbuilt_wire() {
1096        // The encoder reproduces the exact multi-chunk wire that the
1097        // nested-chunk DECODE tests validate (chunked_with_nested_tail):
1098        // outer [Derived,Base] data chunk(42) + nested Inner chunk(0x0badcafe)
1099        // + nested end-tag -2 + outer end-tag -1.
1100        for e in [Endianness::Big, Endianness::Little] {
1101            let inner: Rc<dyn ValueMarshal> = Rc::new(InnerVal { word: 0x0bad_cafe });
1102            let outer: Rc<dyn ValueMarshal> = Rc::new(OuterBaseState { id: 42 });
1103            let inner_node = ChunkedNode {
1104                value: &inner,
1105                base_ids: &[],
1106                nested: &[],
1107            };
1108            let outer_node = ChunkedNode {
1109                value: &outer,
1110                base_ids: &["IDL:Base:1.0"],
1111                nested: core::slice::from_ref(&inner_node),
1112            };
1113            let mut w = BufferWriter::new(e);
1114            ValueWriter::new()
1115                .write_chunked_tree(&mut w, &outer_node)
1116                .unwrap();
1117            assert_eq!(
1118                w.into_bytes(),
1119                chunked_with_nested_tail(e),
1120                "multi-chunk encode != hand-built nested-tail wire"
1121            );
1122        }
1123    }
1124
1125    #[test]
1126    fn chunked_tree_roundtrips_with_base_truncation() {
1127        // Encoder output decodes: a base-only reader truncates to the outer
1128        // base value and consumes the entire nested tail.
1129        for e in [Endianness::Big, Endianness::Little] {
1130            let inner: Rc<dyn ValueMarshal> = Rc::new(InnerVal { word: 0x0bad_cafe });
1131            let outer: Rc<dyn ValueMarshal> = Rc::new(OuterBaseState { id: 99 });
1132            let inner_node = ChunkedNode {
1133                value: &inner,
1134                base_ids: &[],
1135                nested: &[],
1136            };
1137            let outer_node = ChunkedNode {
1138                value: &outer,
1139                base_ids: &["IDL:Base:1.0"],
1140                nested: core::slice::from_ref(&inner_node),
1141            };
1142            let mut w = BufferWriter::new(e);
1143            ValueWriter::new()
1144                .write_chunked_tree(&mut w, &outer_node)
1145                .unwrap();
1146            let bytes = w.into_bytes();
1147
1148            let mut reg = ValueRegistry::new();
1149            base_ctor(&mut reg);
1150            let mut r = BufferReader::new(&bytes, e);
1151            let v = ValueReader::new().read(&mut r, 0, &reg).unwrap().unwrap();
1152            assert_eq!(*v.downcast_ref::<Base>().unwrap(), Base { id: 99 });
1153            assert_eq!(r.position(), bytes.len(), "nested tail not fully consumed");
1154        }
1155    }
1156}