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) => {
94                CborValue::Integer(Integer::try_from(i).map_err(|_| Error::IntegerSizeError)?)
95            }
96            Value::I128(i) => {
97                CborValue::Integer(Integer::try_from(i).map_err(|_| Error::IntegerSizeError)?)
98            }
99            Value::U64(i) => CborValue::Integer(i.into()),
100            Value::I64(i) => CborValue::Integer(i.into()),
101            Value::U32(i) => CborValue::Integer(i.into()),
102            Value::I32(i) => CborValue::Integer(i.into()),
103            Value::U16(i) => CborValue::Integer(i.into()),
104            Value::I16(i) => CborValue::Integer(i.into()),
105            Value::U8(i) => CborValue::Integer(i.into()),
106            Value::I8(i) => CborValue::Integer(i.into()),
107            Value::Bytes(bytes) => CborValue::Bytes(bytes),
108            Value::Bytes20(bytes) => CborValue::Bytes(bytes.to_vec()),
109            Value::Bytes32(bytes) => CborValue::Bytes(bytes.to_vec()),
110            Value::Bytes36(bytes) => CborValue::Bytes(bytes.to_vec()),
111            Value::Float(float) => CborValue::Float(float),
112            Value::Text(string) => CborValue::Text(string),
113            Value::Bool(value) => CborValue::Bool(value),
114            Value::Null => CborValue::Null,
115            Value::Array(array) => CborValue::Array(
116                array
117                    .into_iter()
118                    .map(|value| value.try_into())
119                    .collect::<Result<Vec<CborValue>, Error>>()?,
120            ),
121            Value::Map(mut map) => {
122                map.sort_by_keys();
123                CborValue::Map(
124                    map.into_iter()
125                        .map(|(k, v)| Ok((k.try_into()?, v.try_into()?)))
126                        .collect::<Result<Vec<(CborValue, CborValue)>, Error>>()?,
127                )
128            }
129            Value::Identifier(bytes) => CborValue::Bytes(bytes.to_vec()),
130            Value::EnumU8(_) => {
131                return Err(Error::Unsupported(
132                    "No support for conversion of EnumU8 to JSONValue".to_string(),
133                ))
134            }
135            Value::EnumString(_) => {
136                return Err(Error::Unsupported(
137                    "No support for conversion of EnumString to JSONValue".to_string(),
138                ))
139            }
140        })
141    }
142}
143
144impl TryInto<Box<CborValue>> for Box<Value> {
145    type Error = Error;
146    fn try_into(self) -> Result<Box<CborValue>, Self::Error> {
147        (*self).try_into().map(Box::new)
148    }
149}
150
151#[cfg(test)]
152#[allow(clippy::approx_constant)]
153mod tests {
154    use crate::{Error, Value};
155    use ciborium::value::Integer;
156    use ciborium::Value as CborValue;
157
158    const MIN_NATIVE_CBOR_INTEGER: i128 = -(u64::MAX as i128) - 1;
159
160    // -----------------------------------------------------------------------
161    // Round-trip: Value -> CborValue -> Value for basic types
162    // -----------------------------------------------------------------------
163
164    #[test]
165    fn round_trip_null() {
166        let original = Value::Null;
167        let cbor: CborValue = original.clone().try_into().unwrap();
168        assert_eq!(cbor, CborValue::Null);
169        let back: Value = cbor.try_into().unwrap();
170        assert_eq!(back, Value::Null);
171    }
172
173    #[test]
174    fn round_trip_bool_true() {
175        let original = Value::Bool(true);
176        let cbor: CborValue = original.clone().try_into().unwrap();
177        assert_eq!(cbor, CborValue::Bool(true));
178        let back: Value = cbor.try_into().unwrap();
179        // Comes back as I128(1) since CBOR integers are unified
180        assert_eq!(back, Value::Bool(true));
181    }
182
183    #[test]
184    fn round_trip_bool_false() {
185        let original = Value::Bool(false);
186        let cbor: CborValue = original.clone().try_into().unwrap();
187        let back: Value = cbor.try_into().unwrap();
188        assert_eq!(back, Value::Bool(false));
189    }
190
191    #[test]
192    fn round_trip_text() {
193        let original = Value::Text("hello world".into());
194        let cbor: CborValue = original.clone().try_into().unwrap();
195        assert_eq!(cbor, CborValue::Text("hello world".into()));
196        let back: Value = cbor.try_into().unwrap();
197        assert_eq!(back, original);
198    }
199
200    #[test]
201    fn round_trip_float() {
202        let original = Value::Float(3.14);
203        let cbor: CborValue = original.clone().try_into().unwrap();
204        assert_eq!(cbor, CborValue::Float(3.14));
205        let back: Value = cbor.try_into().unwrap();
206        assert_eq!(back, original);
207    }
208
209    #[test]
210    fn round_trip_bytes() {
211        let original = Value::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]);
212        let cbor: CborValue = original.clone().try_into().unwrap();
213        assert_eq!(cbor, CborValue::Bytes(vec![0xDE, 0xAD, 0xBE, 0xEF]));
214        let back: Value = cbor.try_into().unwrap();
215        assert_eq!(back, original);
216    }
217
218    #[test]
219    fn round_trip_u64() {
220        let original = Value::U64(42);
221        let cbor: CborValue = original.clone().try_into().unwrap();
222        // Comes back as Integer
223        let back: Value = cbor.try_into().unwrap();
224        // CBOR integers come back as I128
225        assert_eq!(back, Value::I128(42));
226    }
227
228    #[test]
229    fn round_trip_i64_negative() {
230        let original = Value::I64(-99);
231        let cbor: CborValue = original.clone().try_into().unwrap();
232        let back: Value = cbor.try_into().unwrap();
233        assert_eq!(back, Value::I128(-99));
234    }
235
236    // -----------------------------------------------------------------------
237    // Tag rejection in TryFrom<CborValue>
238    // -----------------------------------------------------------------------
239
240    #[test]
241    fn cbor_tag_rejected() {
242        let tagged = CborValue::Tag(42, Box::new(CborValue::Null));
243        let result: Result<Value, Error> = tagged.try_into();
244        assert!(matches!(result, Err(Error::Unsupported(_))));
245    }
246
247    #[test]
248    fn cbor_tag_rejection_message() {
249        let tagged = CborValue::Tag(0, Box::new(CborValue::Text("date".into())));
250        let err = Value::try_from(tagged).unwrap_err();
251        match err {
252            Error::Unsupported(msg) => {
253                assert!(msg.contains("tag"), "error message should mention tags");
254            }
255            _ => panic!("expected Unsupported error"),
256        }
257    }
258
259    // -----------------------------------------------------------------------
260    // Byte-array heuristic boundary (10 vs 11 integer elements)
261    // Note: the CBOR heuristic uses > 10 (strictly greater), unlike JSON's >= 10
262    // -----------------------------------------------------------------------
263
264    #[test]
265    fn cbor_array_10_integers_stays_array() {
266        // Exactly 10 elements -> stays as Array (boundary: > 10 needed for bytes)
267        let arr: Vec<CborValue> = (0..10)
268            .map(|i| CborValue::Integer(Integer::from(i as u8)))
269            .collect();
270        let cbor = CborValue::Array(arr);
271        let val: Value = cbor.try_into().unwrap();
272        assert!(
273            matches!(val, Value::Array(_)),
274            "10 elements should stay as Array in CBOR heuristic"
275        );
276    }
277
278    #[test]
279    fn cbor_array_11_integers_becomes_bytes() {
280        // 11 elements, all in u8 range -> becomes Bytes
281        let arr: Vec<CborValue> = (0..11)
282            .map(|i| CborValue::Integer(Integer::from(i as u8)))
283            .collect();
284        let cbor = CborValue::Array(arr);
285        let val: Value = cbor.try_into().unwrap();
286        assert!(
287            matches!(val, Value::Bytes(_)),
288            "11 elements of u8-range integers should become Bytes"
289        );
290        if let Value::Bytes(bytes) = val {
291            assert_eq!(bytes.len(), 11);
292            assert_eq!(bytes[0], 0);
293            assert_eq!(bytes[10], 10);
294        }
295    }
296
297    #[test]
298    fn cbor_array_mixed_types_stays_array() {
299        // 12 elements but mixed types -> stays as Array
300        let mut arr: Vec<CborValue> = (0..11)
301            .map(|i| CborValue::Integer(Integer::from(i as u8)))
302            .collect();
303        arr.push(CborValue::Text("not an int".into()));
304        let cbor = CborValue::Array(arr);
305        let val: Value = cbor.try_into().unwrap();
306        assert!(matches!(val, Value::Array(_)));
307    }
308
309    #[test]
310    fn cbor_array_negative_values_stays_array() {
311        // Negative values are not in 0..=u8::MAX range
312        let arr: Vec<CborValue> = (0..12)
313            .map(|i| CborValue::Integer(Integer::from(-(i as i64))))
314            .collect();
315        let cbor = CborValue::Array(arr);
316        let val: Value = cbor.try_into().unwrap();
317        // First element is 0 which is fine, but most are negative -> fails the ge(0) check
318        assert!(matches!(val, Value::Array(_)));
319    }
320
321    // -----------------------------------------------------------------------
322    // Map key sorting in TryInto<CborValue>
323    // -----------------------------------------------------------------------
324
325    #[test]
326    fn map_keys_sorted_in_cbor_output() {
327        // Keys inserted in reverse order should be sorted in output
328        let map = vec![
329            (Value::Text("z".into()), Value::U64(1)),
330            (Value::Text("a".into()), Value::U64(2)),
331            (Value::Text("m".into()), Value::U64(3)),
332        ];
333        let val = Value::Map(map);
334        let cbor: CborValue = val.try_into().unwrap();
335        if let CborValue::Map(pairs) = cbor {
336            let keys: Vec<String> = pairs
337                .iter()
338                .map(|(k, _)| {
339                    if let CborValue::Text(s) = k {
340                        s.clone()
341                    } else {
342                        panic!("expected text key")
343                    }
344                })
345                .collect();
346            assert_eq!(keys, vec!["a", "m", "z"]);
347        } else {
348            panic!("expected CborValue::Map");
349        }
350    }
351
352    // -----------------------------------------------------------------------
353    // EnumU8 / EnumString error paths
354    // -----------------------------------------------------------------------
355
356    #[test]
357    fn enum_u8_to_cbor_error() {
358        let val = Value::EnumU8(vec![1, 2, 3]);
359        let result: Result<CborValue, Error> = val.try_into();
360        assert!(matches!(result, Err(Error::Unsupported(_))));
361    }
362
363    #[test]
364    fn enum_string_to_cbor_error() {
365        let val = Value::EnumString(vec!["a".into(), "b".into()]);
366        let result: Result<CborValue, Error> = val.try_into();
367        assert!(matches!(result, Err(Error::Unsupported(_))));
368    }
369
370    // -----------------------------------------------------------------------
371    // U128 / I128 conversion in TryInto<CborValue>
372    // -----------------------------------------------------------------------
373
374    #[test]
375    fn u128_in_native_range_converts_to_cbor() {
376        let val = Value::U128(42);
377        let cbor: CborValue = val.try_into().unwrap();
378        assert_eq!(cbor, CborValue::Integer(42u64.into()));
379
380        let max_native = Value::U128(u64::MAX as u128);
381        let cbor: CborValue = max_native.try_into().unwrap();
382        assert_eq!(cbor, CborValue::Integer(u64::MAX.into()));
383    }
384
385    #[test]
386    fn i128_in_native_range_converts_to_cbor() {
387        let val = Value::I128(-99);
388        let cbor: CborValue = val.try_into().unwrap();
389        assert_eq!(cbor, CborValue::Integer((-99i64).into()));
390    }
391
392    #[test]
393    fn u128_rejects_values_outside_the_native_cbor_integer_range() {
394        for value in [u64::MAX as u128 + 1, u128::MAX] {
395            let result: Result<CborValue, Error> = Value::U128(value).try_into();
396            assert_eq!(result.unwrap_err(), Error::IntegerSizeError);
397        }
398    }
399
400    #[test]
401    fn i128_preserves_the_full_native_cbor_integer_range() {
402        for value in [
403            MIN_NATIVE_CBOR_INTEGER,
404            i64::MIN as i128,
405            i64::MAX as i128,
406            u64::MAX as i128,
407        ] {
408            let cbor: CborValue = Value::I128(value).try_into().unwrap();
409            let CborValue::Integer(integer) = cbor else {
410                panic!("expected CBOR integer");
411            };
412            assert_eq!(i128::from(integer), value);
413        }
414    }
415
416    #[test]
417    fn i128_rejects_values_outside_the_native_cbor_integer_range() {
418        for value in [
419            MIN_NATIVE_CBOR_INTEGER - 1,
420            u64::MAX as i128 + 1,
421            i128::MIN,
422            i128::MAX,
423        ] {
424            let result: Result<CborValue, Error> = Value::I128(value).try_into();
425            assert_eq!(result.unwrap_err(), Error::IntegerSizeError);
426        }
427    }
428
429    #[test]
430    fn nested_out_of_range_integer_fails_the_entire_conversion() {
431        let value = Value::Array(vec![Value::Map(vec![(
432            Value::Text("amount".to_string()),
433            Value::U128(u64::MAX as u128 + 1),
434        )])]);
435
436        let result: Result<CborValue, Error> = value.try_into();
437        assert_eq!(result.unwrap_err(), Error::IntegerSizeError);
438    }
439
440    // -----------------------------------------------------------------------
441    // Integer variant from CBOR -> Value
442    // -----------------------------------------------------------------------
443
444    #[test]
445    fn cbor_positive_integer_to_i128() {
446        let cbor = CborValue::Integer(Integer::from(255u8));
447        let val: Value = cbor.try_into().unwrap();
448        assert_eq!(val, Value::I128(255));
449    }
450
451    #[test]
452    fn cbor_negative_integer_to_i128() {
453        let cbor = CborValue::Integer(Integer::from(-1i64));
454        let val: Value = cbor.try_into().unwrap();
455        assert_eq!(val, Value::I128(-1));
456    }
457
458    #[test]
459    fn cbor_zero_integer_to_i128() {
460        let cbor = CborValue::Integer(Integer::from(0));
461        let val: Value = cbor.try_into().unwrap();
462        assert_eq!(val, Value::I128(0));
463    }
464
465    // -----------------------------------------------------------------------
466    // Bytes20 / Bytes32 / Bytes36 / Identifier -> CborValue::Bytes
467    // -----------------------------------------------------------------------
468
469    #[test]
470    fn bytes20_to_cbor_bytes() {
471        let bytes = [0xAAu8; 20];
472        let val = Value::Bytes20(bytes);
473        let cbor: CborValue = val.try_into().unwrap();
474        assert_eq!(cbor, CborValue::Bytes(bytes.to_vec()));
475    }
476
477    #[test]
478    fn bytes32_to_cbor_bytes() {
479        let bytes = [0xBBu8; 32];
480        let val = Value::Bytes32(bytes);
481        let cbor: CborValue = val.try_into().unwrap();
482        assert_eq!(cbor, CborValue::Bytes(bytes.to_vec()));
483    }
484
485    #[test]
486    fn bytes36_to_cbor_bytes() {
487        let bytes = [0xCCu8; 36];
488        let val = Value::Bytes36(bytes);
489        let cbor: CborValue = val.try_into().unwrap();
490        assert_eq!(cbor, CborValue::Bytes(bytes.to_vec()));
491    }
492
493    #[test]
494    fn identifier_to_cbor_bytes() {
495        let bytes = [0x01u8; 32];
496        let val = Value::Identifier(bytes);
497        let cbor: CborValue = val.try_into().unwrap();
498        assert_eq!(cbor, CborValue::Bytes(bytes.to_vec()));
499    }
500
501    // -----------------------------------------------------------------------
502    // Integer types round through CBOR
503    // -----------------------------------------------------------------------
504
505    #[test]
506    fn all_integer_types_to_cbor() {
507        let cases: Vec<Value> = vec![
508            Value::U8(255),
509            Value::I8(-128),
510            Value::U16(65535),
511            Value::I16(-32768),
512            Value::U32(u32::MAX),
513            Value::I32(i32::MIN),
514            Value::U64(u64::MAX),
515            Value::I64(i64::MIN),
516        ];
517        for val in cases {
518            let cbor: CborValue = val.clone().try_into().unwrap();
519            assert!(
520                matches!(cbor, CborValue::Integer(_)),
521                "expected Integer for {:?}",
522                val
523            );
524        }
525    }
526
527    // -----------------------------------------------------------------------
528    // Box<Value> TryInto<Box<CborValue>>
529    // -----------------------------------------------------------------------
530
531    #[test]
532    fn boxed_value_to_boxed_cbor() {
533        let val = Box::new(Value::Text("boxed".into()));
534        let cbor: Box<CborValue> = val.try_into().unwrap();
535        assert_eq!(*cbor, CborValue::Text("boxed".into()));
536    }
537
538    // -----------------------------------------------------------------------
539    // CBOR Map conversion
540    // -----------------------------------------------------------------------
541
542    #[test]
543    fn cbor_map_to_value_map() {
544        let cbor = CborValue::Map(vec![
545            (
546                CborValue::Text("key".into()),
547                CborValue::Integer(42u64.into()),
548            ),
549            (CborValue::Text("flag".into()), CborValue::Bool(true)),
550        ]);
551        let val: Value = cbor.try_into().unwrap();
552        assert!(val.is_map());
553    }
554
555    // -----------------------------------------------------------------------
556    // convert_from_cbor_map / convert_to_cbor_map
557    // -----------------------------------------------------------------------
558
559    #[test]
560    fn convert_from_cbor_map_basic() {
561        let pairs = vec![
562            ("a".to_string(), CborValue::Bool(true)),
563            ("b".to_string(), CborValue::Text("hello".into())),
564        ];
565        let result: std::collections::BTreeMap<String, Value> =
566            Value::convert_from_cbor_map(pairs).unwrap();
567        assert_eq!(result.get("a"), Some(&Value::Bool(true)));
568        assert_eq!(result.get("b"), Some(&Value::Text("hello".into())));
569    }
570
571    #[test]
572    fn convert_to_cbor_map_basic() {
573        let pairs = vec![
574            ("x".to_string(), Value::U64(10)),
575            ("y".to_string(), Value::Bool(false)),
576        ];
577        let result: std::collections::BTreeMap<String, CborValue> =
578            Value::convert_to_cbor_map(pairs).unwrap();
579        assert_eq!(result.get("x"), Some(&CborValue::Integer(10u64.into())));
580        assert_eq!(result.get("y"), Some(&CborValue::Bool(false)));
581    }
582
583    // -----------------------------------------------------------------------
584    // to_cbor_buffer
585    // -----------------------------------------------------------------------
586
587    #[test]
588    fn to_cbor_buffer_roundtrip() {
589        let val = Value::Text("cbor buffer test".into());
590        let buf = val.to_cbor_buffer().unwrap();
591        assert!(!buf.is_empty());
592    }
593
594    #[test]
595    fn to_cbor_buffer_roundtrips_native_cbor_integer_boundaries() {
596        for value in [
597            MIN_NATIVE_CBOR_INTEGER,
598            i64::MIN as i128 - 1,
599            i64::MAX as i128 + 1,
600            u64::MAX as i128,
601        ] {
602            let buf = Value::I128(value).to_cbor_buffer().unwrap();
603            let cbor: CborValue = ciborium::de::from_reader(buf.as_slice()).unwrap();
604            let decoded: Value = cbor.try_into().unwrap();
605            assert_eq!(decoded, Value::I128(value));
606        }
607    }
608
609    // -----------------------------------------------------------------------
610    // CBOR array with nested values
611    // -----------------------------------------------------------------------
612
613    #[test]
614    fn cbor_array_with_nested_map() {
615        let cbor = CborValue::Array(vec![
616            CborValue::Text("item".into()),
617            CborValue::Map(vec![(
618                CborValue::Text("inner".into()),
619                CborValue::Bool(true),
620            )]),
621        ]);
622        let val: Value = cbor.try_into().unwrap();
623        assert!(matches!(val, Value::Array(_)));
624        if let Value::Array(arr) = &val {
625            assert_eq!(arr.len(), 2);
626            assert!(arr[1].is_map());
627        }
628    }
629}