Skip to main content

js_deobfuscator/fold/
binary.rs

1//! BinaryExpression folding — all binary operators.
2//!
3//! `1 + 2` → `3`, `"a" + "b"` → `"ab"`, `5 & 3` → `1`, `1 === 1` → `true`.
4
5use oxc::ast::ast::{BinaryOperator, Expression};
6
7use oxc_traverse::TraverseCtx;
8
9use crate::ast::{create, extract};
10use crate::value::{JsValue, ops};
11
12/// Try to fold a BinaryExpression. Returns `Some(1)` if folded.
13pub fn try_fold<'a>(
14    expr: &mut Expression<'a>,
15    ctx: &mut TraverseCtx<'a, ()>,
16) -> Option<usize> {
17    let Expression::BinaryExpression(bin) = &*expr else {
18        return None;
19    };
20
21    let left = extract::js_value(&bin.left)?;
22    let right = extract::js_value(&bin.right)?;
23
24    let result = match bin.operator {
25        // Arithmetic
26        BinaryOperator::Addition => Some(ops::add(&left, &right)),
27        BinaryOperator::Subtraction => Some(ops::sub(&left, &right)),
28        BinaryOperator::Multiplication => Some(ops::mul(&left, &right)),
29        BinaryOperator::Division => {
30            // Don't fold division by zero to Infinity — leave it for the runtime
31            if let JsValue::Number(r) = &right {
32                if *r == 0.0 { return None; }
33            }
34            Some(ops::div(&left, &right))
35        }
36        BinaryOperator::Remainder => {
37            if let JsValue::Number(r) = &right {
38                if *r == 0.0 { return None; }
39            }
40            Some(ops::rem(&left, &right))
41        }
42        BinaryOperator::Exponential => Some(ops::exp(&left, &right)),
43
44        // Comparison
45        BinaryOperator::StrictEquality => Some(ops::strict_eq(&left, &right)),
46        BinaryOperator::StrictInequality => Some(ops::strict_ne(&left, &right)),
47        BinaryOperator::LessThan => ops::lt(&left, &right),
48        BinaryOperator::GreaterThan => ops::gt(&left, &right),
49        BinaryOperator::LessEqualThan => ops::le(&left, &right),
50        BinaryOperator::GreaterEqualThan => ops::ge(&left, &right),
51
52        // Bitwise
53        BinaryOperator::BitwiseAnd => Some(ops::bit_and(&left, &right)),
54        BinaryOperator::BitwiseOR => Some(ops::bit_or(&left, &right)),
55        BinaryOperator::BitwiseXOR => Some(ops::bit_xor(&left, &right)),
56
57        // Shift
58        BinaryOperator::ShiftLeft => Some(ops::shl(&left, &right)),
59        BinaryOperator::ShiftRight => Some(ops::shr(&left, &right)),
60        BinaryOperator::ShiftRightZeroFill => Some(ops::ushr(&left, &right)),
61
62        // Don't fold: ==, !=, in, instanceof (need type coercion or runtime info)
63        _ => None,
64    }?;
65
66    // Don't fold if result is NaN from non-obvious operations
67    // (NaN from arithmetic is expected, but NaN from string→number coercion is confusing)
68    if let JsValue::Number(n) = &result {
69        if n.is_nan() && !matches!(left, JsValue::Number(_)) {
70            return None;
71        }
72    }
73
74    *expr = create::from_js_value(&result, &ctx.ast);
75    Some(1)
76}
77
78#[cfg(test)]
79mod tests {
80    use super::super::test_utils::fold;
81
82    #[test]
83    fn test_arithmetic() {
84        assert!(fold("1 + 2;").contains("3"));
85        assert!(fold("10 - 3;").contains("7"));
86        assert!(fold("3 * 4;").contains("12"));
87        assert!(fold("10 / 2;").contains("5"));
88        assert!(fold("10 % 3;").contains("1"));
89        assert!(fold("2 ** 10;").contains("1024"));
90    }
91
92    #[test]
93    fn test_string_concat() {
94        assert!(fold("\"hello\" + \" world\";").contains("\"hello world\""));
95        assert!(fold("\"x\" + 1;").contains("\"x1\""));
96    }
97
98    #[test]
99    fn test_comparison() {
100        assert!(fold("1 === 1;").contains("true"));
101        assert!(fold("1 === 2;").contains("false"));
102        assert!(fold("1 !== 2;").contains("true"));
103        assert!(fold("1 < 2;").contains("true"));
104        assert!(fold("2 > 1;").contains("true"));
105        assert!(fold("1 <= 1;").contains("true"));
106        assert!(fold("1 >= 1;").contains("true"));
107    }
108
109    #[test]
110    fn test_bitwise() {
111        assert!(fold("255 & 15;").contains("15"));
112        assert!(fold("240 | 15;").contains("255"));
113        assert!(fold("255 ^ 15;").contains("240"));
114    }
115
116    #[test]
117    fn test_shift() {
118        assert!(fold("1 << 8;").contains("256"));
119        assert!(fold("256 >> 4;").contains("16"));
120    }
121
122    #[test]
123    fn test_division_by_zero_not_folded() {
124        let result = fold("1 / 0;");
125        assert!(result.contains("/"), "division by zero should not fold: {result}");
126    }
127}