Skip to main content

js_deobfuscator/value/
json.rs

1//! JSON.parse / JSON.stringify on JsValue.
2
3use super::JsValue;
4
5/// `JSON.stringify(value)` for primitive values.
6pub fn stringify(val: &JsValue) -> Option<JsValue> {
7    let s = match val {
8        JsValue::Number(n) => {
9            if n.is_nan() || n.is_infinite() { "null".to_string() }
10            else { super::coerce::number_to_string(*n) }
11        }
12        JsValue::String(s) => format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")),
13        JsValue::Boolean(b) => b.to_string(),
14        JsValue::Null => "null".to_string(),
15        JsValue::Undefined => return Some(JsValue::Undefined), // undefined → undefined (omitted)
16    };
17    Some(JsValue::String(s))
18}
19
20/// `JSON.parse(string)` for simple values.
21pub fn parse(s: &str) -> Option<JsValue> {
22    let trimmed = s.trim();
23    match trimmed {
24        "null" => Some(JsValue::Null),
25        "true" => Some(JsValue::Boolean(true)),
26        "false" => Some(JsValue::Boolean(false)),
27        _ if trimmed.starts_with('"') && trimmed.ends_with('"') => {
28            let inner = &trimmed[1..trimmed.len()-1];
29            Some(JsValue::String(json_unescape(inner)))
30        }
31        _ => {
32            trimmed.parse::<f64>().ok().map(JsValue::Number)
33        }
34    }
35}
36
37/// Process JSON escape sequences in correct order.
38fn json_unescape(s: &str) -> String {
39    let mut result = String::with_capacity(s.len());
40    let mut chars = s.chars();
41    while let Some(c) = chars.next() {
42        if c == '\\' {
43            match chars.next() {
44                Some('"') => result.push('"'),
45                Some('\\') => result.push('\\'),
46                Some('/') => result.push('/'),
47                Some('n') => result.push('\n'),
48                Some('r') => result.push('\r'),
49                Some('t') => result.push('\t'),
50                Some('b') => result.push('\u{0008}'),
51                Some('f') => result.push('\u{000C}'),
52                Some('u') => {
53                    let hex: String = chars.by_ref().take(4).collect();
54                    if let Ok(code) = u32::from_str_radix(&hex, 16) {
55                        if let Some(ch) = char::from_u32(code) {
56                            result.push(ch);
57                        }
58                    }
59                }
60                Some(other) => { result.push('\\'); result.push(other); }
61                None => result.push('\\'),
62            }
63        } else {
64            result.push(c);
65        }
66    }
67    result
68}
69
70#[cfg(test)]
71mod tests {
72    use super::*;
73    fn n(v: f64) -> JsValue { JsValue::Number(v) }
74    fn s(v: &str) -> JsValue { JsValue::String(v.into()) }
75
76    #[test]
77    fn test_stringify() {
78        assert_eq!(stringify(&n(42.0)), Some(s("42")));
79        assert_eq!(stringify(&s("hello")), Some(s("\"hello\"")));
80        assert_eq!(stringify(&JsValue::Boolean(true)), Some(s("true")));
81        assert_eq!(stringify(&JsValue::Null), Some(s("null")));
82        assert_eq!(stringify(&n(f64::NAN)), Some(s("null")));
83    }
84
85    #[test]
86    fn test_parse() {
87        assert_eq!(parse("42"), Some(n(42.0)));
88        assert_eq!(parse("\"hello\""), Some(s("hello")));
89        assert_eq!(parse("true"), Some(JsValue::Boolean(true)));
90        assert_eq!(parse("null"), Some(JsValue::Null));
91    }
92
93    #[test]
94    fn test_parse_escape_sequences() {
95        assert_eq!(parse("\"hello\\nworld\""), Some(s("hello\nworld")));
96        assert_eq!(parse("\"tab\\there\""), Some(s("tab\there")));
97        assert_eq!(parse("\"back\\\\slash\""), Some(s("back\\slash")));
98        assert_eq!(parse("\"quote\\\"inside\""), Some(s("quote\"inside")));
99    }
100
101    #[test]
102    fn test_parse_unicode_escape() {
103        assert_eq!(parse("\"\\u0041\""), Some(s("A")));
104    }
105}