Skip to main content

js_deobfuscator/eval/
node.rs

1//! Node.js subprocess management.
2//!
3//! Spawns a persistent `node -e` process with an embedded evaluator script.
4//! Communicates via stdin/stdout JSON lines. Caches results.
5
6use std::collections::HashMap;
7use std::io::{BufRead, BufReader, Write};
8use std::process::{Child, ChildStdin, ChildStdout, Command, Stdio};
9
10const WORKER_JS: &str = r#"
11const readline = require('readline');
12const rl = readline.createInterface({ input: process.stdin });
13rl.on('line', (line) => {
14    try {
15        const result = (0, eval)(line);
16        const type = typeof result;
17        if (type === 'number' || type === 'string' || type === 'boolean' || result === null) {
18            process.stdout.write(JSON.stringify({ ok: true, value: result }) + '\n');
19        } else {
20            process.stdout.write(JSON.stringify({ ok: false, error: 'non-primitive' }) + '\n');
21        }
22    } catch (e) {
23        process.stdout.write(JSON.stringify({ ok: false, error: String(e) }) + '\n');
24    }
25});
26"#;
27
28/// A persistent Node.js subprocess for evaluating JavaScript expressions.
29pub struct NodeProcess {
30    child: Option<Child>,
31    stdin: Option<ChildStdin>,
32    stdout: Option<BufReader<ChildStdout>>,
33    cache: HashMap<String, Option<serde_json::Value>>,
34}
35
36impl NodeProcess {
37    /// Spawn a new Node.js subprocess.
38    pub fn spawn() -> std::io::Result<Self> {
39        let mut child = Command::new("node")
40            .arg("-e")
41            .arg(WORKER_JS)
42            .stdin(Stdio::piped())
43            .stdout(Stdio::piped())
44            .stderr(Stdio::null())
45            .spawn()?;
46
47        let stdin = child.stdin.take();
48        let stdout = child.stdout.take().map(BufReader::new);
49
50        Ok(Self {
51            child: Some(child),
52            stdin,
53            stdout,
54            cache: HashMap::new(),
55        })
56    }
57
58    /// Restart the Node.js process if it died.
59    fn ensure_running(&mut self) -> bool {
60        // Check if process is still running
61        if let Some(ref mut child) = self.child {
62            match child.try_wait() {
63                Ok(Some(_)) => {
64                    // Process exited, need to restart
65                    tracing::warn!("Node.js process died, restarting");
66                }
67                Ok(None) => return true, // Still running
68                Err(_) => {}
69            }
70        }
71
72        // Restart
73        match Self::spawn() {
74            Ok(mut new_proc) => {
75                // Kill old process if any
76                if let Some(ref mut child) = self.child {
77                    let _ = child.kill();
78                }
79                // Use std::mem::take to move out of new_proc safely
80                self.child = std::mem::take(&mut new_proc.child);
81                self.stdin = std::mem::take(&mut new_proc.stdin);
82                self.stdout = std::mem::take(&mut new_proc.stdout);
83                // new_proc now has None fields, Drop will be a no-op
84                true
85            }
86            Err(e) => {
87                tracing::error!("Failed to spawn Node.js: {}", e);
88                false
89            }
90        }
91    }
92
93    /// Evaluate a JavaScript expression. Returns the result as JSON.
94    ///
95    /// Cached — same expression returns the same result without re-evaluating.
96    pub fn eval(&mut self, expr: &str) -> Option<serde_json::Value> {
97        // Check cache
98        if let Some(cached) = self.cache.get(expr) {
99            return cached.clone();
100        }
101
102        let result = self.eval_uncached(expr);
103        self.cache.insert(expr.to_string(), result.clone());
104        result
105    }
106
107    fn eval_uncached(&mut self, expr: &str) -> Option<serde_json::Value> {
108        // Ensure process is running
109        if !self.ensure_running() {
110            return None;
111        }
112
113        // Send expression
114        let stdin = self.stdin.as_mut()?;
115        writeln!(stdin, "{expr}").ok()?;
116        stdin.flush().ok()?;
117
118        // Read response
119        let stdout = self.stdout.as_mut()?;
120        let mut line = String::new();
121        match stdout.read_line(&mut line) {
122            Ok(0) => {
123                // EOF - process died
124                tracing::warn!("Node.js returned EOF");
125                return None;
126            }
127            Ok(_) => {}
128            Err(e) => {
129                tracing::warn!("Node.js read error: {}", e);
130                return None;
131            }
132        }
133
134        let response: serde_json::Value = serde_json::from_str(line.trim()).ok()?;
135
136        if response.get("ok")?.as_bool()? {
137            Some(response.get("value")?.clone())
138        } else {
139            None
140        }
141    }
142
143    /// Check if the Node.js process is healthy.
144    pub fn is_healthy(&mut self) -> bool {
145        if let Some(ref mut child) = self.child {
146            matches!(child.try_wait(), Ok(None))
147        } else {
148            false
149        }
150    }
151}
152
153impl Drop for NodeProcess {
154    fn drop(&mut self) {
155        if let Some(ref mut child) = self.child {
156            let _ = child.kill();
157        }
158    }
159}