1pub fn to_cbor<T: serde::Serialize>(value: &T) -> Vec<u8> {
9 let mut buf = Vec::new();
10 ciborium::into_writer(value, &mut buf).expect("CBOR serialization should not fail");
11 buf
12}
13
14pub fn from_cbor<T: serde::de::DeserializeOwned>(bytes: &[u8]) -> Result<T, CborError> {
16 ciborium::from_reader(bytes).map_err(|e| CborError(format!("CBOR decode failed: {e}")))
17}
18
19fn json_to_cbor_value(v: &serde_json::Value) -> Result<ciborium::value::Value, CborError> {
22 use ciborium::value::Value as C;
23 Ok(match v {
24 serde_json::Value::Null => C::Null,
25 serde_json::Value::Bool(b) => C::Bool(*b),
26 serde_json::Value::Number(n) => {
27 if let Some(i) = n.as_i64() {
28 C::Integer(i.into())
29 } else if let Some(u) = n.as_u64() {
30 C::Integer(u.into())
31 } else if let Some(f) = n.as_f64() {
32 C::Float(f)
33 } else {
34 return Err(CborError("unrepresentable JSON number".into()));
35 }
36 }
37 serde_json::Value::String(s) => C::Text(s.clone()),
38 serde_json::Value::Array(arr) => {
39 let mut out = Vec::with_capacity(arr.len());
40 for item in arr {
41 out.push(json_to_cbor_value(item)?);
42 }
43 C::Array(out)
44 }
45 serde_json::Value::Object(map) => {
46 if let Some(bytes) = decode_bytes_wrapper(map)? {
47 C::Bytes(bytes)
48 } else {
49 let mut entries = Vec::with_capacity(map.len());
50 for (k, val) in map {
51 entries.push((C::Text(unescape_key(k)), json_to_cbor_value(val)?));
52 }
53 C::Map(entries)
54 }
55 }
56 })
57}
58
59pub fn json_to_cbor(value: &serde_json::Value) -> Result<Vec<u8>, CborError> {
61 let cbor = json_to_cbor_value(value)?;
62 let mut buf = Vec::new();
63 ciborium::into_writer(&cbor, &mut buf)
64 .map_err(|e| CborError(format!("JSON→CBOR encode failed: {e}")))?;
65 Ok(buf)
66}
67
68fn cbor_value_to_json(v: &ciborium::value::Value) -> Result<serde_json::Value, CborError> {
71 use ciborium::value::Value as C;
72 Ok(match v {
73 C::Null => serde_json::Value::Null,
74 C::Bool(b) => serde_json::Value::Bool(*b),
75 C::Integer(i) => {
76 let n: i128 = i128::from(*i);
77 if let Ok(i) = i64::try_from(n) {
78 serde_json::Value::Number(i.into())
79 } else if let Ok(u) = u64::try_from(n) {
80 serde_json::Value::Number(u.into())
81 } else {
82 return Err(CborError(format!(
83 "CBOR integer {n} is outside JSON-safe range"
84 )));
85 }
86 }
87 C::Float(f) => serde_json::Number::from_f64(*f)
88 .map(serde_json::Value::Number)
89 .ok_or_else(|| CborError(format!("non-finite float cannot project to JSON: {f}")))?,
90 C::Text(s) => serde_json::Value::String(s.clone()),
91 C::Bytes(b) => {
92 use base64::Engine as _;
93 let mut obj = serde_json::Map::new();
94 obj.insert(
95 "$bytes".to_string(),
96 serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(b)),
97 );
98 serde_json::Value::Object(obj)
99 }
100 C::Array(arr) => {
101 let mut out = Vec::with_capacity(arr.len());
102 for item in arr {
103 out.push(cbor_value_to_json(item)?);
104 }
105 serde_json::Value::Array(out)
106 }
107 C::Map(entries) => {
108 let mut obj = serde_json::Map::new();
109 for (k, val) in entries {
110 let key = match k {
111 C::Text(s) => escape_key(s),
112 _ => {
113 return Err(CborError(
114 "non-string CBOR map key cannot project to JSON".into(),
115 ));
116 }
117 };
118 obj.insert(key, cbor_value_to_json(val)?);
119 }
120 serde_json::Value::Object(obj)
121 }
122 C::Tag(_, inner) => cbor_value_to_json(inner)?,
124 _ => return Err(CborError("unsupported CBOR value type".into())),
125 })
126}
127
128pub fn cbor_to_json(bytes: &[u8]) -> Result<serde_json::Value, CborError> {
130 let value: ciborium::value::Value = ciborium::from_reader(bytes)
131 .map_err(|e| CborError(format!("CBOR→JSON decode failed: {e}")))?;
132 cbor_value_to_json(&value)
133}
134
135pub fn decode_content_data(data: &[u8], mime_type: Option<&str>) -> serde_json::Value {
141 let mime = mime_type.unwrap_or("application/cbor");
142
143 if mime.starts_with("text/") || mime == "application/json" || mime == "application/xml" {
144 serde_json::Value::String(String::from_utf8_lossy(data).into_owned())
146 } else if mime == "application/cbor" {
147 cbor_to_json(data).unwrap_or_else(|_| {
149 use base64::Engine as _;
150 serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(data))
151 })
152 } else {
153 use base64::Engine as _;
155 serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(data))
156 }
157}
158
159fn escape_key(k: &str) -> String {
162 if k.starts_with('$') {
163 format!("${k}")
164 } else {
165 k.to_string()
166 }
167}
168
169fn unescape_key(k: &str) -> String {
171 if k.starts_with("$$") {
172 k[1..].to_string()
173 } else {
174 k.to_string()
175 }
176}
177
178fn decode_bytes_wrapper(
182 map: &serde_json::Map<String, serde_json::Value>,
183) -> Result<Option<Vec<u8>>, CborError> {
184 if map.len() != 1 {
185 return Ok(None);
186 }
187 let Some(value) = map.get("$bytes") else {
188 return Ok(None);
189 };
190 let serde_json::Value::String(b64) = value else {
193 return Err(CborError(
194 "$bytes wrapper value must be a base64 string".into(),
195 ));
196 };
197 use base64::Engine as _;
198 let bytes = base64::engine::general_purpose::STANDARD
199 .decode(b64)
200 .map_err(|e| CborError(format!("invalid base64 in $bytes: {e}")))?;
201 Ok(Some(bytes))
202}
203
204#[derive(Debug, Clone)]
206pub struct CborError(pub String);
207
208impl std::fmt::Display for CborError {
209 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
210 f.write_str(&self.0)
211 }
212}
213
214impl std::error::Error for CborError {}
215
216#[cfg(test)]
217mod tests {
218 use super::*;
219 use serde_json::json;
220
221 #[test]
222 fn escape_roundtrip_dollar_keys() {
223 assert_eq!(escape_key("name"), "name");
224 assert_eq!(escape_key("$bytes"), "$$bytes");
225 assert_eq!(escape_key("$$x"), "$$$x");
226 assert_eq!(unescape_key("name"), "name");
227 assert_eq!(unescape_key("$$bytes"), "$bytes");
228 assert_eq!(unescape_key("$$$x"), "$$x");
229 }
230
231 #[test]
232 fn bytes_wrapper_detected() {
233 let mut m = serde_json::Map::new();
234 m.insert(
235 "$bytes".into(),
236 serde_json::Value::String("aGVsbG8=".into()),
237 );
238 assert_eq!(decode_bytes_wrapper(&m).unwrap(), Some(b"hello".to_vec()));
239
240 let mut m2 = serde_json::Map::new();
241 m2.insert(
242 "$bytes".into(),
243 serde_json::Value::String("aGVsbG8=".into()),
244 );
245 m2.insert("x".into(), serde_json::Value::Null);
246 assert_eq!(decode_bytes_wrapper(&m2).unwrap(), None);
247
248 let mut m3 = serde_json::Map::new();
249 m3.insert("$bytes".into(), serde_json::Value::String("@@@".into()));
250 assert!(decode_bytes_wrapper(&m3).is_err());
251 }
252
253 #[test]
254 fn roundtrip_object() {
255 let input = json!({"a": 2, "b": 3});
256 let cbor = json_to_cbor(&input).unwrap();
257 let output = cbor_to_json(&cbor).unwrap();
258 assert_eq!(input, output);
259 }
260
261 #[test]
262 fn roundtrip_nested() {
263 let input = json!({"config": {"api_key": "abc123"}, "values": [1, 2, 3]});
264 let cbor = json_to_cbor(&input).unwrap();
265 let output = cbor_to_json(&cbor).unwrap();
266 assert_eq!(input, output);
267 }
268
269 #[test]
270 fn roundtrip_null() {
271 let input = json!(null);
272 let cbor = json_to_cbor(&input).unwrap();
273 let output = cbor_to_json(&cbor).unwrap();
274 assert_eq!(input, output);
275 }
276
277 #[test]
278 fn empty_bytes_is_error() {
279 assert!(cbor_to_json(&[]).is_err());
280 }
281
282 #[test]
283 fn generic_roundtrip() {
284 let input = 42u64;
285 let bytes = to_cbor(&input);
286 let output: u64 = from_cbor(&bytes).unwrap();
287 assert_eq!(input, output);
288 }
289
290 #[test]
291 fn decode_text_content() {
292 let data = b"hello world";
293 let result = decode_content_data(data, Some("text/plain"));
294 assert_eq!(result, json!("hello world"));
295 }
296
297 #[test]
298 fn decode_cbor_content() {
299 let data = to_cbor(&json!({"key": "value"}));
300 let result = decode_content_data(&data, None);
301 assert_eq!(result, json!({"key": "value"}));
302 }
303
304 #[test]
305 fn decode_json_content_as_text() {
306 let data = br#"{"pets": [1, 2, 3]}"#;
307 let result = decode_content_data(data, Some("application/json"));
308 assert_eq!(result, json!(r#"{"pets": [1, 2, 3]}"#));
309 }
310
311 #[test]
312 fn decode_invalid_cbor_falls_back_to_base64() {
313 let data = b"\xff\xfe";
314 let result = decode_content_data(data, Some("application/octet-stream"));
315 assert!(result.is_string());
317 }
318
319 #[test]
320 fn decode_image_content_to_base64() {
321 let data = vec![0x89, 0x50, 0x4E, 0x47]; let result = decode_content_data(&data, Some("image/png"));
323 assert!(result.is_string());
324 use base64::Engine as _;
325 let decoded = base64::engine::general_purpose::STANDARD
326 .decode(result.as_str().unwrap())
327 .unwrap();
328 assert_eq!(decoded, data);
329 }
330
331 #[test]
332 fn decode_octet_stream_to_base64() {
333 let data = vec![0xFF, 0xFE, 0x00];
334 let result = decode_content_data(&data, Some("application/octet-stream"));
335 assert!(result.is_string());
336 use base64::Engine as _;
337 let decoded = base64::engine::general_purpose::STANDARD
338 .decode(result.as_str().unwrap())
339 .unwrap();
340 assert_eq!(decoded, data);
341 }
342
343 #[test]
344 fn decode_html_as_text() {
345 let data = b"<h1>Hello</h1>";
346 let result = decode_content_data(data, Some("text/html"));
347 assert_eq!(result, json!("<h1>Hello</h1>"));
348 }
349
350 #[test]
351 fn decode_xml_as_text() {
352 let data = b"<root><item/></root>";
353 let result = decode_content_data(data, Some("application/xml"));
354 assert_eq!(result, json!("<root><item/></root>"));
355 }
356
357 fn cbor_of(v: &ciborium::value::Value) -> Vec<u8> {
358 let mut buf = Vec::new();
359 ciborium::into_writer(v, &mut buf).unwrap();
360 buf
361 }
362
363 #[test]
364 fn cbor_bytes_projects_to_dollar_bytes() {
365 let buf = cbor_of(&ciborium::value::Value::Bytes(b"hello".to_vec()));
366 assert_eq!(cbor_to_json(&buf).unwrap(), json!({"$bytes": "aGVsbG8="}));
367 }
368
369 #[test]
370 fn embedded_bytes_in_map_wrapped() {
371 let v = ciborium::value::Value::Map(vec![
372 (
373 ciborium::value::Value::Text("name".into()),
374 ciborium::value::Value::Text("x".into()),
375 ),
376 (
377 ciborium::value::Value::Text("blob".into()),
378 ciborium::value::Value::Bytes(vec![1, 2]),
379 ),
380 ]);
381 assert_eq!(
382 cbor_to_json(&cbor_of(&v)).unwrap(),
383 json!({"name": "x", "blob": {"$bytes": "AQI="}})
384 );
385 }
386
387 #[test]
388 fn literal_dollar_key_is_escaped_on_output() {
389 let v = ciborium::value::Value::Map(vec![(
390 ciborium::value::Value::Text("$bytes".into()),
391 ciborium::value::Value::Text("hello".into()),
392 )]);
393 assert_eq!(
394 cbor_to_json(&cbor_of(&v)).unwrap(),
395 json!({"$$bytes": "hello"})
396 );
397 }
398
399 #[test]
400 fn dollar_bytes_parses_to_cbor_bytes() {
401 let cbor = json_to_cbor(&json!({"$bytes": "aGVsbG8="})).unwrap();
402 let value: ciborium::value::Value = ciborium::from_reader(&cbor[..]).unwrap();
403 assert_eq!(value, ciborium::value::Value::Bytes(b"hello".to_vec()));
404 }
405
406 #[test]
407 fn bytes_roundtrip_through_json() {
408 let original = cbor_of(&ciborium::value::Value::Bytes(vec![0u8, 1, 2, 255]));
409 let json = cbor_to_json(&original).unwrap();
410 let back = json_to_cbor(&json).unwrap();
411 assert_eq!(original, back);
412 }
413
414 #[test]
415 fn escaped_key_roundtrip_through_json() {
416 let v = ciborium::value::Value::Map(vec![(
417 ciborium::value::Value::Text("$bytes".into()),
418 ciborium::value::Value::Text("hello".into()),
419 )]);
420 let json = cbor_to_json(&cbor_of(&v)).unwrap();
421 let back = json_to_cbor(&json).unwrap();
422 let value: ciborium::value::Value = ciborium::from_reader(&back[..]).unwrap();
423 assert_eq!(value, v);
424 }
425
426 #[test]
427 fn content_cbor_embeds_dollar_bytes() {
428 let data = cbor_of(&ciborium::value::Value::Map(vec![(
429 ciborium::value::Value::Text("thumb".into()),
430 ciborium::value::Value::Bytes(vec![1, 2]),
431 )]));
432 let result = decode_content_data(&data, Some("application/cbor"));
433 assert_eq!(result, json!({"thumb": {"$bytes": "AQI="}}));
434 }
435
436 #[test]
437 fn dollar_bytes_non_string_value_errors() {
438 assert!(json_to_cbor(&json!({"$bytes": 42})).is_err());
439 }
440
441 #[test]
442 fn non_finite_float_errors() {
443 let cbor = cbor_of(&ciborium::value::Value::Float(f64::NAN));
444 assert!(cbor_to_json(&cbor).is_err());
445 }
446}