Skip to main content

rustbinary/
lib.rs

1#![deny(unsafe_op_in_unsafe_fn)]
2#![warn(missing_docs)]
3#![cfg_attr(docsrs, feature(doc_cfg))]
4#![cfg_attr(not(feature = "std"), no_std)]
5
6//! `RustBinary` is a bounded **nextjson** binary codec with explicit wire
7//! profiles.
8//!
9//! Serialization is driven entirely by nextjson's format-neutral contracts
10//! ([`nextjson::NsonSerialize`] / [`nextjson::NsonDeserialize`] +
11//! [`nextjson::FormatEncoder`] / [`nextjson::FormatDecoder`]), replacing the
12//! former Serde dependency. The binary wire format is a type-tagged,
13//! self-describing stream: every value carries a one-byte type tag and
14//! containers are terminator-delimited, so `Option`, `Value`, untagged enums
15//! and borrowed strings all round-trip unambiguously.
16//!
17//! The top-level functions and [`options`] select the strict compact profile:
18//! canonical marker varints, ZigZag signed integers, bounded input, and
19//! rejected trailing bytes. [`legacy_options`] explicitly selects the old
20//! fixed-width, unbounded migration profile. Format-changing systems are
21//! explicit wrappers, so enabling a Cargo feature never silently changes an
22//! existing payload.
23//!
24//! # Quick start
25//!
26//! ```
27//! use nextjson::{NsonDeserialize, NsonSerialize};
28//!
29//! #[derive(Debug, PartialEq, NsonSerialize, NsonDeserialize)]
30//! struct Packet<'a> {
31//!     sequence: u64,
32//!     topic: &'a str,
33//!     #[njson(borrow)]
34//!     note: &'a str,
35//! }
36//!
37//! let config = rustbinary::options()
38//!     .with_limit(4096)
39//!     .with_collection_limit(256);
40//! let packet = Packet {
41//!     sequence: 42,
42//!     topic: "telemetry/temperature",
43//!     note: "ok",
44//! };
45//!
46//! let mut frame = [0_u8; 256];
47//! let written = config.serialize_into_slice(&mut frame, &packet)?;
48//! let decoded: Packet<'_> = config.deserialize(&frame[..written])?;
49//! assert_eq!(decoded, packet);
50//! # Ok::<(), rustbinary::Error>(())
51//! ```
52//!
53//! Borrowed strings point into the input frame. Owned targets such as
54//! `String` and `Vec<T>` may allocate as required by their type.
55//!
56//! # Format selection
57//!
58//! - [`Config`] is the core binary profile.
59//! - `adaptive` contains canonical cost-selected string and integer frames.
60//! - `bitpack` provides generated bit-level layouts.
61//! - `cbor` provides RFC 8949 payloads and deterministic map ordering.
62//! - `evolution` provides stable-field-ID schema evolution.
63//! - `compression` and `encryption` form an ordered transform pipeline.
64//! - `parallel` encodes independent records into deterministic batch frames.
65//!
66//! # Untrusted input
67//!
68//! Always set both [`Config::with_limit`] and
69//! [`Config::with_collection_limit`] at trust boundaries. Encryption authenticates
70//! bytes but does not replace resource limits. Schema fingerprints detect
71//! accidental type/configuration drift; they are not cryptographic hashes.
72
73extern crate self as rustbinary;
74
75// nextjson's `FormatDecoder` contract returns `Cow<'de, str>`, so the core
76// always links `alloc` (matching nextjson, which is `no_std` + `alloc`).
77// The `alloc` Cargo feature remains as a compatibility marker.
78extern crate alloc;
79
80#[cfg(feature = "std")]
81/// Bridges between the slice-based core and `std::io` readers and writers.
82pub mod adapters;
83
84#[cfg(feature = "adaptive")]
85/// Canonical data-aware encodings for strings and integer collections.
86pub mod adaptive;
87#[cfg(feature = "archive")]
88/// Validated relative-pointer archives for read-only memory mapping.
89pub mod archive;
90#[cfg(feature = "bit-packing")]
91/// Bit-level caller-buffer codecs and the [`BitPack`] contract.
92pub mod bitpack;
93#[cfg(feature = "cbor")]
94/// RFC 8949 CBOR configuration and deterministic encoding.
95pub mod cbor;
96#[cfg(feature = "compression")]
97/// Adaptive Zstandard framing.
98pub mod compression;
99/// Core wire-profile configuration.
100pub mod config;
101/// Minimal stable binary codec product surface.
102pub mod core;
103mod decoder;
104#[cfg(feature = "encryption")]
105/// Authenticated XChaCha20-Poly1305 framing.
106pub mod encryption;
107/// Codec result and error types.
108pub mod error;
109#[cfg(feature = "schema-evolution")]
110/// Stable-field-ID schema evolution.
111pub mod evolution;
112#[cfg(feature = "fingerprint")]
113mod frame;
114#[cfg(feature = "parallel")]
115/// Ordered multi-core batch encoding and decoding.
116pub mod parallel;
117/// Optional transform product surface.
118pub mod pipeline;
119/// Schema and wire-governance product surface.
120pub mod protocol;
121#[cfg(feature = "reflection")]
122/// Allocation-free structural metadata generated by [`Reflect`].
123pub mod reflection;
124#[cfg(feature = "fingerprint")]
125/// Compile-time schema identity and fingerprinted frame support.
126pub mod schema;
127mod ser;
128#[cfg(feature = "simd")]
129pub mod simd;
130#[cfg(feature = "static-size")]
131/// Compile-time upper bounds for statically sized data.
132pub mod static_size;
133/// Shared wire-format tag constants.
134mod tags;
135/// Core output sinks for caller-owned and counting serialization.
136pub mod writer;
137
138#[cfg(feature = "alloc")]
139use alloc::vec::Vec;
140#[cfg(feature = "std")]
141use std::io::{Read, Write};
142
143#[cfg(feature = "adaptive")]
144pub use adaptive::{AdaptiveConfig, CollectionStrategy, StringStrategy};
145#[cfg(feature = "bit-packing")]
146pub use bitpack::{BitPack, BitPackedConfig, BitReader, BitValue, BitWriter};
147#[cfg(feature = "cbor")]
148pub use cbor::CborConfig;
149#[cfg(all(feature = "cbor", feature = "fingerprint"))]
150pub use cbor::FingerprintedCborConfig;
151#[cfg(feature = "compression")]
152pub use compression::CompressedConfig;
153pub use config::{
154    Config, Endian, IntEncoding, Options, TrailingBytes, DEFAULT_COLLECTION_LIMIT,
155    DEFAULT_SIZE_LIMIT,
156};
157#[cfg(feature = "encryption")]
158pub use encryption::{EncryptedConfig, EncryptionKey};
159pub use error::{Error, ErrorCategory, Result};
160#[cfg(feature = "schema-evolution")]
161pub use evolution::{
162    EvolutionConfig, FieldDecoder, FieldEncoder, SchemaDecode, SchemaEncode, UnknownField,
163};
164#[cfg(feature = "parallel")]
165pub use parallel::ParallelConfig;
166#[cfg(feature = "reflection")]
167pub use reflection::{FieldInfo, Reflect, TypeShape, VariantInfo};
168#[cfg(feature = "fingerprint")]
169pub use schema::{Fingerprint, FingerprintedConfig};
170#[cfg(feature = "simd")]
171pub use simd::{hardware_capabilities, simd_backend, HardwareCapabilities, SimdBackend};
172#[cfg(feature = "static-size")]
173pub use static_size::StaticSize;
174pub use writer::{CountWriter, EncodeWriter, SliceWriter};
175
176#[cfg(all(feature = "derive", feature = "bit-packing"))]
177pub use rustbinary_derive::BitPacked;
178
179#[cfg(feature = "bit-packing")]
180#[doc(hidden)]
181pub const fn __bitpack_max(left: usize, right: usize) -> usize {
182    if left > right {
183        left
184    } else {
185        right
186    }
187}
188#[cfg(all(feature = "derive", feature = "fingerprint"))]
189pub use rustbinary_derive::Fingerprint;
190#[cfg(all(feature = "derive", feature = "reflection"))]
191pub use rustbinary_derive::Reflect;
192#[cfg(all(feature = "derive", feature = "static-size"))]
193pub use rustbinary_derive::StaticSize;
194
195/// Re-exports nextjson's format-neutral serialization contracts.
196///
197/// `NsonSerialize` and `NsonDeserialize` are both traits and derive macros, so
198/// `#[derive(rustbinary::NsonSerialize, rustbinary::NsonDeserialize)]` works.
199/// Generated code refers to the `::nextjson` crate, so applications must also
200/// depend on `nextjson` (the framework this codec is built on).
201#[cfg(feature = "derive")]
202pub use nextjson::{NsonDeserialize, NsonSchema, NsonSerialize};
203
204/// Returns the standard compact profile.
205pub const fn options() -> Config {
206    Config::standard()
207}
208
209/// Returns the fixed-width compatibility profile used by the top-level API.
210pub const fn legacy_options() -> Config {
211    Config::legacy()
212}
213
214/// Serializes a value with the bounded compact Core profile.
215#[cfg(feature = "alloc")]
216pub fn serialize<T: nextjson::NsonSerialize + ?Sized>(value: &T) -> Result<Vec<u8>> {
217    Config::standard().serialize(value)
218}
219
220/// Serializes a value directly into a writer with the bounded compact Core profile.
221#[cfg(feature = "std")]
222pub fn serialize_into<W: Write, T: nextjson::NsonSerialize + ?Sized>(
223    writer: W,
224    value: &T,
225) -> Result<()> {
226    Config::standard().serialize_into(writer, value)
227}
228
229/// Serializes into a caller-owned slice without codec-owned heap allocation.
230///
231/// Returns the initialized byte count. [`Error::BufferTooSmall`] contains the
232/// exact required capacity when `output` is undersized. User-defined
233/// [`nextjson::NsonSerialize`] implementations remain responsible for their own
234/// allocations.
235pub fn serialize_into_slice<T: nextjson::NsonSerialize + ?Sized>(
236    output: &mut [u8],
237    value: &T,
238) -> Result<usize> {
239    Config::standard().serialize_into_slice(output, value)
240}
241
242/// Computes the exact serialized byte count without allocating an output buffer.
243pub fn serialized_size<T: nextjson::NsonSerialize + ?Sized>(value: &T) -> Result<u64> {
244    Config::standard().serialized_size(value)
245}
246
247/// Deserializes from a slice with the bounded compact Core profile.
248///
249/// The returned value may borrow strings from `input`.
250pub fn deserialize<'de, T: nextjson::NsonDeserialize<'de>>(input: &'de [u8]) -> Result<T> {
251    Config::standard().deserialize(input)
252}
253
254/// Deserializes an owned value from a reader with the bounded compact Core profile.
255#[cfg(feature = "std")]
256pub fn deserialize_from<R: Read, T: for<'de> nextjson::NsonDeserialize<'de>>(
257    reader: R,
258) -> Result<T> {
259    Config::standard().deserialize_from(reader)
260}
261
262#[cfg(all(test, feature = "std"))]
263mod tests {
264    use std::{
265        cell::Cell,
266        collections::BTreeMap,
267        io::{self, Cursor, Write},
268    };
269
270    #[cfg(feature = "cbor")]
271    use std::collections::HashMap;
272
273    use super::*;
274
275    #[derive(Debug, PartialEq, nextjson::NsonSerialize, nextjson::NsonDeserialize)]
276    struct Record<'a> {
277        id: u64,
278        delta: i32,
279        #[njson(borrow)]
280        name: &'a str,
281        payload: Vec<u8>,
282        enabled: Option<bool>,
283    }
284
285    #[derive(Debug, PartialEq, nextjson::NsonSerialize, nextjson::NsonDeserialize)]
286    struct BorrowedEnvelope<'a> {
287        #[njson(borrow)]
288        name: &'a str,
289        #[njson(borrow)]
290        payload: &'a str,
291        nested: BorrowedMetadata<'a>,
292    }
293
294    #[derive(Debug, PartialEq, nextjson::NsonSerialize, nextjson::NsonDeserialize)]
295    struct BorrowedMetadata<'a> {
296        #[njson(borrow)]
297        source: &'a str,
298    }
299
300    #[derive(Debug, PartialEq, nextjson::NsonSerialize, nextjson::NsonDeserialize)]
301    enum Event {
302        Idle,
303        Data(u16),
304        Point { x: i64, y: i64 },
305    }
306
307    #[cfg(all(
308        feature = "fingerprint",
309        feature = "reflection",
310        feature = "static-size"
311    ))]
312    #[derive(
313        Debug,
314        nextjson::NsonSerialize,
315        nextjson::NsonDeserialize,
316        crate::Fingerprint,
317        crate::Reflect,
318        crate::StaticSize,
319    )]
320    struct ProtocolRecord {
321        enabled: bool,
322        count: u16,
323        coordinates: [i32; 2],
324    }
325
326    #[cfg(all(
327        feature = "fingerprint",
328        feature = "reflection",
329        feature = "static-size"
330    ))]
331    #[derive(nextjson::NsonSerialize, nextjson::NsonDeserialize, crate::Fingerprint)]
332    struct ChangedProtocolRecord {
333        count: u16,
334        enabled: bool,
335        coordinates: [i32; 2],
336    }
337
338    #[cfg(feature = "reflection")]
339    #[derive(crate::Reflect)]
340    enum ReflectedEvent {
341        Empty,
342        Tuple(u8, bool),
343        Named { code: u16 },
344    }
345
346    #[cfg(all(feature = "bit-packing", feature = "static-size"))]
347    #[derive(Debug, PartialEq, crate::BitPacked, crate::StaticSize)]
348    struct PackedHeader {
349        #[bits = 3]
350        mode: u8,
351        enabled: bool,
352        #[bits = 7]
353        delta: i16,
354    }
355
356    #[cfg(feature = "bit-packing")]
357    #[derive(Debug, PartialEq, crate::BitPacked)]
358    enum PackedEvent {
359        Empty,
360        Flag(bool),
361        Code(#[bits = 4] u8),
362    }
363
364    #[cfg(feature = "schema-evolution")]
365    #[derive(Debug, PartialEq)]
366    struct SchemaV1 {
367        name: String,
368        count: u32,
369    }
370
371    #[cfg(feature = "schema-evolution")]
372    impl SchemaEncode for SchemaV1 {
373        const SCHEMA_ID: u64 = 0x4859_5048_454e_0001;
374        const SCHEMA_VERSION: u32 = 1;
375
376        fn encode_fields(&self, encoder: &mut FieldEncoder) -> Result<()> {
377            // Deliberately submitted out of order; the frame must canonicalize IDs.
378            encoder.field(2, &self.count)?;
379            encoder.field(1, &self.name)
380        }
381    }
382
383    #[cfg(feature = "schema-evolution")]
384    impl<'de> SchemaDecode<'de> for SchemaV1 {
385        const SCHEMA_ID: u64 = <Self as SchemaEncode>::SCHEMA_ID;
386
387        fn decode_fields(decoder: &mut FieldDecoder<'de>, _version: u32) -> Result<Self> {
388            Ok(Self {
389                name: decoder.required(1)?,
390                count: decoder.required(2)?,
391            })
392        }
393    }
394
395    #[cfg(feature = "schema-evolution")]
396    #[derive(Debug, PartialEq)]
397    struct SchemaV2<'a> {
398        title: &'a str,
399        count: u32,
400        active: bool,
401        source_version: u32,
402    }
403
404    #[cfg(feature = "schema-evolution")]
405    impl SchemaEncode for SchemaV2<'_> {
406        const SCHEMA_ID: u64 = <SchemaV1 as SchemaEncode>::SCHEMA_ID;
407        const SCHEMA_VERSION: u32 = 2;
408
409        fn encode_fields(&self, encoder: &mut FieldEncoder) -> Result<()> {
410            encoder.field(1, self.title)?;
411            encoder.field(2, &self.count)?;
412            encoder.field(3, &self.active)
413        }
414    }
415
416    #[cfg(feature = "schema-evolution")]
417    impl<'de> SchemaDecode<'de> for SchemaV2<'de> {
418        const SCHEMA_ID: u64 = <SchemaV1 as SchemaEncode>::SCHEMA_ID;
419
420        fn decode_fields(decoder: &mut FieldDecoder<'de>, version: u32) -> Result<Self> {
421            Ok(Self {
422                title: decoder.required(1)?,
423                count: decoder.required(2)?,
424                active: decoder.or_default(3)?,
425                source_version: version,
426            })
427        }
428    }
429
430    #[cfg(feature = "schema-evolution")]
431    struct OtherSchema;
432
433    #[cfg(feature = "schema-evolution")]
434    impl<'de> SchemaDecode<'de> for OtherSchema {
435        const SCHEMA_ID: u64 = 0xdead_beef;
436
437        fn decode_fields(_decoder: &mut FieldDecoder<'de>, _version: u32) -> Result<Self> {
438            Ok(Self)
439        }
440    }
441
442    #[cfg(all(feature = "bit-packing", feature = "static-size"))]
443    #[test]
444    fn bit_packed_struct_and_enum_round_trip_within_static_bounds() {
445        let config = options().with_bit_packing();
446        for header in [
447            PackedHeader {
448                mode: 0,
449                enabled: false,
450                delta: -1,
451            },
452            PackedHeader {
453                mode: 7,
454                enabled: true,
455                delta: 63,
456            },
457        ] {
458            let bytes = config.serialize(&header).unwrap();
459            assert_eq!(config.deserialize::<PackedHeader>(&bytes).unwrap(), header);
460            assert!(bytes.len() <= PackedHeader::PACKED_MAX_SIZE);
461        }
462        const { assert!(PackedHeader::MAX_SIZE > 0) };
463        const { assert!(PackedHeader::PACKED_MAX_BITS > 0) };
464
465        for event in [
466            PackedEvent::Empty,
467            PackedEvent::Flag(true),
468            PackedEvent::Code(15),
469        ] {
470            let bytes = config.serialize(&event).unwrap();
471            assert_eq!(config.deserialize::<PackedEvent>(&bytes).unwrap(), event);
472        }
473    }
474
475    #[cfg(feature = "schema-evolution")]
476    #[test]
477    fn schema_evolution_is_forward_compatible_and_rejects_foreign_ids() {
478        let config = options().with_schema_evolution();
479
480        // V1 frame (fields submitted out of order; frame canonicalizes IDs).
481        let v1 = SchemaV1 {
482            name: "alpha".into(),
483            count: 7,
484        };
485        let v1_bytes = config.serialize(&v1).unwrap();
486
487        // V2 decodes a V1 frame: the new `active` field defaults and the
488        // encoded revision is reported for explicit migrations.
489        let decoded_v2: SchemaV2<'_> = config.deserialize(&v1_bytes).unwrap();
490        assert_eq!(
491            decoded_v2,
492            SchemaV2 {
493                title: "alpha",
494                count: 7,
495                active: false,
496                source_version: SchemaV1::SCHEMA_VERSION,
497            }
498        );
499
500        // V2 round-trips its own frame with the encoded revision reported.
501        let v2 = SchemaV2 {
502            title: "beta",
503            count: 9,
504            active: true,
505            source_version: 0,
506        };
507        let v2_bytes = config.serialize(&v2).unwrap();
508        assert_eq!(
509            config.deserialize::<SchemaV2<'_>>(&v2_bytes).unwrap(),
510            SchemaV2 {
511                title: "beta",
512                count: 9,
513                active: true,
514                source_version: SchemaV2::SCHEMA_VERSION,
515            }
516        );
517
518        // A schema with a different stable ID must reject the frame.
519        assert!(matches!(
520            config.deserialize::<OtherSchema>(&v1_bytes),
521            Err(Error::SchemaMismatch {
522                expected: 0xdead_beef,
523                actual: <SchemaV1 as SchemaEncode>::SCHEMA_ID
524            })
525        ));
526    }
527
528    #[test]
529    fn legacy_fixed_vector_is_stable() {
530        let legacy = legacy_options();
531        let bytes = legacy
532            .serialize(&(0x0102u16, -2i32, "A", Event::Data(9)))
533            .unwrap();
534        // Fixed-width integers are always written at u64 width in the unified
535        // nextjson data model; lengths are also fixed u64.
536        assert_eq!(
537            bytes,
538            [
539                0x0a, // tuple array
540                0x03, 0x02, 0x01, 0, 0, 0, 0, 0, 0, // u16 0x0102 as fixed u64 LE
541                0x05, 0xfe, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
542                0xff, // i32 -2 as fixed i64 LE
543                0x09, 1, 0, 0, 0, 0, 0, 0, 0, b'A', // "A": fixed u64 length
544                0x0b, // external enum object
545                0x09, 4, 0, 0, 0, 0, 0, 0, 0, b'D', b'a', b't', b'a', // "Data"
546                0x03, 9, 0, 0, 0, 0, 0, 0, 0,    // Data(9): fixed u64 LE
547                0xff, // enum object end
548                0xff, // tuple end
549            ]
550        );
551        assert_eq!(
552            legacy
553                .deserialize::<(u16, i32, String, Event)>(&bytes)
554                .unwrap(),
555            (0x0102, -2, "A".into(), Event::Data(9))
556        );
557    }
558
559    #[test]
560    fn compact_varints_cover_boundaries_and_signed_values() {
561        let config = options();
562        for value in [
563            0u128,
564            250,
565            251,
566            u16::MAX as u128,
567            u16::MAX as u128 + 1,
568            u32::MAX as u128 + 1,
569            u64::MAX as u128 + 1,
570            u128::MAX,
571        ] {
572            let bytes = config.serialize(&value).unwrap();
573            assert_eq!(config.deserialize::<u128>(&bytes).unwrap(), value);
574        }
575        for value in [
576            i128::MIN,
577            i64::MIN as i128,
578            -251,
579            -1,
580            0,
581            1,
582            251,
583            i64::MAX as i128,
584            i128::MAX,
585        ] {
586            let bytes = config.serialize(&value).unwrap();
587            assert_eq!(config.deserialize::<i128>(&bytes).unwrap(), value);
588        }
589        assert_eq!(config.serialize(&250u64).unwrap(), [3, 250]);
590        assert_eq!(config.serialize(&251u64).unwrap(), [3, 251, 251, 0]);
591    }
592
593    #[test]
594    fn compact_v1_golden_vectors_are_stable() {
595        let compact = options();
596        let unsigned: &[(u64, &[u8])] = &[
597            (0, &[3, 0]),
598            (250, &[3, 250]),
599            (251, &[3, 251, 251, 0]),
600            (65_535, &[3, 251, 255, 255]),
601            (65_536, &[3, 252, 0, 0, 1, 0]),
602            (4_294_967_296, &[3, 253, 0, 0, 0, 0, 1, 0, 0, 0]),
603        ];
604        for &(value, golden) in unsigned {
605            assert_eq!(compact.serialize(&value).unwrap(), golden);
606            assert_eq!(compact.deserialize::<u64>(golden).unwrap(), value);
607        }
608
609        let record = Record {
610            id: 42,
611            delta: -7,
612            name: "zero-copy",
613            payload: vec![0, 1, 255],
614            enabled: Some(true),
615        };
616        let golden = [
617            0x0b, // object
618            0x09, 2, b'i', b'd', 0x03, 42, // id = 42
619            0x09, 5, b'd', b'e', b'l', b't', b'a', 0x05, 13, // delta = -7 (zigzag)
620            0x09, 4, b'n', b'a', b'm', b'e', 0x09, 9, b'z', b'e', b'r', b'o', b'-', b'c', b'o',
621            b'p', b'y', // name = "zero-copy"
622            0x09, 7, b'p', b'a', b'y', b'l', b'o', b'a', b'd', 0x0a, // payload array
623            0x03, 0, 0x03, 1, 0x03, 251, 255, 0,    // [0, 1, 255]
624            0xff, // payload end
625            0x09, 7, b'e', b'n', b'a', b'b', b'l', b'e', b'd', 0x02, // enabled = true
626            0xff, // object end
627        ];
628        assert_eq!(compact.serialize(&record).unwrap(), golden);
629        assert_eq!(compact.deserialize::<Record<'_>>(&golden).unwrap(), record);
630
631        let big_fixed = compact.with_big_endian().with_fixint_encoding();
632        assert_eq!(
633            big_fixed.serialize(&(0x0102u16, -2i32, 1.5f32)).unwrap(),
634            [
635                0x0a, // tuple array
636                0x03, 0, 0, 0, 0, 0, 0, 1, 2, // u16 0x0102 as fixed u64 BE
637                0x05, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
638                0xfe, // i32 -2 as fixed i64 BE
639                0x08, 0x3f, 0xc0, 0x00, 0x00, // f32 1.5, big endian
640                0xff, // tuple end
641            ]
642        );
643    }
644
645    #[test]
646    fn round_trips_full_data_model_and_borrows_strings() {
647        let record = Record {
648            id: 42,
649            delta: -7,
650            name: "zero-copy",
651            payload: vec![0, 1, 255],
652            enabled: Some(true),
653        };
654        let bytes = options().serialize(&record).unwrap();
655        let decoded: Record<'_> = options().deserialize(&bytes).unwrap();
656        assert_eq!(decoded, record);
657        let start = bytes.as_ptr() as usize;
658        assert!((start..start + bytes.len()).contains(&(decoded.name.as_ptr() as usize)));
659
660        for event in [
661            Event::Idle,
662            Event::Data(65535),
663            Event::Point { x: -9, y: 17 },
664        ] {
665            let encoded = options().serialize(&event).unwrap();
666            assert_eq!(options().deserialize::<Event>(&encoded).unwrap(), event);
667        }
668    }
669
670    #[test]
671    fn nested_borrowed_fields_point_into_the_input_frame() {
672        let value = BorrowedEnvelope {
673            name: "zero-copy",
674            payload: "borrowed-payload",
675            nested: BorrowedMetadata { source: "edge-07" },
676        };
677        let config = options().with_limit(1024);
678        let frame = config.serialize(&value).unwrap();
679        let decoded: BorrowedEnvelope<'_> = config.deserialize(&frame).unwrap();
680        assert_eq!(decoded, value);
681
682        let start = frame.as_ptr() as usize;
683        let end = start + frame.len();
684        for borrowed in [
685            decoded.name.as_bytes(),
686            decoded.payload.as_bytes(),
687            decoded.nested.source.as_bytes(),
688        ] {
689            let pointer = borrowed.as_ptr() as usize;
690            assert!(pointer >= start && pointer + borrowed.len() <= end);
691        }
692    }
693
694    #[test]
695    fn supports_endianness_floats_chars_maps_and_non_finite_values() {
696        assert_eq!(
697            options()
698                .with_big_endian()
699                .with_fixint_encoding()
700                .serialize(&0x0102u16)
701                .unwrap(),
702            [3, 0, 0, 0, 0, 0, 0, 1, 2]
703        );
704        for value in ['a', 'é', '汉', '🚀'] {
705            let bytes = options().serialize(&value).unwrap();
706            assert_eq!(options().deserialize::<char>(&bytes).unwrap(), value);
707        }
708        let map = BTreeMap::from([
709            ("one".to_owned(), "1".to_owned()),
710            ("two".to_owned(), "2".to_owned()),
711        ]);
712        let bytes = options().serialize(&map).unwrap();
713        assert_eq!(
714            options()
715                .deserialize::<BTreeMap<String, String>>(&bytes)
716                .unwrap(),
717            map
718        );
719        let nan = f64::NAN;
720        assert!(options()
721            .deserialize::<f64>(&options().serialize(&nan).unwrap())
722            .unwrap()
723            .is_nan());
724    }
725
726    #[test]
727    fn streaming_size_limits_and_trailing_policy_are_enforced() {
728        let value = vec![1u32, 2, 3, 65_536];
729        let config = options().with_limit(64);
730        let mut stream = Vec::new();
731        config.serialize_into(&mut stream, &value).unwrap();
732        assert_eq!(config.serialized_size(&value).unwrap(), stream.len() as u64);
733        assert_eq!(
734            config
735                .deserialize_from::<_, Vec<u32>>(Cursor::new(&stream))
736                .unwrap(),
737            value
738        );
739        assert!(matches!(
740            options().with_limit(2).serialize(&u64::MAX),
741            Err(Error::SizeLimit { limit: 2 })
742        ));
743
744        let mut trailing = options().serialize(&7u8).unwrap();
745        trailing.push(8);
746        assert!(matches!(
747            options().deserialize::<u8>(&trailing),
748            Err(Error::TrailingBytes { remaining: 1 })
749        ));
750        assert_eq!(
751            options()
752                .allow_trailing_bytes()
753                .deserialize::<u8>(&trailing)
754                .unwrap(),
755            7
756        );
757    }
758
759    #[test]
760    fn malformed_inputs_are_rejected_without_panics() {
761        // A raw bool tag is unambiguous in the self-describing format.
762        assert!(options().deserialize::<bool>(&[2]).unwrap());
763        assert!(matches!(
764            options().deserialize::<Option<u8>>(&[3]),
765            Err(Error::UnexpectedEnd)
766        ));
767        assert!(matches!(
768            options().deserialize::<u64>(&[255]),
769            Err(Error::Custom(_))
770        ));
771        assert!(matches!(
772            options().deserialize::<u64>(&[251, 1, 0]),
773            Err(Error::Custom(_))
774        ));
775        assert!(matches!(
776            options().deserialize::<char>(&[0x09, 2, b'a', b'b']),
777            Err(Error::InvalidChar)
778        ));
779        // An array of 65 unit values exceeds the collection limit.
780        let mut hostile = vec![0x0a];
781        hostile.extend(std::iter::repeat_n(0x00, 65));
782        hostile.push(0xff);
783        assert!(matches!(
784            legacy_options()
785                .with_collection_limit(64)
786                .deserialize::<Vec<()>>(&hostile),
787            Err(Error::CollectionLimit { limit: 64 })
788        ));
789
790        for len in 0..48 {
791            for fill in [0, 1, 0x7f, 0xfb, 0xff] {
792                let input = vec![fill; len];
793                assert!(
794                    std::panic::catch_unwind(|| options().deserialize::<Record<'_>>(&input))
795                        .is_ok()
796                );
797            }
798        }
799    }
800
801    struct Stateful<'a>(&'a Cell<u8>);
802
803    impl nextjson::NsonSchema for Stateful<'_> {
804        const SCHEMA: nextjson::TypeSchema = nextjson::TypeSchema::U8;
805    }
806    impl nextjson::NsonSerialize for Stateful<'_> {
807        fn nextencode<E: nextjson::FormatEncoder>(
808            &self,
809            encoder: &mut E,
810        ) -> ::core::result::Result<(), E::Error> {
811            let next = self.0.get() + 1;
812            self.0.set(next);
813            encoder.write_u64(next as u64)
814        }
815    }
816
817    struct UnbalancedContainer;
818
819    impl nextjson::NsonSchema for UnbalancedContainer {
820        const SCHEMA: nextjson::TypeSchema = nextjson::TypeSchema::Seq(&nextjson::TypeSchema::Unit);
821    }
822    impl nextjson::NsonSerialize for UnbalancedContainer {
823        fn nextencode<E: nextjson::FormatEncoder>(
824            &self,
825            encoder: &mut E,
826        ) -> ::core::result::Result<(), E::Error> {
827            // Emits a container end without a matching start.
828            encoder.end_array()
829        }
830    }
831
832    struct FailingWriter {
833        remaining: usize,
834    }
835
836    #[cfg(any(feature = "compression", feature = "encryption"))]
837    struct HeaderOnlyReader {
838        header: Cursor<Vec<u8>>,
839    }
840
841    #[cfg(any(feature = "compression", feature = "encryption"))]
842    impl Read for HeaderOnlyReader {
843        fn read(&mut self, output: &mut [u8]) -> io::Result<usize> {
844            if self.header.position() == self.header.get_ref().len() as u64 {
845                panic!("frame body must not be read after a rejected header");
846            }
847            self.header.read(output)
848        }
849    }
850
851    impl Write for FailingWriter {
852        fn write(&mut self, bytes: &[u8]) -> io::Result<usize> {
853            if self.remaining == 0 {
854                return Err(io::Error::new(io::ErrorKind::BrokenPipe, "test writer"));
855            }
856            let written = self.remaining.min(bytes.len());
857            self.remaining -= written;
858            Ok(written)
859        }
860
861        fn flush(&mut self) -> io::Result<()> {
862            Ok(())
863        }
864    }
865
866    #[test]
867    fn serializer_runs_once_and_io_failures_are_preserved() {
868        let calls = Cell::new(0);
869        assert_eq!(options().serialize(&Stateful(&calls)).unwrap(), [3, 1]);
870        assert_eq!(calls.get(), 1);
871        assert!(matches!(
872            options().serialize(&UnbalancedContainer),
873            Err(Error::Custom(_))
874        ));
875        let failure = options().serialize_into(FailingWriter { remaining: 2 }, &u64::MAX);
876        assert!(
877            matches!(failure, Err(Error::Io(error)) if error.kind() == io::ErrorKind::BrokenPipe)
878        );
879    }
880
881    #[test]
882    fn slice_serialization_is_single_pass_and_allocation_free() {
883        let value = (513u16, "zero allocation", vec![1u8, 2, 3]);
884        let expected = options().serialize(&value).unwrap();
885        let mut exact = [0u8; 64];
886        let written = options().serialize_into_slice(&mut exact, &value).unwrap();
887        assert_eq!(&exact[..written], expected);
888
889        let calls = Cell::new(0);
890        let mut two = [0u8; 2];
891        assert_eq!(
892            options()
893                .serialize_into_slice(&mut two, &Stateful(&calls))
894                .unwrap(),
895            2
896        );
897        assert_eq!(calls.get(), 1);
898
899        let mut short = [0u8; 3];
900        assert!(matches!(
901            options().serialize_into_slice(&mut short, &value),
902            Err(Error::BufferTooSmall {
903                required,
904                available: 3
905            }) if required == expected.len()
906        ));
907        assert_eq!(&short, &expected[..3]);
908    }
909
910    #[cfg(all(
911        feature = "fingerprint",
912        feature = "reflection",
913        feature = "static-size"
914    ))]
915    #[test]
916    fn derives_produce_checked_schema_bounds_and_reflection() {
917        let value = ProtocolRecord {
918            enabled: true,
919            count: 513,
920            coordinates: [-1, i32::MAX],
921        };
922        assert!(options().serialize(&value).unwrap().len() <= ProtocolRecord::MAX_SIZE);
923        assert!(legacy_options().serialize(&value).unwrap().len() <= ProtocolRecord::MAX_SIZE);
924        const { assert!(ProtocolRecord::MAX_SIZE > 0) };
925        const { assert!(ProtocolRecord::PACKED_MAX_BITS > 0) };
926
927        let TypeShape::Struct(fields) = ProtocolRecord::SHAPE else {
928            panic!("record must reflect as a struct");
929        };
930        assert_eq!(fields.len(), 3);
931        assert_eq!(fields[0].name, "enabled");
932        assert_eq!(fields[1].type_name, "u16");
933        assert_eq!(fields[2].index, 2);
934
935        let TypeShape::Enum(variants) = ReflectedEvent::SHAPE else {
936            panic!("event must reflect as an enum");
937        };
938        let _constructed = (
939            ReflectedEvent::Empty,
940            ReflectedEvent::Tuple(1, true),
941            ReflectedEvent::Named { code: 2 },
942        );
943        let ReflectedEvent::Tuple(tuple_number, tuple_flag) = _constructed.1 else {
944            unreachable!()
945        };
946        let ReflectedEvent::Named { code: named_code } = _constructed.2 else {
947            unreachable!()
948        };
949        assert_eq!((tuple_number, tuple_flag, named_code), (1, true, 2));
950        assert_eq!(variants[1].name, "Tuple");
951        assert_eq!(variants[1].fields[0].name, "0");
952        assert_eq!(variants[2].fields[0].type_name, "u16");
953
954        assert_ne!(
955            ProtocolRecord::TYPE_FINGERPRINT,
956            ChangedProtocolRecord::TYPE_FINGERPRINT
957        );
958        assert_ne!(
959            ProtocolRecord::fingerprint(options()),
960            ProtocolRecord::fingerprint(options().with_big_endian())
961        );
962        assert_ne!(
963            ProtocolRecord::fingerprint(options()),
964            ProtocolRecord::fingerprint(options().with_fixint_encoding())
965        );
966    }
967
968    #[cfg(all(
969        feature = "fingerprint",
970        feature = "reflection",
971        feature = "static-size"
972    ))]
973    #[test]
974    fn fingerprint_frames_reject_schema_and_configuration_drift() {
975        let value = ProtocolRecord {
976            enabled: true,
977            count: 7,
978            coordinates: [2, 3],
979        };
980        let framed = options().with_fingerprint().serialize(&value).unwrap();
981        let decoded: ProtocolRecord = options().with_fingerprint().deserialize(&framed).unwrap();
982        assert_eq!(decoded.count, value.count);
983
984        assert!(matches!(
985            options()
986                .with_fingerprint()
987                .deserialize::<ChangedProtocolRecord>(&framed),
988            Err(Error::SchemaMismatch { .. })
989        ));
990        assert!(matches!(
991            options()
992                .with_big_endian()
993                .with_fingerprint()
994                .deserialize::<ProtocolRecord>(&framed),
995            Err(Error::SchemaMismatch { .. })
996        ));
997
998        let mut output = [0u8; 128];
999        let written = options()
1000            .with_fingerprint()
1001            .serialize_into_slice(&mut output, &value)
1002            .unwrap();
1003        assert_eq!(&output[..written], framed);
1004        assert_eq!(
1005            options()
1006                .with_fingerprint()
1007                .serialized_size(&value)
1008                .unwrap(),
1009            written as u64
1010        );
1011
1012        let mut corrupt = framed.clone();
1013        corrupt[0] = 0;
1014        assert!(matches!(
1015            options()
1016                .with_fingerprint()
1017                .deserialize::<ProtocolRecord>(&corrupt),
1018            Err(Error::InvalidFrame("bad fingerprint magic"))
1019        ));
1020    }
1021
1022    #[cfg(feature = "cbor")]
1023    #[test]
1024    fn cbor_matches_rfc_vectors_and_deterministic_map_order() {
1025        // nextjson relays JSON-compatible events into CBOR; arrays and maps use
1026        // RFC 8949 indefinite-length forms (0x9f / 0xbf ... break 0xff).
1027        assert_eq!(
1028            options().with_cbor_format().serialize(&0u8).unwrap(),
1029            [0x00]
1030        );
1031        assert_eq!(
1032            options().with_cbor_format().serialize(&24u8).unwrap(),
1033            [0x18, 0x18]
1034        );
1035        assert_eq!(
1036            options().with_cbor_format().serialize("a").unwrap(),
1037            [0x61, b'a']
1038        );
1039        assert_eq!(
1040            options()
1041                .with_cbor_format()
1042                .serialize(&vec![1u8, 2, 3])
1043                .unwrap(),
1044            [0x9f, 0x01, 0x02, 0x03, 0xff]
1045        );
1046
1047        let first = HashMap::from([("aa", 1u8), ("b", 2)]);
1048        let second = HashMap::from([("b", 2u8), ("aa", 1)]);
1049        let deterministic = options().with_cbor_format().with_deterministic_encoding();
1050        let encoded = deterministic.serialize(&first).unwrap();
1051        assert_eq!(encoded, deterministic.serialize(&second).unwrap());
1052        assert_eq!(
1053            encoded,
1054            [0xbf, 0x61, b'b', 0x02, 0x62, b'a', b'a', 0x01, 0xff]
1055        );
1056        assert_eq!(
1057            deterministic
1058                .deserialize::<HashMap<String, u8>>(&encoded)
1059                .unwrap(),
1060            HashMap::from([("aa".into(), 1), ("b".into(), 2)])
1061        );
1062
1063        let mut trailing = encoded.clone();
1064        trailing.push(0);
1065        assert!(matches!(
1066            deterministic.deserialize::<HashMap<String, u8>>(&trailing),
1067            Err(Error::Cbor(_))
1068        ));
1069        assert!(matches!(
1070            deterministic.deserialize_from::<_, HashMap<String, u8>>(Cursor::new(&trailing)),
1071            Err(Error::Cbor(_))
1072        ));
1073        assert_eq!(
1074            options()
1075                .with_limit(1)
1076                .with_cbor_format()
1077                .deserialize_from::<_, u8>(Cursor::new([0x00]))
1078                .unwrap(),
1079            0
1080        );
1081        assert!(matches!(
1082            options()
1083                .with_limit(2)
1084                .with_cbor_format()
1085                .serialize(&vec![1u8, 2, 3]),
1086            Err(Error::SizeLimit { limit: 2 })
1087        ));
1088    }
1089
1090    #[cfg(all(
1091        feature = "cbor",
1092        feature = "fingerprint",
1093        feature = "reflection",
1094        feature = "static-size"
1095    ))]
1096    #[test]
1097    fn cbor_fingerprint_covers_format_and_determinism() {
1098        let value = ProtocolRecord {
1099            enabled: false,
1100            count: 9,
1101            coordinates: [4, 5],
1102        };
1103        let binary = ProtocolRecord::fingerprint(options());
1104        let regular = options().with_cbor_format();
1105        let deterministic = regular.with_deterministic_encoding();
1106        assert_ne!(binary, regular.fingerprint::<ProtocolRecord>());
1107        assert_ne!(
1108            regular.fingerprint::<ProtocolRecord>(),
1109            deterministic.fingerprint::<ProtocolRecord>()
1110        );
1111
1112        let frame = deterministic.with_fingerprint().serialize(&value).unwrap();
1113        let decoded: ProtocolRecord = deterministic
1114            .with_fingerprint()
1115            .deserialize(&frame)
1116            .unwrap();
1117        assert_eq!(decoded.coordinates, value.coordinates);
1118        assert!(matches!(
1119            regular
1120                .with_fingerprint()
1121                .deserialize::<ProtocolRecord>(&frame),
1122            Err(Error::SchemaMismatch { .. })
1123        ));
1124    }
1125
1126    #[cfg(feature = "compression")]
1127    #[test]
1128    fn compression_is_adaptive_bounded_and_round_trips() {
1129        // In the tagged format each u8 costs a tag byte, so a 2000-element
1130        // array serializes to ~4000 bytes, comfortably under the limit.
1131        let repeated = vec![0u8; 2000];
1132        let compressed = options()
1133            .with_limit(8192)
1134            .with_zstd_compression(3)
1135            .with_compression_threshold(128);
1136        let frame = compressed.serialize(&repeated).unwrap();
1137        assert_eq!(&frame[..4], b"RBZ1");
1138        assert_eq!(u16::from_le_bytes([frame[6], frame[7]]), 1);
1139        assert!(frame.len() < repeated.len() / 4);
1140        assert_eq!(compressed.deserialize::<Vec<u8>>(&frame).unwrap(), repeated);
1141
1142        let small = options().with_zstd_compression(3).serialize(&7u8).unwrap();
1143        assert_eq!(u16::from_le_bytes([small[6], small[7]]), 0);
1144        assert_eq!(
1145            options()
1146                .with_zstd_compression(3)
1147                .deserialize::<u8>(&small)
1148                .unwrap(),
1149            7
1150        );
1151
1152        let mut hostile = frame.clone();
1153        hostile[8..16].copy_from_slice(&8193u64.to_le_bytes());
1154        assert!(matches!(
1155            compressed.deserialize::<Vec<u8>>(&hostile),
1156            Err(Error::SizeLimit { limit: 8192 })
1157        ));
1158        assert!(matches!(
1159            compressed.deserialize::<Vec<u8>>(&frame[..frame.len() - 1]),
1160            Err(Error::UnexpectedEnd)
1161        ));
1162    }
1163
1164    #[cfg(feature = "compression")]
1165    #[test]
1166    fn compressed_stream_rejects_oversized_header_before_reading_body() {
1167        let mut header = Vec::from(*b"RBZ1");
1168        header.extend_from_slice(&1u16.to_le_bytes());
1169        header.extend_from_slice(&1u16.to_le_bytes());
1170        header.extend_from_slice(&1025u64.to_le_bytes());
1171        header.extend_from_slice(&1u64.to_le_bytes());
1172        let reader = HeaderOnlyReader {
1173            header: Cursor::new(header),
1174        };
1175
1176        assert!(matches!(
1177            options()
1178                .with_limit(1024)
1179                .with_zstd_compression(3)
1180                .deserialize_from::<_, Vec<u8>>(reader),
1181            Err(Error::SizeLimit { limit: 1024 })
1182        ));
1183    }
1184
1185    #[cfg(all(feature = "compression", feature = "cbor"))]
1186    #[test]
1187    fn deterministic_cbor_can_be_compressed_as_one_pipeline() {
1188        let value = BTreeMap::from([("payload".to_owned(), "x".repeat(2048))]);
1189        let config = options()
1190            .with_cbor_format()
1191            .with_deterministic_encoding()
1192            .with_zstd_compression(5)
1193            .with_compression_threshold(64);
1194        let frame = config.serialize(&value).unwrap();
1195        assert_eq!(
1196            config
1197                .deserialize::<BTreeMap<String, String>>(&frame)
1198                .unwrap(),
1199            value
1200        );
1201    }
1202
1203    #[cfg(feature = "encryption")]
1204    #[test]
1205    fn authenticated_encryption_uses_random_nonces_and_rejects_tampering() {
1206        let value = (42u64, "classified".to_owned(), vec![7u8; 512]);
1207        let config = options()
1208            .with_limit(4096)
1209            .with_encryption(EncryptionKey::new([0x42; 32]));
1210        assert_eq!(
1211            format!("{:?}", EncryptionKey::new([0x42; 32])),
1212            "EncryptionKey([REDACTED])"
1213        );
1214
1215        let first = config.serialize(&value).unwrap();
1216        let second = config.serialize(&value).unwrap();
1217        assert_eq!(&first[..4], b"RBX1");
1218        assert_ne!(&first[8..32], &second[8..32]);
1219        assert_ne!(first, second);
1220        assert_eq!(
1221            config
1222                .deserialize::<(u64, String, Vec<u8>)>(&first)
1223                .unwrap(),
1224            value
1225        );
1226
1227        let mut tampered = first.clone();
1228        *tampered.last_mut().unwrap() ^= 1;
1229        assert!(matches!(
1230            config.deserialize::<(u64, String, Vec<u8>)>(&tampered),
1231            Err(Error::Encryption)
1232        ));
1233        let wrong_key = options()
1234            .with_limit(4096)
1235            .with_encryption(EncryptionKey::new([0x24; 32]));
1236        assert!(matches!(
1237            wrong_key.deserialize::<(u64, String, Vec<u8>)>(&first),
1238            Err(Error::Encryption)
1239        ));
1240
1241        let mut hostile = first.clone();
1242        hostile[32..40].copy_from_slice(&4097u64.to_le_bytes());
1243        hostile[40..48].copy_from_slice(&4113u64.to_le_bytes());
1244        assert!(matches!(
1245            config.deserialize::<(u64, String, Vec<u8>)>(&hostile),
1246            Err(Error::SizeLimit { limit: 4096 })
1247        ));
1248
1249        let mut stream = Cursor::new([first.as_slice(), b"next-frame"].concat());
1250        assert_eq!(
1251            config
1252                .deserialize_from::<_, (u64, String, Vec<u8>)>(&mut stream)
1253                .unwrap(),
1254            value
1255        );
1256        assert_eq!(stream.position(), first.len() as u64);
1257    }
1258
1259    #[cfg(feature = "encryption")]
1260    #[test]
1261    fn encrypted_stream_rejects_oversized_header_before_reading_body() {
1262        // A full 48-byte header whose declared plaintext length exceeds the
1263        // limit; deserialization must reject it before reading the body.
1264        let mut header = Vec::from(*b"RBX1");
1265        header.extend_from_slice(&1u16.to_le_bytes());
1266        header.extend_from_slice(&1u16.to_le_bytes());
1267        header.extend_from_slice(&[0u8; 24]); // nonce
1268        header.extend_from_slice(&1025u64.to_le_bytes()); // plaintext length
1269        header.extend_from_slice(&1041u64.to_le_bytes()); // ciphertext length
1270        let reader = HeaderOnlyReader {
1271            header: Cursor::new(header),
1272        };
1273
1274        assert!(matches!(
1275            options()
1276                .with_limit(1024)
1277                .with_encryption(EncryptionKey::new([0x42; 32]))
1278                .deserialize_from::<_, (u64, String, Vec<u8>)>(reader),
1279            Err(Error::SizeLimit { limit: 1024 })
1280        ));
1281    }
1282}