Skip to main content

js_deobfuscator/ast/
extract.rs

1//! Zero-copy value extraction from AST expressions.
2//!
3//! These borrow from the arena — no heap allocation on the hot path.
4//! All handle `ParenthesizedExpression` recursively.
5
6use oxc::ast::ast::{ArrayExpressionElement, Expression, UnaryOperator};
7
8use tracing::trace;
9
10use crate::value::JsValue;
11
12// ============================================================================
13// Zero-copy extractors (borrow from arena)
14// ============================================================================
15
16/// Extract a string from an expression. Zero-copy — borrows from arena.
17#[inline]
18pub fn string<'a>(expr: &'a Expression<'a>) -> Option<&'a str> {
19    match expr {
20        Expression::StringLiteral(lit) => Some(lit.value.as_str()),
21        Expression::ParenthesizedExpression(p) => string(&p.expression),
22        _ => None,
23    }
24}
25
26/// Extract a number from an expression. Handles unary +/-, Infinity, NaN.
27#[inline]
28pub fn number(expr: &Expression) -> Option<f64> {
29    match expr {
30        Expression::NumericLiteral(lit) => Some(lit.value),
31        Expression::Identifier(id) if id.name == "Infinity" => Some(f64::INFINITY),
32        Expression::Identifier(id) if id.name == "NaN" => Some(f64::NAN),
33        Expression::UnaryExpression(u) => match u.operator {
34            UnaryOperator::UnaryNegation => number(&u.argument).map(|v| -v),
35            UnaryOperator::UnaryPlus => number(&u.argument),
36            _ => None,
37        },
38        Expression::ParenthesizedExpression(p) => number(&p.expression),
39        _ => None,
40    }
41}
42
43/// Extract a boolean from an expression.
44#[inline]
45pub fn boolean(expr: &Expression) -> Option<bool> {
46    match expr {
47        Expression::BooleanLiteral(lit) => Some(lit.value),
48        Expression::ParenthesizedExpression(p) => boolean(&p.expression),
49        _ => None,
50    }
51}
52
53/// Check if expression is `null`.
54#[inline]
55pub fn is_null(expr: &Expression) -> bool {
56    match expr {
57        Expression::NullLiteral(_) => true,
58        Expression::ParenthesizedExpression(p) => is_null(&p.expression),
59        _ => false,
60    }
61}
62
63/// Check if expression is `undefined`.
64#[inline]
65pub fn is_undefined(expr: &Expression) -> bool {
66    match expr {
67        Expression::Identifier(id) if id.name == "undefined" => true,
68        Expression::UnaryExpression(u) if u.operator == UnaryOperator::Void => true,
69        Expression::ParenthesizedExpression(p) => is_undefined(&p.expression),
70        _ => false,
71    }
72}
73
74// ============================================================================
75// Owned extractor (heap-allocates for cross-pass storage)
76// ============================================================================
77
78/// Extract an owned `JsValue` from an expression.
79///
80/// Use this when storing values in maps (constant propagation, eval cache).
81/// For single-traversal folding, prefer `string()`, `number()`, `boolean()`.
82pub fn js_value(expr: &Expression) -> Option<JsValue> {
83    match expr {
84        Expression::NumericLiteral(lit) => Some(JsValue::Number(lit.value)),
85        Expression::StringLiteral(lit) => Some(JsValue::String(lit.value.to_string())),
86        Expression::BooleanLiteral(lit) => Some(JsValue::Boolean(lit.value)),
87        Expression::NullLiteral(_) => Some(JsValue::Null),
88        Expression::Identifier(id) if id.name == "undefined" => Some(JsValue::Undefined),
89        Expression::Identifier(id) if id.name == "Infinity" => Some(JsValue::Number(f64::INFINITY)),
90        Expression::Identifier(id) if id.name == "NaN" => Some(JsValue::Number(f64::NAN)),
91        Expression::UnaryExpression(u) => match u.operator {
92            UnaryOperator::UnaryNegation => {
93                if let Some(JsValue::Number(n)) = js_value(&u.argument) {
94                    Some(JsValue::Number(-n))
95                } else {
96                    None
97                }
98            }
99            UnaryOperator::UnaryPlus => {
100                if let Some(JsValue::Number(n)) = js_value(&u.argument) {
101                    Some(JsValue::Number(n))
102                } else {
103                    None
104                }
105            }
106            _ => None,
107        },
108        Expression::ParenthesizedExpression(p) => js_value(&p.expression),
109        _ => None,
110    }
111}
112
113// ============================================================================
114// Array element extraction
115// ============================================================================
116
117/// Extract all elements from an array expression.
118///
119/// Returns `None` if the array contains spread or holes.
120/// Logs a trace message when spread/elision is encountered.
121pub fn array_elements<'a>(expr: &'a Expression<'a>) -> Option<Vec<&'a Expression<'a>>> {
122    match expr {
123        Expression::ArrayExpression(arr) => {
124            let mut elements = Vec::with_capacity(arr.elements.len());
125            for elem in &arr.elements {
126                match elem {
127                    ArrayExpressionElement::SpreadElement(_) => {
128                        trace!("array_elements: skipping array with spread element");
129                        return None;
130                    }
131                    ArrayExpressionElement::Elision(_) => {
132                        trace!("array_elements: skipping array with hole/elision");
133                        return None;
134                    }
135                    _ => elements.push(elem.as_expression()?),
136                }
137            }
138            Some(elements)
139        }
140        Expression::ParenthesizedExpression(p) => array_elements(&p.expression),
141        _ => None,
142    }
143}
144
145// ============================================================================
146// Tests
147// ============================================================================
148
149#[cfg(test)]
150mod tests {
151    use super::*;
152    use oxc::allocator::Allocator;
153    use oxc::parser::Parser;
154    use oxc::span::SourceType;
155
156    fn parse_expr<'a>(alloc: &'a Allocator, src: &'a str) -> Expression<'a> {
157        Parser::new(alloc, src, SourceType::mjs())
158            .parse_expression()
159            .unwrap()
160    }
161
162    #[test]
163    fn test_string() {
164        let a = Allocator::default();
165        assert_eq!(string(&parse_expr(&a, "\"hello\"")), Some("hello"));
166        assert_eq!(string(&parse_expr(&a, "(\"world\")")), Some("world"));
167        assert_eq!(string(&parse_expr(&a, "42")), None);
168    }
169
170    #[test]
171    fn test_number() {
172        let a = Allocator::default();
173        assert_eq!(number(&parse_expr(&a, "42")), Some(42.0));
174        assert_eq!(number(&parse_expr(&a, "-5")), Some(-5.0));
175        assert_eq!(number(&parse_expr(&a, "+3")), Some(3.0));
176        assert_eq!(number(&parse_expr(&a, "(42)")), Some(42.0));
177        assert_eq!(number(&parse_expr(&a, "\"x\"")), None);
178    }
179
180    #[test]
181    fn test_boolean() {
182        let a = Allocator::default();
183        assert_eq!(boolean(&parse_expr(&a, "true")), Some(true));
184        assert_eq!(boolean(&parse_expr(&a, "false")), Some(false));
185        assert_eq!(boolean(&parse_expr(&a, "42")), None);
186    }
187
188    #[test]
189    fn test_null_undefined() {
190        let a = Allocator::default();
191        assert!(is_null(&parse_expr(&a, "null")));
192        assert!(!is_null(&parse_expr(&a, "42")));
193        assert!(is_undefined(&parse_expr(&a, "undefined")));
194        assert!(is_undefined(&parse_expr(&a, "void 0")));
195    }
196
197    #[test]
198    fn test_js_value() {
199        let a = Allocator::default();
200        assert_eq!(js_value(&parse_expr(&a, "42")), Some(JsValue::Number(42.0)));
201        assert_eq!(js_value(&parse_expr(&a, "\"hi\"")), Some(JsValue::String("hi".into())));
202        assert_eq!(js_value(&parse_expr(&a, "true")), Some(JsValue::Boolean(true)));
203        assert_eq!(js_value(&parse_expr(&a, "null")), Some(JsValue::Null));
204        assert_eq!(js_value(&parse_expr(&a, "undefined")), Some(JsValue::Undefined));
205        assert_eq!(js_value(&parse_expr(&a, "-5")), Some(JsValue::Number(-5.0)));
206        assert_eq!(js_value(&parse_expr(&a, "foo")), None);
207    }
208
209    #[test]
210    fn test_array_elements() {
211        let a = Allocator::default();
212        let expr = parse_expr(&a, "[1, 2, 3]");
213        let elems = array_elements(&expr).unwrap();
214        assert_eq!(elems.len(), 3);
215        assert_eq!(number(elems[0]), Some(1.0));
216        assert_eq!(number(elems[2]), Some(3.0));
217    }
218
219    #[test]
220    fn test_array_with_spread_returns_none() {
221        let a = Allocator::default();
222        let expr = parse_expr(&a, "[1, ...x]");
223        assert!(array_elements(&expr).is_none());
224    }
225}