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/
codec.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//! Canonical byte codec for native frame metadata: the *UFrame metadata
15//! field block*, version 1.
16//!
17//! This is the language-neutral serialization of [`UFrameMetadata`] used by
18//! the canonical selected-wire metadata codec and the native whole-frame
19//! envelope. Transports place these bytes wherever their physical layout
20//! wants them (a Zenoh attachment, the variable metadata prefix behind
21//! iceoryx2's placement header, the metadata block behind LoLa's `ULOL`
22//! header, ...); the block itself is deliberately variable-length and
23//! presence-driven so that absent fields cost zero bytes.
24//!
25//! Layout (all multi-byte integers little-endian):
26//!
27//! ```text
28//! u8   block_version        = 1
29//! u8   kind                 FrameMessageKind wire code (1..=4)
30//! u8   priority             0 = absent, 1..=7 = CS0..=CS6
31//! u8   reserved             = 0
32//! u32  presence             FIELD_* bitmask; unknown bits MUST be zero
33//! u64  id.msb
34//! u64  id.lsb
35//! [FIELD_REQID]             u64 msb, u64 lsb
36//! [FIELD_TTL]               u64 ttl in nanoseconds
37//! [FIELD_COMM_STATUS]       i32 UCode value
38//! [FIELD_PERMISSION_LEVEL]  u32
39//! source UUri block         u32 ue_id, u16 resource_id, u8 ue_version_major,
40//!                           u8 authority_len, authority bytes (UTF-8)
41//! [FIELD_SINK]              UUri block
42//! [FIELD_TOKEN]             u16 len, token bytes (UTF-8)
43//! [FIELD_TRACEPARENT]       u8 len, traceparent bytes (UTF-8)
44//! [FIELD_PAYLOAD_ENCODING]  u8 component bitmask (ENCODING_HAS_*), then in order:
45//!                           [u32 registry_id] [u16 len + literal id bytes]
46//!                           [u16 len + content type bytes]
47//! ```
48//!
49//! Values that do not fit a length field are rejected at encode time —
50//! encoding never truncates.
51//!
52//! ## Walkthrough
53//!
54//! 1. Construct and validate semantic [`UFrameMetadata`].
55//! 2. Call [`encode_frame_metadata_fields`] once; transports carry the returned
56//!    block as opaque bytes in their binding-specific placement.
57//! 3. On receive, call [`decode_frame_metadata_fields`]. Version, reserved bits,
58//!    lengths, UTF-8, payload identity, and metadata invariants are checked
59//!    before a semantic frame is exposed.
60//! 4. Pin changes with round trips, malformed/truncated inputs, unknown-bit
61//!    rejection, and golden vectors in `wire_metadata_conformance` and
62//!    `wire_metadata_golden`.
63//!
64//! This is a metadata profile, not a whole-frame envelope and not an
65//! application payload codec. `up-spec/basics/uframe.adoc` defines the profile
66//! registry; `up-spec/up-l1/transport_families.adoc` defines selected-wire
67//! identity and rejection behavior.
68
69use std::time::Duration;
70
71use crate::{
72    FrameMessageKind, FramePriority, PayloadEncoding, UCode, UFrameMetadata, UFrameMetadataError,
73    UUri, UUID,
74};
75
76/// Version of the field block emitted by [`encode_frame_metadata_fields`].
77pub const FRAME_FIELDS_VERSION: u8 = 1;
78
79/// Presence bit: the frame has a sink URI.
80pub const FIELD_SINK: u32 = 1 << 0;
81/// Presence bit: the frame has a correlated request id.
82pub const FIELD_REQID: u32 = 1 << 1;
83/// Presence bit: the frame has a time-to-live.
84pub const FIELD_TTL: u32 = 1 << 2;
85/// Presence bit: the frame has a communication status.
86pub const FIELD_COMM_STATUS: u32 = 1 << 3;
87/// Presence bit: the frame has a permission level.
88pub const FIELD_PERMISSION_LEVEL: u32 = 1 << 4;
89/// Presence bit: the frame has an access token.
90pub const FIELD_TOKEN: u32 = 1 << 5;
91/// Presence bit: the frame has a W3C traceparent.
92pub const FIELD_TRACEPARENT: u32 = 1 << 6;
93/// Presence bit: the frame has a payload encoding.
94pub const FIELD_PAYLOAD_ENCODING: u32 = 1 << 7;
95/// All presence bits defined by field block version 1.
96pub const FIELD_MASK_V1: u32 = (1 << 8) - 1;
97
98const ENCODING_HAS_REGISTRY_ID: u8 = 1 << 0;
99const ENCODING_HAS_LITERAL_ID: u8 = 1 << 1;
100const ENCODING_HAS_CONTENT_TYPE: u8 = 1 << 2;
101const ENCODING_MASK_V1: u8 = (1 << 3) - 1;
102
103/// Errors returned by the frame metadata field block codec.
104#[derive(Clone, Debug, Eq, PartialEq)]
105pub enum UFrameFieldsError {
106    /// The input bytes are not a well-formed field block.
107    Malformed(String),
108    /// A value does not fit its field block length field.
109    ValueTooLong {
110        /// Name of the offending field.
111        field: &'static str,
112    },
113    /// The (decoded or to-be-encoded) metadata violates frame invariants.
114    Metadata(UFrameMetadataError),
115}
116
117impl std::fmt::Display for UFrameFieldsError {
118    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119        match self {
120            Self::Malformed(message) => write!(f, "malformed frame metadata fields: {message}"),
121            Self::ValueTooLong { field } => {
122                write!(
123                    f,
124                    "frame metadata field `{field}` does not fit the field block"
125                )
126            }
127            Self::Metadata(error) => write!(f, "invalid frame metadata: {error}"),
128        }
129    }
130}
131
132impl std::error::Error for UFrameFieldsError {}
133
134impl From<UFrameMetadataError> for UFrameFieldsError {
135    fn from(value: UFrameMetadataError) -> Self {
136        Self::Metadata(value)
137    }
138}
139
140/// Encodes validated frame metadata into its canonical field block bytes.
141///
142/// # Errors
143///
144/// Returns an error if the metadata is invalid or a value does not fit its
145/// length field. Encoding never truncates.
146pub fn encode_frame_metadata_fields(
147    metadata: &UFrameMetadata,
148) -> Result<Vec<u8>, UFrameFieldsError> {
149    metadata.validate()?;
150
151    let mut presence = 0_u32;
152    if metadata.sink().is_some() {
153        presence |= FIELD_SINK;
154    }
155    if metadata.reqid().is_some() {
156        presence |= FIELD_REQID;
157    }
158    if metadata.ttl().is_some() {
159        presence |= FIELD_TTL;
160    }
161    if metadata.comm_status().is_some() {
162        presence |= FIELD_COMM_STATUS;
163    }
164    if metadata.permission_level().is_some() {
165        presence |= FIELD_PERMISSION_LEVEL;
166    }
167    if metadata.token().is_some() {
168        presence |= FIELD_TOKEN;
169    }
170    if metadata.traceparent().is_some() {
171        presence |= FIELD_TRACEPARENT;
172    }
173    if metadata.payload_encoding().is_some() {
174        presence |= FIELD_PAYLOAD_ENCODING;
175    }
176
177    let mut out = Vec::with_capacity(64);
178    out.push(FRAME_FIELDS_VERSION);
179    out.push(metadata.kind().wire_code());
180    out.push(metadata.priority().map_or(0, FramePriority::wire_code));
181    out.push(0); // reserved
182    out.extend_from_slice(&presence.to_le_bytes());
183    write_uuid(&mut out, metadata.id());
184    if let Some(reqid) = metadata.reqid() {
185        write_uuid(&mut out, reqid);
186    }
187    if let Some(ttl) = metadata.ttl() {
188        let nanos = u64::try_from(ttl.as_nanos())
189            .map_err(|_| UFrameFieldsError::ValueTooLong { field: "ttl" })?;
190        out.extend_from_slice(&nanos.to_le_bytes());
191    }
192    if let Some(comm_status) = metadata.comm_status() {
193        out.extend_from_slice(&comm_status.value().to_le_bytes());
194    }
195    if let Some(permission_level) = metadata.permission_level() {
196        out.extend_from_slice(&permission_level.to_le_bytes());
197    }
198    write_uuri(&mut out, metadata.source())?;
199    if let Some(sink) = metadata.sink() {
200        write_uuri(&mut out, sink)?;
201    }
202    if let Some(token) = metadata.token() {
203        let len = u16::try_from(token.len())
204            .map_err(|_| UFrameFieldsError::ValueTooLong { field: "token" })?;
205        out.extend_from_slice(&len.to_le_bytes());
206        out.extend_from_slice(token.as_bytes());
207    }
208    if let Some(traceparent) = metadata.traceparent() {
209        let len = u8::try_from(traceparent.len()).map_err(|_| UFrameFieldsError::ValueTooLong {
210            field: "traceparent",
211        })?;
212        out.push(len);
213        out.extend_from_slice(traceparent.as_bytes());
214    }
215    if let Some(encoding) = metadata.payload_encoding() {
216        write_payload_encoding(&mut out, encoding)?;
217    }
218    Ok(out)
219}
220
221/// Decodes and validates frame metadata from its canonical field block bytes.
222///
223/// The entire input must be consumed; trailing bytes are rejected.
224///
225/// # Errors
226///
227/// Returns an error if the bytes are malformed, use an unsupported block
228/// version, contain unknown presence bits, or decode into invalid metadata.
229pub fn decode_frame_metadata_fields(src: &[u8]) -> Result<UFrameMetadata, UFrameFieldsError> {
230    let mut reader = FieldsReader { src, pos: 0 };
231
232    let version = reader.u8()?;
233    if version != FRAME_FIELDS_VERSION {
234        return Err(UFrameFieldsError::Malformed(format!(
235            "unsupported field block version {version}"
236        )));
237    }
238    let kind = FrameMessageKind::from_wire_code(reader.u8()?).ok_or_else(|| {
239        UFrameFieldsError::Malformed("unknown frame message kind code".to_string())
240    })?;
241    let priority = match reader.u8()? {
242        0 => None,
243        code => Some(FramePriority::from_wire_code(code).ok_or_else(|| {
244            UFrameFieldsError::Malformed(format!("unknown frame priority code {code}"))
245        })?),
246    };
247    let reserved = reader.u8()?;
248    if reserved != 0 {
249        return Err(UFrameFieldsError::Malformed(
250            "reserved byte must be zero".to_string(),
251        ));
252    }
253    let presence = reader.u32()?;
254    if presence & !FIELD_MASK_V1 != 0 {
255        return Err(UFrameFieldsError::Malformed(format!(
256            "unknown presence bits {:#010x}",
257            presence & !FIELD_MASK_V1
258        )));
259    }
260
261    let id = reader.uuid()?;
262    let reqid = if presence & FIELD_REQID != 0 {
263        Some(reader.uuid()?)
264    } else {
265        None
266    };
267    let ttl = if presence & FIELD_TTL != 0 {
268        Some(Duration::from_nanos(reader.u64()?))
269    } else {
270        None
271    };
272    let comm_status = if presence & FIELD_COMM_STATUS != 0 {
273        let raw = reader.i32()?;
274        Some(UCode::try_from_i32(raw).map_err(|_| {
275            UFrameFieldsError::Malformed(format!("unknown communication status code {raw}"))
276        })?)
277    } else {
278        None
279    };
280    let permission_level = if presence & FIELD_PERMISSION_LEVEL != 0 {
281        Some(reader.u32()?)
282    } else {
283        None
284    };
285    let source = reader.uuri()?;
286    let sink = if presence & FIELD_SINK != 0 {
287        Some(reader.uuri()?)
288    } else {
289        None
290    };
291    let token = if presence & FIELD_TOKEN != 0 {
292        let len = usize::from(reader.u16()?);
293        Some(reader.utf8(len, "token")?)
294    } else {
295        None
296    };
297    let traceparent = if presence & FIELD_TRACEPARENT != 0 {
298        let len = usize::from(reader.u8()?);
299        Some(reader.utf8(len, "traceparent")?)
300    } else {
301        None
302    };
303    let payload_encoding = if presence & FIELD_PAYLOAD_ENCODING != 0 {
304        Some(reader.payload_encoding()?)
305    } else {
306        None
307    };
308    reader.finish()?;
309
310    let metadata = UFrameMetadata::from_decoded_parts(
311        kind,
312        id,
313        source,
314        sink,
315        reqid,
316        priority,
317        ttl,
318        comm_status,
319        permission_level,
320        token,
321        traceparent,
322        payload_encoding,
323    );
324    metadata.validate()?;
325    Ok(metadata)
326}
327
328fn write_uuid(out: &mut Vec<u8>, uuid: &UUID) {
329    let (msb, lsb) = uuid.as_u64_pair();
330    out.extend_from_slice(&msb.to_le_bytes());
331    out.extend_from_slice(&lsb.to_le_bytes());
332}
333
334fn write_uuri(out: &mut Vec<u8>, uri: &UUri) -> Result<(), UFrameFieldsError> {
335    let ue_id = (u32::from(uri.uentity_instance_id()) << 16) | u32::from(uri.uentity_type_id());
336    out.extend_from_slice(&ue_id.to_le_bytes());
337    out.extend_from_slice(&uri.resource_id().to_le_bytes());
338    out.push(uri.uentity_major_version());
339    let authority = uri.authority_name();
340    let len = u8::try_from(authority.len()).map_err(|_| UFrameFieldsError::ValueTooLong {
341        field: "authority_name",
342    })?;
343    out.push(len);
344    out.extend_from_slice(authority.as_bytes());
345    Ok(())
346}
347
348fn write_payload_encoding(
349    out: &mut Vec<u8>,
350    encoding: &PayloadEncoding,
351) -> Result<(), UFrameFieldsError> {
352    let mut components = 0_u8;
353    if encoding.registry_id().is_some() {
354        components |= ENCODING_HAS_REGISTRY_ID;
355    }
356    if encoding.literal_id().is_some() {
357        components |= ENCODING_HAS_LITERAL_ID;
358    }
359    if encoding.content_type().is_some() {
360        components |= ENCODING_HAS_CONTENT_TYPE;
361    }
362    out.push(components);
363    if let Some(registry_id) = encoding.registry_id() {
364        out.extend_from_slice(&registry_id.to_le_bytes());
365    }
366    if let Some(literal_id) = encoding.literal_id() {
367        let len = u16::try_from(literal_id.len()).map_err(|_| UFrameFieldsError::ValueTooLong {
368            field: "payload_encoding.literal_id",
369        })?;
370        out.extend_from_slice(&len.to_le_bytes());
371        out.extend_from_slice(literal_id.as_bytes());
372    }
373    if let Some(content_type) = encoding.content_type() {
374        let len =
375            u16::try_from(content_type.len()).map_err(|_| UFrameFieldsError::ValueTooLong {
376                field: "payload_encoding.content_type",
377            })?;
378        out.extend_from_slice(&len.to_le_bytes());
379        out.extend_from_slice(content_type.as_bytes());
380    }
381    Ok(())
382}
383
384struct FieldsReader<'a> {
385    src: &'a [u8],
386    pos: usize,
387}
388
389impl FieldsReader<'_> {
390    fn take(&mut self, len: usize) -> Result<&[u8], UFrameFieldsError> {
391        let end = self
392            .pos
393            .checked_add(len)
394            .ok_or_else(|| UFrameFieldsError::Malformed("length overflow".to_string()))?;
395        let chunk = self.src.get(self.pos..end).ok_or_else(|| {
396            UFrameFieldsError::Malformed(format!(
397                "needed {len} byte(s) at offset {}, input has {} byte(s)",
398                self.pos,
399                self.src.len()
400            ))
401        })?;
402        self.pos = end;
403        Ok(chunk)
404    }
405
406    fn finish(&self) -> Result<(), UFrameFieldsError> {
407        if self.pos == self.src.len() {
408            Ok(())
409        } else {
410            Err(UFrameFieldsError::Malformed(format!(
411                "{} trailing byte(s)",
412                self.src.len() - self.pos
413            )))
414        }
415    }
416
417    fn u8(&mut self) -> Result<u8, UFrameFieldsError> {
418        self.take(1)?
419            .first()
420            .copied()
421            .ok_or_else(|| UFrameFieldsError::Malformed("missing byte".to_string()))
422    }
423
424    fn u16(&mut self) -> Result<u16, UFrameFieldsError> {
425        Ok(u16::from_le_bytes(
426            self.take(2)?.try_into().expect("2 bytes"),
427        ))
428    }
429
430    fn u32(&mut self) -> Result<u32, UFrameFieldsError> {
431        Ok(u32::from_le_bytes(
432            self.take(4)?.try_into().expect("4 bytes"),
433        ))
434    }
435
436    fn i32(&mut self) -> Result<i32, UFrameFieldsError> {
437        Ok(i32::from_le_bytes(
438            self.take(4)?.try_into().expect("4 bytes"),
439        ))
440    }
441
442    fn u64(&mut self) -> Result<u64, UFrameFieldsError> {
443        Ok(u64::from_le_bytes(
444            self.take(8)?.try_into().expect("8 bytes"),
445        ))
446    }
447
448    fn utf8(&mut self, len: usize, field: &'static str) -> Result<String, UFrameFieldsError> {
449        let bytes = self.take(len)?;
450        std::str::from_utf8(bytes)
451            .map(ToOwned::to_owned)
452            .map_err(|error| {
453                UFrameFieldsError::Malformed(format!("field `{field}` is not UTF-8: {error}"))
454            })
455    }
456
457    fn uuid(&mut self) -> Result<UUID, UFrameFieldsError> {
458        let msb = self.u64()?;
459        let lsb = self.u64()?;
460        UUID::from_u64_pair(msb, lsb)
461            .map_err(|error| UFrameFieldsError::Malformed(format!("invalid UUID: {error}")))
462    }
463
464    fn uuri(&mut self) -> Result<UUri, UFrameFieldsError> {
465        let ue_id = self.u32()?;
466        let resource_id = self.u16()?;
467        let ue_version_major = self.u8()?;
468        let authority_len = usize::from(self.u8()?);
469        let authority = self.utf8(authority_len, "authority_name")?;
470        UUri::try_from_parts(&authority, ue_id, ue_version_major, resource_id)
471            .map_err(|error| UFrameFieldsError::Malformed(format!("invalid UUri: {error}")))
472    }
473
474    fn payload_encoding(&mut self) -> Result<PayloadEncoding, UFrameFieldsError> {
475        let components = self.u8()?;
476        if components & !ENCODING_MASK_V1 != 0 {
477            return Err(UFrameFieldsError::Malformed(format!(
478                "unknown payload encoding component bits {:#04x}",
479                components & !ENCODING_MASK_V1
480            )));
481        }
482        let registry_id = if components & ENCODING_HAS_REGISTRY_ID != 0 {
483            Some(self.u32()?)
484        } else {
485            None
486        };
487        let literal_id = if components & ENCODING_HAS_LITERAL_ID != 0 {
488            let len = usize::from(self.u16()?);
489            Some(self.utf8(len, "payload_encoding.literal_id")?)
490        } else {
491            None
492        };
493        let content_type = if components & ENCODING_HAS_CONTENT_TYPE != 0 {
494            let len = usize::from(self.u16()?);
495            Some(self.utf8(len, "payload_encoding.content_type")?)
496        } else {
497            None
498        };
499        PayloadEncoding::from_parts(registry_id, literal_id, content_type)
500            .map_err(UFrameFieldsError::from)
501    }
502}
503
504#[cfg(test)]
505mod tests {
506    use pretty_assertions::assert_eq;
507
508    use super::*;
509
510    fn topic() -> UUri {
511        UUri::try_from_parts("vehicle", 0x4210, 0x01, 0x9000).expect("topic")
512    }
513
514    fn method() -> UUri {
515        UUri::try_from_parts("vehicle", 0x4210, 0x01, 0x00b1).expect("method")
516    }
517
518    fn reply_to() -> UUri {
519        UUri::try_from_parts("cloud", 0x10ab, 0x02, 0x0000).expect("reply-to")
520    }
521
522    #[test]
523    fn minimal_publish_metadata_round_trips() {
524        let metadata = UFrameMetadata::publish(topic()).build().expect("metadata");
525        let bytes = encode_frame_metadata_fields(&metadata).expect("encode");
526        let decoded = decode_frame_metadata_fields(&bytes).expect("decode");
527        assert_eq!(decoded, metadata);
528    }
529
530    #[test]
531    fn fully_populated_request_metadata_round_trips() {
532        let metadata = UFrameMetadata::request(method(), reply_to(), Duration::from_millis(250))
533            .with_priority(FramePriority::CS5)
534            .with_comm_status(UCode::Ok)
535            .with_permission_level(4)
536            .with_token("bearer-token")
537            .with_traceparent("00-4bf92f3577b34da6a3ce929d0e0e4736-00f067aa0ba902b7-01")
538            .with_payload_encoding(
539                PayloadEncoding::custom("up.xcdr-v2", "application/vnd.uprotocol.xcdr-v2")
540                    .expect("encoding"),
541            )
542            .build()
543            .expect("metadata");
544
545        let bytes = encode_frame_metadata_fields(&metadata).expect("encode");
546        let decoded = decode_frame_metadata_fields(&bytes).expect("decode");
547        assert_eq!(decoded, metadata);
548    }
549
550    #[test]
551    fn registered_encoding_round_trips_all_components() {
552        let metadata = UFrameMetadata::publish(topic())
553            .with_payload_encoding(PayloadEncoding::PROTOBUF)
554            .build()
555            .expect("metadata");
556        let decoded =
557            decode_frame_metadata_fields(&encode_frame_metadata_fields(&metadata).expect("encode"))
558                .expect("decode");
559        assert_eq!(decoded.payload_encoding(), Some(&PayloadEncoding::PROTOBUF));
560    }
561
562    #[test]
563    fn trailing_bytes_are_rejected() {
564        let metadata = UFrameMetadata::publish(topic()).build().expect("metadata");
565        let mut bytes = encode_frame_metadata_fields(&metadata).expect("encode");
566        bytes.push(0);
567        assert!(matches!(
568            decode_frame_metadata_fields(&bytes),
569            Err(UFrameFieldsError::Malformed(_))
570        ));
571    }
572
573    #[test]
574    fn unknown_presence_bits_are_rejected() {
575        let metadata = UFrameMetadata::publish(topic()).build().expect("metadata");
576        let mut bytes = encode_frame_metadata_fields(&metadata).expect("encode");
577        *bytes.get_mut(5).expect("presence byte") |= 0x01_u8; // undefined presence bit 8
578        assert!(matches!(
579            decode_frame_metadata_fields(&bytes),
580            Err(UFrameFieldsError::Malformed(_))
581        ));
582    }
583
584    #[test]
585    fn unsupported_version_is_rejected() {
586        let metadata = UFrameMetadata::publish(topic()).build().expect("metadata");
587        let mut bytes = encode_frame_metadata_fields(&metadata).expect("encode");
588        *bytes.first_mut().expect("version byte") = 2;
589        assert!(matches!(
590            decode_frame_metadata_fields(&bytes),
591            Err(UFrameFieldsError::Malformed(_))
592        ));
593    }
594
595    #[test]
596    fn oversized_traceparent_fails_instead_of_truncating() {
597        let metadata = UFrameMetadata::publish(topic())
598            .with_traceparent("t".repeat(256))
599            .build()
600            .expect("metadata");
601        assert_eq!(
602            encode_frame_metadata_fields(&metadata).unwrap_err(),
603            UFrameFieldsError::ValueTooLong {
604                field: "traceparent"
605            }
606        );
607    }
608}