use rustc_hash::{FxHashMap, FxHashSet};
use oxc::allocator::Allocator;
use oxc::ast::ast::*;
use oxc::ast_visit::{Visit, walk};
use oxc::semantic::{Scoping, SymbolId};
use oxc::span::SPAN;
use oxc_traverse::{Traverse, TraverseCtx, traverse_mut};
use crate::ast::{codegen, create, query};
use crate::engine::error::Result;
use crate::engine::module::{Module, TransformResult};
use crate::eval::node::NodeProcess;
use crate::scope::resolve;
#[derive(Default)]
pub struct StringArrayDecoder {
node: Option<NodeProcess>,
}
impl Module for StringArrayDecoder {
fn name(&self) -> &'static str { "StringArrayDecoder" }
fn changes_symbols(&self) -> bool { true }
fn transform<'a>(
&mut self,
allocator: &'a Allocator,
program: &mut Program<'a>,
scoping: Scoping,
) -> Result<TransformResult> {
let mut detector = ShufflerDetector::default();
let scoping = traverse_mut(&mut detector, allocator, program, scoping, ());
if detector.systems.is_empty() {
return Ok(TransformResult { modifications: 0, scoping });
}
let mut total_mods = 0;
let mut scoping = scoping;
let mut component_symbols: Vec<SymbolId> = Vec::new();
for sys in &detector.systems {
component_symbols.push(sys.array_symbol_id);
component_symbols.push(sys.accessor_symbol_id);
}
let mut component_collector = ComponentCollector::new(component_symbols);
scoping = traverse_mut(&mut component_collector, allocator, program, scoping, ());
for sys in &detector.systems {
let mut call_collector = CallCollector::new(sys.accessor_symbol_id);
scoping = traverse_mut(&mut call_collector, allocator, program, scoping, ());
let calls: Vec<String> = call_collector.calls.into_iter().collect();
if calls.is_empty() { continue; }
let array_code = component_collector.code_map.get(&sys.array_symbol_id)
.map(|s| s.as_str()).unwrap_or("");
let accessor_code = component_collector.code_map.get(&sys.accessor_symbol_id)
.map(|s| s.as_str()).unwrap_or("");
let decoded = match self.execute(array_code, accessor_code, &sys.shuffler_code, &calls) {
Some(d) => d,
None => {
tracing::warn!("string array decode failed for system");
continue;
}
};
let mut inliner = StringInliner::new(sys.accessor_symbol_id, decoded);
scoping = traverse_mut(&mut inliner, allocator, program, scoping, ());
total_mods += inliner.modifications;
}
Ok(TransformResult { modifications: total_mods, scoping })
}
}
impl StringArrayDecoder {
fn ensure_node(&mut self) -> Option<&mut NodeProcess> {
if self.node.is_none() {
self.node = NodeProcess::spawn().ok();
}
self.node.as_mut()
}
fn execute(
&mut self,
array_code: &str,
accessor_code: &str,
shuffler_code: &str,
calls: &[String],
) -> Option<FxHashMap<String, String>> {
if calls.is_empty() { return Some(FxHashMap::default()); }
let node = self.ensure_node()?;
let setup = format!("{array_code}\n{accessor_code}\n{shuffler_code}");
node.eval(&setup)?;
let mut script = String::from("(function() { var r = {};\n");
for (i, call) in calls.iter().enumerate() {
script.push_str(&format!("try {{ r[{i}] = {call}; }} catch(e) {{ r[{i}] = null; }}\n"));
}
script.push_str("return JSON.stringify(r); })()");
let result = node.eval(&script)?;
let result_str = result.as_str()?;
let obj: serde_json::Value = serde_json::from_str(result_str).ok()?;
let mut decoded = FxHashMap::default();
for (i, call) in calls.iter().enumerate() {
if let Some(s) = obj.get(i.to_string()).and_then(|v| v.as_str()) {
decoded.insert(call.clone(), s.to_string());
}
}
Some(decoded)
}
}
struct DetectedSystem {
array_symbol_id: SymbolId,
accessor_symbol_id: SymbolId,
shuffler_code: String,
}
#[derive(Default)]
struct ShufflerDetector {
systems: Vec<DetectedSystem>,
}
impl<'a> Traverse<'a, ()> for ShufflerDetector {
fn exit_statement(&mut self, stmt: &mut Statement<'a>, ctx: &mut TraverseCtx<'a, ()>) {
let Statement::ExpressionStatement(expr_stmt) = stmt else { return; };
if let Some(sys) = detect_shuffler_in_expr(&expr_stmt.expression, ctx.scoping()) {
self.systems.push(sys);
*stmt = ctx.ast.statement_empty(SPAN);
}
}
}
fn detect_shuffler_in_expr(expr: &Expression, scoping: &Scoping) -> Option<DetectedSystem> {
let call = match expr {
Expression::CallExpression(c) if is_iife(c) => c,
Expression::UnaryExpression(u) => {
if let Expression::CallExpression(c) = &u.argument {
if is_iife(c) { c } else { return None; }
} else { return None; }
}
Expression::SequenceExpression(seq) => {
for sub in &seq.expressions {
if let Some(sys) = detect_shuffler_in_expr(sub, scoping) {
return Some(sys);
}
}
return None;
}
Expression::ParenthesizedExpression(p) => {
return detect_shuffler_in_expr(&p.expression, scoping);
}
_ => return None,
};
let first_expr = call.arguments.first()?.as_expression()?;
let Expression::Identifier(array_id) = first_expr else { return None; };
let array_symbol_id = resolve::get_reference_symbol(scoping, array_id)?;
let callee = unwrap_parens(&call.callee);
let Expression::FunctionExpression(func) = callee else { return None; };
let body = func.body.as_ref()?;
let mut validator = ShufflerValidator::new(scoping, array_symbol_id);
for param in &func.params.items {
if let Some(b) = param.pattern.get_binding_identifier() {
if let Some(sym) = b.symbol_id.get() { validator.local_symbols.insert(sym); }
}
}
for stmt in &body.statements {
if let Statement::FunctionDeclaration(f) = stmt {
if let Some(id) = &f.id {
if let Some(sym) = id.symbol_id.get() { validator.local_symbols.insert(sym); }
}
}
if let Statement::VariableDeclaration(vd) = stmt {
for decl in &vd.declarations {
if let Some(b) = decl.id.get_binding_identifier() {
if let Some(sym) = b.symbol_id.get() { validator.local_symbols.insert(sym); }
}
}
}
}
validator.visit_function_body(body);
if validator.push_shift_count < 1 || validator.parse_int_count < 2 {
return None;
}
let accessor_symbol_id = validator.external_calls.iter()
.find(|s| **s != array_symbol_id)
.copied()?;
let shuffler_code = codegen::expr_to_code(expr);
Some(DetectedSystem { array_symbol_id, accessor_symbol_id, shuffler_code })
}
fn is_iife(call: &CallExpression) -> bool {
matches!(unwrap_parens(&call.callee), Expression::FunctionExpression(_))
}
fn unwrap_parens<'a>(expr: &'a Expression<'a>) -> &'a Expression<'a> {
match expr {
Expression::ParenthesizedExpression(p) => unwrap_parens(&p.expression),
e => e,
}
}
struct ShufflerValidator<'s> {
scoping: &'s Scoping,
push_shift_count: usize,
parse_int_count: usize,
external_calls: FxHashSet<SymbolId>,
local_symbols: FxHashSet<SymbolId>,
array_symbol_id: SymbolId,
}
impl<'s> ShufflerValidator<'s> {
fn new(scoping: &'s Scoping, array_symbol_id: SymbolId) -> Self {
Self {
scoping, push_shift_count: 0, parse_int_count: 0,
external_calls: FxHashSet::default(),
local_symbols: FxHashSet::default(),
array_symbol_id,
}
}
}
impl<'a> Visit<'a> for ShufflerValidator<'_> {
fn visit_call_expression(&mut self, call: &CallExpression<'a>) {
let is_push = match &call.callee {
Expression::StaticMemberExpression(m) => m.property.name == "push",
_ => false,
};
if is_push {
if let Some(Expression::CallExpression(inner_call)) = call.arguments.first().and_then(|a| a.as_expression()) {
if matches!(&inner_call.callee, Expression::StaticMemberExpression(m) if m.property.name == "shift") {
self.push_shift_count += 1;
}
}
}
if let Expression::Identifier(id) = &call.callee {
if id.name == "parseInt" { self.parse_int_count += 1; }
}
if let Expression::Identifier(id) = &call.callee {
if let Some(sym) = resolve::get_reference_symbol(self.scoping, id) {
if sym != self.array_symbol_id && !self.local_symbols.contains(&sym) {
self.external_calls.insert(sym);
}
}
}
walk::walk_call_expression(self, call);
}
}
struct ComponentCollector {
targets: Vec<SymbolId>,
code_map: FxHashMap<SymbolId, String>,
}
impl ComponentCollector {
fn new(targets: Vec<SymbolId>) -> Self {
Self { targets, code_map: FxHashMap::default() }
}
}
impl<'a> Traverse<'a, ()> for ComponentCollector {
fn enter_statement(&mut self, stmt: &mut Statement<'a>, _ctx: &mut TraverseCtx<'a, ()>) {
if let Statement::FunctionDeclaration(func) = stmt {
if let Some(id) = &func.id {
if let Some(sym) = id.symbol_id.get() {
if self.targets.contains(&sym) {
self.code_map.insert(sym, codegen::stmt_to_code(stmt));
}
}
}
}
}
}
struct CallCollector {
accessor_symbol_id: SymbolId,
calls: FxHashSet<String>,
}
impl CallCollector {
fn new(accessor_symbol_id: SymbolId) -> Self {
Self { accessor_symbol_id, calls: FxHashSet::default() }
}
}
impl<'a> Traverse<'a, ()> for CallCollector {
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a, ()>) {
let Expression::CallExpression(call) = &*expr else { return; };
let Expression::Identifier(id) = &call.callee else { return; };
let Some(sym) = resolve::get_reference_symbol(ctx.scoping(), id) else { return; };
if sym != self.accessor_symbol_id { return; }
if !call.arguments.iter().all(|a| a.as_expression().is_some_and(query::is_literal)) {
return;
}
self.calls.insert(codegen::expr_to_code(expr));
}
}
struct StringInliner {
accessor_symbol_id: SymbolId,
decoded: FxHashMap<String, String>,
modifications: usize,
}
impl StringInliner {
fn new(accessor_symbol_id: SymbolId, decoded: FxHashMap<String, String>) -> Self {
Self { accessor_symbol_id, decoded, modifications: 0 }
}
}
impl<'a> Traverse<'a, ()> for StringInliner {
fn exit_expression(&mut self, expr: &mut Expression<'a>, ctx: &mut TraverseCtx<'a, ()>) {
let Expression::CallExpression(call) = &*expr else { return; };
let Expression::Identifier(id) = &call.callee else { return; };
let Some(sym) = resolve::get_reference_symbol(ctx.scoping(), id) else { return; };
if sym != self.accessor_symbol_id { return; }
let code = codegen::expr_to_code(expr);
if let Some(decoded_str) = self.decoded.get(&code) {
*expr = create::make_string(decoded_str, &ctx.ast);
self.modifications += 1;
}
}
}