// JS lexer helpers. Mirrors NPE's utils/jsextractor/JavaScriptExtractor.java // + Lexer.java + EcmaScriptTokenStream.java. // // NPE's lexer is vendored from Rhino 1.7.14 to handle regex-vs-division // disambiguation. The `ress` crate is the direct Rust analog — same shape, // pure-rust scanner. We delegate to it. // // Public surface mirrors NPE's static `matchToClosingBrace(src, start)`: // given a substring `start` (e.g. `"xyz=function"`), find its first // occurrence in `src`, then walk forward through tokens balancing braces // (skipping braces inside strings, regex literals, comments) until the // matching `}` of the function body is consumed. Returns the slice from // the first `{` of the body through that closing `}`, inclusive. use ress::tokens::{Punct, Token}; use ress::Scanner; use crate::youtube::js::DeobfError; /// Returns the substring of `src` starting immediately after `start` and /// ending at the matching `}` of the first function body that follows /// (inclusive). Everything between the anchor and the first `{` (e.g. /// `(a)` for `xyz=function`) is included. /// /// Mirrors `JavaScriptExtractor.matchToClosingBrace(src, start)` from NPE. pub fn match_to_closing_brace(src: &str, start: &str) -> Result { let prefix_idx = src .find(start) .ok_or_else(|| DeobfError::SigBodyParseFailed(format!("anchor not found: {start}")))?; let scan_from = prefix_idx + start.len(); let scanner = Scanner::new(&src[scan_from..]); let mut depth = 0i32; let mut saw_open = false; let mut last_brace: Option = None; for item in scanner { let item = item.map_err(|e| { DeobfError::SigBodyParseFailed(format!("lexer error at byte {}: {e}", scan_from)) })?; match item.token { Token::Punct(Punct::OpenBrace) => { saw_open = true; depth += 1; } Token::Punct(Punct::CloseBrace) => { depth -= 1; if depth == 0 && saw_open { last_brace = Some(item.span.end); break; } } _ => {} } } let last = last_brace.ok_or_else(|| { DeobfError::SigBodyParseFailed("no matching closing brace found".into()) })?; Ok(src[scan_from..scan_from + last].to_string()) } #[cfg(test)] mod tests { use super::*; #[test] fn balances_simple_function() { let src = "var a=1;xyz=function(b){return b;};var c=2;"; let body = match_to_closing_brace(src, "xyz=function").unwrap(); assert_eq!(body, "(b){return b;}"); } #[test] fn skips_braces_inside_strings() { let src = r#"xyz=function(a){var x="}}";return a+x;}"#; let body = match_to_closing_brace(src, "xyz=function").unwrap(); assert_eq!(body, r#"(a){var x="}}";return a+x;}"#); } #[test] fn skips_braces_inside_regex() { let src = r#"xyz=function(a){var re=/}{/;return a.replace(re,"");}"#; let body = match_to_closing_brace(src, "xyz=function").unwrap(); assert!(body.starts_with("(a){")); assert!(body.ends_with("}")); } #[test] fn handles_nested_blocks() { let src = r#" xyz=function(a){ if (a.length > 3) { a = a.split(""); for (var i=0; i