casper_types/
json_pretty_printer.rs

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