zerodds_amqp_endpoint/
mapping.rs1use alloc::string::{String, ToString};
9use alloc::vec::Vec;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
13pub enum BodyEncodingMode {
14 #[default]
16 PassThrough,
17 Json,
19 AmqpNative,
21}
22
23#[derive(Debug, Clone, PartialEq, Eq)]
25pub enum MappingError {
26 InvalidUtf8,
28 InvalidJson(String),
30 EmptyBody,
32}
33
34pub fn encode_dds_to_amqp_body(
41 sample_xcdr2: &[u8],
42 mode: BodyEncodingMode,
43) -> Result<(&'static str, Vec<u8>), MappingError> {
44 if sample_xcdr2.is_empty() {
45 return Err(MappingError::EmptyBody);
46 }
47 match mode {
48 BodyEncodingMode::PassThrough => Ok(("application/vnd.dds.xcdr2", sample_xcdr2.to_vec())),
49 BodyEncodingMode::Json => {
50 let mut json = String::from("{\"_xcdr2\":\"");
56 for b in sample_xcdr2 {
57 let _ = core::fmt::Write::write_fmt(&mut json, core::format_args!("{b:02x}"));
58 }
59 json.push_str("\"}");
60 Ok(("application/json", json.into_bytes()))
61 }
62 BodyEncodingMode::AmqpNative => {
63 Ok(("application/vnd.dds.amqp-native", sample_xcdr2.to_vec()))
64 }
65 }
66}
67
68pub fn parse_amqp_body(body: &[u8], content_type: Option<&str>) -> Result<Vec<u8>, MappingError> {
74 if body.is_empty() {
75 return Err(MappingError::EmptyBody);
76 }
77 let mode = match content_type {
78 Some("application/vnd.dds.xcdr2") => BodyEncodingMode::PassThrough,
79 Some("application/json") => BodyEncodingMode::Json,
80 Some("application/vnd.dds.amqp-native") => BodyEncodingMode::AmqpNative,
81 _ => BodyEncodingMode::PassThrough, };
83 match mode {
84 BodyEncodingMode::PassThrough | BodyEncodingMode::AmqpNative => Ok(body.to_vec()),
85 BodyEncodingMode::Json => {
86 let s = core::str::from_utf8(body).map_err(|_| MappingError::InvalidUtf8)?;
87 let key = "\"_xcdr2\":\"";
90 let start = s
91 .find(key)
92 .ok_or_else(|| MappingError::InvalidJson("missing _xcdr2 field".to_string()))?;
93 let hex_start = start + key.len();
94 let hex_end = s[hex_start..]
95 .find('"')
96 .ok_or_else(|| MappingError::InvalidJson("unterminated _xcdr2 hex".to_string()))?;
97 let hex = &s[hex_start..hex_start + hex_end];
98 decode_hex(hex).map_err(|e| MappingError::InvalidJson(e.to_string()))
99 }
100 }
101}
102
103fn decode_hex(s: &str) -> Result<Vec<u8>, &'static str> {
104 if s.len() % 2 != 0 {
105 return Err("hex length not even");
106 }
107 let mut out = Vec::with_capacity(s.len() / 2);
108 let bytes = s.as_bytes();
109 let mut i = 0;
110 while i < bytes.len() {
111 let hi = hex_digit(bytes[i])?;
112 let lo = hex_digit(bytes[i + 1])?;
113 out.push((hi << 4) | lo);
114 i += 2;
115 }
116 Ok(out)
117}
118
119const fn hex_digit(b: u8) -> Result<u8, &'static str> {
120 match b {
121 b'0'..=b'9' => Ok(b - b'0'),
122 b'a'..=b'f' => Ok(b - b'a' + 10),
123 b'A'..=b'F' => Ok(b - b'A' + 10),
124 _ => Err("invalid hex digit"),
125 }
126}
127
128#[cfg(test)]
129#[allow(clippy::expect_used)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn passthrough_round_trip_preserves_bytes() {
135 let sample = alloc::vec![0xDE, 0xAD, 0xBE, 0xEF];
136 let (ct, body) =
137 encode_dds_to_amqp_body(&sample, BodyEncodingMode::PassThrough).expect("encode");
138 assert_eq!(ct, "application/vnd.dds.xcdr2");
139 let parsed = parse_amqp_body(&body, Some(ct)).expect("parse");
140 assert_eq!(parsed, sample);
141 }
142
143 #[test]
144 fn json_round_trip_via_hex_field() {
145 let sample = alloc::vec![0x01, 0x02, 0x03, 0xFE];
146 let (ct, body) = encode_dds_to_amqp_body(&sample, BodyEncodingMode::Json).expect("encode");
147 assert_eq!(ct, "application/json");
148 let parsed = parse_amqp_body(&body, Some(ct)).expect("parse");
149 assert_eq!(parsed, sample);
150 }
151
152 #[test]
153 fn amqp_native_uses_correct_content_type() {
154 let sample = alloc::vec![0xCA, 0xFE];
155 let (ct, _) =
156 encode_dds_to_amqp_body(&sample, BodyEncodingMode::AmqpNative).expect("encode");
157 assert_eq!(ct, "application/vnd.dds.amqp-native");
158 }
159
160 #[test]
161 fn empty_body_yields_error_in_encode() {
162 assert!(matches!(
163 encode_dds_to_amqp_body(&[], BodyEncodingMode::PassThrough),
164 Err(MappingError::EmptyBody)
165 ));
166 }
167
168 #[test]
169 fn empty_body_yields_error_in_parse() {
170 assert!(matches!(
171 parse_amqp_body(&[], None),
172 Err(MappingError::EmptyBody)
173 ));
174 }
175
176 #[test]
177 fn unknown_content_type_falls_back_to_pass_through() {
178 let body = alloc::vec![1, 2, 3];
179 let parsed = parse_amqp_body(&body, Some("application/octet-stream")).expect("parse");
180 assert_eq!(parsed, body);
181 }
182
183 #[test]
184 fn invalid_json_yields_error() {
185 let r = parse_amqp_body(b"{invalid", Some("application/json"));
186 assert!(matches!(r, Err(MappingError::InvalidJson(_))));
187 }
188
189 #[test]
190 fn default_mode_is_pass_through() {
191 assert_eq!(BodyEncodingMode::default(), BodyEncodingMode::PassThrough);
192 }
193}