Preview of the proposed up-rust native-frame-model branch (up-rust b6b99c6d, up-spec f0e9b17) — not released documentation. branch · write-up

up_rust/payload/
stable.rs

1/********************************************************************************
2 * Copyright (c) 2026 Contributors to the Eclipse Foundation
3 *
4 * See the NOTICE file(s) distributed with this work for additional
5 * information regarding copyright ownership.
6 *
7 * This program and the accompanying materials are made available under the
8 * terms of the Apache License Version 2.0 which is available at
9 * https://www.apache.org/licenses/LICENSE-2.0
10 *
11 * SPDX-License-Identifier: Apache-2.0
12 ********************************************************************************/
13
14//! # Stable payloads: explicit layouts initialized in transport storage
15//!
16//! A stable payload is one fixed-layout value whose type name, size, alignment,
17//! fields, and padding are known to both peers. The supported path is:
18//!
19//! 1. define a `#[repr(C)]` struct and make every padding byte explicit;
20//! 2. derive `StablePayload` for layout/type identity;
21//! 3. derive `ByteBackedStablePayload` only when every byte pattern is valid;
22//! 4. derive `StablePayloadInit` for the generated typestate initializer;
23//! 5. initialize the value inside the higher-ranked closure accepted by
24//!    `send_uninit_stable_payload`, then return `finish()`.
25//!
26//! ```text
27//! #[repr(C)]
28//! #[derive(StablePayload, ByteBackedStablePayload, StablePayloadInit)]
29//! #[stable_payload(type_name = "com.example.CanFrameV1")]
30//! struct CanFrameV1 {
31//!     id: u32,
32//!     len: u8,
33//!     flags: u8,
34//!     data: [u8; 64],
35//!     reserved: [u8; 2], // explicit trailing padding
36//! }
37//!
38//! transport.send_uninit_stable_payload::<CanFrameV1>(metadata, |init| {
39//!     init.id(0x1ff)
40//!         .len(64)
41//!         .flags(0)
42//!         .data_from_fn(|index| index as u8)
43//!         .reserved([0; 2])
44//!         .finish()
45//! }).await?;
46//! ```
47//!
48//! The snippet is schematic because transport construction and metadata are
49//! application choices. The derive and generated setter names are the real
50//! shape.
51//!
52//! ## Where the guarantee lives
53//!
54//! `InitializedStablePayload` has no public constructor. Generated typestate
55//! exposes `finish()` only after every semantic field and generated padding gap
56//! has been written. The send helper accepts a `for<'payload>` closure, so a
57//! proof tied to one invocation cannot escape that invocation or be substituted
58//! into another in safe Rust. The final witness discharge is the implementation
59//! of `req~zero-copy-transport-two-phase~1`.
60//!
61//! Generated field writes route through centralized bounds-checked copy/fill
62//! kernels. Unsafe codec implementation, byte-backed layout proof, and typed
63//! borrow remain separate expert contracts; deriving these traits is the normal
64//! user path. Compile-fail tests in `tests/ui/stable_payload/`, Miri tests, exact
65//! byte fixtures, and selected-wire round trips are the executable arbiters.
66
67use std::{
68    fmt::Display,
69    marker::PhantomData,
70    mem,
71    ptr::{self, NonNull},
72};
73
74use mediatype::ReadParams;
75
76#[cfg(feature = "zero-copy-transport")]
77use crate::zero_copy::LoanedPayloadUninitMut;
78use crate::PayloadEncoding;
79
80#[cfg(feature = "zero-copy-transport")]
81use super::loan::{LoanUninitPayload, LoanedUninitPayload};
82use super::{
83    codec::{PayloadCodec, PayloadLayout},
84    loan::{BorrowPayload, LoanPayload},
85    UWireError,
86};
87
88const STABLE_CONTAINER_ENCODING_ID: &str = "up.stable-container";
89const STABLE_CONTAINER_MEDIA_TYPE: &str = "application/vnd.uprotocol.stable-container";
90
91/// Completion proof returned by generated stable payload initializers.
92///
93/// This token is intentionally not a typed reference into the loan. It proves
94/// that a generated typestate builder reached `finish()` after initializing all
95/// semantic fields and generated padding gaps.
96#[must_use = "return this completion witness to the transmit path or consume it explicitly"]
97pub struct InitializedStablePayload<'a, T> {
98    _marker: PhantomData<fn(&'a mut T) -> &'a mut T>,
99}
100
101impl<'a, T> core::fmt::Debug for InitializedStablePayload<'a, T> {
102    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
103        f.debug_struct("InitializedStablePayload")
104            .finish_non_exhaustive()
105    }
106}
107
108impl<'a, T> InitializedStablePayload<'a, T> {
109    /// Creates a completion proof for a fully initialized internal slot.
110    ///
111    /// # Safety
112    ///
113    /// The corresponding slot must be exclusively borrowed for `'a` and contain
114    /// one valid initialized `T`, including all implicit padding bytes.
115    #[must_use = "return this completion witness to the transmit path or consume it explicitly"]
116    #[inline(always)]
117    unsafe fn new_unchecked() -> Self {
118        Self {
119            _marker: PhantomData,
120        }
121    }
122}
123
124/// Stable payload type variant used in stable-container metadata.
125///
126/// `USR-04A` supports one fixed-size value per payload. Runtime-length stable
127/// slices are intentionally left for a later API.
128#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
129pub enum StablePayloadVariant {
130    /// One fixed-size `T` value with exact payload length `size_of::<T>()`.
131    FixedSize,
132}
133
134impl StablePayloadVariant {
135    /// Returns the stable media-type spelling for this variant.
136    #[must_use]
137    pub const fn as_str(self) -> &'static str {
138        match self {
139            Self::FixedSize => "fixed",
140        }
141    }
142
143    fn parse(value: &str) -> Option<Self> {
144        match value {
145            "fixed" => Some(Self::FixedSize),
146            _ => None,
147        }
148    }
149}
150
151impl Display for StablePayloadVariant {
152    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
153        f.write_str(self.as_str())
154    }
155}
156
157/// Runtime type detail carried by stable-container metadata.
158#[derive(Clone, Copy, Debug, Eq, PartialEq)]
159pub struct StableTypeDetail<'a> {
160    /// Fixed-size stable-container payload variant.
161    pub variant: StablePayloadVariant,
162    /// Cross-process and cross-language stable type identity.
163    pub type_name: &'a str,
164    /// Payload value size in bytes.
165    pub size: usize,
166    /// Required payload value alignment in bytes.
167    pub alignment: usize,
168}
169
170impl Display for StableTypeDetail<'_> {
171    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
172        write!(
173            f,
174            "type={};variant={};size={};align={}",
175            self.type_name, self.variant, self.size, self.alignment
176        )
177    }
178}
179
180/// Hidden recursive field proof used by the `StablePayload` derive.
181///
182/// Application code should use `#[derive(StablePayload)]` instead of naming this
183/// trait directly. Manual implementations are only for externally-audited FFI or
184/// codegen payload fields whose layout and ownership are known to be stable.
185///
186/// # Safety
187///
188/// Implementors must not contain process-local references, raw pointers,
189/// function pointers, heap ownership, or drop glue that would make the value
190/// invalid as part of a stable-container payload representation.
191#[doc(hidden)]
192pub unsafe trait StablePayloadField: Sized + 'static {
193    #[doc(hidden)]
194    fn __stable_payload_field_check(&self) {}
195}
196
197macro_rules! impl_stable_payload_field {
198    ($($ty:ty),* $(,)?) => {
199        $(
200            // SAFETY: Primitive scalar values have no process-local ownership or
201            // drop glue, and arrays recurse through this field proof below.
202            unsafe impl StablePayloadField for $ty {}
203        )*
204    };
205}
206
207impl_stable_payload_field!(
208    (),
209    bool,
210    char,
211    u8,
212    u16,
213    u32,
214    u64,
215    u128,
216    usize,
217    i8,
218    i16,
219    i32,
220    i64,
221    i128,
222    isize,
223    f32,
224    f64,
225);
226
227// SAFETY: Arrays are stable fields when their element type is stable by the same
228// recursive field proof.
229unsafe impl<T, const N: usize> StablePayloadField for [T; N]
230where
231    T: StablePayloadField,
232{
233    fn __stable_payload_field_check(&self) {
234        for item in self {
235            item.__stable_payload_field_check();
236        }
237    }
238}
239
240/// Stable payload identity used by `StableContainerPayload<T>`.
241///
242/// This phase uses the trait only to build and verify stable-container metadata.
243/// Future borrow/no-zero phases consume the same identity when proving that bytes
244/// may safely be viewed as `T`.
245///
246/// # Safety
247///
248/// Implementors must choose a stable cross-process type name and only implement
249/// the trait for types whose size/alignment and initialized byte representation
250/// are suitable for the stable-container contract used by the application.
251#[diagnostic::on_unimplemented(
252    message = "`{Self}` does not implement the stable-payload contract",
253    label = "this type cannot be carried as a stable payload",
254    note = "add `#[repr(C)]`, derive `StablePayload`, and provide `#[stable_payload(type_name = \"...\")]`",
255    note = "derive `ByteBackedStablePayload` only when every field and byte representation satisfies its stronger safety contract"
256)]
257pub unsafe trait StablePayload: Sized + 'static {
258    /// Stable-container variant supported by this type.
259    const VARIANT: StablePayloadVariant = StablePayloadVariant::FixedSize;
260
261    /// Stable cross-process type name.
262    const TYPE_NAME: &'static str;
263
264    /// Returns the stable cross-process type name.
265    #[must_use]
266    fn stable_type_name() -> &'static str {
267        Self::TYPE_NAME
268    }
269
270    /// Returns the stable metadata detail for this type.
271    #[must_use]
272    fn stable_type_detail() -> StableTypeDetail<'static> {
273        StableTypeDetail {
274            variant: Self::VARIANT,
275            type_name: Self::stable_type_name(),
276            size: mem::size_of::<Self>(),
277            alignment: mem::align_of::<Self>(),
278        }
279    }
280
281    /// Hidden recursive field check emitted by the `StablePayload` derive.
282    #[doc(hidden)]
283    fn __stable_payload_field_check(&self) {}
284}
285
286mod byte_backed_stable_field_seal {
287    pub trait Sealed {}
288
289    macro_rules! impl_sealed_field {
290        ($($ty:ty),* $(,)?) => {
291            $(
292                impl Sealed for $ty {}
293            )*
294        };
295    }
296
297    impl_sealed_field!(
298        (),
299        bool,
300        char,
301        u8,
302        u16,
303        u32,
304        u64,
305        u128,
306        usize,
307        i8,
308        i16,
309        i32,
310        i64,
311        i128,
312        isize,
313        f32,
314        f64,
315    );
316
317    impl<T, const N: usize> Sealed for [T; N] where T: Sealed {}
318
319    impl<T> Sealed for T where T: super::ByteBackedStablePayload {}
320}
321
322/// Hidden recursive proof that a field is safe for byte-backed stable payloads.
323///
324/// This trait is derive-support plumbing, not an application extension point.
325/// Use [`ByteBackedStablePayload`] for payload-level public bounds.
326#[doc(hidden)]
327#[allow(private_bounds)]
328#[diagnostic::on_unimplemented(
329    message = "`{Self}` is not a byte-backed stable-payload field",
330    label = "this field does not satisfy the recursive byte-backed contract",
331    note = "use a supported primitive/array field, or derive `ByteBackedStablePayload` for an eligible nested stable type"
332)]
333pub trait ByteBackedStablePayloadField:
334    byte_backed_stable_field_seal::Sealed + Sized + 'static
335{
336    #[doc(hidden)]
337    const SUPPORTS_BYTE_BACKED_STABLE_FIELD: bool;
338    /// Views this plain-old-data value as its initialized bytes.
339    #[doc(hidden)]
340    fn init_bytes(&self) -> &[u8] {
341        // SAFETY: this sealed trait is implemented only for primitive fields,
342        // recursively byte-backed arrays, and types whose unsafe
343        // `ByteBackedStablePayload` contract proves the full live value has no
344        // uninitialized padding or process-local state.
345        unsafe {
346            core::slice::from_raw_parts(
347                (self as *const Self).cast::<u8>(),
348                core::mem::size_of::<Self>(),
349            )
350        }
351    }
352}
353
354macro_rules! impl_byte_backed_stable_field {
355    ($($ty:ty),* $(,)?) => {
356        $(
357            impl ByteBackedStablePayloadField for $ty {
358                const SUPPORTS_BYTE_BACKED_STABLE_FIELD: bool = true;
359            }
360        )*
361    };
362}
363
364impl_byte_backed_stable_field!(
365    (),
366    bool,
367    char,
368    u8,
369    u16,
370    u32,
371    u64,
372    u128,
373    usize,
374    i8,
375    i16,
376    i32,
377    i64,
378    i128,
379    isize,
380    f32,
381    f64,
382);
383
384impl<T, const N: usize> ByteBackedStablePayloadField for [T; N]
385where
386    T: ByteBackedStablePayloadField,
387{
388    const SUPPORTS_BYTE_BACKED_STABLE_FIELD: bool = T::SUPPORTS_BYTE_BACKED_STABLE_FIELD;
389}
390
391impl<T> ByteBackedStablePayloadField for T
392where
393    T: ByteBackedStablePayload,
394{
395    const SUPPORTS_BYTE_BACKED_STABLE_FIELD: bool = true;
396}
397
398/// Stronger stable payload proof for byte-backed stable-container paths.
399///
400/// `StablePayload` proves stable type identity, representation eligibility, and
401/// receive-side metadata compatibility. `ByteBackedStablePayload` additionally
402/// proves that the type has no implicit inter-field or trailing padding and that
403/// every field is recursively byte-backed.
404///
405/// Prefer `#[derive(StablePayload, ByteBackedStablePayload)]` for hand-written
406/// payload types. Manual implementations are reserved for externally audited FFI
407/// or code-generated layouts with an equivalent whole-object initialization
408/// proof.
409///
410/// # Safety
411///
412/// Implementors must guarantee that copying or initializing exactly
413/// `size_of::<Self>()` bytes is a sound representation of one valid `Self` for
414/// stable-container paths. That requires stable layout, no implicit padding, no
415/// drop glue, interior mutability, and recursively byte-backed fields.
416#[diagnostic::on_unimplemented(
417    message = "`{Self}` does not implement the byte-backed stable-payload contract",
418    label = "this type cannot use byte-backed stable-container paths",
419    note = "derive `ByteBackedStablePayload` only for an eligible `#[repr(C)]` type with no implicit padding and recursively byte-backed fields"
420)]
421pub unsafe trait ByteBackedStablePayload: StablePayload {
422    /// Whether this type's full object representation can be used by
423    /// byte-backed stable-container paths.
424    const SUPPORTS_BYTE_BACKED_UNINIT: bool = true;
425
426    #[doc(hidden)]
427    const BYTE_BACKED_STABLE_PAYLOAD_CHECK: () = {
428        assert!(Self::SUPPORTS_BYTE_BACKED_UNINIT);
429        assert!(!mem::needs_drop::<Self>());
430    };
431}
432
433/// Returns whether `T` can use byte-backed stable-container paths.
434#[must_use]
435pub const fn stable_payload_supports_byte_backed_uninit<T: ByteBackedStablePayload>() -> bool {
436    T::SUPPORTS_BYTE_BACKED_UNINIT && !mem::needs_drop::<T>()
437}
438
439/// Compile-time assertion for byte-backed stable-container eligibility.
440pub const fn assert_stable_payload_byte_backed_uninit<T>()
441where
442    T: ByteBackedStablePayload,
443{
444    assert!(T::SUPPORTS_BYTE_BACKED_UNINIT);
445    assert!(!mem::needs_drop::<T>());
446    #[allow(path_statements)]
447    T::BYTE_BACKED_STABLE_PAYLOAD_CHECK;
448}
449
450mod stable_payload_init_complete_value_seal {
451    pub trait Sealed {}
452
453    impl<T> Sealed for T where T: super::ByteBackedStablePayload {}
454
455    impl<T, const N: usize> Sealed for [T; N] where T: super::ByteBackedStablePayloadField {}
456}
457
458/// Hidden proof that a complete by-value nested initializer can be moved into a field.
459///
460/// This is derive support, not an application extension point. It is implemented
461/// only for byte-backed stable payloads, where moving the complete value into
462/// loan storage cannot expose uninitialized implicit padding bytes.
463#[doc(hidden)]
464#[allow(private_bounds)]
465pub trait StablePayloadInitCompleteValue<T>:
466    stable_payload_init_complete_value_seal::Sealed + Sized
467{
468    #[doc(hidden)]
469    fn into_complete_value(self) -> T;
470}
471
472impl<T> StablePayloadInitCompleteValue<T> for T
473where
474    T: ByteBackedStablePayload,
475{
476    fn into_complete_value(self) -> T {
477        self
478    }
479}
480
481impl<T, const N: usize> StablePayloadInitCompleteValue<[T; N]> for [T; N]
482where
483    T: ByteBackedStablePayloadField,
484{
485    fn into_complete_value(self) -> [T; N] {
486        self
487    }
488}
489
490/// Hidden typestate marker used by `StablePayloadInit` derive output.
491#[doc(hidden)]
492#[derive(Debug)]
493pub enum StablePayloadInitUnset {}
494
495/// Hidden typestate marker used by `StablePayloadInit` derive output.
496#[doc(hidden)]
497#[derive(Debug)]
498pub enum StablePayloadInitSet {}
499
500/// Generative context for initializing one stable payload slot.
501///
502/// The callback APIs that provide this context quantify over a fresh invariant
503/// slot lifetime. A completion proof can therefore be produced only by
504/// finishing the initializer carried by that callback invocation; a proof from
505/// separate storage cannot be substituted in safe Rust.
506#[must_use = "consume this context to initialize the loaned stable-payload slot"]
507pub struct StablePayloadInitContext<'slot, T: StablePayloadInit> {
508    init: T::Init<'slot>,
509    _invariant: PhantomData<fn(&'slot mut T) -> &'slot mut T>,
510}
511
512impl<'slot, T: StablePayloadInit> core::fmt::Debug for StablePayloadInitContext<'slot, T> {
513    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
514        f.debug_struct("StablePayloadInitContext")
515            .finish_non_exhaustive()
516    }
517}
518
519impl<'slot, T: StablePayloadInit> StablePayloadInitContext<'slot, T> {
520    #[inline(always)]
521    pub(crate) fn new(init: T::Init<'slot>) -> Self {
522        Self {
523            init,
524            _invariant: PhantomData,
525        }
526    }
527
528    /// Consumes the context and returns the generated typestate initializer.
529    #[must_use]
530    #[inline(always)]
531    pub fn into_init(self) -> T::Init<'slot> {
532        self.init
533    }
534}
535
536/// Safe generated initialization proof for stable-container payloads.
537///
538/// Derived implementations initialize one stable payload directly in
539/// uninitialized storage. Generated builders expose named typed setters and make
540/// `finish()` available only after all required fields are set. Implementations
541/// must also initialize any implicit and trailing padding bytes they own without
542/// blanket-zeroing the full payload.
543///
544/// # Safety
545///
546/// Implementors must guarantee that every successful `finish()` for `Self::Init`
547/// returns only after the full transported `size_of::<Self>()` byte range,
548/// including implicit padding, contains one valid initialized `Self` in the
549/// stable-container representation. Ordinary payload types should use the derive
550/// macro; manual implementations are an expert unsafe extension point.
551#[diagnostic::on_unimplemented(
552    message = "`{Self}` has no stable-payload initializer",
553    label = "missing `StablePayloadInit` implementation",
554    note = "derive `StablePayloadInit` to get the typestate builder used by `send_uninit_stable_payload`"
555)]
556pub unsafe trait StablePayloadInit: StablePayload {
557    /// Generated any-order typestate initializer for this payload type.
558    type Init<'a>;
559
560    /// Creates a generated initializer from an exact uninitialized payload range.
561    ///
562    /// # Errors
563    ///
564    /// Returns an error if the byte range does not match this stable payload's
565    /// size and alignment.
566    fn init_from_uninit_bytes<'a>(
567        payload: &'a mut [mem::MaybeUninit<u8>],
568    ) -> Result<Self::Init<'a>, UWireError> {
569        let slot = StablePayloadInitSlot::<Self>::from_uninit_bytes(payload)?;
570        Self::__init_from_slot(slot)
571    }
572
573    /// Creates a generated initializer from a transport loan's visible payload range.
574    ///
575    /// # Errors
576    ///
577    /// Returns an error if the payload range does not match this stable payload's
578    /// size and alignment.
579    #[cfg(feature = "zero-copy-transport")]
580    fn init_from_uninit_payload<'a>(
581        payload: LoanedPayloadUninitMut<'a>,
582    ) -> Result<Self::Init<'a>, UWireError> {
583        Self::init_from_uninit_bytes(payload.into_uninit_bytes_mut_internal())
584    }
585
586    /// Creates a generated initializer from a nested stable payload slot.
587    #[doc(hidden)]
588    fn __init_from_slot<'a>(
589        slot: StablePayloadInitSlot<'a, Self>,
590    ) -> Result<Self::Init<'a>, UWireError>;
591
592    /// Creates a generative context from a nested slot.
593    #[doc(hidden)]
594    #[inline(always)]
595    fn __init_context_from_slot<'a>(
596        slot: StablePayloadInitSlot<'a, Self>,
597    ) -> Result<StablePayloadInitContext<'a, Self>, UWireError> {
598        Self::__init_from_slot(slot).map(StablePayloadInitContext::new)
599    }
600}
601
602/// Hidden typed view over uninitialized storage used by generated initializers.
603///
604/// The type is public only so derive output in downstream crates can name it.
605/// Application code should use `#[derive(StablePayloadInit)]` and the generated
606/// setters instead of constructing or manipulating slots directly.
607#[doc(hidden)]
608pub struct StablePayloadInitSlot<'a, T> {
609    // Safe-by-construction: the slot IS the exclusive borrow of the target
610    // region. All writers below are bounds-checked safe code; the only
611    // `unsafe` in the initialization path is the final witness discharge in
612    // `assume_init` and the POD byte view in
613    // `ByteBackedStablePayloadField::init_bytes`.
614    bytes: &'a mut [mem::MaybeUninit<u8>],
615    _marker: PhantomData<&'a mut mem::MaybeUninit<T>>,
616}
617
618impl<'a, T> core::fmt::Debug for StablePayloadInitSlot<'a, T> {
619    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
620        f.debug_struct("StablePayloadInitSlot")
621            .finish_non_exhaustive()
622    }
623}
624
625impl<'a, T> StablePayloadInitSlot<'a, T> {
626    /// Creates a slot from uninitialized payload bytes after exact stable layout validation.
627    #[inline(always)]
628    fn from_uninit_bytes(bytes: &'a mut [mem::MaybeUninit<u8>]) -> Result<Self, UWireError>
629    where
630        T: StablePayload,
631    {
632        StableContainerPayload::<T>::check_uninit_layout(bytes)?;
633        Ok(Self {
634            bytes,
635            _marker: PhantomData,
636        })
637    }
638
639    /// Copies initialized bytes into an uninitialized destination region.
640    ///
641    /// This is one of exactly two `unsafe` kernels in the initialization
642    /// write path; every safe writer routes through it so that field and
643    /// array writes compile to `memcpy`, never to per-byte loops — a hard
644    /// requirement for zero-copy transports moving large fixed-size
645    /// payloads (64 KiB stream chunks, radar detection lists).
646    #[inline(always)]
647    fn copy_to_uninit(dst: &mut [mem::MaybeUninit<u8>], src: &[u8]) {
648        debug_assert_eq!(dst.len(), src.len());
649        // SAFETY: `dst` and `src` have equal length (checked above and by
650        // every bounds-checked caller); the regions cannot overlap because
651        // `dst` is an exclusive borrow of transport-loaned storage while
652        // `src` borrows caller memory; writing `u8` bytes through
653        // `MaybeUninit<u8>` is always valid.
654        unsafe {
655            ptr::copy_nonoverlapping(src.as_ptr(), dst.as_mut_ptr().cast::<u8>(), src.len());
656        }
657    }
658
659    /// Fills an uninitialized destination region with one byte value.
660    ///
661    /// The second of the two write-path `unsafe` kernels; compiles to
662    /// `memset`.
663    #[inline(always)]
664    fn fill_uninit(dst: &mut [mem::MaybeUninit<u8>], value: u8) {
665        // SAFETY: writing `u8` bytes through `MaybeUninit<u8>` within the
666        // exclusive borrow is always valid.
667        unsafe {
668            ptr::write_bytes(dst.as_mut_ptr().cast::<u8>(), value, dst.len());
669        }
670    }
671
672    #[inline(always)]
673    fn checked_range(&mut self, offset: usize, len: usize) -> &mut [mem::MaybeUninit<u8>] {
674        let end = offset
675            .checked_add(len)
676            .expect("stable payload field range overflow: layout and derive offsets disagree");
677        self.bytes
678            .get_mut(offset..end)
679            .expect("stable payload field write out of bounds: layout and derive offsets disagree")
680    }
681
682    /// Zero-fills `len` bytes at `offset` (padding regions).
683    #[doc(hidden)]
684    #[inline(always)]
685    pub fn write_padding(&mut self, offset: usize, len: usize) {
686        Self::fill_uninit(self.checked_range(offset, len), 0);
687    }
688
689    /// Writes one plain-old-data field value at `offset`.
690    #[doc(hidden)]
691    #[inline(always)]
692    pub fn write_field<U: ByteBackedStablePayloadField>(&mut self, offset: usize, value: U) {
693        let src = value.init_bytes();
694        Self::copy_to_uninit(self.checked_range(offset, src.len()), src);
695    }
696
697    /// Copies raw bytes at `offset`.
698    #[doc(hidden)]
699    #[inline(always)]
700    pub fn write_bytes(&mut self, offset: usize, src: &[u8]) {
701        Self::copy_to_uninit(self.checked_range(offset, src.len()), src);
702    }
703
704    /// Fills `len` bytes at `offset` with `value`.
705    #[doc(hidden)]
706    #[inline(always)]
707    pub fn fill_bytes(&mut self, offset: usize, len: usize, value: u8) {
708        Self::fill_uninit(self.checked_range(offset, len), value);
709    }
710
711    /// Fills `len` bytes at `offset` from a byte-producing function.
712    #[doc(hidden)]
713    #[inline(always)]
714    pub fn fill_bytes_with(
715        &mut self,
716        offset: usize,
717        len: usize,
718        mut value: impl FnMut(usize) -> u8,
719    ) {
720        for (index, dst) in self.checked_range(offset, len).iter_mut().enumerate() {
721            *dst = mem::MaybeUninit::new(value(index));
722        }
723    }
724
725    /// Copies a `[U; N]`-shaped region from a slice of POD elements.
726    #[doc(hidden)]
727    #[inline(always)]
728    pub fn copy_array_from_slice<U: ByteBackedStablePayloadField>(
729        &mut self,
730        offset: usize,
731        src: &[U],
732        expected: usize,
733    ) -> Result<(), UWireError> {
734        if src.len() != expected {
735            return Err(UWireError::invalid_payload_length(expected, src.len()));
736        }
737        // POD contract: a `[U]` of byte-backed fields is itself a contiguous
738        // initialized byte region, so the whole array is ONE memcpy.
739        let byte_len = mem::size_of_val(src);
740        // SAFETY: `U: ByteBackedStablePayloadField` guarantees `U` (and
741        // therefore `[U]`) is plain-old-data with no padding surprises; the
742        // slice's bytes are fully initialized.
743        let src_bytes = unsafe { core::slice::from_raw_parts(src.as_ptr().cast::<u8>(), byte_len) };
744        Self::copy_to_uninit(self.checked_range(offset, byte_len), src_bytes);
745        Ok(())
746    }
747
748    /// Fills a `[U; N]`-shaped region with copies of one POD element.
749    #[doc(hidden)]
750    #[inline(always)]
751    pub fn fill_array<U: ByteBackedStablePayloadField>(
752        &mut self,
753        offset: usize,
754        len: usize,
755        value: U,
756    ) {
757        let elem = mem::size_of::<U>();
758        let byte_len = elem
759            .checked_mul(len)
760            .expect("stable payload array fill length overflow");
761        if elem == 0 {
762            return;
763        }
764        let bytes = value.init_bytes();
765        for element in self.checked_range(offset, byte_len).chunks_exact_mut(elem) {
766            Self::copy_to_uninit(element, bytes);
767        }
768    }
769
770    /// Re-borrows the sub-region for one field as a typed slot.
771    #[doc(hidden)]
772    #[inline(always)]
773    pub fn field_slot<U>(&mut self, offset: usize) -> StablePayloadInitSlot<'_, U> {
774        let len = mem::size_of::<U>();
775        let end = offset
776            .checked_add(len)
777            .expect("stable payload field slot offset overflow");
778        let bytes = self
779            .bytes
780            .get_mut(offset..end)
781            .expect("stable payload field slot out of bounds");
782        assert_eq!(
783            bytes.as_mut_ptr().align_offset(mem::align_of::<U>()),
784            0,
785            "stable payload field slot is misaligned"
786        );
787        StablePayloadInitSlot {
788            bytes,
789            _marker: PhantomData,
790        }
791    }
792
793    /// Re-borrows the sub-region for one array element as a typed slot.
794    #[doc(hidden)]
795    #[inline(always)]
796    pub fn array_element_slot<U>(
797        &mut self,
798        offset: usize,
799        index: usize,
800    ) -> StablePayloadInitSlot<'_, U> {
801        let element_offset = index
802            .checked_mul(mem::size_of::<U>())
803            .and_then(|relative| offset.checked_add(relative))
804            .expect("stable payload array element offset overflow");
805        self.field_slot::<U>(element_offset)
806    }
807
808    /// Discharges the initialization witness.
809    ///
810    /// This is available only to generated all-set typestate builders. Safe
811    /// code cannot construct a slot directly.
812    #[doc(hidden)]
813    #[must_use = "the discharged witness must be returned to or consumed by the completion path"]
814    #[inline(always)]
815    pub fn assume_init(self) -> InitializedStablePayload<'a, T> {
816        // SAFETY: slots can originate only in `StablePayloadInit` entry points,
817        // and generated code exposes this method only on its all-set typestate.
818        unsafe { InitializedStablePayload::new_unchecked() }
819    }
820}
821
822/// Type-agnostic stable-container metadata parsed from a payload encoding.
823#[derive(Clone, Debug, Eq, PartialEq)]
824pub struct StableContainerPayloadInfo {
825    /// Cross-process and cross-language stable type identity.
826    pub type_name: String,
827    /// Stable-container payload variant.
828    pub variant: StablePayloadVariant,
829    /// Exact payload size in bytes.
830    pub size: usize,
831    /// Advertised payload alignment in bytes.
832    pub alignment: usize,
833}
834
835impl StableContainerPayloadInfo {
836    /// Native custom encoding id for stable-container payloads.
837    pub const ENCODING_ID: &'static str = STABLE_CONTAINER_ENCODING_ID;
838
839    /// Parses stable-container metadata from a payload encoding.
840    ///
841    /// # Errors
842    ///
843    /// Returns an error if the encoding is not stable-container metadata or if
844    /// the stable-container content type is malformed.
845    pub fn parse(encoding: &PayloadEncoding) -> Result<Self, UWireError> {
846        let expected = PayloadEncoding::custom(Self::ENCODING_ID, STABLE_CONTAINER_MEDIA_TYPE)
847            .expect("stable-container media type is valid");
848        let Some((id, content_type)) = encoding.custom_identity() else {
849            return Err(UWireError::UnsupportedEncoding {
850                expected: Box::new(expected),
851                actual: Box::new(encoding.clone()),
852            });
853        };
854        if id != Self::ENCODING_ID {
855            return Err(UWireError::UnsupportedEncoding {
856                expected: Box::new(expected),
857                actual: Box::new(encoding.clone()),
858            });
859        }
860        Self::parse_content_type(content_type).map_err(UWireError::invalid_payload)
861    }
862
863    /// Returns whether this metadata is compatible with local stable payload `T`.
864    #[must_use]
865    pub fn is_compatible_with<T>(&self) -> bool
866    where
867        T: StablePayload,
868    {
869        let expected = T::stable_type_detail();
870        self.type_name == expected.type_name
871            && self.variant == expected.variant
872            && self.size == expected.size
873            && self.alignment >= expected.alignment
874    }
875
876    fn parse_content_type(content_type: &str) -> Result<Self, String> {
877        let media_type = mediatype::MediaType::parse(content_type)
878            .map_err(|error| format!("invalid media type: {error}"))?;
879        let expected_media_type = mediatype::MediaType::parse(STABLE_CONTAINER_MEDIA_TYPE)
880            .expect("stable-container media type is valid");
881        if media_type.essence() != expected_media_type {
882            return Err(format!(
883                "media type must be {}",
884                expected_media_type.essence()
885            ));
886        }
887
888        let type_name = required_stable_parameter(&media_type, "type")?;
889        if type_name.is_empty() {
890            return Err("type parameter must not be empty".to_string());
891        }
892        let variant = required_stable_parameter(&media_type, "variant")?;
893        let variant = StablePayloadVariant::parse(&variant).ok_or_else(|| {
894            format!(
895                "variant parameter must be {}",
896                StablePayloadVariant::FixedSize
897            )
898        })?;
899        let size = required_stable_parameter(&media_type, "size")?
900            .parse::<usize>()
901            .map_err(|error| format!("invalid size parameter: {error}"))?;
902        let alignment = required_stable_parameter(&media_type, "align")?
903            .parse::<usize>()
904            .map_err(|error| format!("invalid align parameter: {error}"))?;
905        PayloadLayout::new(size, alignment).map_err(|error| error.to_string())?;
906
907        Ok(Self {
908            type_name,
909            variant,
910            size,
911            alignment,
912        })
913    }
914}
915
916fn required_stable_parameter(
917    media_type: &mediatype::MediaType<'_>,
918    name: &'static str,
919) -> Result<String, String> {
920    media_type
921        .get_param(mediatype::Name::new_unchecked(name))
922        .map(|value| value.unquoted_str().into_owned())
923        .ok_or_else(|| format!("missing {name} parameter"))
924}
925
926/// Transport-independent stable-container payload identity.
927///
928/// `USR-04A` uses this type for metadata and owned-byte preservation. Typed
929/// zero-copy borrows are intentionally deferred to the stable borrow proof API.
930pub struct StableContainerPayload<T>(PhantomData<T>);
931
932impl<T> core::fmt::Debug for StableContainerPayload<T> {
933    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
934        f.debug_struct("StableContainerPayload")
935            .finish_non_exhaustive()
936    }
937}
938
939impl<T: StablePayload> StableContainerPayload<T> {
940    /// Native custom encoding id for stable-container payloads.
941    pub const ENCODING_ID: &'static str = STABLE_CONTAINER_ENCODING_ID;
942
943    const MEDIA_TYPE: &'static str = STABLE_CONTAINER_MEDIA_TYPE;
944
945    /// Returns the stable-container payload encoding for `T`.
946    #[must_use]
947    pub fn encoding() -> PayloadEncoding {
948        <Self as PayloadCodec>::payload_encoding()
949    }
950
951    fn stable_content_type() -> String {
952        let detail = T::stable_type_detail();
953        format!(
954            "{};type={};variant={};size={};align={}",
955            Self::MEDIA_TYPE,
956            Self::quote_parameter(detail.type_name),
957            detail.variant,
958            detail.size,
959            detail.alignment
960        )
961    }
962
963    fn quote_parameter(value: &str) -> String {
964        let quoted = mediatype::Value::quote(value);
965        if quoted.starts_with('"') {
966            quoted.into_owned()
967        } else {
968            format!("\"{quoted}\"")
969        }
970    }
971
972    fn verify_content_type(content_type: &str) -> Result<(), String> {
973        let info = StableContainerPayloadInfo::parse_content_type(content_type)?;
974        let expected = T::stable_type_detail();
975        if info.type_name != expected.type_name {
976            return Err(format!("type parameter must be {}", expected.type_name));
977        }
978        if info.variant != expected.variant {
979            return Err(format!(
980                "variant parameter must be {}",
981                expected.variant.as_str()
982            ));
983        }
984        if info.size != expected.size {
985            return Err(format!("size parameter must be {}", expected.size));
986        }
987        if info.alignment < expected.alignment {
988            return Err(format!(
989                "align parameter must be at least {}",
990                expected.alignment
991            ));
992        }
993        Ok(())
994    }
995
996    pub(crate) fn borrow_checked_payload(src: &[u8]) -> Result<&T, UWireError> {
997        let expected_len = mem::size_of::<T>();
998        if src.len() != expected_len {
999            return Err(UWireError::invalid_payload(format!(
1000                "payload length must be {expected_len}, got {}",
1001                src.len()
1002            )));
1003        }
1004
1005        let alignment = mem::align_of::<T>();
1006        let address = src.as_ptr() as usize;
1007        if !address.is_multiple_of(alignment) {
1008            return Err(UWireError::invalid_payload(format!(
1009                "payload address {address} is not aligned to {alignment}"
1010            )));
1011        }
1012
1013        // SAFETY: length and alignment were verified above. Reaching this helper
1014        // requires the loan-backed receive proof, and `T: StablePayload` is the
1015        // unsafe contract that the bytes represent one initialized `T`.
1016        Ok(unsafe { &*src.as_ptr().cast::<T>() })
1017    }
1018
1019    pub(crate) fn check_uninit_layout(src: &[mem::MaybeUninit<u8>]) -> Result<(), UWireError> {
1020        let expected_len = mem::size_of::<T>();
1021        if src.len() != expected_len {
1022            return Err(UWireError::invalid_payload_length(expected_len, src.len()));
1023        }
1024
1025        let alignment = mem::align_of::<T>();
1026        let address = src.as_ptr() as usize;
1027        if !address.is_multiple_of(alignment) {
1028            return Err(UWireError::invalid_payload(format!(
1029                "payload address {address} is not aligned to {alignment}"
1030            )));
1031        }
1032        Ok(())
1033    }
1034
1035    fn check_initialized_layout(src: &[u8]) -> Result<(), UWireError> {
1036        let expected_len = mem::size_of::<T>();
1037        if src.len() != expected_len {
1038            return Err(UWireError::invalid_payload_length(expected_len, src.len()));
1039        }
1040
1041        if expected_len == 0 {
1042            return Ok(());
1043        }
1044
1045        let alignment = mem::align_of::<T>();
1046        let address = src.as_ptr() as usize;
1047        if !address.is_multiple_of(alignment) {
1048            return Err(UWireError::invalid_payload(format!(
1049                "payload address {address} is not aligned to {alignment}"
1050            )));
1051        }
1052        Ok(())
1053    }
1054}
1055
1056impl<T> PayloadCodec for StableContainerPayload<T>
1057where
1058    T: StablePayload,
1059{
1060    fn codec_name() -> &'static str {
1061        "stable-container"
1062    }
1063
1064    fn payload_encoding() -> PayloadEncoding {
1065        PayloadEncoding::custom(Self::ENCODING_ID, Self::stable_content_type())
1066            .expect("stable-container payload encoding is valid")
1067    }
1068
1069    fn verify_encoding(actual: Option<&PayloadEncoding>) -> Result<(), UWireError> {
1070        let expected = Self::payload_encoding();
1071        let actual = actual.ok_or(UWireError::MissingEncoding)?;
1072        let Some((id, content_type)) = actual.custom_identity() else {
1073            return Err(UWireError::UnsupportedEncoding {
1074                expected: Box::new(expected),
1075                actual: Box::new(actual.clone()),
1076            });
1077        };
1078        if id != Self::ENCODING_ID {
1079            return Err(UWireError::UnsupportedEncoding {
1080                expected: Box::new(expected),
1081                actual: Box::new(actual.clone()),
1082            });
1083        }
1084        Self::verify_content_type(content_type).map_err(|reason| {
1085            UWireError::invalid_payload(format!(
1086                "incompatible stable payload: expected {}; actual {} ({reason})",
1087                T::stable_type_detail(),
1088                content_type
1089            ))
1090        })
1091    }
1092}
1093
1094// SAFETY:
1095// - `loan_payload` verifies exact length and alignment before casting the loaned
1096//   destination to `*mut T`.
1097// - `T: ByteBackedStablePayload + Default` provides a no-padding stable payload
1098//   proof and a safe initialized value before `&mut T` is exposed to callers.
1099unsafe impl<T> LoanPayload<T> for StableContainerPayload<T>
1100where
1101    T: ByteBackedStablePayload + Default,
1102{
1103    fn loan_layout() -> Result<PayloadLayout, UWireError> {
1104        PayloadLayout::new(mem::size_of::<T>(), mem::align_of::<T>())
1105    }
1106
1107    fn loan_payload(dst: &mut [u8]) -> Result<&mut T, UWireError> {
1108        Self::check_initialized_layout(dst)?;
1109        dst.fill(0);
1110        let ptr = if mem::size_of::<T>() == 0 {
1111            NonNull::<T>::dangling().as_ptr()
1112        } else {
1113            dst.as_mut_ptr().cast::<T>()
1114        };
1115        // SAFETY: the checked byte range has the exact `T` layout, or `T` is a
1116        // zero-sized byte-backed type using an aligned dangling pointer. Writing
1117        // `T::default()` creates one valid initialized `T` before returning the
1118        // unique mutable reference tied to `dst`'s borrow.
1119        unsafe { ptr.write(T::default()) };
1120        // SAFETY: `ptr` now points to one initialized `T`; `dst` is exclusively
1121        // borrowed for the returned lifetime and no other reference to the value
1122        // is created by this helper.
1123        Ok(unsafe { &mut *ptr })
1124    }
1125}
1126
1127// SAFETY:
1128// - `loan_uninit_payload` checks exact length and alignment before constructing a
1129//   typed uninit slot.
1130// - `T: ByteBackedStablePayload` proves safe no-zero TX cannot expose
1131//   uninitialized implicit padding when the full `size_of::<T>()` byte range is
1132//   committed after the returned initialized marker is produced.
1133#[cfg(feature = "zero-copy-transport")]
1134unsafe impl<T> LoanUninitPayload<T> for StableContainerPayload<T>
1135where
1136    T: ByteBackedStablePayload,
1137{
1138    fn loan_uninit_layout() -> Result<PayloadLayout, UWireError> {
1139        PayloadLayout::new(mem::size_of::<T>(), mem::align_of::<T>())
1140    }
1141
1142    fn loan_uninit_payload<'a>(
1143        mut dst: LoanedPayloadUninitMut<'a>,
1144    ) -> Result<LoanedUninitPayload<'a, T>, UWireError> {
1145        let bytes = dst.as_uninit_bytes_mut_internal();
1146        Self::check_uninit_layout(bytes)?;
1147        // This is the ONE byte-region -> typed-slot entry point of the safe
1148        // loan path: layout is verified (exact `T` size, alignment) and the
1149        // exclusive byte borrow is reinterpreted as an exclusive borrow of a
1150        // single uninitialized `T`.
1151        // SAFETY: `check_uninit_layout` verified exact `T` length and
1152        // alignment; `MaybeUninit<T>` has no validity requirements; the
1153        // returned reference inherits the exclusive `'a` borrow.
1154        let slot = unsafe { &mut *bytes.as_mut_ptr().cast::<mem::MaybeUninit<T>>() };
1155        Ok(LoanedUninitPayload::new(slot))
1156    }
1157}
1158
1159// SAFETY:
1160// - `borrow_payload` verifies exact length and alignment before casting.
1161// - `T: StablePayload` is the stable-container safety contract that the visible
1162//   payload bytes represent one initialized `T` for receive-side borrowing.
1163unsafe impl<T> BorrowPayload<T> for StableContainerPayload<T>
1164where
1165    T: StablePayload,
1166{
1167    fn borrow_payload(src: &[u8]) -> Result<&T, UWireError> {
1168        Self::borrow_checked_payload(src)
1169    }
1170}
1171
1172#[cfg(test)]
1173mod tests {
1174    #[cfg(feature = "owned-frame-transport")]
1175    use bytes::Bytes;
1176
1177    #[cfg(feature = "owned-frame-transport")]
1178    use crate::{UMessageBuilder, UOwnedFrame, UUri};
1179
1180    use super::*;
1181
1182    #[cfg(feature = "owned-frame-transport")]
1183    fn topic() -> UUri {
1184        UUri::try_from_parts("vehicle", 0x4210, 0x01, 0x9000).expect("topic")
1185    }
1186
1187    #[repr(C)]
1188    #[derive(Clone, Copy, Debug, Eq, PartialEq)]
1189    struct VehiclePose {
1190        x: i32,
1191        y: i32,
1192    }
1193
1194    // SAFETY: upholds the POD layout contract: repr(C), declared padding
1195    // only, every byte of a live value initialized.
1196    unsafe impl StablePayload for VehiclePose {
1197        const TYPE_NAME: &'static str = "example.vehicle.VehiclePose";
1198    }
1199
1200    fn stable_container_encoding(
1201        type_name: &str,
1202        variant: &str,
1203        size: usize,
1204        align: usize,
1205    ) -> PayloadEncoding {
1206        PayloadEncoding::custom(
1207            StableContainerPayload::<VehiclePose>::ENCODING_ID,
1208            format!(
1209                "application/vnd.uprotocol.stable-container;type=\"{type_name}\";variant={variant};size={size};align={align}"
1210            ),
1211        )
1212        .expect("valid custom encoding")
1213    }
1214
1215    #[test]
1216    fn stable_payload_type_detail_is_used_in_encoding() {
1217        let detail = VehiclePose::stable_type_detail();
1218
1219        assert_eq!(detail.variant, StablePayloadVariant::FixedSize);
1220        assert_eq!(detail.type_name, "example.vehicle.VehiclePose");
1221        assert_eq!(detail.size, mem::size_of::<VehiclePose>());
1222        assert_eq!(detail.alignment, mem::align_of::<VehiclePose>());
1223
1224        let encoding = StableContainerPayload::<VehiclePose>::encoding();
1225        let (id, content_type) = encoding.custom_identity().expect("custom encoding");
1226        assert_eq!(id, StableContainerPayload::<VehiclePose>::ENCODING_ID);
1227        assert_eq!(
1228            content_type,
1229            "application/vnd.uprotocol.stable-container;type=\"example.vehicle.VehiclePose\";variant=fixed;size=8;align=4"
1230        );
1231    }
1232
1233    #[test]
1234    fn stable_container_payload_info_parses_type_agnostic_metadata() {
1235        let info =
1236            StableContainerPayloadInfo::parse(&StableContainerPayload::<VehiclePose>::encoding())
1237                .expect("parse stable metadata");
1238
1239        assert_eq!(info.type_name, "example.vehicle.VehiclePose");
1240        assert_eq!(info.variant, StablePayloadVariant::FixedSize);
1241        assert_eq!(info.size, mem::size_of::<VehiclePose>());
1242        assert_eq!(info.alignment, mem::align_of::<VehiclePose>());
1243        assert!(info.is_compatible_with::<VehiclePose>());
1244    }
1245
1246    #[test]
1247    fn stable_container_payload_info_rejects_malformed_metadata() {
1248        let encoding = PayloadEncoding::custom(
1249            StableContainerPayloadInfo::ENCODING_ID,
1250            "application/vnd.uprotocol.stable-container;variant=fixed;size=8;align=4",
1251        )
1252        .expect("custom encoding");
1253
1254        let error = StableContainerPayloadInfo::parse(&encoding).unwrap_err();
1255
1256        assert!(
1257            matches!(error, UWireError::InvalidPayload(message) if message.contains("missing type"))
1258        );
1259    }
1260
1261    #[test]
1262    fn stable_container_verify_accepts_larger_advertised_alignment() {
1263        let encoding = stable_container_encoding(
1264            "example.vehicle.VehiclePose",
1265            "fixed",
1266            mem::size_of::<VehiclePose>(),
1267            mem::align_of::<VehiclePose>() * 2,
1268        );
1269
1270        StableContainerPayload::<VehiclePose>::verify_encoding(Some(&encoding))
1271            .expect("larger alignment is compatible");
1272    }
1273
1274    #[test]
1275    fn stable_container_verify_rejects_incompatible_metadata() {
1276        let wrong_type = stable_container_encoding(
1277            "example.vehicle.OtherPose",
1278            "fixed",
1279            mem::size_of::<VehiclePose>(),
1280            mem::align_of::<VehiclePose>(),
1281        );
1282        let wrong_size = stable_container_encoding(
1283            "example.vehicle.VehiclePose",
1284            "fixed",
1285            mem::size_of::<VehiclePose>() + 1,
1286            mem::align_of::<VehiclePose>(),
1287        );
1288        let insufficient_alignment = stable_container_encoding(
1289            "example.vehicle.VehiclePose",
1290            "fixed",
1291            mem::size_of::<VehiclePose>(),
1292            mem::align_of::<VehiclePose>() / 2,
1293        );
1294
1295        for encoding in [wrong_type, wrong_size, insufficient_alignment] {
1296            assert!(matches!(
1297                StableContainerPayload::<VehiclePose>::verify_encoding(Some(&encoding)),
1298                Err(UWireError::InvalidPayload(_))
1299            ));
1300        }
1301    }
1302
1303    #[cfg(feature = "owned-frame-transport")]
1304    #[test]
1305    fn stable_container_owned_frame_preserves_bytes_and_custom_metadata() {
1306        let payload = Bytes::from_static(b"\x0a\x00\x00\x00\x14\x00\x00\x00");
1307        assert_eq!(payload.len(), mem::size_of::<VehiclePose>());
1308        let message = UMessageBuilder::publish(topic()).build().expect("message");
1309        let metadata = crate::frame::metadata::try_project_attributes_to_frame_metadata(
1310            message.attributes(),
1311            Some(StableContainerPayload::<VehiclePose>::encoding()),
1312        )
1313        .expect("metadata");
1314
1315        let frame = UOwnedFrame::with_payload(metadata, payload.clone()).expect("owned frame");
1316
1317        assert_eq!(
1318            frame.metadata().payload_encoding(),
1319            Some(&StableContainerPayload::<VehiclePose>::encoding())
1320        );
1321        assert_eq!(frame.payload(), Some(&payload));
1322        assert_eq!(frame.payload_bytes(), payload.as_ref());
1323    }
1324}