Skip to main content

act_types/
cbor.rs

1// CBOR <-> JSON/serde conversion utilities.
2//
3// Note: ciborium produces standard CBOR, not strict dCBOR (RFC 8949 §4.2).
4// For JSON-originating data this is practically deterministic, but does not
5// guarantee shortest integer encoding, sorted map keys, or preferred floats.
6
7/// Encode a serializable value as CBOR bytes.
8pub 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
14/// Decode CBOR bytes into a deserializable value.
15pub 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
19/// Convert a JSON value to CBOR bytes.
20pub fn json_to_cbor(value: &serde_json::Value) -> Result<Vec<u8>, CborError> {
21    let mut buf = Vec::new();
22    ciborium::into_writer(value, &mut buf)
23        .map_err(|e| CborError(format!("JSON→CBOR encode failed: {e}")))?;
24    Ok(buf)
25}
26
27/// Convert CBOR bytes to a JSON value.
28pub fn cbor_to_json(bytes: &[u8]) -> Result<serde_json::Value, CborError> {
29    ciborium::from_reader(bytes).map_err(|e| CborError(format!("CBOR→JSON decode failed: {e}")))
30}
31
32/// Decode content-part data based on MIME type for JSON representation.
33///
34/// - `text/*`, `application/json`, `application/xml` — raw UTF-8 bytes → JSON string
35/// - `application/cbor` — CBOR-decoded to JSON value
36/// - everything else (image/*, octet-stream, etc.) — base64-encoded string
37pub fn decode_content_data(data: &[u8], mime_type: Option<&str>) -> serde_json::Value {
38    let mime = mime_type.unwrap_or("application/cbor");
39
40    if mime.starts_with("text/") || mime == "application/json" || mime == "application/xml" {
41        // Text-like: inline as string
42        serde_json::Value::String(String::from_utf8_lossy(data).into_owned())
43    } else if mime == "application/cbor" {
44        // Structured: CBOR → JSON value
45        cbor_to_json(data).unwrap_or_else(|_| {
46            use base64::Engine as _;
47            serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(data))
48        })
49    } else {
50        // Binary (image/*, octet-stream, pdf, etc.): base64
51        use base64::Engine as _;
52        serde_json::Value::String(base64::engine::general_purpose::STANDARD.encode(data))
53    }
54}
55
56/// CBOR conversion error.
57#[derive(Debug, Clone)]
58pub struct CborError(pub String);
59
60impl std::fmt::Display for CborError {
61    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
62        f.write_str(&self.0)
63    }
64}
65
66impl std::error::Error for CborError {}
67
68#[cfg(test)]
69mod tests {
70    use super::*;
71    use serde_json::json;
72
73    #[test]
74    fn roundtrip_object() {
75        let input = json!({"a": 2, "b": 3});
76        let cbor = json_to_cbor(&input).unwrap();
77        let output = cbor_to_json(&cbor).unwrap();
78        assert_eq!(input, output);
79    }
80
81    #[test]
82    fn roundtrip_nested() {
83        let input = json!({"config": {"api_key": "abc123"}, "values": [1, 2, 3]});
84        let cbor = json_to_cbor(&input).unwrap();
85        let output = cbor_to_json(&cbor).unwrap();
86        assert_eq!(input, output);
87    }
88
89    #[test]
90    fn roundtrip_null() {
91        let input = json!(null);
92        let cbor = json_to_cbor(&input).unwrap();
93        let output = cbor_to_json(&cbor).unwrap();
94        assert_eq!(input, output);
95    }
96
97    #[test]
98    fn empty_bytes_is_error() {
99        assert!(cbor_to_json(&[]).is_err());
100    }
101
102    #[test]
103    fn generic_roundtrip() {
104        let input = 42u64;
105        let bytes = to_cbor(&input);
106        let output: u64 = from_cbor(&bytes).unwrap();
107        assert_eq!(input, output);
108    }
109
110    #[test]
111    fn decode_text_content() {
112        let data = b"hello world";
113        let result = decode_content_data(data, Some("text/plain"));
114        assert_eq!(result, json!("hello world"));
115    }
116
117    #[test]
118    fn decode_cbor_content() {
119        let data = to_cbor(&json!({"key": "value"}));
120        let result = decode_content_data(&data, None);
121        assert_eq!(result, json!({"key": "value"}));
122    }
123
124    #[test]
125    fn decode_json_content_as_text() {
126        let data = br#"{"pets": [1, 2, 3]}"#;
127        let result = decode_content_data(data, Some("application/json"));
128        assert_eq!(result, json!(r#"{"pets": [1, 2, 3]}"#));
129    }
130
131    #[test]
132    fn decode_invalid_cbor_falls_back_to_base64() {
133        let data = b"\xff\xfe";
134        let result = decode_content_data(data, Some("application/octet-stream"));
135        // Should be base64 string
136        assert!(result.is_string());
137    }
138
139    #[test]
140    fn decode_image_content_to_base64() {
141        let data = vec![0x89, 0x50, 0x4E, 0x47]; // PNG magic bytes
142        let result = decode_content_data(&data, Some("image/png"));
143        assert!(result.is_string());
144        use base64::Engine as _;
145        let decoded = base64::engine::general_purpose::STANDARD
146            .decode(result.as_str().unwrap())
147            .unwrap();
148        assert_eq!(decoded, data);
149    }
150
151    #[test]
152    fn decode_octet_stream_to_base64() {
153        let data = vec![0xFF, 0xFE, 0x00];
154        let result = decode_content_data(&data, Some("application/octet-stream"));
155        assert!(result.is_string());
156        use base64::Engine as _;
157        let decoded = base64::engine::general_purpose::STANDARD
158            .decode(result.as_str().unwrap())
159            .unwrap();
160        assert_eq!(decoded, data);
161    }
162
163    #[test]
164    fn decode_html_as_text() {
165        let data = b"<h1>Hello</h1>";
166        let result = decode_content_data(data, Some("text/html"));
167        assert_eq!(result, json!("<h1>Hello</h1>"));
168    }
169
170    #[test]
171    fn decode_xml_as_text() {
172        let data = b"<root><item/></root>";
173        let result = decode_content_data(data, Some("application/xml"));
174        assert_eq!(result, json!("<root><item/></root>"));
175    }
176}