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

up_rust/frame/
abi.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//! Fixed-layout ABI **profile** of native frame metadata, version 1.
15//!
16//! MAINTENANCE NOTE (dead-code audits): the consumers of this module are
17//! **cross-language** — C/C++ peers reading typed shared-memory headers and
18//! future polyglot bindings. Zero references from Rust repositories is the
19//! expected steady state and is NOT evidence this module is unused. Do not
20//! remove it on the basis of a Rust-side usage census.
21//!
22//! [`UFrameMetadataAbiV1`] is a *derived representation* of the semantic
23//! [`UFrameMetadata`] — not the canonical model, not the default wire form.
24//! It exists for the boundaries that genuinely need a fixed, directly
25//! readable C/C++/Rust struct: for example an iceoryx2 service whose peers
26//! want metadata as a typed user header instead of parsing the canonical
27//! field block, or deterministic byte-image test fixtures.
28//!
29//! Most transports do not need this profile: the selected-wire layer carries
30//! variable metadata bytes produced by a metadata codec, and the
31//! shared-memory transports (iceoryx2, LoLa) already use a small fixed
32//! *placement* header plus that variable block. Use this profile only when
33//! both sides explicitly agree on it.
34//!
35//! Layout rules (the shared-memory contract): `#[repr(C)]`, self-contained
36//! (no pointers, no heap, no `Drop`), fixed size, explicit padding only,
37//! fixed-width integers and byte arrays only, little-endian in serialized
38//! form and native-endian in same-host shared memory. Compile-time
39//! assertions at the bottom of this file fail the build if the layout
40//! drifts.
41//!
42//! Conversions to and from [`UFrameMetadata`] are fallible and **never
43//! truncate**: a value that exceeds a profile capacity is an error. The
44//! per-field capacities are profile policy (documented on each constant),
45//! not limits of the semantic model.
46//!
47//! ## Cross-language reader checklist
48//!
49//! 1. Match `UFRAME_ABI_TYPE_NAME`, size, alignment, magic, and version before
50//!    interpreting a header.
51//! 2. Mirror every field with fixed-width integer/byte-array types and C layout;
52//!    never substitute native pointer, string, boolean, or enum layouts.
53//! 3. Check the presence mask before reading optional fields and reject unknown
54//!    mask/flag bits.
55//! 4. Treat stored lengths as untrusted and verify each against its documented
56//!    capacity before constructing a language-level string/view.
57//! 5. Use the fallible conversion/golden byte-image tests as the arbiter. A
58//!    value too large for this profile remains valid semantic metadata; choose
59//!    the canonical field block instead of truncating it.
60//!
61//! The normative semantic model and metadata-profile registry live in
62//! `up-spec/basics/uframe.adoc`; this Rust struct is one derived ABI profile,
63//! not a language-specific conformance requirement.
64
65use core::mem::{align_of, offset_of, size_of};
66use std::time::Duration;
67
68use crate::{FrameMessageKind, FramePriority, PayloadEncoding, UCode, UFrameMetadata, UUri, UUID};
69
70// Reuse the canonical presence-bit vocabulary of the field block so the two
71// representations stay aligned.
72pub use crate::frame::codec::{
73    FIELD_COMM_STATUS, FIELD_MASK_V1, FIELD_PAYLOAD_ENCODING, FIELD_PERMISSION_LEVEL, FIELD_REQID,
74    FIELD_SINK, FIELD_TOKEN, FIELD_TRACEPARENT, FIELD_TTL,
75};
76
77/// Magic bytes at offset 0 of every [`UFrameMetadataAbiV1`]: `"UFA1"`.
78pub const UFRAME_ABI_MAGIC: [u8; 4] = *b"UFA1";
79
80/// ABI profile version described by this module.
81pub const UFRAME_ABI_VERSION: u8 = 1;
82
83/// Total size of [`UFrameMetadataAbiV1`] in bytes.
84pub const UFRAME_ABI_SIZE: usize = 1096;
85
86/// Alignment of [`UFrameMetadataAbiV1`] in bytes.
87pub const UFRAME_ABI_ALIGN: usize = 8;
88
89/// Semantic type name for cross-language service matching (e.g. iceoryx2
90/// `#[type_name(...)]` in Rust and `IOX2_TYPE_NAME` in C++).
91pub const UFRAME_ABI_TYPE_NAME: &str = "uprotocol.v2.UFrameMetadataAbiV1";
92
93/// Flag bit: producer stored multi-byte integers little-endian. MUST be set
94/// in the serialized form of profile v1.
95pub const UFRAME_ABI_FLAG_LITTLE_ENDIAN: u8 = 0b0000_0001;
96
97/// Profile capacity for a UUri authority name (the UUri spec limit).
98pub const UFRAME_ABI_AUTHORITY_CAPACITY: usize = 128;
99/// Profile capacity for a payload encoding literal id.
100pub const UFRAME_ABI_LITERAL_ID_CAPACITY: usize = 64;
101/// Profile capacity for a payload encoding content type.
102pub const UFRAME_ABI_CONTENT_TYPE_CAPACITY: usize = 100;
103/// Profile capacity for a W3C traceparent (version 00 uses 55 characters).
104pub const UFRAME_ABI_TRACEPARENT_CAPACITY: usize = 63;
105/// Profile capacity for an access token. This is a policy limit of this
106/// profile only; conversions fail (never truncate) for larger tokens, which
107/// remain fully supported by the semantic model and the canonical field
108/// block.
109pub const UFRAME_ABI_TOKEN_CAPACITY: usize = 510;
110
111/// Errors returned by ABI profile conversions.
112#[derive(Clone, Debug, Eq, PartialEq)]
113pub enum UFrameAbiError {
114    /// A value exceeds a fixed capacity of this profile.
115    CapacityExceeded {
116        /// Name of the offending field.
117        field: &'static str,
118    },
119    /// The struct is not a structurally valid profile v1 value.
120    InvalidProfile(String),
121    /// The metadata violates frame invariants.
122    Metadata(crate::UFrameMetadataError),
123}
124
125impl std::fmt::Display for UFrameAbiError {
126    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
127        match self {
128            Self::CapacityExceeded { field } => {
129                write!(f, "value of `{field}` exceeds the ABI profile capacity")
130            }
131            Self::InvalidProfile(message) => write!(f, "invalid ABI profile value: {message}"),
132            Self::Metadata(error) => write!(f, "invalid frame metadata: {error}"),
133        }
134    }
135}
136
137impl std::error::Error for UFrameAbiError {}
138
139impl From<crate::UFrameMetadataError> for UFrameAbiError {
140    fn from(value: crate::UFrameMetadataError) -> Self {
141        Self::Metadata(value)
142    }
143}
144
145/// UUID as two 64-bit halves. size 16, align 8, no padding.
146#[repr(C)]
147#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
148pub struct UUuidAbi {
149    /// Most significant 64 bits of the UUID.
150    pub msb: u64,
151    /// Least significant 64 bits of the UUID.
152    pub lsb: u64,
153}
154
155/// Fixed-capacity UUri. size 136, align 4, no padding.
156#[repr(C)]
157#[derive(Clone, Copy, Debug, Eq, PartialEq)]
158pub struct UUriAbi {
159    /// uEntity identifier.
160    pub ue_id: u32,
161    /// Resource identifier within the uEntity.
162    pub resource_id: u16,
163    /// uEntity API major version.
164    pub ue_version_major: u8,
165    /// Length in bytes of the authority name.
166    pub authority_name_len: u8,
167    /// Authority name bytes (fixed capacity; `authority_name_len` bytes valid).
168    pub authority_name: [u8; UFRAME_ABI_AUTHORITY_CAPACITY],
169}
170
171impl Default for UUriAbi {
172    fn default() -> Self {
173        Self {
174            ue_id: 0,
175            resource_id: 0,
176            ue_version_major: 0,
177            authority_name_len: 0,
178            authority_name: [0; UFRAME_ABI_AUTHORITY_CAPACITY],
179        }
180    }
181}
182
183/// Fixed-capacity open payload encoding. size 172, align 4, no padding.
184///
185/// Mirrors the three identity components of [`PayloadEncoding`]:
186/// `registry_id` (0 when absent — presence tracked via the component flags),
187/// a literal id string, and a content type string.
188#[repr(C)]
189#[derive(Clone, Copy, Debug, Eq, PartialEq)]
190pub struct UPayloadEncodingAbi {
191    /// Compact registry identifier of the payload encoding.
192    pub registry_id: u32,
193    /// Length in bytes of the literal encoding id.
194    pub literal_id_len: u8,
195    /// Length in bytes of the content type.
196    pub content_type_len: u8,
197    /// bit0: registry_id present; bits 1..=7 MUST be zero.
198    pub component_flags: u8,
199    /// MUST be zero.
200    pub _reserved: u8,
201    /// Literal encoding id bytes (fixed capacity; `literal_id_len` bytes valid).
202    pub literal_id: [u8; UFRAME_ABI_LITERAL_ID_CAPACITY],
203    /// Content type bytes (fixed capacity; `content_type_len` bytes valid).
204    pub content_type: [u8; UFRAME_ABI_CONTENT_TYPE_CAPACITY],
205}
206
207/// Component flag: `registry_id` carries a value.
208pub const UFRAME_ABI_ENCODING_HAS_REGISTRY_ID: u8 = 0b0000_0001;
209
210impl Default for UPayloadEncodingAbi {
211    fn default() -> Self {
212        Self {
213            registry_id: 0,
214            literal_id_len: 0,
215            content_type_len: 0,
216            component_flags: 0,
217            _reserved: 0,
218            literal_id: [0; UFRAME_ABI_LITERAL_ID_CAPACITY],
219            content_type: [0; UFRAME_ABI_CONTENT_TYPE_CAPACITY],
220        }
221    }
222}
223
224/// Fixed-capacity traceparent. size 64, align 1, no padding.
225#[repr(C)]
226#[derive(Clone, Copy, Debug, Eq, PartialEq)]
227pub struct UTraceparentAbi {
228    /// Length in bytes of the traceparent value.
229    pub len: u8,
230    /// Traceparent bytes (fixed capacity; `len` bytes valid).
231    pub bytes: [u8; UFRAME_ABI_TRACEPARENT_CAPACITY],
232}
233
234impl Default for UTraceparentAbi {
235    fn default() -> Self {
236        Self {
237            len: 0,
238            bytes: [0; UFRAME_ABI_TRACEPARENT_CAPACITY],
239        }
240    }
241}
242
243/// Fixed-capacity access token. size 512, align 2, no padding.
244#[repr(C)]
245#[derive(Clone, Copy, Debug, Eq, PartialEq)]
246pub struct UTokenAbi {
247    /// Length in bytes of the token value.
248    pub len: u16,
249    /// Token bytes (fixed capacity; `len` bytes valid).
250    pub bytes: [u8; UFRAME_ABI_TOKEN_CAPACITY],
251}
252
253impl Default for UTokenAbi {
254    fn default() -> Self {
255        Self {
256            len: 0,
257            bytes: [0; UFRAME_ABI_TOKEN_CAPACITY],
258        }
259    }
260}
261
262/// Fixed-layout ABI profile v1 of native frame metadata.
263///
264/// size 1096, align 8, no implicit padding. A field guarded by a `FIELD_*`
265/// presence bit is meaningful only when that bit is set; producers MUST
266/// zero absent fields. `payload_size` is meaningful if and only if
267/// [`FIELD_PAYLOAD_ENCODING`] is set (the v1 invariant: a frame carries
268/// payload bytes exactly when it declares a payload encoding).
269#[repr(C)]
270#[derive(Clone, Copy, Debug, PartialEq)]
271pub struct UFrameMetadataAbiV1 {
272    // identification / evolution block: 0..8
273    /// MUST equal [`UFRAME_ABI_MAGIC`].
274    pub magic: [u8; 4],
275    /// MUST equal [`UFRAME_ABI_VERSION`].
276    pub version: u8,
277    /// See `UFRAME_ABI_FLAG_*`.
278    pub flags: u8,
279    /// MUST equal [`UFRAME_ABI_SIZE`] in v1; consumers use this to skip
280    /// trailing fields added by newer minor revisions.
281    pub metadata_size: u16,
282
283    // presence & small scalars: 8..16
284    /// Bitwise OR of `FIELD_*` presence bits (shared with the canonical
285    /// field block).
286    pub presence: u32,
287    /// [`FrameMessageKind`] wire code (1..=4).
288    pub kind: u8,
289    /// [`FramePriority`] wire code; 0 = absent.
290    pub priority: u8,
291    /// `UCode` value; valid iff [`FIELD_COMM_STATUS`].
292    pub comm_status: u8,
293    /// MUST be zero.
294    pub _reserved0: u8,
295
296    // scalars: 16..32
297    /// Time-to-live in nanoseconds; valid iff [`FIELD_TTL`].
298    pub ttl_ns: u64,
299    /// Valid iff [`FIELD_PERMISSION_LEVEL`].
300    pub permission_level: u32,
301    /// MUST be zero.
302    pub _reserved1: u32,
303
304    // identifiers: 32..64
305    /// Message id as a fixed-layout UUID.
306    pub id: UUuidAbi,
307    /// Valid iff [`FIELD_REQID`].
308    pub reqid: UUuidAbi,
309
310    // payload description: 64..244
311    /// Number of payload bytes belonging to the frame; valid iff
312    /// [`FIELD_PAYLOAD_ENCODING`], MUST be zero otherwise.
313    pub payload_size: u64,
314    /// Valid iff [`FIELD_PAYLOAD_ENCODING`].
315    pub payload_encoding: UPayloadEncodingAbi,
316
317    // addressing: 244..516
318    /// Source address as a fixed-layout URI.
319    pub source: UUriAbi,
320    /// Valid iff [`FIELD_SINK`].
321    pub sink: UUriAbi,
322
323    // tracing / auth: 516..1092
324    /// Valid iff [`FIELD_TRACEPARENT`].
325    pub traceparent: UTraceparentAbi,
326    /// Valid iff [`FIELD_TOKEN`].
327    pub token: UTokenAbi,
328
329    // explicit tail padding: 1092..1096
330    /// MUST be zero.
331    pub _reserved_tail: [u8; 4],
332}
333
334impl Default for UFrameMetadataAbiV1 {
335    fn default() -> Self {
336        Self {
337            magic: UFRAME_ABI_MAGIC,
338            version: UFRAME_ABI_VERSION,
339            flags: UFRAME_ABI_FLAG_LITTLE_ENDIAN,
340            metadata_size: UFRAME_ABI_SIZE as u16,
341            presence: 0,
342            kind: 0,
343            priority: 0,
344            comm_status: 0,
345            _reserved0: 0,
346            ttl_ns: 0,
347            permission_level: 0,
348            _reserved1: 0,
349            id: UUuidAbi::default(),
350            reqid: UUuidAbi::default(),
351            payload_size: 0,
352            payload_encoding: UPayloadEncodingAbi::default(),
353            source: UUriAbi::default(),
354            sink: UUriAbi::default(),
355            traceparent: UTraceparentAbi::default(),
356            token: UTokenAbi::default(),
357            _reserved_tail: [0; 4],
358        }
359    }
360}
361
362fn fill_str(
363    target: &mut [u8],
364    target_len_max: usize,
365    value: &str,
366    field: &'static str,
367) -> Result<usize, UFrameAbiError> {
368    let bytes = value.as_bytes();
369    if bytes.len() > target_len_max {
370        return Err(UFrameAbiError::CapacityExceeded { field });
371    }
372    let target = target
373        .get_mut(..bytes.len())
374        .ok_or(UFrameAbiError::CapacityExceeded { field })?;
375    target.copy_from_slice(bytes);
376    Ok(bytes.len())
377}
378
379fn read_str<'a>(
380    bytes: &'a [u8],
381    len: usize,
382    capacity: usize,
383    field: &'static str,
384) -> Result<&'a str, UFrameAbiError> {
385    if len > capacity {
386        return Err(UFrameAbiError::InvalidProfile(format!(
387            "`{field}` length {len} exceeds capacity {capacity}"
388        )));
389    }
390    let bytes = bytes.get(..len).ok_or_else(|| {
391        UFrameAbiError::InvalidProfile(format!(
392            "`{field}` length {len} exceeds available bytes {}",
393            bytes.len()
394        ))
395    })?;
396    core::str::from_utf8(bytes)
397        .map_err(|error| UFrameAbiError::InvalidProfile(format!("`{field}` is not UTF-8: {error}")))
398}
399
400fn uuri_to_abi(uri: &UUri, field: &'static str) -> Result<UUriAbi, UFrameAbiError> {
401    let mut abi = UUriAbi {
402        ue_id: (u32::from(uri.uentity_instance_id()) << 16) | u32::from(uri.uentity_type_id()),
403        resource_id: uri.resource_id(),
404        ue_version_major: uri.uentity_major_version(),
405        ..Default::default()
406    };
407    let len = fill_str(
408        &mut abi.authority_name,
409        UFRAME_ABI_AUTHORITY_CAPACITY,
410        uri.authority_name(),
411        field,
412    )?;
413    abi.authority_name_len = len as u8;
414    Ok(abi)
415}
416
417fn uuri_from_abi(abi: &UUriAbi, field: &'static str) -> Result<UUri, UFrameAbiError> {
418    let authority = read_str(
419        &abi.authority_name,
420        usize::from(abi.authority_name_len),
421        UFRAME_ABI_AUTHORITY_CAPACITY,
422        field,
423    )?;
424    UUri::try_from_parts(authority, abi.ue_id, abi.ue_version_major, abi.resource_id)
425        .map_err(|error| UFrameAbiError::InvalidProfile(format!("invalid `{field}`: {error}")))
426}
427
428impl UFrameMetadataAbiV1 {
429    /// Converts semantic frame metadata (plus the frame's payload size, when
430    /// it carries a payload) into the fixed ABI profile.
431    ///
432    /// `payload_size` must be `Some` exactly when the metadata has a payload
433    /// encoding, mirroring the frame-level invariant.
434    ///
435    /// # Errors
436    ///
437    /// Returns an error if the metadata is invalid, payload presence is
438    /// inconsistent, or a value exceeds a profile capacity. Conversion never
439    /// truncates.
440    pub fn try_from_metadata(
441        metadata: &UFrameMetadata,
442        payload_size: Option<u64>,
443    ) -> Result<Self, UFrameAbiError> {
444        metadata.validate()?;
445        match (
446            payload_size.is_some(),
447            metadata.payload_encoding().is_some(),
448        ) {
449            (true, true) | (false, false) => {}
450            (true, false) => {
451                return Err(UFrameAbiError::Metadata(
452                    crate::UFrameMetadataError::PayloadWithoutEncoding,
453                ))
454            }
455            (false, true) => {
456                return Err(UFrameAbiError::Metadata(
457                    crate::UFrameMetadataError::EncodingWithoutPayload,
458                ))
459            }
460        }
461
462        let (msb, lsb) = metadata.id().as_u64_pair();
463        let mut abi = Self {
464            kind: metadata.kind().wire_code(),
465            priority: metadata.priority().map_or(0, FramePriority::wire_code),
466            id: UUuidAbi { msb, lsb },
467            source: uuri_to_abi(metadata.source(), "source.authority_name")?,
468            ..Self::default()
469        };
470
471        if let Some(sink) = metadata.sink() {
472            abi.sink = uuri_to_abi(sink, "sink.authority_name")?;
473            abi.presence |= FIELD_SINK;
474        }
475        if let Some(reqid) = metadata.reqid() {
476            let (msb, lsb) = reqid.as_u64_pair();
477            abi.reqid = UUuidAbi { msb, lsb };
478            abi.presence |= FIELD_REQID;
479        }
480        if let Some(ttl) = metadata.ttl() {
481            abi.ttl_ns = u64::try_from(ttl.as_nanos())
482                .map_err(|_| UFrameAbiError::CapacityExceeded { field: "ttl" })?;
483            abi.presence |= FIELD_TTL;
484        }
485        if let Some(comm_status) = metadata.comm_status() {
486            abi.comm_status = u8::try_from(comm_status.value()).map_err(|_| {
487                UFrameAbiError::CapacityExceeded {
488                    field: "comm_status",
489                }
490            })?;
491            abi.presence |= FIELD_COMM_STATUS;
492        }
493        if let Some(permission_level) = metadata.permission_level() {
494            abi.permission_level = permission_level;
495            abi.presence |= FIELD_PERMISSION_LEVEL;
496        }
497        if let Some(token) = metadata.token() {
498            let len = fill_str(
499                &mut abi.token.bytes,
500                UFRAME_ABI_TOKEN_CAPACITY,
501                token,
502                "token",
503            )?;
504            abi.token.len = len as u16;
505            abi.presence |= FIELD_TOKEN;
506        }
507        if let Some(traceparent) = metadata.traceparent() {
508            let len = fill_str(
509                &mut abi.traceparent.bytes,
510                UFRAME_ABI_TRACEPARENT_CAPACITY,
511                traceparent,
512                "traceparent",
513            )?;
514            abi.traceparent.len = len as u8;
515            abi.presence |= FIELD_TRACEPARENT;
516        }
517        if let Some(encoding) = metadata.payload_encoding() {
518            if let Some(registry_id) = encoding.registry_id() {
519                abi.payload_encoding.registry_id = registry_id;
520                abi.payload_encoding.component_flags |= UFRAME_ABI_ENCODING_HAS_REGISTRY_ID;
521            }
522            if let Some(literal_id) = encoding.literal_id() {
523                let len = fill_str(
524                    &mut abi.payload_encoding.literal_id,
525                    UFRAME_ABI_LITERAL_ID_CAPACITY,
526                    literal_id,
527                    "payload_encoding.literal_id",
528                )?;
529                abi.payload_encoding.literal_id_len = len as u8;
530            }
531            if let Some(content_type) = encoding.content_type() {
532                let len = fill_str(
533                    &mut abi.payload_encoding.content_type,
534                    UFRAME_ABI_CONTENT_TYPE_CAPACITY,
535                    content_type,
536                    "payload_encoding.content_type",
537                )?;
538                abi.payload_encoding.content_type_len = len as u8;
539            }
540            abi.payload_size = payload_size.unwrap_or_default();
541            abi.presence |= FIELD_PAYLOAD_ENCODING;
542        }
543        Ok(abi)
544    }
545
546    /// Converts this profile value back into semantic frame metadata plus the
547    /// frame's payload size (when the frame carries a payload).
548    ///
549    /// # Errors
550    ///
551    /// Returns an error if the profile value is structurally invalid or
552    /// decodes into invalid metadata.
553    pub fn try_to_metadata(&self) -> Result<(UFrameMetadata, Option<u64>), UFrameAbiError> {
554        if self.magic != UFRAME_ABI_MAGIC {
555            return Err(UFrameAbiError::InvalidProfile("wrong magic".to_string()));
556        }
557        if self.version != UFRAME_ABI_VERSION {
558            return Err(UFrameAbiError::InvalidProfile(format!(
559                "unsupported profile version {}",
560                self.version
561            )));
562        }
563        if usize::from(self.metadata_size) < UFRAME_ABI_SIZE {
564            return Err(UFrameAbiError::InvalidProfile(
565                "declared metadata_size is smaller than profile v1".to_string(),
566            ));
567        }
568        if self.presence & !FIELD_MASK_V1 != 0 {
569            return Err(UFrameAbiError::InvalidProfile(
570                "unknown presence bits".to_string(),
571            ));
572        }
573        if self._reserved0 != 0 || self._reserved1 != 0 || self._reserved_tail != [0; 4] {
574            return Err(UFrameAbiError::InvalidProfile(
575                "reserved fields must be zero".to_string(),
576            ));
577        }
578
579        let kind = FrameMessageKind::from_wire_code(self.kind).ok_or_else(|| {
580            UFrameAbiError::InvalidProfile(format!("unknown kind code {}", self.kind))
581        })?;
582        let priority = match self.priority {
583            0 => None,
584            code => Some(FramePriority::from_wire_code(code).ok_or_else(|| {
585                UFrameAbiError::InvalidProfile(format!("unknown priority code {code}"))
586            })?),
587        };
588        let id = UUID::from_u64_pair(self.id.msb, self.id.lsb)
589            .map_err(|error| UFrameAbiError::InvalidProfile(format!("invalid id: {error}")))?;
590        let source = uuri_from_abi(&self.source, "source")?;
591        let sink = if self.presence & FIELD_SINK != 0 {
592            Some(uuri_from_abi(&self.sink, "sink")?)
593        } else {
594            None
595        };
596        let reqid = if self.presence & FIELD_REQID != 0 {
597            Some(
598                UUID::from_u64_pair(self.reqid.msb, self.reqid.lsb).map_err(|error| {
599                    UFrameAbiError::InvalidProfile(format!("invalid reqid: {error}"))
600                })?,
601            )
602        } else {
603            None
604        };
605        let ttl = (self.presence & FIELD_TTL != 0).then(|| Duration::from_nanos(self.ttl_ns));
606        let comm_status = if self.presence & FIELD_COMM_STATUS != 0 {
607            Some(
608                UCode::try_from_i32(i32::from(self.comm_status)).map_err(|_| {
609                    UFrameAbiError::InvalidProfile(format!(
610                        "unknown communication status code {}",
611                        self.comm_status
612                    ))
613                })?,
614            )
615        } else {
616            None
617        };
618        let permission_level =
619            (self.presence & FIELD_PERMISSION_LEVEL != 0).then_some(self.permission_level);
620        let token = if self.presence & FIELD_TOKEN != 0 {
621            Some(
622                read_str(
623                    &self.token.bytes,
624                    usize::from(self.token.len),
625                    UFRAME_ABI_TOKEN_CAPACITY,
626                    "token",
627                )?
628                .to_owned(),
629            )
630        } else {
631            None
632        };
633        let traceparent = if self.presence & FIELD_TRACEPARENT != 0 {
634            Some(
635                read_str(
636                    &self.traceparent.bytes,
637                    usize::from(self.traceparent.len),
638                    UFRAME_ABI_TRACEPARENT_CAPACITY,
639                    "traceparent",
640                )?
641                .to_owned(),
642            )
643        } else {
644            None
645        };
646        let (payload_encoding, payload_size) = if self.presence & FIELD_PAYLOAD_ENCODING != 0 {
647            let enc = &self.payload_encoding;
648            if enc.component_flags & !UFRAME_ABI_ENCODING_HAS_REGISTRY_ID != 0 || enc._reserved != 0
649            {
650                return Err(UFrameAbiError::InvalidProfile(
651                    "unknown payload encoding component flags".to_string(),
652                ));
653            }
654            let registry_id = (enc.component_flags & UFRAME_ABI_ENCODING_HAS_REGISTRY_ID != 0)
655                .then_some(enc.registry_id);
656            let literal_id = if enc.literal_id_len > 0 {
657                Some(
658                    read_str(
659                        &enc.literal_id,
660                        usize::from(enc.literal_id_len),
661                        UFRAME_ABI_LITERAL_ID_CAPACITY,
662                        "payload_encoding.literal_id",
663                    )?
664                    .to_owned(),
665                )
666            } else {
667                None
668            };
669            let content_type = if enc.content_type_len > 0 {
670                Some(
671                    read_str(
672                        &enc.content_type,
673                        usize::from(enc.content_type_len),
674                        UFRAME_ABI_CONTENT_TYPE_CAPACITY,
675                        "payload_encoding.content_type",
676                    )?
677                    .to_owned(),
678                )
679            } else {
680                None
681            };
682            (
683                Some(PayloadEncoding::from_parts(
684                    registry_id,
685                    literal_id,
686                    content_type,
687                )?),
688                Some(self.payload_size),
689            )
690        } else {
691            if self.payload_size != 0 {
692                return Err(UFrameAbiError::InvalidProfile(
693                    "payload_size must be zero without a payload encoding".to_string(),
694                ));
695            }
696            (None, None)
697        };
698
699        let metadata = UFrameMetadata::from_decoded_parts(
700            kind,
701            id,
702            source,
703            sink,
704            reqid,
705            priority,
706            ttl,
707            comm_status,
708            permission_level,
709            token,
710            traceparent,
711            payload_encoding,
712        );
713        metadata.validate()?;
714        Ok((metadata, payload_size))
715    }
716}
717
718// ---------------------------------------------------------------------------
719// Compile-time layout conformance checks (normative for profile v1)
720// ---------------------------------------------------------------------------
721
722const _: () = {
723    assert!(size_of::<UUuidAbi>() == 16);
724    assert!(align_of::<UUuidAbi>() == 8);
725    assert!(size_of::<UUriAbi>() == 136);
726    assert!(align_of::<UUriAbi>() == 4);
727    assert!(size_of::<UPayloadEncodingAbi>() == 172);
728    assert!(align_of::<UPayloadEncodingAbi>() == 4);
729    assert!(size_of::<UTraceparentAbi>() == 64);
730    assert!(align_of::<UTraceparentAbi>() == 1);
731    assert!(size_of::<UTokenAbi>() == 512);
732    assert!(align_of::<UTokenAbi>() == 2);
733
734    assert!(size_of::<UFrameMetadataAbiV1>() == UFRAME_ABI_SIZE);
735    assert!(align_of::<UFrameMetadataAbiV1>() == UFRAME_ABI_ALIGN);
736
737    assert!(offset_of!(UFrameMetadataAbiV1, magic) == 0);
738    assert!(offset_of!(UFrameMetadataAbiV1, version) == 4);
739    assert!(offset_of!(UFrameMetadataAbiV1, flags) == 5);
740    assert!(offset_of!(UFrameMetadataAbiV1, metadata_size) == 6);
741    assert!(offset_of!(UFrameMetadataAbiV1, presence) == 8);
742    assert!(offset_of!(UFrameMetadataAbiV1, kind) == 12);
743    assert!(offset_of!(UFrameMetadataAbiV1, priority) == 13);
744    assert!(offset_of!(UFrameMetadataAbiV1, comm_status) == 14);
745    assert!(offset_of!(UFrameMetadataAbiV1, _reserved0) == 15);
746    assert!(offset_of!(UFrameMetadataAbiV1, ttl_ns) == 16);
747    assert!(offset_of!(UFrameMetadataAbiV1, permission_level) == 24);
748    assert!(offset_of!(UFrameMetadataAbiV1, _reserved1) == 28);
749    assert!(offset_of!(UFrameMetadataAbiV1, id) == 32);
750    assert!(offset_of!(UFrameMetadataAbiV1, reqid) == 48);
751    assert!(offset_of!(UFrameMetadataAbiV1, payload_size) == 64);
752    assert!(offset_of!(UFrameMetadataAbiV1, payload_encoding) == 72);
753    assert!(offset_of!(UFrameMetadataAbiV1, source) == 244);
754    assert!(offset_of!(UFrameMetadataAbiV1, sink) == 380);
755    assert!(offset_of!(UFrameMetadataAbiV1, traceparent) == 516);
756    assert!(offset_of!(UFrameMetadataAbiV1, token) == 580);
757    assert!(offset_of!(UFrameMetadataAbiV1, _reserved_tail) == 1092);
758
759    assert!(offset_of!(UPayloadEncodingAbi, registry_id) == 0);
760    assert!(offset_of!(UPayloadEncodingAbi, literal_id_len) == 4);
761    assert!(offset_of!(UPayloadEncodingAbi, content_type_len) == 5);
762    assert!(offset_of!(UPayloadEncodingAbi, component_flags) == 6);
763    assert!(offset_of!(UPayloadEncodingAbi, _reserved) == 7);
764    assert!(offset_of!(UPayloadEncodingAbi, literal_id) == 8);
765    assert!(offset_of!(UPayloadEncodingAbi, content_type) == 72);
766};
767
768#[cfg(test)]
769mod tests {
770    use pretty_assertions::assert_eq;
771
772    use super::*;
773
774    fn topic() -> UUri {
775        UUri::try_from_parts("vehicle", 0x4210, 0x01, 0x9000).expect("topic")
776    }
777
778    fn method() -> UUri {
779        UUri::try_from_parts("vehicle", 0x4210, 0x01, 0x00b1).expect("method")
780    }
781
782    fn reply_to() -> UUri {
783        UUri::try_from_parts("cloud", 0x10ab, 0x02, 0x0000).expect("reply-to")
784    }
785
786    #[test]
787    fn minimal_publish_metadata_round_trips_through_profile() {
788        let metadata = UFrameMetadata::publish(topic()).build().expect("metadata");
789        let abi = UFrameMetadataAbiV1::try_from_metadata(&metadata, None).expect("profile");
790        let (decoded, payload_size) = abi.try_to_metadata().expect("metadata");
791        assert_eq!(decoded, metadata);
792        assert_eq!(payload_size, None);
793    }
794
795    #[test]
796    fn fully_populated_request_round_trips_through_profile() {
797        let metadata = UFrameMetadata::request(method(), reply_to(), Duration::from_millis(250))
798            .with_priority(FramePriority::CS6)
799            .with_comm_status(UCode::Ok)
800            .with_permission_level(3)
801            .with_token("token-value")
802            .with_traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
803            .with_payload_encoding(
804                PayloadEncoding::custom("up.xcdr-v2", "application/vnd.uprotocol.xcdr-v2")
805                    .expect("encoding"),
806            )
807            .build()
808            .expect("metadata");
809
810        let abi = UFrameMetadataAbiV1::try_from_metadata(&metadata, Some(64)).expect("profile");
811        let (decoded, payload_size) = abi.try_to_metadata().expect("metadata");
812        assert_eq!(decoded, metadata);
813        assert_eq!(payload_size, Some(64));
814    }
815
816    #[test]
817    fn oversized_token_fails_instead_of_truncating() {
818        let metadata = UFrameMetadata::publish(topic())
819            .with_token("t".repeat(UFRAME_ABI_TOKEN_CAPACITY + 1))
820            .build()
821            .expect("metadata");
822        assert_eq!(
823            UFrameMetadataAbiV1::try_from_metadata(&metadata, None).unwrap_err(),
824            UFrameAbiError::CapacityExceeded { field: "token" }
825        );
826    }
827
828    #[test]
829    fn payload_size_and_encoding_presence_must_agree() {
830        let metadata = UFrameMetadata::publish(topic()).build().expect("metadata");
831        assert!(matches!(
832            UFrameMetadataAbiV1::try_from_metadata(&metadata, Some(8)).unwrap_err(),
833            UFrameAbiError::Metadata(crate::UFrameMetadataError::PayloadWithoutEncoding)
834        ));
835
836        let metadata = UFrameMetadata::publish(topic())
837            .with_payload_encoding(PayloadEncoding::RAW)
838            .build()
839            .expect("metadata");
840        assert!(matches!(
841            UFrameMetadataAbiV1::try_from_metadata(&metadata, None).unwrap_err(),
842            UFrameAbiError::Metadata(crate::UFrameMetadataError::EncodingWithoutPayload)
843        ));
844    }
845
846    #[test]
847    fn structural_validation_rejects_corrupted_profiles() {
848        let metadata = UFrameMetadata::publish(topic()).build().expect("metadata");
849        let abi = UFrameMetadataAbiV1::try_from_metadata(&metadata, None).expect("profile");
850
851        let mut bad = abi;
852        bad.magic = *b"XXXX";
853        assert!(bad.try_to_metadata().is_err());
854
855        let mut bad = abi;
856        bad.presence |= 1 << 30;
857        assert!(bad.try_to_metadata().is_err());
858
859        let mut bad = abi;
860        bad._reserved_tail = [1; 4];
861        assert!(bad.try_to_metadata().is_err());
862    }
863}