openzeppelin_relayer/utils/serde/
i64_serializer.rs

1//! Serde helpers for i64 values stored as strings.
2//!
3//! Large i64 values (e.g. Stellar sequence numbers) must be serialized as JSON
4//! strings so they survive a Redis/Lua `cjson` decode -> encode round-trip.
5//! Redis bundles Lua 5.1, which represents every number as an IEEE-754 double,
6//! so a bare integer is re-emitted as a float (e.g. `643918676885760.0`). That
7//! both loses precision above 2^53 and fails to deserialize back into `i64`
8//! ("invalid type: floating point ..., expected i64"). A quoted string is
9//! opaque to `cjson` and round-trips untouched.
10//!
11//! The deserializer accepts three historical on-disk forms for backward
12//! compatibility:
13//! - the string form (`"643918676885760"`) written after this fix,
14//! - the legacy bare integer (`643918676885760`) written before it, and
15//! - the corrupted float (`643918676885760.0`) already sitting in Redis.
16
17use std::fmt;
18
19use serde::{de, Deserialize, Deserializer, Serializer};
20
21use super::safe_float::f64_to_safe_integer;
22
23#[derive(Debug)]
24struct I64Visitor;
25
26impl de::Visitor<'_> for I64Visitor {
27    type Value = i64;
28
29    fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
30        formatter.write_str("a string, integer, or float representing an i64")
31    }
32
33    // String form written after this fix: "643918676885760".
34    fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
35    where
36        E: de::Error,
37    {
38        value.parse::<i64>().map_err(de::Error::custom)
39    }
40
41    // Legacy bare integer form: 643918676885760.
42    fn visit_i64<E>(self, value: i64) -> Result<Self::Value, E>
43    where
44        E: de::Error,
45    {
46        Ok(value)
47    }
48
49    fn visit_u64<E>(self, value: u64) -> Result<Self::Value, E>
50    where
51        E: de::Error,
52    {
53        i64::try_from(value).map_err(de::Error::custom)
54    }
55
56    // Corrupted form left in Redis by a Lua cjson round-trip: 643918676885760.0.
57    // Values beyond 2^53 are rejected (see `safe_float`) because precision was
58    // already lost and the original integer cannot be recovered.
59    fn visit_f64<E>(self, value: f64) -> Result<Self::Value, E>
60    where
61        E: de::Error,
62    {
63        let n = f64_to_safe_integer::<E>(value)?;
64        i64::try_from(n).map_err(de::Error::custom)
65    }
66}
67
68/// Deserialize a non-optional `i64`, accepting string, integer, or float JSON
69/// forms. See the module docs for why all three forms must be tolerated.
70pub fn deserialize_i64<'de, D>(deserializer: D) -> Result<i64, D::Error>
71where
72    D: Deserializer<'de>,
73{
74    deserializer.deserialize_any(I64Visitor)
75}
76
77/// Serialize a non-optional `i64` as a JSON string so it survives a Redis/Lua
78/// `cjson` round-trip.
79pub fn serialize_i64<S>(value: &i64, serializer: S) -> Result<S::Ok, S::Error>
80where
81    S: Serializer,
82{
83    serializer.serialize_str(&value.to_string())
84}
85
86/// Deserialize `Option<i64>`, accepting string, integer, or float JSON forms.
87///
88/// See the module docs for why all three forms must be tolerated.
89pub fn deserialize_optional_i64<'de, D>(deserializer: D) -> Result<Option<i64>, D::Error>
90where
91    D: Deserializer<'de>,
92{
93    #[derive(Deserialize)]
94    struct Helper(#[serde(deserialize_with = "deserialize_i64")] i64);
95
96    let helper = Option::<Helper>::deserialize(deserializer)?;
97    Ok(helper.map(|Helper(value)| value))
98}
99
100/// Serialize `Option<i64>` as a JSON string (or `null`) so it survives a
101/// Redis/Lua `cjson` round-trip.
102pub fn serialize_optional_i64<S>(value: &Option<i64>, serializer: S) -> Result<S::Ok, S::Error>
103where
104    S: Serializer,
105{
106    match value {
107        Some(v) => serializer.serialize_some(&v.to_string()),
108        None => serializer.serialize_none(),
109    }
110}
111
112#[cfg(test)]
113mod tests {
114    use super::*;
115
116    #[derive(serde::Serialize, serde::Deserialize, PartialEq, Debug)]
117    struct Wrapper {
118        #[serde(
119            serialize_with = "serialize_optional_i64",
120            deserialize_with = "deserialize_optional_i64",
121            default
122        )]
123        seq: Option<i64>,
124    }
125
126    #[test]
127    fn serializes_as_string() {
128        let json = serde_json::to_string(&Wrapper {
129            seq: Some(643918676885760),
130        })
131        .unwrap();
132        assert_eq!(json, r#"{"seq":"643918676885760"}"#);
133    }
134
135    #[test]
136    fn serializes_none_as_null() {
137        let json = serde_json::to_string(&Wrapper { seq: None }).unwrap();
138        assert_eq!(json, r#"{"seq":null}"#);
139    }
140
141    #[test]
142    fn deserializes_from_string() {
143        let w: Wrapper = serde_json::from_str(r#"{"seq":"643918676885760"}"#).unwrap();
144        assert_eq!(w.seq, Some(643918676885760));
145    }
146
147    #[test]
148    fn deserializes_from_legacy_integer() {
149        let w: Wrapper = serde_json::from_str(r#"{"seq":643918676885760}"#).unwrap();
150        assert_eq!(w.seq, Some(643918676885760));
151    }
152
153    #[test]
154    fn deserializes_from_corrupted_float() {
155        let w: Wrapper = serde_json::from_str(r#"{"seq":643918676885760.0}"#).unwrap();
156        assert_eq!(w.seq, Some(643918676885760));
157    }
158
159    #[test]
160    fn deserializes_negative() {
161        let w: Wrapper = serde_json::from_str(r#"{"seq":-5}"#).unwrap();
162        assert_eq!(w.seq, Some(-5));
163        let w: Wrapper = serde_json::from_str(r#"{"seq":"-5"}"#).unwrap();
164        assert_eq!(w.seq, Some(-5));
165    }
166
167    #[test]
168    fn deserializes_null_and_missing() {
169        let w: Wrapper = serde_json::from_str(r#"{"seq":null}"#).unwrap();
170        assert_eq!(w.seq, None);
171        let w: Wrapper = serde_json::from_str(r#"{}"#).unwrap();
172        assert_eq!(w.seq, None);
173    }
174
175    #[test]
176    fn rejects_non_integral_float() {
177        assert!(serde_json::from_str::<Wrapper>(r#"{"seq":1.5}"#).is_err());
178    }
179
180    #[test]
181    fn rejects_float_beyond_safe_integer_range() {
182        // 2^53 + 1 cannot be represented exactly; recovering it from a float
183        // would silently return the wrong value, so we reject it instead.
184        assert!(serde_json::from_str::<Wrapper>(r#"{"seq":9007199254740993.0}"#).is_err());
185    }
186
187    #[test]
188    fn deserializes_large_string_beyond_safe_range() {
189        // The going-forward string form has no precision limit.
190        let w: Wrapper = serde_json::from_str(r#"{"seq":"9007199254740993"}"#).unwrap();
191        assert_eq!(w.seq, Some(9007199254740993));
192    }
193}