casper_types/
json_pretty_printer.rs

1extern crate alloc;
2
3use alloc::{format, string::String, vec::Vec};
4
5use serde::Serialize;
6use serde_json::{json, Value};
7
8const MAX_STRING_LEN: usize = 150;
9
10/// Represents the information about a substring found in a string.
11#[derive(Debug)]
12struct SubstringSpec {
13    /// Index of the first character.
14    start_index: usize,
15    /// Length of the substring.
16    length: usize,
17}
18
19impl SubstringSpec {
20    /// Constructs a new StringSpec with the given start index and length.
21    fn new(start_index: usize, length: usize) -> Self {
22        Self {
23            start_index,
24            length,
25        }
26    }
27}
28
29/// Serializes the given data structure as a pretty-printed `String` of JSON using
30/// `serde_json::to_string_pretty()`, but after first reducing any large hex-string values.
31///
32/// A large hex-string is one containing only hex characters and which is over `MAX_STRING_LEN`.
33/// Such hex-strings will be replaced by an indication of the number of chars redacted, for example
34/// `[130 hex chars]`.
35pub fn json_pretty_print<T>(value: &T) -> serde_json::Result<String>
36where
37    T: ?Sized + Serialize,
38{
39    let mut json_value = json!(value);
40    shorten_string_field(&mut json_value);
41
42    serde_json::to_string_pretty(&json_value)
43}
44
45/// Searches the given string for all occurrences of hex substrings
46/// that are longer than the specified `max_len`.
47fn find_hex_strings_longer_than(string: &str, max_len: usize) -> Vec<SubstringSpec> {
48    let mut ranges_to_remove = Vec::new();
49    let mut start_index = 0;
50    let mut contiguous_hex_count = 0;
51
52    // Record all large hex-strings' start positions and lengths.
53    for (index, char) in string.char_indices() {
54        if char.is_ascii_hexdigit() {
55            if contiguous_hex_count == 0 {
56                // This is the start of a new hex-string.
57                start_index = index;
58            }
59            contiguous_hex_count += 1;
60        } else if contiguous_hex_count != 0 {
61            // This is the end of a hex-string: if it's too long, record it.
62            if contiguous_hex_count > max_len {
63                ranges_to_remove.push(SubstringSpec::new(start_index, contiguous_hex_count));
64            }
65            contiguous_hex_count = 0;
66        }
67    }
68    // If the string contains a large hex-string at the end, record it now.
69    if contiguous_hex_count > max_len {
70        ranges_to_remove.push(SubstringSpec::new(start_index, contiguous_hex_count));
71    }
72    ranges_to_remove
73}
74
75fn shorten_string_field(value: &mut Value) {
76    match value {
77        Value::String(string) => {
78            // Iterate over the ranges to remove from last to first so each
79            // replacement start index remains valid.
80            find_hex_strings_longer_than(string, MAX_STRING_LEN)
81                .into_iter()
82                .rev()
83                .for_each(
84                    |SubstringSpec {
85                         start_index,
86                         length,
87                     }| {
88                        let range = start_index..(start_index + length);
89                        string.replace_range(range, &format!("[{} hex chars]", length));
90                    },
91                )
92        }
93        Value::Array(values) => {
94            for value in values {
95                shorten_string_field(value);
96            }
97        }
98        Value::Object(map) => {
99            for map_value in map.values_mut() {
100                shorten_string_field(map_value);
101            }
102        }
103        Value::Null | Value::Bool(_) | Value::Number(_) => {}
104    }
105}
106
107#[cfg(test)]
108mod tests {
109    use super::*;
110
111    fn hex_string(length: usize) -> String {
112        "0123456789abcdef".chars().cycle().take(length).collect()
113    }
114
115    impl PartialEq<(usize, usize)> for SubstringSpec {
116        fn eq(&self, other: &(usize, usize)) -> bool {
117            self.start_index == other.0 && self.length == other.1
118        }
119    }
120
121    #[test]
122    fn finds_hex_strings_longer_than() {
123        const TESTING_LEN: usize = 3;
124
125        let input = "01234";
126        let expected = vec![(0, 5)];
127        let actual = find_hex_strings_longer_than(input, TESTING_LEN);
128        assert_eq!(actual, expected);
129
130        let input = "01234-0123";
131        let expected = vec![(0, 5), (6, 4)];
132        let actual = find_hex_strings_longer_than(input, TESTING_LEN);
133        assert_eq!(actual, expected);
134
135        let input = "012-34-0123";
136        let expected = vec![(7, 4)];
137        let actual = find_hex_strings_longer_than(input, TESTING_LEN);
138        assert_eq!(actual, expected);
139
140        let input = "012-34-01-23";
141        let expected: Vec<(usize, usize)> = vec![];
142        let actual = find_hex_strings_longer_than(input, TESTING_LEN);
143        assert_eq!(actual, expected);
144
145        let input = "0";
146        let expected: Vec<(usize, usize)> = vec![];
147        let actual = find_hex_strings_longer_than(input, TESTING_LEN);
148        assert_eq!(actual, expected);
149
150        let input = "";
151        let expected: Vec<(usize, usize)> = vec![];
152        let actual = find_hex_strings_longer_than(input, TESTING_LEN);
153        assert_eq!(actual, expected);
154    }
155
156    #[test]
157    fn respects_length() {
158        let input = "I like beef";
159        let expected = vec![(7, 4)];
160        let actual = find_hex_strings_longer_than(input, 3);
161        assert_eq!(actual, expected);
162
163        let input = "I like beef";
164        let expected: Vec<(usize, usize)> = vec![];
165        let actual = find_hex_strings_longer_than(input, 1000);
166        assert_eq!(actual, expected);
167    }
168
169    #[test]
170    fn should_shorten_long_strings() {
171        let max_unshortened_hex_string = hex_string(MAX_STRING_LEN);
172        let long_hex_string = hex_string(MAX_STRING_LEN + 1);
173        let long_non_hex_string: String = "g".repeat(MAX_STRING_LEN + 1);
174        let long_hex_substring = format!("a-{}-b", hex_string(MAX_STRING_LEN + 1));
175        let multiple_long_hex_substrings =
176            format!("a: {0}, b: {0}, c: {0}", hex_string(MAX_STRING_LEN + 1));
177
178        let mut long_strings: Vec<String> = vec![];
179        for i in 1..=5 {
180            long_strings.push("a".repeat(MAX_STRING_LEN + i));
181        }
182        let value = json!({
183            "field_1": Option::<usize>::None,
184            "field_2": true,
185            "field_3": 123,
186            "field_4": max_unshortened_hex_string,
187            "field_5": ["short string value", long_hex_string],
188            "field_6": {
189                "f1": Option::<usize>::None,
190                "f2": false,
191                "f3": -123,
192                "f4": long_non_hex_string,
193                "f5": ["short string value", long_hex_substring],
194                "f6": {
195                    "final long string": multiple_long_hex_substrings
196                }
197            }
198        });
199
200        let expected = r#"{
201  "field_1": null,
202  "field_2": true,
203  "field_3": 123,
204  "field_4": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef012345",
205  "field_5": [
206    "short string value",
207    "[151 hex chars]"
208  ],
209  "field_6": {
210    "f1": null,
211    "f2": false,
212    "f3": -123,
213    "f4": "ggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggggg",
214    "f5": [
215      "short string value",
216      "a-[151 hex chars]-b"
217    ],
218    "f6": {
219      "final long string": "a: [151 hex chars], b: [151 hex chars], c: [151 hex chars]"
220    }
221  }
222}"#;
223
224        let output = json_pretty_print(&value).unwrap();
225        assert_eq!(
226            output, expected,
227            "Actual:\n{}\nExpected:\n{}\n",
228            output, expected
229        );
230    }
231
232    #[test]
233    fn should_not_modify_short_strings() {
234        let max_string: String = "a".repeat(MAX_STRING_LEN);
235        let value = json!({
236            "field_1": Option::<usize>::None,
237            "field_2": true,
238            "field_3": 123,
239            "field_4": max_string,
240            "field_5": [
241                "short string value",
242                "another short string"
243            ],
244            "field_6": {
245                "f1": Option::<usize>::None,
246                "f2": false,
247                "f3": -123,
248                "f4": "short",
249                "f5": [
250                    "short string value",
251                    "another short string"
252                ],
253                "f6": {
254                    "final string": "the last short string"
255                }
256            }
257        });
258
259        let expected = serde_json::to_string_pretty(&value).unwrap();
260        let output = json_pretty_print(&value).unwrap();
261        assert_eq!(
262            output, expected,
263            "Actual:\n{}\nExpected:\n{}\n",
264            output, expected
265        );
266    }
267
268    #[test]
269    /// Ref: https://github.com/casper-network/casper-node/issues/1456
270    fn regression_1456() {
271        let long_string = r#"state query failed: ValueNotFound("Failed to find base key at path: Key::Account(72698d4dc715a28347b15920b09b4f0f1d633be5a33f4686d06992415b0825e2)")"#;
272        assert_eq!(long_string.len(), 148);
273
274        let value = json!({
275            "code": -32003,
276            "message": long_string,
277        });
278
279        let expected = r#"{
280  "code": -32003,
281  "message": "state query failed: ValueNotFound(\"Failed to find base key at path: Key::Account(72698d4dc715a28347b15920b09b4f0f1d633be5a33f4686d06992415b0825e2)\")"
282}"#;
283
284        let output = json_pretty_print(&value).unwrap();
285        assert_eq!(
286            output, expected,
287            "Actual:\n{}\nExpected:\n{}\n",
288            output, expected
289        );
290    }
291}