Skip to main content

js_deobfuscator/value/
array.rs

1//! Array.prototype method evaluation.
2//!
3//! Operates on `Vec<JsValue>` (extracted from AST ArrayExpression by fold/call.rs).
4
5use super::JsValue;
6use super::coerce::{to_number, to_string};
7
8/// Evaluate `[...].method(args)`.
9pub fn call(this: &[JsValue], method: &str, args: &[JsValue]) -> Option<JsValue> {
10    match method {
11        "join" => join(this, args),
12        "indexOf" => index_of(this, args),
13        "lastIndexOf" => last_index_of(this, args),
14        "includes" => includes(this, args),
15        "at" => at(this, args),
16        "toString" => join(this, &[]),
17        "concat" => concat(this, args),
18        "reverse" => reverse(this),
19        "flat" => flat(this),
20        _ => None,
21    }
22}
23
24/// Array property access: `.length`, `[index]`.
25pub fn property(this: &[JsValue], prop: &str) -> Option<JsValue> {
26    if prop == "length" {
27        return Some(JsValue::Number(this.len() as f64));
28    }
29    if let Ok(idx) = prop.parse::<usize>() {
30        return this.get(idx).cloned();
31    }
32    None
33}
34
35fn join(this: &[JsValue], args: &[JsValue]) -> Option<JsValue> {
36    let sep = args.first().map(to_string).unwrap_or_else(|| ",".to_string());
37    let parts: Vec<String> = this.iter().map(|v| match v {
38        JsValue::Null | JsValue::Undefined => String::new(),
39        other => to_string(other),
40    }).collect();
41    Some(JsValue::String(parts.join(&sep)))
42}
43
44fn index_of(this: &[JsValue], args: &[JsValue]) -> Option<JsValue> {
45    let search = args.first()?;
46    let from = args.get(1).map(|v| to_number(v) as i32).unwrap_or(0).max(0) as usize;
47    for (i, item) in this.iter().enumerate().skip(from) {
48        if super::ops::strict_eq_bool(item, search) {
49            return Some(JsValue::Number(i as f64));
50        }
51    }
52    Some(JsValue::Number(-1.0))
53}
54
55fn last_index_of(this: &[JsValue], args: &[JsValue]) -> Option<JsValue> {
56    let search = args.first()?;
57    for (i, item) in this.iter().enumerate().rev() {
58        if super::ops::strict_eq_bool(item, search) {
59            return Some(JsValue::Number(i as f64));
60        }
61    }
62    Some(JsValue::Number(-1.0))
63}
64
65fn includes(this: &[JsValue], args: &[JsValue]) -> Option<JsValue> {
66    let search = args.first()?;
67    let from = args.get(1).map(|v| to_number(v) as i32).unwrap_or(0).max(0) as usize;
68    let found = this.iter().skip(from).any(|item| super::ops::strict_eq_bool(item, search));
69    Some(JsValue::Boolean(found))
70}
71
72fn at(this: &[JsValue], args: &[JsValue]) -> Option<JsValue> {
73    let idx = args.first().map(|v| to_number(v) as i32).unwrap_or(0);
74    let len = this.len() as i32;
75    let resolved = if idx < 0 { len + idx } else { idx };
76    if resolved < 0 || resolved >= len {
77        return Some(JsValue::Undefined);
78    }
79    this.get(resolved as usize).cloned()
80}
81
82fn concat(this: &[JsValue], args: &[JsValue]) -> Option<JsValue> {
83    // Returns None — JsValue has no Array variant. fold/call.rs handles AST creation.
84    let _ = (this, args);
85    None
86}
87
88fn reverse(this: &[JsValue]) -> Option<JsValue> {
89    let _ = this;
90    None // Mutating — fold/call.rs handles AST creation for reversed array.
91}
92
93fn flat(this: &[JsValue]) -> Option<JsValue> {
94    let _ = this;
95    None // Complex — skip for now.
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101    fn n(v: f64) -> JsValue { JsValue::Number(v) }
102    fn s(v: &str) -> JsValue { JsValue::String(v.into()) }
103
104    #[test]
105    fn test_join() {
106        let arr = [s("a"), s("b"), s("c")];
107        assert_eq!(call(&arr, "join", &[s(",")]), Some(s("a,b,c")));
108        assert_eq!(call(&arr, "join", &[s("")]), Some(s("abc")));
109        assert_eq!(call(&arr, "join", &[]), Some(s("a,b,c")));
110    }
111
112    #[test]
113    fn test_index_of() {
114        let arr = [n(1.0), n(2.0), n(3.0), n(2.0)];
115        assert_eq!(call(&arr, "indexOf", &[n(2.0)]), Some(n(1.0)));
116        assert_eq!(call(&arr, "indexOf", &[n(5.0)]), Some(n(-1.0)));
117    }
118
119    #[test]
120    fn test_includes() {
121        let arr = [n(1.0), n(2.0), n(3.0)];
122        assert_eq!(call(&arr, "includes", &[n(2.0)]), Some(JsValue::Boolean(true)));
123        assert_eq!(call(&arr, "includes", &[n(5.0)]), Some(JsValue::Boolean(false)));
124    }
125
126    #[test]
127    fn test_at() {
128        let arr = [s("a"), s("b"), s("c")];
129        assert_eq!(call(&arr, "at", &[n(0.0)]), Some(s("a")));
130        assert_eq!(call(&arr, "at", &[n(-1.0)]), Some(s("c")));
131        assert_eq!(call(&arr, "at", &[n(5.0)]), Some(JsValue::Undefined));
132    }
133
134    #[test]
135    fn test_property_length() {
136        let arr = [n(1.0), n(2.0), n(3.0)];
137        assert_eq!(property(&arr, "length"), Some(n(3.0)));
138    }
139
140    #[test]
141    fn test_bracket_access() {
142        let arr = [s("x"), s("y")];
143        assert_eq!(property(&arr, "0"), Some(s("x")));
144        assert_eq!(property(&arr, "1"), Some(s("y")));
145    }
146
147    #[test]
148    fn test_join_with_null() {
149        let arr = [s("a"), JsValue::Null, s("c")];
150        assert_eq!(call(&arr, "join", &[s(",")]), Some(s("a,,c")));
151    }
152
153    #[test]
154    fn test_to_string() {
155        let arr = [n(1.0), n(2.0), n(3.0)];
156        assert_eq!(call(&arr, "toString", &[]), Some(s("1,2,3")));
157    }
158}