Skip to main content

platform_value/converter/
ciborium.rs

1use crate::value_map::ValueMap;
2use crate::{Error, Value, ValueMapHelper};
3use ciborium::value::Integer;
4use ciborium::Value as CborValue;
5
6impl Value {
7    pub fn convert_from_cbor_map<I, R>(map: I) -> Result<R, Error>
8    where
9        I: IntoIterator<Item = (String, CborValue)>,
10        R: FromIterator<(String, Value)>,
11    {
12        map.into_iter()
13            .map(|(key, cbor_value)| Ok((key, cbor_value.try_into()?)))
14            .collect()
15    }
16
17    pub fn convert_to_cbor_map<I, R>(map: I) -> Result<R, Error>
18    where
19        I: IntoIterator<Item = (String, Value)>,
20        R: FromIterator<(String, CborValue)>,
21    {
22        map.into_iter()
23            .map(|(key, value)| Ok((key, value.try_into()?)))
24            .collect()
25    }
26
27    pub fn to_cbor_buffer(&self) -> Result<Vec<u8>, Error> {
28        let mut buffer: Vec<u8> = Vec::new();
29        ciborium::ser::into_writer(self, &mut buffer)
30            .map_err(|e| Error::CborSerializationError(e.to_string()))?;
31
32        Ok(buffer)
33    }
34}
35
36impl TryFrom<CborValue> for Value {
37    type Error = Error;
38
39    fn try_from(value: CborValue) -> Result<Self, Error> {
40        Ok(match value {
41            CborValue::Integer(integer) => Self::I128(integer.into()),
42            CborValue::Bytes(bytes) => Self::Bytes(bytes),
43            CborValue::Float(float) => Self::Float(float),
44            CborValue::Text(string) => Self::Text(string),
45            CborValue::Bool(value) => Self::Bool(value),
46            CborValue::Null => Self::Null,
47            CborValue::Tag(_, _) => {
48                return Err(Error::Unsupported(
49                    "conversion from cbor tags are currently not supported".to_string(),
50                ))
51            }
52            CborValue::Array(array) => {
53                let len = array.len();
54                if len > 10
55                    && array.iter().all(|v| {
56                        let Some(int) = v.as_integer() else {
57                            return false;
58                        };
59                        int.le(&Integer::from(u8::MAX)) && int.ge(&Integer::from(0))
60                    })
61                {
62                    //this is an array of bytes
63                    Self::Bytes(
64                        array
65                            .into_iter()
66                            .map(|v| v.into_integer().unwrap().try_into().unwrap())
67                            .collect(),
68                    )
69                } else {
70                    Self::Array(
71                        array
72                            .into_iter()
73                            .map(|v| v.try_into())
74                            .collect::<Result<Vec<Value>, Error>>()?,
75                    )
76                }
77            }
78            CborValue::Map(map) => Self::Map(
79                map.into_iter()
80                    .map(|(k, v)| Ok((k.try_into()?, v.try_into()?)))
81                    .collect::<Result<ValueMap, Error>>()?,
82            ),
83            _ => panic!("unsupported"),
84        })
85    }
86}
87
88impl TryInto<CborValue> for Value {
89    type Error = Error;
90
91    fn try_into(self) -> Result<CborValue, Self::Error> {
92        Ok(match self {
93            Value::U128(i) => CborValue::Integer((i as u64).into()),
94            Value::I128(i) => CborValue::Integer((i as i64).into()),
95            Value::U64(i) => CborValue::Integer(i.into()),
96            Value::I64(i) => CborValue::Integer(i.into()),
97            Value::U32(i) => CborValue::Integer(i.into()),
98            Value::I32(i) => CborValue::Integer(i.into()),
99            Value::U16(i) => CborValue::Integer(i.into()),
100            Value::I16(i) => CborValue::Integer(i.into()),
101            Value::U8(i) => CborValue::Integer(i.into()),
102            Value::I8(i) => CborValue::Integer(i.into()),
103            Value::Bytes(bytes) => CborValue::Bytes(bytes),
104            Value::Bytes20(bytes) => CborValue::Bytes(bytes.to_vec()),
105            Value::Bytes32(bytes) => CborValue::Bytes(bytes.to_vec()),
106            Value::Bytes36(bytes) => CborValue::Bytes(bytes.to_vec()),
107            Value::Float(float) => CborValue::Float(float),
108            Value::Text(string) => CborValue::Text(string),
109            Value::Bool(value) => CborValue::Bool(value),
110            Value::Null => CborValue::Null,
111            Value::Array(array) => CborValue::Array(
112                array
113                    .into_iter()
114                    .map(|value| value.try_into())
115                    .collect::<Result<Vec<CborValue>, Error>>()?,
116            ),
117            Value::Map(mut map) => {
118                map.sort_by_keys();
119                CborValue::Map(
120                    map.into_iter()
121                        .map(|(k, v)| Ok((k.try_into()?, v.try_into()?)))
122                        .collect::<Result<Vec<(CborValue, CborValue)>, Error>>()?,
123                )
124            }
125            Value::Identifier(bytes) => CborValue::Bytes(bytes.to_vec()),
126            Value::EnumU8(_) => {
127                return Err(Error::Unsupported(
128                    "No support for conversion of EnumU8 to JSONValue".to_string(),
129                ))
130            }
131            Value::EnumString(_) => {
132                return Err(Error::Unsupported(
133                    "No support for conversion of EnumString to JSONValue".to_string(),
134                ))
135            }
136        })
137    }
138}
139
140impl TryInto<Box<CborValue>> for Box<Value> {
141    type Error = Error;
142    fn try_into(self) -> Result<Box<CborValue>, Self::Error> {
143        (*self).try_into().map(Box::new)
144    }
145}
146
147#[cfg(test)]
148#[allow(clippy::approx_constant)]
149mod tests {
150    use crate::{Error, Value};
151    use ciborium::value::Integer;
152    use ciborium::Value as CborValue;
153
154    // -----------------------------------------------------------------------
155    // Round-trip: Value -> CborValue -> Value for basic types
156    // -----------------------------------------------------------------------
157
158    #[test]
159    fn round_trip_null() {
160        let original = Value::Null;
161        let cbor: CborValue = original.clone().try_into().unwrap();
162        assert_eq!(cbor, CborValue::Null);
163        let back: Value = cbor.try_into().unwrap();
164        assert_eq!(back, Value::Null);
165    }
166
167    #[test]
168    fn round_trip_bool_true() {
169        let original = Value::Bool(true);
170        let cbor: CborValue = original.clone().try_into().unwrap();
171        assert_eq!(cbor, CborValue::Bool(true));
172        let back: Value = cbor.try_into().unwrap();
173        // Comes back as I128(1) since CBOR integers are unified
174        assert_eq!(back, Value::Bool(true));
175    }
176
177    #[test]
178    fn round_trip_bool_false() {
179        let original = Value::Bool(false);
180        let cbor: CborValue = original.clone().try_into().unwrap();
181        let back: Value = cbor.try_into().unwrap();
182        assert_eq!(back, Value::Bool(false));
183    }
184
185    #[test]
186    fn round_trip_text() {
187        let original = Value::Text("hello world".into());
188        let cbor: CborValue = original.clone().try_into().unwrap();
189        assert_eq!(cbor, CborValue::Text("hello world".into()));
190        let back: Value = cbor.try_into().unwrap();
191        assert_eq!(back, original);
192    }
193
194    #[test]
195    fn round_trip_float() {
196        let original = Value::Float(3.14);
197        let cbor: CborValue = original.clone().try_into().unwrap();
198        assert_eq!(cbor, CborValue::Float(3.14));
199        let back: Value = cbor.try_into().unwrap();
200        assert_eq!(back, original);
201    }
202
203    #[test]
204    fn round_trip_bytes() {
205        let original = Value::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]);
206        let cbor: CborValue = original.clone().try_into().unwrap();
207        assert_eq!(cbor, CborValue::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]));
208        let back: Value = cbor.try_into().unwrap();
209        assert_eq!(back, original);
210    }
211
212    #[test]
213    fn round_trip_u64() {
214        let original = Value::U64(42);
215        let cbor: CborValue = original.clone().try_into().unwrap();
216        // Comes back as Integer
217        let back: Value = cbor.try_into().unwrap();
218        // CBOR integers come back as I128
219        assert_eq!(back, Value::I128(42));
220    }
221
222    #[test]
223    fn round_trip_i64_negative() {
224        let original = Value::I64(-99);
225        let cbor: CborValue = original.clone().try_into().unwrap();
226        let back: Value = cbor.try_into().unwrap();
227        assert_eq!(back, Value::I128(-99));
228    }
229
230    // -----------------------------------------------------------------------
231    // Tag rejection in TryFrom<CborValue>
232    // -----------------------------------------------------------------------
233
234    #[test]
235    fn cbor_tag_rejected() {
236        let tagged = CborValue::Tag(42, Box::new(CborValue::Null));
237        let result: Result<Value, Error> = tagged.try_into();
238        assert!(matches!(result, Err(Error::Unsupported(_))));
239    }
240
241    #[test]
242    fn cbor_tag_rejection_message() {
243        let tagged = CborValue::Tag(0, Box::new(CborValue::Text("date".into())));
244        let err = Value::try_from(tagged).unwrap_err();
245        match err {
246            Error::Unsupported(msg) => {
247                assert!(msg.contains("tag"), "error message should mention tags");
248            }
249            _ => panic!("expected Unsupported error"),
250        }
251    }
252
253    // -----------------------------------------------------------------------
254    // Byte-array heuristic boundary (10 vs 11 integer elements)
255    // Note: the CBOR heuristic uses > 10 (strictly greater), unlike JSON's >= 10
256    // -----------------------------------------------------------------------
257
258    #[test]
259    fn cbor_array_10_integers_stays_array() {
260        // Exactly 10 elements -> stays as Array (boundary: > 10 needed for bytes)
261        let arr: Vec<CborValue> = (0..10)
262            .map(|i| CborValue::Integer(Integer::from(i as u8)))
263            .collect();
264        let cbor = CborValue::Array(arr);
265        let val: Value = cbor.try_into().unwrap();
266        assert!(
267            matches!(val, Value::Array(_)),
268            "10 elements should stay as Array in CBOR heuristic"
269        );
270    }
271
272    #[test]
273    fn cbor_array_11_integers_becomes_bytes() {
274        // 11 elements, all in u8 range -> becomes Bytes
275        let arr: Vec<CborValue> = (0..11)
276            .map(|i| CborValue::Integer(Integer::from(i as u8)))
277            .collect();
278        let cbor = CborValue::Array(arr);
279        let val: Value = cbor.try_into().unwrap();
280        assert!(
281            matches!(val, Value::Bytes(_)),
282            "11 elements of u8-range integers should become Bytes"
283        );
284        if let Value::Bytes(bytes) = val {
285            assert_eq!(bytes.len(), 11);
286            assert_eq!(bytes[0], 0);
287            assert_eq!(bytes[10], 10);
288        }
289    }
290
291    #[test]
292    fn cbor_array_mixed_types_stays_array() {
293        // 12 elements but mixed types -> stays as Array
294        let mut arr: Vec<CborValue> = (0..11)
295            .map(|i| CborValue::Integer(Integer::from(i as u8)))
296            .collect();
297        arr.push(CborValue::Text("not an int".into()));
298        let cbor = CborValue::Array(arr);
299        let val: Value = cbor.try_into().unwrap();
300        assert!(matches!(val, Value::Array(_)));
301    }
302
303    #[test]
304    fn cbor_array_negative_values_stays_array() {
305        // Negative values are not in 0..=u8::MAX range
306        let arr: Vec<CborValue> = (0..12)
307            .map(|i| CborValue::Integer(Integer::from(-(i as i64))))
308            .collect();
309        let cbor = CborValue::Array(arr);
310        let val: Value = cbor.try_into().unwrap();
311        // First element is 0 which is fine, but most are negative -> fails the ge(0) check
312        assert!(matches!(val, Value::Array(_)));
313    }
314
315    // -----------------------------------------------------------------------
316    // Map key sorting in TryInto<CborValue>
317    // -----------------------------------------------------------------------
318
319    #[test]
320    fn map_keys_sorted_in_cbor_output() {
321        // Keys inserted in reverse order should be sorted in output
322        let map = vec![
323            (Value::Text("z".into()), Value::U64(1)),
324            (Value::Text("a".into()), Value::U64(2)),
325            (Value::Text("m".into()), Value::U64(3)),
326        ];
327        let val = Value::Map(map);
328        let cbor: CborValue = val.try_into().unwrap();
329        if let CborValue::Map(pairs) = cbor {
330            let keys: Vec<String> = pairs
331                .iter()
332                .map(|(k, _)| {
333                    if let CborValue::Text(s) = k {
334                        s.clone()
335                    } else {
336                        panic!("expected text key")
337                    }
338                })
339                .collect();
340            assert_eq!(keys, vec!["a", "m", "z"]);
341        } else {
342            panic!("expected CborValue::Map");
343        }
344    }
345
346    // -----------------------------------------------------------------------
347    // EnumU8 / EnumString error paths
348    // -----------------------------------------------------------------------
349
350    #[test]
351    fn enum_u8_to_cbor_error() {
352        let val = Value::EnumU8(vec![1, 2, 3]);
353        let result: Result<CborValue, Error> = val.try_into();
354        assert!(matches!(result, Err(Error::Unsupported(_))));
355    }
356
357    #[test]
358    fn enum_string_to_cbor_error() {
359        let val = Value::EnumString(vec!["a".into(), "b".into()]);
360        let result: Result<CborValue, Error> = val.try_into();
361        assert!(matches!(result, Err(Error::Unsupported(_))));
362    }
363
364    // -----------------------------------------------------------------------
365    // U128 / I128 narrowing in TryInto<CborValue>
366    // -----------------------------------------------------------------------
367
368    #[test]
369    fn u128_narrowed_to_u64_in_cbor() {
370        // U128 is cast to u64 when converting to CborValue::Integer
371        let val = Value::U128(42);
372        let cbor: CborValue = val.try_into().unwrap();
373        assert_eq!(cbor, CborValue::Integer(42u64.into()));
374    }
375
376    #[test]
377    fn i128_narrowed_to_i64_in_cbor() {
378        // I128 is cast to i64 when converting to CborValue::Integer
379        let val = Value::I128(-99);
380        let cbor: CborValue = val.try_into().unwrap();
381        assert_eq!(cbor, CborValue::Integer((-99i64).into()));
382    }
383
384    // -----------------------------------------------------------------------
385    // Integer variant from CBOR -> Value
386    // -----------------------------------------------------------------------
387
388    #[test]
389    fn cbor_positive_integer_to_i128() {
390        let cbor = CborValue::Integer(Integer::from(255u8));
391        let val: Value = cbor.try_into().unwrap();
392        assert_eq!(val, Value::I128(255));
393    }
394
395    #[test]
396    fn cbor_negative_integer_to_i128() {
397        let cbor = CborValue::Integer(Integer::from(-1i64));
398        let val: Value = cbor.try_into().unwrap();
399        assert_eq!(val, Value::I128(-1));
400    }
401
402    #[test]
403    fn cbor_zero_integer_to_i128() {
404        let cbor = CborValue::Integer(Integer::from(0));
405        let val: Value = cbor.try_into().unwrap();
406        assert_eq!(val, Value::I128(0));
407    }
408
409    // -----------------------------------------------------------------------
410    // Bytes20 / Bytes32 / Bytes36 / Identifier -> CborValue::Bytes
411    // -----------------------------------------------------------------------
412
413    #[test]
414    fn bytes20_to_cbor_bytes() {
415        let bytes = [0xAAu8; 20];
416        let val = Value::Bytes20(bytes);
417        let cbor: CborValue = val.try_into().unwrap();
418        assert_eq!(cbor, CborValue::Bytes(bytes.to_vec()));
419    }
420
421    #[test]
422    fn bytes32_to_cbor_bytes() {
423        let bytes = [0xBBu8; 32];
424        let val = Value::Bytes32(bytes);
425        let cbor: CborValue = val.try_into().unwrap();
426        assert_eq!(cbor, CborValue::Bytes(bytes.to_vec()));
427    }
428
429    #[test]
430    fn bytes36_to_cbor_bytes() {
431        let bytes = [0xCCu8; 36];
432        let val = Value::Bytes36(bytes);
433        let cbor: CborValue = val.try_into().unwrap();
434        assert_eq!(cbor, CborValue::Bytes(bytes.to_vec()));
435    }
436
437    #[test]
438    fn identifier_to_cbor_bytes() {
439        let bytes = [0x01u8; 32];
440        let val = Value::Identifier(bytes);
441        let cbor: CborValue = val.try_into().unwrap();
442        assert_eq!(cbor, CborValue::Bytes(bytes.to_vec()));
443    }
444
445    // -----------------------------------------------------------------------
446    // Integer types round through CBOR
447    // -----------------------------------------------------------------------
448
449    #[test]
450    fn all_integer_types_to_cbor() {
451        let cases: Vec<Value> = vec![
452            Value::U8(255),
453            Value::I8(-128),
454            Value::U16(65535),
455            Value::I16(-32768),
456            Value::U32(u32::MAX),
457            Value::I32(i32::MIN),
458            Value::U64(u64::MAX),
459            Value::I64(i64::MIN),
460        ];
461        for val in cases {
462            let cbor: CborValue = val.clone().try_into().unwrap();
463            assert!(
464                matches!(cbor, CborValue::Integer(_)),
465                "expected Integer for {:?}",
466                val
467            );
468        }
469    }
470
471    // -----------------------------------------------------------------------
472    // Box<Value> TryInto<Box<CborValue>>
473    // -----------------------------------------------------------------------
474
475    #[test]
476    fn boxed_value_to_boxed_cbor() {
477        let val = Box::new(Value::Text("boxed".into()));
478        let cbor: Box<CborValue> = val.try_into().unwrap();
479        assert_eq!(*cbor, CborValue::Text("boxed".into()));
480    }
481
482    // -----------------------------------------------------------------------
483    // CBOR Map conversion
484    // -----------------------------------------------------------------------
485
486    #[test]
487    fn cbor_map_to_value_map() {
488        let cbor = CborValue::Map(vec![
489            (
490                CborValue::Text("key".into()),
491                CborValue::Integer(42u64.into()),
492            ),
493            (CborValue::Text("flag".into()), CborValue::Bool(true)),
494        ]);
495        let val: Value = cbor.try_into().unwrap();
496        assert!(val.is_map());
497    }
498
499    // -----------------------------------------------------------------------
500    // convert_from_cbor_map / convert_to_cbor_map
501    // -----------------------------------------------------------------------
502
503    #[test]
504    fn convert_from_cbor_map_basic() {
505        let pairs = vec![
506            ("a".to_string(), CborValue::Bool(true)),
507            ("b".to_string(), CborValue::Text("hello".into())),
508        ];
509        let result: std::collections::BTreeMap<String, Value> =
510            Value::convert_from_cbor_map(pairs).unwrap();
511        assert_eq!(result.get("a"), Some(&Value::Bool(true)));
512        assert_eq!(result.get("b"), Some(&Value::Text("hello".into())));
513    }
514
515    #[test]
516    fn convert_to_cbor_map_basic() {
517        let pairs = vec![
518            ("x".to_string(), Value::U64(10)),
519            ("y".to_string(), Value::Bool(false)),
520        ];
521        let result: std::collections::BTreeMap<String, CborValue> =
522            Value::convert_to_cbor_map(pairs).unwrap();
523        assert_eq!(result.get("x"), Some(&CborValue::Integer(10u64.into())));
524        assert_eq!(result.get("y"), Some(&CborValue::Bool(false)));
525    }
526
527    // -----------------------------------------------------------------------
528    // to_cbor_buffer
529    // -----------------------------------------------------------------------
530
531    #[test]
532    fn to_cbor_buffer_roundtrip() {
533        let val = Value::Text("cbor buffer test".into());
534        let buf = val.to_cbor_buffer().unwrap();
535        assert!(!buf.is_empty());
536    }
537
538    // -----------------------------------------------------------------------
539    // CBOR array with nested values
540    // -----------------------------------------------------------------------
541
542    #[test]
543    fn cbor_array_with_nested_map() {
544        let cbor = CborValue::Array(vec![
545            CborValue::Text("item".into()),
546            CborValue::Map(vec![(
547                CborValue::Text("inner".into()),
548                CborValue::Bool(true),
549            )]),
550        ]);
551        let val: Value = cbor.try_into().unwrap();
552        assert!(matches!(val, Value::Array(_)));
553        if let Value::Array(arr) = &val {
554            assert_eq!(arr.len(), 2);
555            assert!(arr[1].is_map());
556        }
557    }
558}