Skip to main content

js_deobfuscator/value/
string.rs

1//! String.prototype method evaluation.
2//!
3//! Pure Rust implementations of JavaScript string methods.
4//! Input: `&str` + method name + args. Output: `Option<JsValue>`.
5
6use super::JsValue;
7use super::coerce::to_int32;
8
9// ============================================================================
10// Method dispatch
11// ============================================================================
12
13/// Evaluate a string method call: `"abc".method(args)`.
14pub fn call(this: &str, method: &str, args: &[JsValue]) -> Option<JsValue> {
15    match method {
16        "charAt" => char_at(this, args),
17        "charCodeAt" => char_code_at(this, args),
18        "codePointAt" => code_point_at(this, args),
19        "at" => at(this, args),
20        "indexOf" => index_of(this, args),
21        "lastIndexOf" => last_index_of(this, args),
22        "includes" => includes(this, args),
23        "startsWith" => starts_with(this, args),
24        "endsWith" => ends_with(this, args),
25        "slice" => slice(this, args),
26        "substring" => substring(this, args),
27        "substr" => substr(this, args),
28        "toUpperCase" | "toLocaleUpperCase" => Some(JsValue::String(this.to_uppercase())),
29        "toLowerCase" | "toLocaleLowerCase" => Some(JsValue::String(this.to_lowercase())),
30        "trim" => Some(JsValue::String(this.trim().to_string())),
31        "trimStart" | "trimLeft" => Some(JsValue::String(this.trim_start().to_string())),
32        "trimEnd" | "trimRight" => Some(JsValue::String(this.trim_end().to_string())),
33        "repeat" => repeat(this, args),
34        "padStart" => pad_start(this, args),
35        "padEnd" => pad_end(this, args),
36        "replace" => replace(this, args),
37        "concat" => concat(this, args),
38        "toString" | "valueOf" => Some(JsValue::String(this.to_string())),
39        _ => None,
40    }
41}
42
43/// String property access: `.length`, `[index]`.
44pub fn property(this: &str, prop: &str) -> Option<JsValue> {
45    if prop == "length" {
46        let len: usize = this.chars().map(|c| if c as u32 > 0xFFFF { 2 } else { 1 }).sum();
47        return Some(JsValue::Number(len as f64));
48    }
49    if let Ok(idx) = prop.parse::<usize>() {
50        return this.chars().nth(idx).map(|c| JsValue::String(c.to_string()));
51    }
52    None
53}
54
55// ============================================================================
56// Static methods
57// ============================================================================
58
59/// `String.fromCharCode(...codes)`.
60pub fn from_char_code(args: &[JsValue]) -> Option<JsValue> {
61    let mut result = String::new();
62    for arg in args {
63        let code = to_int32(arg) as u16;
64        result.push(char::from_u32(code as u32).unwrap_or('\u{FFFD}'));
65    }
66    Some(JsValue::String(result))
67}
68
69/// `String.fromCodePoint(...codes)`.
70pub fn from_code_point(args: &[JsValue]) -> Option<JsValue> {
71    let mut result = String::new();
72    for arg in args {
73        let code = to_int32(arg);
74        if !(0..=0x10FFFF).contains(&code) { return None; }
75        result.push(char::from_u32(code as u32)?);
76    }
77    Some(JsValue::String(result))
78}
79
80// ============================================================================
81// Instance methods
82// ============================================================================
83
84fn char_at(this: &str, args: &[JsValue]) -> Option<JsValue> {
85    let idx = arg_int(args, 0, 0) as usize;
86    Some(JsValue::String(this.chars().nth(idx).map_or(String::new(), |c| c.to_string())))
87}
88
89fn char_code_at(this: &str, args: &[JsValue]) -> Option<JsValue> {
90    let idx = arg_int(args, 0, 0) as usize;
91    Some(this.chars().nth(idx).map_or(JsValue::Number(f64::NAN), |c| JsValue::Number(c as u32 as f64)))
92}
93
94fn code_point_at(this: &str, args: &[JsValue]) -> Option<JsValue> {
95    let idx = arg_int(args, 0, 0) as usize;
96    Some(this.chars().nth(idx).map_or(JsValue::Undefined, |c| JsValue::Number(c as u32 as f64)))
97}
98
99fn at(this: &str, args: &[JsValue]) -> Option<JsValue> {
100    let idx = arg_int(args, 0, 0);
101    let len = this.chars().count() as i32;
102    let resolved = if idx < 0 { len + idx } else { idx };
103    if resolved < 0 || resolved >= len {
104        return Some(JsValue::Undefined);
105    }
106    this.chars().nth(resolved as usize).map(|c| JsValue::String(c.to_string()))
107}
108
109fn index_of(this: &str, args: &[JsValue]) -> Option<JsValue> {
110    let search = arg_str(args, 0)?;
111    let from = arg_int(args, 1, 0).max(0) as usize;
112    if from > this.len() {
113        return Some(JsValue::Number(-1.0));
114    }
115    Some(JsValue::Number(this[from..].find(&*search).map_or(-1.0, |p| (p + from) as f64)))
116}
117
118fn last_index_of(this: &str, args: &[JsValue]) -> Option<JsValue> {
119    let search = arg_str(args, 0)?;
120    Some(JsValue::Number(this.rfind(&*search).map_or(-1.0, |p| p as f64)))
121}
122
123fn includes(this: &str, args: &[JsValue]) -> Option<JsValue> {
124    let search = arg_str(args, 0)?;
125    let from = arg_int(args, 1, 0).max(0) as usize;
126    if from > this.len() {
127        return Some(JsValue::Boolean(false));
128    }
129    Some(JsValue::Boolean(this[from..].contains(&*search)))
130}
131
132fn starts_with(this: &str, args: &[JsValue]) -> Option<JsValue> {
133    let search = arg_str(args, 0)?;
134    let pos = arg_int(args, 1, 0).max(0) as usize;
135    if pos > this.len() { return Some(JsValue::Boolean(false)); }
136    Some(JsValue::Boolean(this[pos..].starts_with(&*search)))
137}
138
139fn ends_with(this: &str, args: &[JsValue]) -> Option<JsValue> {
140    let search = arg_str(args, 0)?;
141    Some(JsValue::Boolean(this.ends_with(&*search)))
142}
143
144fn slice(this: &str, args: &[JsValue]) -> Option<JsValue> {
145    let len = this.chars().count() as i32;
146    let start = resolve_index(arg_int(args, 0, 0), len);
147    let end = resolve_index(arg_int(args, 1, len), len);
148    if start >= end { return Some(JsValue::String(String::new())); }
149    Some(JsValue::String(this.chars().skip(start as usize).take((end - start) as usize).collect()))
150}
151
152fn substring(this: &str, args: &[JsValue]) -> Option<JsValue> {
153    let len = this.chars().count() as i32;
154    let mut start = arg_int(args, 0, 0).clamp(0, len);
155    let mut end = arg_int(args, 1, len).clamp(0, len);
156    if start > end { std::mem::swap(&mut start, &mut end); }
157    Some(JsValue::String(this.chars().skip(start as usize).take((end - start) as usize).collect()))
158}
159
160fn substr(this: &str, args: &[JsValue]) -> Option<JsValue> {
161    let len = this.chars().count() as i32;
162    let start = resolve_index(arg_int(args, 0, 0), len);
163    let count = arg_int(args, 1, len).max(0);
164    if start >= len || count <= 0 { return Some(JsValue::String(String::new())); }
165    Some(JsValue::String(this.chars().skip(start as usize).take(count as usize).collect()))
166}
167
168fn repeat(this: &str, args: &[JsValue]) -> Option<JsValue> {
169    let count = arg_int(args, 0, 0);
170    if !(0..=10_000).contains(&count) { return None; }
171    Some(JsValue::String(this.repeat(count as usize)))
172}
173
174fn pad_start(this: &str, args: &[JsValue]) -> Option<JsValue> {
175    let target_len = arg_int(args, 0, 0) as usize;
176    let fill = arg_str(args, 1).unwrap_or_else(|| " ".to_string());
177    if this.len() >= target_len || fill.is_empty() { return Some(JsValue::String(this.to_string())); }
178    let padding: String = fill.chars().cycle().take(target_len - this.len()).collect();
179    Some(JsValue::String(format!("{padding}{this}")))
180}
181
182fn pad_end(this: &str, args: &[JsValue]) -> Option<JsValue> {
183    let target_len = arg_int(args, 0, 0) as usize;
184    let fill = arg_str(args, 1).unwrap_or_else(|| " ".to_string());
185    if this.len() >= target_len || fill.is_empty() { return Some(JsValue::String(this.to_string())); }
186    let padding: String = fill.chars().cycle().take(target_len - this.len()).collect();
187    Some(JsValue::String(format!("{this}{padding}")))
188}
189
190fn replace(this: &str, args: &[JsValue]) -> Option<JsValue> {
191    let search = arg_str(args, 0)?;
192    let replacement = arg_str(args, 1)?;
193    Some(JsValue::String(this.replacen(&*search, &replacement, 1)))
194}
195
196fn concat(this: &str, args: &[JsValue]) -> Option<JsValue> {
197    let mut result = this.to_string();
198    for arg in args {
199        result.push_str(&super::coerce::to_string(arg));
200    }
201    Some(JsValue::String(result))
202}
203
204// ============================================================================
205// Helpers
206// ============================================================================
207
208fn arg_int(args: &[JsValue], index: usize, default: i32) -> i32 {
209    args.get(index).map_or(default, super::coerce::to_int32)
210}
211
212fn arg_str(args: &[JsValue], index: usize) -> Option<String> {
213    args.get(index).map(super::coerce::to_string)
214}
215
216fn resolve_index(idx: i32, len: i32) -> i32 {
217    if idx < 0 { (len + idx).max(0) } else { idx.min(len) }
218}
219
220// ============================================================================
221// Tests
222// ============================================================================
223
224#[cfg(test)]
225mod tests {
226    use super::*;
227
228    fn s(v: &str) -> JsValue { JsValue::String(v.into()) }
229    fn n(v: f64) -> JsValue { JsValue::Number(v) }
230
231    #[test]
232    fn test_char_at() {
233        assert_eq!(call("abc", "charAt", &[n(0.0)]), Some(s("a")));
234        assert_eq!(call("abc", "charAt", &[n(5.0)]), Some(s("")));
235    }
236
237    #[test]
238    fn test_char_code_at() {
239        assert_eq!(call("A", "charCodeAt", &[n(0.0)]), Some(n(65.0)));
240    }
241
242    #[test]
243    fn test_index_of() {
244        assert_eq!(call("hello world", "indexOf", &[s("world")]), Some(n(6.0)));
245        assert_eq!(call("hello", "indexOf", &[s("x")]), Some(n(-1.0)));
246    }
247
248    #[test]
249    fn test_includes() {
250        assert_eq!(call("hello", "includes", &[s("ell")]), Some(JsValue::Boolean(true)));
251        assert_eq!(call("hello", "includes", &[s("xyz")]), Some(JsValue::Boolean(false)));
252    }
253
254    #[test]
255    fn test_slice() {
256        assert_eq!(call("hello", "slice", &[n(1.0), n(3.0)]), Some(s("el")));
257        assert_eq!(call("hello", "slice", &[n(-3.0)]), Some(s("llo")));
258    }
259
260    #[test]
261    fn test_case() {
262        assert_eq!(call("Hello", "toUpperCase", &[]), Some(s("HELLO")));
263        assert_eq!(call("Hello", "toLowerCase", &[]), Some(s("hello")));
264    }
265
266    #[test]
267    fn test_trim() {
268        assert_eq!(call("  hi  ", "trim", &[]), Some(s("hi")));
269    }
270
271    #[test]
272    fn test_repeat() {
273        assert_eq!(call("ab", "repeat", &[n(3.0)]), Some(s("ababab")));
274    }
275
276    #[test]
277    fn test_pad() {
278        assert_eq!(call("5", "padStart", &[n(3.0), s("0")]), Some(s("005")));
279        assert_eq!(call("5", "padEnd", &[n(3.0), s("0")]), Some(s("500")));
280    }
281
282    #[test]
283    fn test_replace() {
284        assert_eq!(call("aabbcc", "replace", &[s("bb"), s("XX")]), Some(s("aaXXcc")));
285        assert_eq!(call("abab", "replace", &[s("ab"), s("X")]), Some(s("Xab")));
286    }
287
288    #[test]
289    fn test_from_char_code() {
290        assert_eq!(from_char_code(&[n(72.0), n(101.0), n(108.0), n(108.0), n(111.0)]), Some(s("Hello")));
291    }
292
293    #[test]
294    fn test_at() {
295        assert_eq!(call("abc", "at", &[n(0.0)]), Some(s("a")));
296        assert_eq!(call("abc", "at", &[n(-1.0)]), Some(s("c")));
297    }
298
299    #[test]
300    fn test_property_length() {
301        assert_eq!(property("hello", "length"), Some(n(5.0)));
302    }
303
304    #[test]
305    fn test_bracket_access() {
306        assert_eq!(property("abc", "0"), Some(s("a")));
307        assert_eq!(property("abc", "2"), Some(s("c")));
308    }
309}