forest/lotus_json/
ipld.rs

1// Copyright 2019-2025 ChainSafe Systems
2// SPDX-License-Identifier: Apache-2.0, MIT
3
4//! # Differences between serializers
5//!
6//! The serializer created here uses `multihash` and `libipld-json` uses plain
7//! `base64`. That means one has an extra `m` in front of all the encoded byte
8//! values, using our serializer.
9//!
10//! For example:
11//!
12//! this:
13//! `{ "/": { "bytes": "mVGhlIHF1aQ" } }`
14//!
15//! `libipld-json`:
16//! `{ "/": { "bytes": "VGhlIHF1aQ" } }`
17//!
18//! Since `Lotus` is also using `multihash-base64` and we're trying to be
19//! compatible, we cannot switch to `libipld-json`.
20//!
21//! # Tech debt
22//! - The real way to do this is to implement [`ipld_core::codec::Codec`] bits appropriately,
23//!   or embrace using our own struct.
24
25use std::{collections::BTreeMap, fmt};
26
27use super::*;
28
29use ::cid::multibase;
30use ipld_core::{ipld, ipld::Ipld};
31use serde::de;
32
33#[derive(Serialize, Deserialize, JsonSchema)]
34#[schemars(rename = "Ipld")]
35pub struct IpldLotusJson(
36    #[serde(with = "self")]
37    #[schemars(with = "serde_json::Value")] // opt-out of JsonSchema for now
38    Ipld,
39);
40
41impl HasLotusJson for Ipld {
42    type LotusJson = IpldLotusJson;
43    #[cfg(test)]
44    fn snapshots() -> Vec<(serde_json::Value, Self)> {
45        vec![
46            (
47                json!({
48                    "my_link": {
49                        "/": "QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n"
50                    },
51                    "my_bytes": {
52                        "/": { "bytes": "mVGhlIHF1aQ" }
53                    },
54                    "my_string": "Some data",
55                    "my_float": {
56                        "/": { "float": "10.5" }
57                    },
58                    "my_int": {
59                        "/": { "int": "8" }
60                    },
61                    "my_neg_int": {
62                        "/": { "int": "-20" }
63                    },
64                    "my_null": null,
65                    "my_list": [
66                        null,
67                        { "/": "bafy2bzaceaa466o2jfc4g4ggrmtf55ygigvkmxvkr5mvhy4qbwlxetbmlkqjk" },
68                        {"/": { "int": "1" }},
69                    ]
70                }),
71                ipld!({
72                    "my_link": Ipld::Link("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n".parse().unwrap()),
73                    "my_bytes": Ipld::Bytes(vec![0x54, 0x68, 0x65, 0x20, 0x71, 0x75, 0x69]),
74                    "my_string": "Some data",
75                    "my_float": 10.5,
76                    "my_int": 8,
77                    "my_neg_int": -20,
78                    "my_null": null,
79                    "my_list": [
80                        null,
81                        Ipld::Link("bafy2bzaceaa466o2jfc4g4ggrmtf55ygigvkmxvkr5mvhy4qbwlxetbmlkqjk".parse().unwrap()),
82                        1,
83                    ],
84                }),
85            ),
86            // Test ported from go-ipld-prime (making sure edge case is handled)
87            (
88                json!({"/":{"/":"QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n"}}),
89                ipld!({"/": Ipld::Link("QmdfTbBqBPQ7VNxZEYEj14VmRuZBkqFbiwReogJgS1zR1n".parse().unwrap())}),
90            ),
91        ]
92    }
93    fn into_lotus_json(self) -> Self::LotusJson {
94        IpldLotusJson(self)
95    }
96    fn from_lotus_json(IpldLotusJson(it): Self::LotusJson) -> Self {
97        it
98    }
99}
100
101const BYTES_JSON_KEY: &str = "bytes";
102const INT_JSON_KEY: &str = "int";
103const FLOAT_JSON_KEY: &str = "float";
104
105/// Wrapper for serializing a IPLD reference to JSON.
106#[derive(Serialize)]
107#[serde(transparent)]
108struct Ref<'a>(#[serde(with = "self")] pub &'a Ipld);
109
110fn serialize<S>(ipld: &Ipld, serializer: S) -> Result<S::Ok, S::Error>
111where
112    S: Serializer,
113{
114    match &ipld {
115        Ipld::Null => serializer.serialize_none(),
116        Ipld::Bool(bool) => serializer.serialize_bool(*bool),
117        Ipld::Integer(i128) => serialize(
118            &ipld!({ "/": { INT_JSON_KEY: i128.to_string() } }),
119            serializer,
120        ),
121        Ipld::Float(f64) => serialize(
122            &ipld!({ "/": { FLOAT_JSON_KEY: f64.to_string() } }),
123            serializer,
124        ),
125        Ipld::String(string) => serializer.serialize_str(string),
126        Ipld::Bytes(bytes) => serialize(
127            &ipld!({ "/": { BYTES_JSON_KEY: multibase::encode(multibase::Base::Base64, bytes) } }),
128            serializer,
129        ),
130        Ipld::List(list) => {
131            let wrapped = list.iter().map(Ref);
132            serializer.collect_seq(wrapped)
133        }
134        Ipld::Map(map) => {
135            let wrapped = map.iter().map(|(key, ipld)| (key, Ref(ipld)));
136            serializer.collect_map(wrapped)
137        }
138        Ipld::Link(cid) => serialize(&ipld!({ "/": cid.to_string() }), serializer),
139    }
140}
141
142fn deserialize<'de, D>(deserializer: D) -> Result<Ipld, D::Error>
143where
144    D: Deserializer<'de>,
145{
146    deserializer.deserialize_any(JSONVisitor)
147}
148
149/// JSON visitor for generating IPLD from JSON
150struct JSONVisitor;
151impl<'de> de::Visitor<'de> for JSONVisitor {
152    type Value = Ipld;
153
154    fn expecting(&self, fmt: &mut fmt::Formatter<'_>) -> fmt::Result {
155        fmt.write_str("any valid JSON value")
156    }
157
158    #[inline]
159    fn visit_str<E: de::Error>(self, value: &str) -> Result<Self::Value, E> {
160        self.visit_string(String::from(value))
161    }
162
163    #[inline]
164    fn visit_string<E: de::Error>(self, value: String) -> Result<Self::Value, E> {
165        Ok(Ipld::String(value))
166    }
167    #[inline]
168    fn visit_bytes<E: de::Error>(self, v: &[u8]) -> Result<Self::Value, E> {
169        self.visit_byte_buf(v.to_owned())
170    }
171
172    #[inline]
173    fn visit_byte_buf<E: de::Error>(self, v: Vec<u8>) -> Result<Self::Value, E> {
174        Ok(Ipld::Bytes(v))
175    }
176
177    #[inline]
178    fn visit_u64<E: de::Error>(self, v: u64) -> Result<Self::Value, E> {
179        Ok(Ipld::Integer(v.into()))
180    }
181
182    #[inline]
183    fn visit_i64<E: de::Error>(self, v: i64) -> Result<Self::Value, E> {
184        Ok(Ipld::Integer(v.into()))
185    }
186
187    #[inline]
188    fn visit_i128<E: de::Error>(self, v: i128) -> Result<Self::Value, E> {
189        Ok(Ipld::Integer(v))
190    }
191
192    #[inline]
193    fn visit_bool<E: de::Error>(self, v: bool) -> Result<Self::Value, E> {
194        Ok(Ipld::Bool(v))
195    }
196
197    #[inline]
198    fn visit_none<E: de::Error>(self) -> Result<Self::Value, E> {
199        self.visit_unit()
200    }
201
202    #[inline]
203    fn visit_unit<E: de::Error>(self) -> Result<Self::Value, E> {
204        Ok(Ipld::Null)
205    }
206
207    #[inline]
208    fn visit_seq<V: de::SeqAccess<'de>>(self, mut visitor: V) -> Result<Self::Value, V::Error> {
209        let mut vec = Vec::new();
210
211        while let Some(IpldLotusJson(elem)) = visitor.next_element()? {
212            vec.push(elem);
213        }
214
215        Ok(Ipld::List(vec))
216    }
217
218    #[inline]
219    fn visit_map<V>(self, mut visitor: V) -> Result<Self::Value, V::Error>
220    where
221        V: de::MapAccess<'de>,
222    {
223        let mut map = BTreeMap::new();
224
225        while let Some((key, IpldLotusJson(value))) = visitor.next_entry()? {
226            map.insert(key, value);
227        }
228
229        if map.len() == 1
230            && let Some(v) = map.get("/")
231        {
232            match v {
233                Ipld::String(s) => {
234                    // { "/": ".." } Json block is a Cid
235                    return Ok(Ipld::Link(s.parse().map_err(de::Error::custom)?));
236                }
237                Ipld::Map(obj) => {
238                    if let Some(Ipld::String(s)) = obj.get(BYTES_JSON_KEY) {
239                        // { "/": { "bytes": "<multibase>" } } Json block are bytes encoded
240                        let (_, bz) =
241                            multibase::decode(s).map_err(|e| de::Error::custom(e.to_string()))?;
242                        return Ok(Ipld::Bytes(bz));
243                    }
244                    if let Some(Ipld::String(s)) = obj.get(INT_JSON_KEY) {
245                        // { "/": { "int": "i128" } }
246                        let s = s
247                            .parse::<i128>()
248                            .map_err(|e| de::Error::custom(e.to_string()))?;
249                        return Ok(Ipld::Integer(s));
250                    }
251                    if let Some(Ipld::String(s)) = obj.get(FLOAT_JSON_KEY) {
252                        // { "/": { "float": "f64" } }
253                        let s = s
254                            .parse::<f64>()
255                            .map_err(|e| de::Error::custom(e.to_string()))?;
256                        return Ok(Ipld::Float(s));
257                    }
258                }
259                _ => (),
260            }
261        }
262
263        Ok(Ipld::Map(map))
264    }
265
266    #[inline]
267    fn visit_f64<E: de::Error>(self, v: f64) -> Result<Self::Value, E> {
268        Ok(Ipld::Float(v))
269    }
270}
271
272#[test]
273fn snapshots() {
274    assert_all_snapshots::<Ipld>()
275}
276
277#[cfg(test)]
278quickcheck::quickcheck! {
279    fn quickcheck(val: Ipld) -> () {
280        let mut val = val;
281        /// `NaN != NaN`, which breaks our round-trip tests.
282        /// Correct this by changing any `NaN`s to zero.
283        fn fixup_floats(ipld: &mut Ipld) {
284            match ipld {
285                Ipld::Float(v) => {
286                    if v.is_nan() {
287                        *ipld = Ipld::Float(0.0);
288                    }
289                }
290                Ipld::List(list) => {
291                    for item in list {
292                        fixup_floats(item);
293                    }
294                }
295                Ipld::Map(map) => {
296                    for item in map.values_mut() {
297                        fixup_floats(item);
298                    }
299                }
300                _ => {}
301            }
302        }
303        fixup_floats(&mut val);
304        assert_unchanged_via_json(val)
305    }
306}
307
308/// [`quickcheck`] [found a round-trip bug in CI][failing job], tracked by [#3383][issue]
309///
310/// ```text
311/// thread 'ipld::json::tests::ipld_roundtrip' panicked at '[quickcheck] TEST FAILED (runtime error).
312/// Arguments: ([[{"": {"": [[[{"": [{"": [{"": [{"": [[{"": [[{"": {"/": ""}}]]}]]}]}]}]}]]]}}]])
313/// Error: "called `Result::unwrap()` on an `Err` value: Error(\"Input too short\", line: 1, column: 52)"',
314/// ```
315/// The actual error message is a little ambiguous with regards to the cause
316/// because [`ipld_core`] has a custom debug implementation [unhelpful]
317///
318/// Here's what the minimal test case (or simply another bug) is after trying to understand the above.
319///
320/// [issue]: https://github.com/ChainSafe/forest/issues/3383
321/// [failing job]: https://github.com/ChainSafe/forest/actions/runs/5877726416/job/15938386821?pr=3382#step:9:1835
322/// [unhelpful]: https://github.com/ipld/libipld/blob/8478d6d66576636b9970cb3b00a232be7a88ea42/core/src/ipld.rs#L53-L63
323#[test]
324#[should_panic = "Input too short"]
325fn issue_3383() {
326    let poison = Ipld::Map(BTreeMap::from_iter([(
327        String::from("/"),
328        Ipld::String(String::from("")),
329    )]));
330    let serialized = serde_json::to_value(Ref(&poison)).unwrap();
331
332    // we try and parse the map as a CID, even though it's meant to be a map...
333    let IpldLotusJson(round_tripped) = serde_json::from_value(serialized).unwrap();
334
335    pretty_assertions::assert_eq!(round_tripped, poison); // we never make it here
336}