1use std::borrow::Cow;
4
5pub 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 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 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}