Skip to main content

zerodds_rpc/
wire_codec.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3
4//! Wire-Codec fuer Request- und Reply-Samples (Foundation C6.1.C).
5//!
6//! In dieser Stufe transportieren Requester/Replier ihre Samples ueber
7//! generische `RawBytes`-Topics — der DDS-RPC-Wire-Frame ist daher
8//! anwendungsseitig sichtbar:
9//!
10//! ```text
11//! REQUEST-Frame:
12//!   RequestHeader (XCDR2-LE)  ||  user-payload-bytes
13//! REPLY-Frame:
14//!   ReplyHeader (XCDR2-LE)    ||  user-payload-bytes
15//! ```
16//!
17//! Decoder muessen das Header-Format beidseitig kennen, weil DCPS-DataReader
18//! den Sample-Buffer als ein einziges `Vec<u8>` ausliefert. Der Wire-Frame
19//! ist mit den XCDR2-Encodings aus [`crate::common_types`] kompatibel.
20
21extern crate alloc;
22
23use alloc::vec::Vec;
24
25use crate::common_types::{ReplyHeader, RequestHeader};
26use crate::error::{RpcError, RpcResult};
27
28/// Encoded ein Request-Frame: `RequestHeader` (XCDR2-LE) gefolgt von den
29/// XCDR2-encodeden User-Payload-Bytes.
30///
31/// `user_payload` ist die Ausgabe von `T::encode` und wird **byte-identisch**
32/// hinter den Header geschrieben — kein zusaetzliches Padding.
33#[must_use]
34pub fn encode_request_frame(header: &RequestHeader, user_payload: &[u8]) -> Vec<u8> {
35    let mut out = header.to_cdr_le();
36    out.extend_from_slice(user_payload);
37    out
38}
39
40/// Splittet ein Request-Frame in `(RequestHeader, &user-payload)`.
41///
42/// # Errors
43/// `RpcError::Codec` wenn der Header nicht parsen will oder die Payload
44/// truncated ist.
45pub fn decode_request_frame(bytes: &[u8]) -> RpcResult<(RequestHeader, &[u8])> {
46    let header = RequestHeader::from_cdr_le(bytes)?;
47    let consumed = encoded_request_header_len(&header);
48    if consumed > bytes.len() {
49        return Err(RpcError::codec("request frame truncated"));
50    }
51    Ok((header, &bytes[consumed..]))
52}
53
54/// Encoded ein Reply-Frame.
55#[must_use]
56pub fn encode_reply_frame(header: &ReplyHeader, user_payload: &[u8]) -> Vec<u8> {
57    let mut out = header.to_cdr_le();
58    out.extend_from_slice(user_payload);
59    out
60}
61
62/// Splittet ein Reply-Frame in `(ReplyHeader, &user-payload)`.
63///
64/// # Errors
65/// `RpcError::Codec` wenn der Header nicht parsen will oder die Payload
66/// truncated ist.
67pub fn decode_reply_frame(bytes: &[u8]) -> RpcResult<(ReplyHeader, &[u8])> {
68    let header = ReplyHeader::from_cdr_le(bytes)?;
69    let consumed = encoded_reply_header_len();
70    if consumed > bytes.len() {
71        return Err(RpcError::codec("reply frame truncated"));
72    }
73    Ok((header, &bytes[consumed..]))
74}
75
76fn encoded_request_header_len(header: &RequestHeader) -> usize {
77    // Wir kalkulieren die Laenge per Re-Encode — billig (24-30 byte typisch),
78    // robust gegen Padding-Aenderungen im Encoder.
79    header.to_cdr_le().len()
80}
81
82fn encoded_reply_header_len() -> usize {
83    // ReplyHeader ist immer 28 byte (16 GUID + 8 SN + 4 ex-code).
84    28
85}
86
87#[cfg(test)]
88#[allow(clippy::unwrap_used, clippy::expect_used)]
89mod tests {
90    use super::*;
91    use crate::common_types::{RemoteExceptionCode, SampleIdentity};
92
93    fn id() -> SampleIdentity {
94        SampleIdentity::new([0x42; 16], 7)
95    }
96
97    #[test]
98    fn request_frame_roundtrip_empty_payload() {
99        let h = RequestHeader::new(id(), "");
100        let frame = encode_request_frame(&h, &[]);
101        let (back, payload) = decode_request_frame(&frame).unwrap();
102        assert_eq!(back, h);
103        assert!(payload.is_empty());
104    }
105
106    #[test]
107    fn request_frame_roundtrip_with_payload() {
108        let h = RequestHeader::new(id(), "calc-A");
109        let frame = encode_request_frame(&h, &[1, 2, 3, 4]);
110        let (back, payload) = decode_request_frame(&frame).unwrap();
111        assert_eq!(back, h);
112        assert_eq!(payload, &[1, 2, 3, 4]);
113    }
114
115    #[test]
116    fn reply_frame_roundtrip() {
117        let h = ReplyHeader::new(id(), RemoteExceptionCode::Ok);
118        let frame = encode_reply_frame(&h, &[9, 8, 7]);
119        let (back, payload) = decode_reply_frame(&frame).unwrap();
120        assert_eq!(back, h);
121        assert_eq!(payload, &[9, 8, 7]);
122    }
123
124    #[test]
125    fn reply_frame_carries_exception_code() {
126        let h = ReplyHeader::new(id(), RemoteExceptionCode::InvalidArgument);
127        let frame = encode_reply_frame(&h, &[]);
128        let (back, _) = decode_reply_frame(&frame).unwrap();
129        assert_eq!(back.remote_ex, RemoteExceptionCode::InvalidArgument);
130    }
131
132    #[test]
133    fn decode_request_frame_truncated_header_is_error() {
134        let bytes = [0u8; 8];
135        assert!(decode_request_frame(&bytes).is_err());
136    }
137
138    #[test]
139    fn decode_reply_frame_truncated_is_error() {
140        let bytes = [0u8; 4];
141        assert!(decode_reply_frame(&bytes).is_err());
142    }
143
144    #[test]
145    fn reply_header_consumed_28_bytes() {
146        let h = ReplyHeader::new(SampleIdentity::UNKNOWN, RemoteExceptionCode::Ok);
147        let bytes = h.to_cdr_le();
148        assert_eq!(bytes.len(), 28);
149    }
150}