Skip to main content

js_deobfuscator/value/
runtime.rs

1//! Browser runtime APIs in pure Rust: atob, btoa, escape, unescape.
2//!
3//! These are NOT ECMAScript spec — they're Web API / browser globals.
4//! Implemented in Rust for fast-path evaluation without Node.js.
5
6use super::JsValue;
7
8/// Evaluate a browser runtime function.
9pub fn call(func: &str, args: &[JsValue]) -> Option<JsValue> {
10    let s = match args.first()? {
11        JsValue::String(s) => s.as_str(),
12        _ => return None,
13    };
14    let result = match func {
15        "atob" => atob(s)?,
16        "btoa" => btoa(s)?,
17        "escape" => escape(s),
18        "unescape" => unescape(s)?,
19        _ => return None,
20    };
21    Some(JsValue::String(result))
22}
23
24/// Base64 decode (browser `atob`). Latin-1 semantics.
25fn atob(input: &str) -> Option<String> {
26    use base64::Engine as _;
27    let bytes = base64::engine::general_purpose::STANDARD.decode(input).ok()?;
28    Some(bytes.iter().map(|&b| b as char).collect())
29}
30
31/// Base64 encode (browser `btoa`). Only accepts Latin-1 characters.
32fn btoa(input: &str) -> Option<String> {
33    if input.chars().any(|c| c as u32 > 0xFF) {
34        return None; // InvalidCharacterError
35    }
36    use base64::Engine as _;
37    let bytes: Vec<u8> = input.chars().map(|c| c as u8).collect();
38    Some(base64::engine::general_purpose::STANDARD.encode(&bytes))
39}
40
41/// Percent-encode (browser `escape`). Encodes non-ASCII and special chars.
42fn escape(input: &str) -> String {
43    let mut result = String::with_capacity(input.len());
44    for c in input.chars() {
45        match c {
46            'A'..='Z' | 'a'..='z' | '0'..='9' | '@' | '*' | '_' | '+' | '-' | '.' | '/' => {
47                result.push(c);
48            }
49            c if (c as u32) <= 0xFF => {
50                result.push_str(&format!("%{:02X}", c as u32));
51            }
52            c => {
53                result.push_str(&format!("%u{:04X}", c as u32));
54            }
55        }
56    }
57    result
58}
59
60/// Percent-decode (browser `unescape`).
61fn unescape(input: &str) -> Option<String> {
62    let mut result = String::with_capacity(input.len());
63    let chars: Vec<char> = input.chars().collect();
64    let mut i = 0;
65    while i < chars.len() {
66        if chars[i] == '%' {
67            if i + 6 <= chars.len() && chars[i + 1] == 'u' {
68                // %uXXXX — try to decode, pass through on failure
69                let hex: String = chars[i+2..i+6].iter().collect();
70                if let Some(code) = u32::from_str_radix(&hex, 16).ok().and_then(char::from_u32) {
71                    result.push(code);
72                    i += 6;
73                    continue;
74                }
75            }
76            if i + 3 <= chars.len() {
77                // %XX — try to decode, pass through on failure
78                let hex: String = chars[i+1..i+3].iter().collect();
79                if let Ok(code) = u8::from_str_radix(&hex, 16) {
80                    result.push(code as char);
81                    i += 3;
82                    continue;
83                }
84            }
85            // Malformed — pass through the %
86            result.push('%');
87            i += 1;
88        } else {
89            result.push(chars[i]);
90            i += 1;
91        }
92    }
93    Some(result)
94}
95
96#[cfg(test)]
97mod tests {
98    use super::*;
99    fn s(v: &str) -> JsValue { JsValue::String(v.into()) }
100
101    #[test]
102    fn test_atob() {
103        assert_eq!(call("atob", &[s("SGVsbG8=")]), Some(s("Hello")));
104        assert_eq!(call("atob", &[s("dGVzdA==")]), Some(s("test")));
105    }
106
107    #[test]
108    fn test_btoa() {
109        assert_eq!(call("btoa", &[s("Hello")]), Some(s("SGVsbG8=")));
110        assert_eq!(call("btoa", &[s("test")]), Some(s("dGVzdA==")));
111    }
112
113    #[test]
114    fn test_roundtrip() {
115        let original = "Hello, World!";
116        let encoded = btoa(original).unwrap();
117        let decoded = atob(&encoded).unwrap();
118        assert_eq!(decoded, original);
119    }
120
121    #[test]
122    fn test_escape() {
123        assert_eq!(call("escape", &[s("hello world")]), Some(s("hello%20world")));
124        assert_eq!(call("escape", &[s("abc")]), Some(s("abc")));
125    }
126
127    #[test]
128    fn test_unescape() {
129        assert_eq!(call("unescape", &[s("hello%20world")]), Some(s("hello world")));
130        assert_eq!(call("unescape", &[s("%u0041")]), Some(s("A")));
131    }
132
133    #[test]
134    fn test_unescape_boundary() {
135        // %uXXXX at end of string — regression for bounds check
136        assert_eq!(call("unescape", &[s("%u0042")]), Some(s("B")));
137        // Truncated %u at end should not panic
138        assert_eq!(call("unescape", &[s("abc%u004")]), Some(s("abc%u004")));
139    }
140}