Skip to main content

archiver/
json.rs

1//! Tolerant JSON: the archiver writes NaN and infinities as bare tokens.
2
3use std::borrow::Cow;
4
5/// Replace the bare tokens `NaN`, `Infinity` and `-Infinity` outside strings
6/// with `null`, so `serde_json` accepts the body. Borrowed when there are none.
7pub fn sanitize_json(body: &str) -> Cow<'_, str> {
8    if !body.contains("NaN") && !body.contains("Infinity") {
9        return Cow::Borrowed(body);
10    }
11    let b = body.as_bytes();
12    let mut out = String::with_capacity(body.len());
13    // `out` takes whole slices of `body` between replacements, so it stays
14    // valid UTF-8 (every split point is an ASCII byte).
15    let mut copied = 0;
16    let mut in_string = false;
17    let mut i = 0;
18    while i < b.len() {
19        let c = b[i];
20        if in_string {
21            match c {
22                b'\\' => i += 1,
23                b'"' => in_string = false,
24                _ => {}
25            }
26            i += 1;
27            continue;
28        }
29        let token = match c {
30            b'"' => {
31                in_string = true;
32                None
33            }
34            b'N' if b[i..].starts_with(b"NaN") => Some(3),
35            b'I' if b[i..].starts_with(b"Infinity") => Some(8),
36            b'-' if b[i..].starts_with(b"-Infinity") => Some(9),
37            b'+' if b[i..].starts_with(b"+Infinity") => Some(9),
38            _ => None,
39        };
40        match token {
41            Some(len) => {
42                out.push_str(&body[copied..i]);
43                out.push_str("null");
44                i += len;
45                copied = i;
46            }
47            None => i += 1,
48        }
49    }
50    if copied == 0 {
51        return Cow::Borrowed(body);
52    }
53    out.push_str(&body[copied..]);
54    Cow::Owned(out)
55}
56
57#[cfg(test)]
58mod tests {
59    use super::*;
60
61    #[test]
62    fn replaces_bare_tokens_only() {
63        let body = r#"[{"val": NaN, "a": [Infinity,-Infinity, 1.0E-5], "s": "NaN \"Infinity\" -Infinity"}]"#;
64        let s = sanitize_json(body);
65        assert_eq!(
66            s,
67            r#"[{"val": null, "a": [null,null, 1.0E-5], "s": "NaN \"Infinity\" -Infinity"}]"#
68        );
69        assert!(serde_json::from_str::<serde_json::Value>(&s).is_ok());
70    }
71
72    #[test]
73    fn borrows_when_clean() {
74        assert!(matches!(sanitize_json("[1, 2]"), Cow::Borrowed(_)));
75        // Mentions only inside strings: unchanged and still borrowed.
76        assert!(matches!(
77            sanitize_json(r#"["NaN", "Infinity\\"]"#),
78            Cow::Borrowed(_)
79        ));
80    }
81
82    #[test]
83    fn keeps_unicode() {
84        let s = sanitize_json("[\"\u{2013} é\", NaN]");
85        assert_eq!(s, "[\"\u{2013} é\", null]");
86    }
87}