zerodds_wire_codec/lib.rs
1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Crate `zerodds-wire-codec`. Safety classification: **SAFE**.
4//!
5//! One layout plan, one value, the bytes between them — in both
6//! directions, over any representation of a value.
7//!
8//! ## What is here and what is not
9//!
10//! [`crate::encode()`] walks the plan and writes; [`crate::decode()`] walks
11//! the same plan and reads. Neither decides a DHEADER, an EMHEADER, a
12//! length code or a dialect answer: all of those are
13//! `zerodds_ast_layout`'s, and a walk that decided them again would
14//! agree with the plan only until one of the two changed.
15//!
16//! [`crate::contract`] is the single answer to what a value *means* —
17//! whether a key names a member, whether `null` is a value, whether an
18//! integer is expressible — because three backends ask it and a
19//! difference between their answers would be a difference in the sample
20//! read as a difference in the serialisers.
21//!
22//! ## Why it is generic over the value
23//!
24//! The walk was already generic on the type side: `&Ast`, an
25//! `Id<TypeNode>` and a `LayoutKey`. It was concrete only on the value
26//! side, against `serde_json::Value`, and a second value representation
27//! — a `DynamicData` — would otherwise have needed a second copy of
28//! every framing decision this crate makes once.
29//!
30//! So the value side is two traits: [`source::Source`] for the direction
31//! that reads a value and writes bytes, [`sink::Sink`] for the direction
32//! that reads bytes and builds a value. `zerodds-wire-probe` implements
33//! both for `serde_json::Value`.
34//!
35//! **Nothing here fixes a coordinate.** A [`zerodds_ast_layout::WirePosition`],
36//! a dialect setting and a [`zerodds_ast::PrimitiveKind`] are arguments
37//! at every site that has one, including inside the traits — a
38//! representation that wants to know the width it is being handed is
39//! told, and a representation that does not may ignore it. A coordinate
40//! passed as a literal "because it is always the same here" is how the
41//! framing defect survived five emitters.
42//!
43//! ## Layer position
44//!
45//! A tool over `zerodds-ast-layout` and `zerodds-wire`. It names no
46//! parser: turning IDL text into a model is the caller's business.
47#![forbid(unsafe_code)]
48
49pub mod canonical;
50pub mod contract;
51pub mod decode;
52pub mod default;
53pub mod encode;
54#[cfg(feature = "serde-json")]
55pub mod json;
56pub mod sink;
57pub mod source;
58pub mod value;
59
60/// How many nested aggregates a single value may hold before either
61/// walk refuses it.
62///
63/// ## What it is for
64///
65/// A recursive type is legal — IDL 4.2 §7.4.1.4.4.4.4 for a cycle
66/// through a collection, and the same property in the other form for a
67/// cycle through an `@optional` member — because every *value* of one
68/// carries its own terminator: a sequence of length zero, an absent
69/// member. The type is a graph and the value is a tree.
70///
71/// A value whose graph really does have a cycle — `a.next = a` — has no
72/// terminator anywhere in it, and XTypes 1.3 §7.3.1.2.1.4 says of
73/// exactly that case "if the graph of objects has cycles, it is not
74/// serializable". Neither walk can see the cycle: a [`source::Source`]
75/// hands the encoder one member at a time and a byte stream hands the
76/// decoder one header at a time, and in both directions the shape is
77/// only ever known one step ahead. So the walks count, and a value past
78/// the count is refused by name — [`encode::ProbeError::ValueTooDeep`]
79/// — rather than truncated, looped on, or run into the stack. A silent
80/// prefix of an infinite stream is the failure class this project ranks
81/// above every other.
82///
83/// ## Why this number
84///
85/// Both walks are recursive descent — one `write_aggregate` frame or
86/// one `read_aggregate` frame per level — so the real ceiling is the
87/// thread's stack, and the limit has to sit below it with room to
88/// spare. Measured 2026-08-26 with the guard raised out of the way, in
89/// an unoptimized build on the 2 MiB stack the test harness gives a
90/// spawned thread, over the `Node` chain of
91/// `matrix/cases/recursive-optional-external`:
92///
93/// | direction | deepest chain that completed | overflowed at |
94/// |---|---|---|
95/// | decode | 160 | between 160 and 176 |
96/// | encode | 208 | between 208 and 224 |
97///
98/// The decoder is the tighter of the two and the one that faces a
99/// hostile peer, so it sets the number: 64 leaves a factor of about
100/// 2.5 against the worst measured case, and more than that in a release
101/// build or on a main thread's larger stack.
102///
103/// It is a bound on legitimate values too, and that is stated rather
104/// than hidden: a sample carrying a linked list of more than 64 nodes
105/// is refused by this walk. The deepest aggregate nesting in the
106/// interop corpus is four. Raising the number means raising the
107/// measurement above with it, or making the walks iterative.
108///
109/// It is deliberately *not* `zerodds_ast_layout::plan::MAX_DEPTH`, which
110/// bounds a descent through the **type** graph and answers a different
111/// question: the type of a thousand-node list is two levels deep.
112pub const MAX_VALUE_DEPTH: usize = 64;
113
114pub use canonical::{Difference, canonicalise};
115pub use decode::{decode, decode_continuing, decode_prefix};
116pub use default::of_type as default_of_type;
117pub use encode::{ProbeError, encode, encode_continuing};
118pub use sink::{Aggregate, Sink};
119pub use source::{Fields, Source};
120pub use value::{Emit, ProbeRequest};
121
122#[cfg(feature = "serde-json")]
123pub use json::EXACT_JSON_INTEGER;