1use base64::Engine;
18use serde_json::{Map, Number, Value};
19
20pub use ciborium::Value as CborValue;
21
22pub const CBOR_CONTENT_TYPE: &str = "application/cbor";
24pub const SMITHY_PROTOCOL_HEADER: &str = "smithy-protocol";
26pub const RPC_V2_CBOR: &str = "rpc-v2-cbor";
28
29const EPOCH_TIMESTAMP_TAG: u64 = 1;
32
33pub fn parse_rpc_v2_path(path: &str) -> Option<(&str, &str)> {
37 let segs: Vec<&str> = path.trim_end_matches('/').split('/').collect();
38 match segs.as_slice() {
39 [.., "service", service, "operation", operation]
40 if !service.is_empty() && !operation.is_empty() =>
41 {
42 Some((service, operation))
43 }
44 _ => None,
45 }
46}
47
48pub fn decode_to_json(body: &[u8]) -> Result<Value, String> {
51 if body.is_empty() {
52 return Ok(Value::Object(Map::new()));
53 }
54 let value: CborValue = ciborium::from_reader(body).map_err(|e| e.to_string())?;
55 cbor_to_json(value)
56}
57
58fn cbor_to_json(value: CborValue) -> Result<Value, String> {
59 Ok(match value {
60 CborValue::Null => Value::Null,
61 CborValue::Bool(b) => Value::Bool(b),
62 CborValue::Integer(i) => {
63 let i = i128::from(i);
64 if let Ok(v) = i64::try_from(i) {
65 Value::Number(v.into())
66 } else if let Ok(v) = u64::try_from(i) {
67 Value::Number(v.into())
68 } else {
69 return Err(format!("integer {i} out of range"));
70 }
71 }
72 CborValue::Float(f) => float_to_json(f),
73 CborValue::Text(s) => Value::String(s),
74 CborValue::Bytes(b) => Value::String(base64::engine::general_purpose::STANDARD.encode(b)),
76 CborValue::Array(items) => Value::Array(
77 items
78 .into_iter()
79 .map(cbor_to_json)
80 .collect::<Result<_, _>>()?,
81 ),
82 CborValue::Map(entries) => {
83 let mut obj = Map::new();
84 for (k, v) in entries {
85 let CborValue::Text(key) = k else {
86 return Err("map keys must be text strings".to_string());
87 };
88 if v.is_null() {
91 continue;
92 }
93 obj.insert(key, cbor_to_json(v)?);
94 }
95 Value::Object(obj)
96 }
97 CborValue::Tag(_, inner) => cbor_to_json(*inner)?,
101 other => return Err(format!("unsupported CBOR value {other:?}")),
102 })
103}
104
105fn float_to_json(f: f64) -> Value {
108 match Number::from_f64(f) {
109 Some(n) => Value::Number(n),
110 None if f.is_nan() => Value::String("NaN".to_string()),
111 None if f > 0.0 => Value::String("Infinity".to_string()),
112 None => Value::String("-Infinity".to_string()),
113 }
114}
115
116pub fn timestamp(epoch_seconds: f64) -> CborValue {
120 CborValue::Tag(
121 EPOCH_TIMESTAMP_TAG,
122 Box::new(CborValue::Float(epoch_seconds)),
123 )
124}
125
126pub fn encode(value: &CborValue) -> Vec<u8> {
131 let mut out = Vec::new();
132 encode_into(value, &mut out);
133 out
134}
135
136fn write_head(major: u8, arg: u64, out: &mut Vec<u8>) {
138 let m = major << 5;
139 if arg < 24 {
140 out.push(m | arg as u8);
141 } else if arg <= u8::MAX as u64 {
142 out.push(m | 24);
143 out.push(arg as u8);
144 } else if arg <= u16::MAX as u64 {
145 out.push(m | 25);
146 out.extend_from_slice(&(arg as u16).to_be_bytes());
147 } else if arg <= u32::MAX as u64 {
148 out.push(m | 26);
149 out.extend_from_slice(&(arg as u32).to_be_bytes());
150 } else {
151 out.push(m | 27);
152 out.extend_from_slice(&arg.to_be_bytes());
153 }
154}
155
156fn encode_into(value: &CborValue, out: &mut Vec<u8>) {
157 match value {
158 CborValue::Integer(i) => {
159 let i = i128::from(*i);
160 if i >= 0 {
161 write_head(0, i as u64, out);
162 } else {
163 write_head(1, (-1 - i) as u64, out);
164 }
165 }
166 CborValue::Bytes(b) => {
167 write_head(2, b.len() as u64, out);
168 out.extend_from_slice(b);
169 }
170 CborValue::Text(t) => {
171 write_head(3, t.len() as u64, out);
172 out.extend_from_slice(t.as_bytes());
173 }
174 CborValue::Array(items) => {
175 write_head(4, items.len() as u64, out);
176 for item in items {
177 encode_into(item, out);
178 }
179 }
180 CborValue::Map(entries) => {
181 write_head(5, entries.len() as u64, out);
182 for (k, v) in entries {
183 encode_into(k, out);
184 encode_into(v, out);
185 }
186 }
187 CborValue::Tag(tag, inner) => {
188 write_head(6, *tag, out);
189 encode_into(inner, out);
190 }
191 CborValue::Bool(false) => out.push(0xf4),
192 CborValue::Bool(true) => out.push(0xf5),
193 CborValue::Null => out.push(0xf6),
194 CborValue::Float(f) => {
195 out.push(0xfb);
196 out.extend_from_slice(&f.to_be_bytes());
197 }
198 _ => out.push(0xf6),
200 }
201}
202
203pub fn json_to_cbor(value: &Value) -> CborValue {
208 match value {
209 Value::Null => CborValue::Null,
210 Value::Bool(b) => CborValue::Bool(*b),
211 Value::Number(n) => {
212 if let Some(i) = n.as_i64() {
213 CborValue::Integer(i.into())
214 } else if let Some(u) = n.as_u64() {
215 CborValue::Integer(u.into())
216 } else {
217 CborValue::Float(n.as_f64().unwrap_or(0.0))
218 }
219 }
220 Value::String(s) => CborValue::Text(s.clone()),
221 Value::Array(items) => CborValue::Array(items.iter().map(json_to_cbor).collect()),
222 Value::Object(obj) => CborValue::Map(
223 obj.iter()
224 .map(|(k, v)| (CborValue::Text(k.clone()), json_to_cbor(v)))
225 .collect(),
226 ),
227 }
228}
229
230pub fn error_body(code: &str, message: &str, extra_fields: &[(String, String)]) -> Vec<u8> {
234 let mut entries = vec![
235 (
236 CborValue::Text("__type".to_string()),
237 CborValue::Text(code.to_string()),
238 ),
239 (
240 CborValue::Text("message".to_string()),
241 CborValue::Text(message.to_string()),
242 ),
243 ];
244 for (key, value) in extra_fields {
245 let parsed = serde_json::from_str::<Value>(value)
246 .map(|v| json_to_cbor(&v))
247 .unwrap_or_else(|_| CborValue::Text(value.clone()));
248 entries.push((CborValue::Text(key.clone()), parsed));
249 }
250 encode(&CborValue::Map(entries))
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn cbor(v: &CborValue) -> Vec<u8> {
258 encode(v)
259 }
260
261 fn text(s: &str) -> CborValue {
262 CborValue::Text(s.to_string())
263 }
264
265 #[test]
266 fn parses_rpc_v2_path() {
267 assert_eq!(
268 parse_rpc_v2_path("/service/GraniteServiceVersion20100801/operation/GetMetricData"),
269 Some(("GraniteServiceVersion20100801", "GetMetricData"))
270 );
271 assert_eq!(
272 parse_rpc_v2_path("/prefix/service/Svc/operation/Op"),
273 Some(("Svc", "Op"))
274 );
275 assert_eq!(parse_rpc_v2_path("/service/Svc"), None);
276 assert_eq!(parse_rpc_v2_path("/service//operation/Op"), None);
277 assert_eq!(parse_rpc_v2_path("/"), None);
278 }
279
280 #[test]
281 fn decodes_empty_body_as_empty_object() {
282 assert_eq!(decode_to_json(b"").unwrap(), serde_json::json!({}));
283 }
284
285 #[test]
286 fn decodes_typed_members_to_aws_json() {
287 let body = cbor(&CborValue::Map(vec![
288 (text("Namespace"), text("App")),
289 (text("Period"), CborValue::Integer(60.into())),
290 (text("Value"), CborValue::Float(1.5)),
291 (text("Enabled"), CborValue::Bool(true)),
292 (text("StartTime"), timestamp(1_577_836_800.0)),
293 (text("EndTime"), timestamp(1_577_836_800.5)),
294 (text("Blob"), CborValue::Bytes(b"hi".to_vec())),
295 (text("Absent"), CborValue::Null),
296 (
297 text("Values"),
298 CborValue::Array(vec![CborValue::Float(1.0), CborValue::Float(f64::NAN)]),
299 ),
300 ]));
301 let json = decode_to_json(&body).unwrap();
302 assert_eq!(json["Namespace"], "App");
303 assert_eq!(json["Period"], 60);
304 assert_eq!(json["Value"], 1.5);
305 assert_eq!(json["Enabled"], true);
306 assert_eq!(json["StartTime"], 1_577_836_800.0);
307 assert_eq!(json["EndTime"], 1_577_836_800.5);
308 assert_eq!(json["Blob"], "aGk=");
309 assert!(json.get("Absent").is_none());
310 assert_eq!(json["Values"][0], 1.0);
311 assert_eq!(json["Values"][1], "NaN");
312 }
313
314 #[test]
315 fn rejects_malformed_cbor() {
316 assert!(decode_to_json(&[0xff, 0x00]).is_err());
317 let body = cbor(&CborValue::Map(vec![(
319 CborValue::Integer(1.into()),
320 text("x"),
321 )]));
322 assert!(decode_to_json(&body).is_err());
323 }
324
325 #[test]
326 fn timestamp_is_tag_one_over_a_double() {
327 assert_eq!(
328 timestamp(10.0),
329 CborValue::Tag(1, Box::new(CborValue::Float(10.0)))
330 );
331 assert_eq!(
332 encode(×tamp(10.0)),
333 [&[0xc1, 0xfb][..], &10.0f64.to_be_bytes()].concat()
334 );
335 }
336
337 #[test]
338 fn encoder_round_trips_and_keeps_doubles_wide() {
339 assert_eq!(
340 encode(&CborValue::Float(1.0)),
341 [&[0xfb][..], &1.0f64.to_be_bytes()].concat()
342 );
343 assert_eq!(
344 encode(&CborValue::Integer(500.into())),
345 vec![0x19, 0x01, 0xf4]
346 );
347 assert_eq!(encode(&CborValue::Integer((-1).into())), vec![0x20]);
348 let value = CborValue::Map(vec![
349 (text("n"), CborValue::Integer(u64::MAX.into())),
350 (text("neg"), CborValue::Integer(i64::MIN.into())),
351 (text("b"), CborValue::Bytes(vec![1, 2, 3])),
352 (
353 text("l"),
354 CborValue::Array(vec![CborValue::Bool(true), CborValue::Null]),
355 ),
356 (text("s"), text(&"x".repeat(300))),
357 (text("t"), timestamp(1.5)),
358 ]);
359 let back: CborValue = ciborium::from_reader(encode(&value).as_slice()).unwrap();
360 assert_eq!(back, value);
361 }
362
363 #[test]
364 fn json_to_cbor_keeps_number_kinds() {
365 let v = json_to_cbor(&serde_json::json!({"a": 1, "b": 1.5, "c": "s", "d": [true]}));
366 let back: CborValue = ciborium::from_reader(encode(&v).as_slice()).unwrap();
367 let CborValue::Map(entries) = back else {
368 panic!("expected map");
369 };
370 let get = |k: &str| {
371 entries
372 .iter()
373 .find(|(key, _)| key == &text(k))
374 .map(|(_, v)| v.clone())
375 .unwrap()
376 };
377 assert_eq!(get("a"), CborValue::Integer(1.into()));
378 assert_eq!(get("b"), CborValue::Float(1.5));
379 assert_eq!(get("c"), text("s"));
380 assert_eq!(get("d"), CborValue::Array(vec![CborValue::Bool(true)]));
381 }
382
383 #[test]
384 fn error_body_has_type_and_message() {
385 let body = error_body(
386 "InvalidParameterValue",
387 "bad",
388 &[("Extra".to_string(), "{\"k\":1}".to_string())],
389 );
390 let json = decode_to_json(&body).unwrap();
391 assert_eq!(json["__type"], "InvalidParameterValue");
392 assert_eq!(json["message"], "bad");
393 assert_eq!(json["Extra"]["k"], 1);
394 }
395}