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/
envelope.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//! # Whole-frame envelopes
15//!
16//! A whole-frame envelope serializes semantic metadata and application payload
17//! into one envelope byte value. It is distinct from selected-wire `UPWM`, which
18//! prefixes a configured metadata profile while the encoded core carries
19//! payload storage separately.
20//!
21//! ## Walkthrough
22//!
23//! 1. Start with a validated `UOwnedFrame`.
24//! 2. Use `NativeUFrameEnvelope::serialize_frame` when open payload identities
25//!    must survive losslessly. Use `ProtobufUMessageFrame` only for legacy
26//!    compatibility and accept its representability checks.
27//! 3. Carry the resulting bytes through an ordinary byte channel.
28//! 4. Deserialize with the same `UFrameWireFormat`; malformed lengths,
29//!    reserved fields, metadata, payload presence, and unsupported identities
30//!    fail before a frame is returned.
31//! 5. Verify changes with the envelope round-trip, malformed-input, and
32//!    projection tests in this module.
33
34use std::{error::Error, fmt::Display};
35
36use bytes::Bytes;
37
38#[cfg(feature = "protobuf-support")]
39use crate::{ProtobufMappable, SerializationError, UMessage};
40use crate::{UFrameMetadataError, UOwnedFrame};
41
42/// Error type used by whole-frame wire formats.
43#[derive(Clone, Debug, Eq, PartialEq)]
44pub enum UFrameWireError {
45    /// The encoded bytes do not contain a valid frame or violate native frame invariants.
46    InvalidFrame(String),
47    /// The selected wire format cannot faithfully represent the frame payload encoding.
48    UnsupportedPayloadEncoding(String),
49    /// The wire format encoder or decoder failed while converting bytes.
50    SerializationError(String),
51}
52
53impl UFrameWireError {
54    /// Creates an [`UFrameWireError::InvalidFrame`] value.
55    #[must_use]
56    pub fn invalid_frame(message: impl Into<String>) -> Self {
57        Self::InvalidFrame(message.into())
58    }
59
60    /// Creates an [`UFrameWireError::UnsupportedPayloadEncoding`] value.
61    #[must_use]
62    pub fn unsupported_payload_encoding(message: impl Into<String>) -> Self {
63        Self::UnsupportedPayloadEncoding(message.into())
64    }
65
66    /// Creates an [`UFrameWireError::SerializationError`] value.
67    #[must_use]
68    pub fn serialization_error(message: impl Into<String>) -> Self {
69        Self::SerializationError(message.into())
70    }
71}
72
73impl Display for UFrameWireError {
74    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
75        match self {
76            Self::InvalidFrame(message) => write!(f, "invalid frame: {message}"),
77            Self::UnsupportedPayloadEncoding(message) => {
78                write!(f, "unsupported payload encoding: {message}")
79            }
80            Self::SerializationError(message) => write!(f, "frame serialization error: {message}"),
81        }
82    }
83}
84
85impl Error for UFrameWireError {}
86
87#[cfg(feature = "protobuf-support")]
88impl From<SerializationError> for UFrameWireError {
89    fn from(value: SerializationError) -> Self {
90        Self::serialization_error(value.to_string())
91    }
92}
93
94impl From<UFrameMetadataError> for UFrameWireError {
95    fn from(value: UFrameMetadataError) -> Self {
96        match value {
97            UFrameMetadataError::EncodingNotRepresentable { encoding } => {
98                Self::unsupported_payload_encoding(format!(
99                    "payload encoding `{encoding}` cannot be represented by generated UMessage payload_format"
100                ))
101            }
102            other @ (UFrameMetadataError::EmptyPayloadEncoding
103            | UFrameMetadataError::EmptyCustomEncodingId
104            | UFrameMetadataError::EmptyCustomEncodingContentType
105            | UFrameMetadataError::InvalidCustomEncodingContentType(_)
106            | UFrameMetadataError::RegisteredPayloadEncodingAlias { .. }
107            | UFrameMetadataError::UnspecifiedPayloadFormat
108            | UFrameMetadataError::PayloadWithoutEncoding
109            | UFrameMetadataError::EncodingWithoutPayload
110            | UFrameMetadataError::FieldNotRepresentable { .. }
111            | UFrameMetadataError::InvalidMetadata(_)
112            | UFrameMetadataError::MessageBuildError(_)) => Self::invalid_frame(other.to_string()),
113        }
114    }
115}
116
117/// Whole-frame wire format for transporting a complete native uProtocol frame.
118///
119/// This is distinct from payload codecs, which only transform an application
120/// value into frame payload bytes. Implementations must preserve native frame
121/// metadata and payload presence. If an envelope cannot represent a native-only
122/// [`crate::PayloadEncoding`], it should return
123/// [`UFrameWireError::UnsupportedPayloadEncoding`] instead of silently dropping
124/// metadata.
125pub trait UFrameWireFormat {
126    /// Stable implementation name for logs, diagnostics, and configuration.
127    fn name() -> &'static str;
128
129    /// Media type of bytes emitted by [`Self::serialize_frame`].
130    fn content_type() -> &'static str;
131
132    /// Serializes a complete native frame, including metadata and payload bytes.
133    ///
134    /// # Errors
135    ///
136    /// Returns an error if the frame is invalid or cannot be represented by this
137    /// wire format.
138    fn serialize_frame(frame: &UOwnedFrame) -> Result<Bytes, UFrameWireError>;
139
140    /// Deserializes a complete native frame from this wire format's bytes.
141    ///
142    /// # Errors
143    ///
144    /// Returns an error if `src` is malformed or violates native frame invariants.
145    fn deserialize_frame(src: &[u8]) -> Result<UOwnedFrame, UFrameWireError>;
146}
147
148/// **Legacy compatibility** whole-frame wire format using the generated
149/// `UMessage` Protocol Buffers envelope.
150///
151/// This envelope projects native frames through legacy `UMessage`, so it is
152/// *not* full-fidelity: frames whose payload encoding has no legacy
153/// `UPayloadFormat` equivalent are rejected. Use [`NativeUFrameEnvelope`]
154/// for lossless transport of native frames over ordinary byte channels.
155#[cfg(feature = "protobuf-support")]
156#[derive(Debug)]
157pub struct ProtobufUMessageFrame;
158
159#[cfg(feature = "protobuf-support")]
160impl UFrameWireFormat for ProtobufUMessageFrame {
161    fn name() -> &'static str {
162        "protobuf-umessage"
163    }
164
165    fn content_type() -> &'static str {
166        "application/x-uprotocol-umessage+protobuf"
167    }
168
169    fn serialize_frame(frame: &UOwnedFrame) -> Result<Bytes, UFrameWireError> {
170        let message = crate::frame::metadata::try_project_frame_to_umessage(
171            frame.metadata().clone(),
172            frame.payload().cloned(),
173        )?;
174        message
175            .write_to_protobuf_bytes()
176            .map(Bytes::from)
177            .map_err(UFrameWireError::from)
178    }
179
180    fn deserialize_frame(src: &[u8]) -> Result<UOwnedFrame, UFrameWireError> {
181        let message = UMessage::parse_from_protobuf_bytes(src)?;
182        let metadata = crate::frame::metadata::try_project_umessage_to_frame_metadata(&message)?;
183        let payload = message.payload().as_deref().map(Bytes::copy_from_slice);
184        UOwnedFrame::new(metadata, payload).map_err(UFrameWireError::from)
185    }
186}
187
188/// Canonical whole-frame envelope carrying native frame metadata and payload
189/// bytes with full fidelity.
190///
191/// The envelope mirrors the physical layout the shared-memory transports
192/// already use — a small fixed *placement* header followed by the variable
193/// canonical metadata field block, followed by the payload bytes:
194///
195/// ```text
196/// offset  size  field
197/// 0       4     magic "UPFE"
198/// 4       1     envelope version (1)
199/// 5       1     payload presence (0 = absent, 1 = present)
200/// 6       2     reserved, MUST be zero
201/// 8       4     metadata_len (u32, little-endian)
202/// 12      8     payload_len (u64, little-endian; 0 when absent)
203/// 20      ...   metadata field block (see [`crate::frame::codec`])
204/// 20+m    ...   payload bytes
205/// ```
206///
207/// This is the recommended carrier for native frames over ordinary byte
208/// channels (an MQTT payload, a SOME/IP payload, a Zenoh attachment+payload
209/// pair collapsed into one buffer, a file, ...). Unlike
210/// [`ProtobufUMessageFrame`] it preserves open payload encodings and does
211/// not require protobuf support.
212#[derive(Debug)]
213pub struct NativeUFrameEnvelope;
214
215/// Magic bytes of the native whole-frame envelope.
216pub const NATIVE_ENVELOPE_MAGIC: [u8; 4] = *b"UPFE";
217/// Version of the native whole-frame envelope emitted by this module.
218pub const NATIVE_ENVELOPE_VERSION: u8 = 1;
219/// Size of the fixed native-envelope placement header in bytes.
220pub const NATIVE_ENVELOPE_HEADER_LEN: usize = 20;
221
222impl UFrameWireFormat for NativeUFrameEnvelope {
223    fn name() -> &'static str {
224        "native-uframe-envelope"
225    }
226
227    fn content_type() -> &'static str {
228        "application/vnd.uprotocol.uframe;version=1"
229    }
230
231    fn serialize_frame(frame: &UOwnedFrame) -> Result<Bytes, UFrameWireError> {
232        // `UOwnedFrame<Validated>` by type: validity is guaranteed by construction.
233        let metadata = crate::frame::codec::encode_frame_metadata_fields(frame.metadata())
234            .map_err(|error| UFrameWireError::invalid_frame(error.to_string()))?;
235        let metadata_len = u32::try_from(metadata.len()).map_err(|_| {
236            UFrameWireError::invalid_frame("metadata exceeds the u32 envelope limit")
237        })?;
238        let payload = frame.payload().map(bytes::Bytes::as_ref);
239        let payload_len = payload.map_or(0_u64, |payload| payload.len() as u64);
240
241        let mut out = Vec::with_capacity(
242            NATIVE_ENVELOPE_HEADER_LEN + metadata.len() + payload.map_or(0, <[u8]>::len),
243        );
244        out.extend_from_slice(&NATIVE_ENVELOPE_MAGIC);
245        out.push(NATIVE_ENVELOPE_VERSION);
246        out.push(u8::from(payload.is_some()));
247        out.extend_from_slice(&0_u16.to_le_bytes());
248        out.extend_from_slice(&metadata_len.to_le_bytes());
249        out.extend_from_slice(&payload_len.to_le_bytes());
250        out.extend_from_slice(&metadata);
251        if let Some(payload) = payload {
252            out.extend_from_slice(payload);
253        }
254        Ok(Bytes::from(out))
255    }
256
257    fn deserialize_frame(src: &[u8]) -> Result<UOwnedFrame, UFrameWireError> {
258        let header = src
259            .get(..NATIVE_ENVELOPE_HEADER_LEN)
260            .ok_or_else(|| UFrameWireError::invalid_frame("input shorter than envelope header"))?;
261        let magic = header
262            .get(..4)
263            .ok_or_else(|| UFrameWireError::invalid_frame("input shorter than envelope magic"))?;
264        if magic != NATIVE_ENVELOPE_MAGIC {
265            return Err(UFrameWireError::invalid_frame("wrong envelope magic"));
266        }
267        let version = *header
268            .get(4)
269            .ok_or_else(|| UFrameWireError::invalid_frame("input shorter than envelope version"))?;
270        if version != NATIVE_ENVELOPE_VERSION {
271            return Err(UFrameWireError::invalid_frame(format!(
272                "unsupported envelope version {}",
273                version
274            )));
275        }
276        let payload_marker = *header.get(5).ok_or_else(|| {
277            UFrameWireError::invalid_frame("input shorter than payload presence marker")
278        })?;
279        let payload_present = match payload_marker {
280            0 => false,
281            1 => true,
282            other => {
283                return Err(UFrameWireError::invalid_frame(format!(
284                    "invalid payload presence marker {other}"
285                )))
286            }
287        };
288        let reserved = header.get(6..8).ok_or_else(|| {
289            UFrameWireError::invalid_frame("input shorter than reserved envelope bytes")
290        })?;
291        if reserved != [0, 0] {
292            return Err(UFrameWireError::invalid_frame(
293                "reserved envelope bytes must be zero",
294            ));
295        }
296        let metadata_len_bytes = header
297            .get(8..12)
298            .ok_or_else(|| UFrameWireError::invalid_frame("input shorter than metadata length"))?;
299        let metadata_len =
300            u32::from_le_bytes(metadata_len_bytes.try_into().expect("4 bytes")) as usize;
301        let payload_len_bytes = header
302            .get(12..20)
303            .ok_or_else(|| UFrameWireError::invalid_frame("input shorter than payload length"))?;
304        let payload_len = u64::from_le_bytes(payload_len_bytes.try_into().expect("8 bytes"));
305
306        let body = src
307            .get(NATIVE_ENVELOPE_HEADER_LEN..)
308            .ok_or_else(|| UFrameWireError::invalid_frame("input shorter than envelope header"))?;
309        let metadata_bytes = body.get(..metadata_len).ok_or_else(|| {
310            UFrameWireError::invalid_frame("input shorter than declared metadata length")
311        })?;
312        let payload_bytes = body.get(metadata_len..).ok_or_else(|| {
313            UFrameWireError::invalid_frame("input shorter than declared metadata length")
314        })?;
315        if payload_bytes.len() as u64 != payload_len || (!payload_present && payload_len != 0) {
316            return Err(UFrameWireError::invalid_frame(
317                "payload length disagrees with envelope header",
318            ));
319        }
320
321        let metadata = crate::frame::codec::decode_frame_metadata_fields(metadata_bytes)
322            .map_err(|error| UFrameWireError::invalid_frame(error.to_string()))?;
323        let payload = payload_present.then(|| Bytes::copy_from_slice(payload_bytes));
324        UOwnedFrame::new(metadata, payload).map_err(UFrameWireError::from)
325    }
326}
327
328#[cfg(test)]
329mod tests {
330    use super::*;
331    #[cfg(feature = "protobuf-support")]
332    use crate::payload::codec::RawBytes;
333    use crate::{PayloadEncoding, UFrameMetadata, UUri};
334    use crate::{UMessageBuilder, UPayloadFormat};
335
336    fn topic() -> UUri {
337        UUri::try_from_parts("vehicle", 0x4210, 0x01, 0x9000).expect("topic")
338    }
339
340    fn metadata_with_raw_encoding() -> UFrameMetadata {
341        let message = UMessageBuilder::publish(topic())
342            .build_with_payload(Bytes::new(), UPayloadFormat::Raw)
343            .expect("message");
344        crate::frame::metadata::try_project_attributes_to_frame_metadata(
345            message.attributes(),
346            Some(RawBytes::encoding()),
347        )
348        .expect("metadata")
349    }
350
351    #[test]
352    fn protobuf_umessage_frame_round_trips_raw_payload() {
353        let frame = UOwnedFrame::with_payload(
354            metadata_with_raw_encoding(),
355            Bytes::from_static(b"raw payload"),
356        )
357        .expect("frame");
358
359        let encoded = ProtobufUMessageFrame::serialize_frame(&frame).expect("serialize frame");
360        let decoded =
361            ProtobufUMessageFrame::deserialize_frame(&encoded).expect("deserialize frame");
362
363        assert_eq!(decoded, frame);
364    }
365
366    #[test]
367    fn protobuf_umessage_frame_round_trips_custom_payload_encoding() {
368        let message = UMessageBuilder::publish(topic()).build().expect("message");
369        let encoding =
370            PayloadEncoding::custom("com.example.native", "application/vnd.example.native")
371                .expect("custom encoding");
372        let metadata = crate::frame::metadata::try_project_attributes_to_frame_metadata(
373            message.attributes(),
374            Some(encoding.clone()),
375        )
376        .expect("metadata");
377        let frame = UOwnedFrame::with_payload(metadata, Bytes::from_static(b"native payload"))
378            .expect("frame");
379
380        let encoded = ProtobufUMessageFrame::serialize_frame(&frame).expect("serialize frame");
381        let decoded =
382            ProtobufUMessageFrame::deserialize_frame(&encoded).expect("deserialize frame");
383
384        assert_eq!(decoded.metadata().payload_encoding(), Some(&encoding));
385        assert_eq!(
386            decoded.payload(),
387            Some(&Bytes::from_static(b"native payload"))
388        );
389    }
390
391    #[test]
392    fn protobuf_umessage_frame_rejects_invalid_metadata() {
393        let message = UMessageBuilder::publish(topic()).build().expect("message");
394        let metadata = crate::frame::metadata::try_project_umessage_to_frame_metadata(&message)
395            .expect("metadata");
396        // payload bytes without a payload encoding violate the frame invariant
397        let frame = UOwnedFrame::with_payload_unchecked(metadata, Bytes::from_static(b"payload"));
398
399        // An invalid frame cannot reach serialize_frame; the rejection lives at
400        // the typestate transition:
401        let error = frame.validate().unwrap_err();
402
403        assert!(matches!(error, UFrameMetadataError::PayloadWithoutEncoding));
404    }
405
406    #[test]
407    fn protobuf_umessage_frame_round_trips_unspecified_absent_payload() {
408        let message = UMessageBuilder::publish(topic()).build().expect("message");
409        let metadata = crate::frame::metadata::try_project_attributes_to_frame_metadata(
410            message.attributes(),
411            None,
412        )
413        .expect("metadata");
414        let frame = UOwnedFrame::without_payload(metadata).expect("frame");
415
416        let encoded = ProtobufUMessageFrame::serialize_frame(&frame).expect("serialize frame");
417        let decoded =
418            ProtobufUMessageFrame::deserialize_frame(&encoded).expect("deserialize frame");
419
420        assert_eq!(decoded.metadata().payload_encoding(), None);
421        assert!(!decoded.has_payload());
422    }
423
424    #[test]
425    fn protobuf_umessage_frame_rejects_unknown_payload_format_with_payload() {
426        let message = UMessageBuilder::publish(topic())
427            .build_with_payload(Bytes::from_static(b"payload"), UPayloadFormat::Unspecified)
428            .expect("message");
429        let encoded = message
430            .write_to_protobuf_bytes()
431            .expect("serialize message");
432
433        let error = ProtobufUMessageFrame::deserialize_frame(&encoded).unwrap_err();
434
435        assert!(matches!(error, UFrameWireError::InvalidFrame(_)));
436    }
437
438    #[test]
439    fn native_envelope_round_trips_custom_encoding_frame() {
440        let metadata = UFrameMetadata::publish(topic())
441            .with_payload_encoding(
442                PayloadEncoding::custom("com.example.native", "application/vnd.example.native")
443                    .expect("custom encoding"),
444            )
445            .build()
446            .expect("metadata");
447        let frame = UOwnedFrame::with_payload(metadata, Bytes::from_static(b"native payload"))
448            .expect("frame");
449
450        let encoded = NativeUFrameEnvelope::serialize_frame(&frame).expect("serialize frame");
451        let decoded = NativeUFrameEnvelope::deserialize_frame(&encoded).expect("deserialize");
452
453        assert_eq!(decoded, frame);
454    }
455
456    #[test]
457    fn native_envelope_preserves_present_empty_payload() {
458        let metadata = UFrameMetadata::publish(topic())
459            .with_payload_encoding(PayloadEncoding::RAW)
460            .build()
461            .expect("metadata");
462        let frame = UOwnedFrame::with_payload(metadata, Bytes::new()).expect("frame");
463
464        let decoded = NativeUFrameEnvelope::deserialize_frame(
465            &NativeUFrameEnvelope::serialize_frame(&frame).expect("serialize"),
466        )
467        .expect("deserialize");
468
469        assert!(decoded.has_payload());
470        assert_eq!(decoded.payload(), Some(&Bytes::new()));
471    }
472
473    #[test]
474    fn native_envelope_round_trips_absent_payload() {
475        let metadata = UFrameMetadata::publish(topic()).build().expect("metadata");
476        let frame = UOwnedFrame::without_payload(metadata).expect("frame");
477
478        let decoded = NativeUFrameEnvelope::deserialize_frame(
479            &NativeUFrameEnvelope::serialize_frame(&frame).expect("serialize"),
480        )
481        .expect("deserialize");
482
483        assert!(!decoded.has_payload());
484        assert_eq!(decoded, frame);
485    }
486
487    #[test]
488    fn native_envelope_rejects_corrupted_input() {
489        let metadata = UFrameMetadata::publish(topic()).build().expect("metadata");
490        let frame = UOwnedFrame::without_payload(metadata).expect("frame");
491        let encoded = NativeUFrameEnvelope::serialize_frame(&frame).expect("serialize");
492
493        let mut bad = encoded.to_vec();
494        *bad.first_mut().expect("magic byte") = b'X';
495        assert!(NativeUFrameEnvelope::deserialize_frame(&bad).is_err());
496
497        let mut bad = encoded.to_vec();
498        bad.push(0);
499        assert!(NativeUFrameEnvelope::deserialize_frame(&bad).is_err());
500    }
501}