//! QuickJS REPL: persistent context, ptrace target lives behind an Rc, //! globals are installed once at construction. //! //! Native globals installed here: //! spawn(path, ...args) -> { pid, status } //! __step_native(n=1) / __cont_native() -> stop-reason string //! (public step()/cont() are defined in prelude.js and fire listeners) //! print(...args), ctx (persistent object) //! state.pc, state.sp -> BigInt (live ptrace) //! state.registers -> object of BigInts //! state.memory(addr) -> { u8/u32/u64/i64/bytes } //! state.disasm(n=8) -> [{addr, bytes, text, target?, target_sym?}] //! state.stack -> { top, usable_base, guard_size } //! prev -> register snapshot from before last resume //! sym(addr), addr(name), symbols() //! disasm(addr, n=8) //! breakpoint / bp: .set, .remove, .list //! //! JS-side helpers (in prelude.js): hex, unhex, dump, repr, addrstr, here, //! until, finish, show.{disasm,bp,regs}. use crate::target::Target; use rquickjs::function::Rest; use rquickjs::object::Accessor; use rquickjs::{CatchResultExt, CaughtError, Context, Ctx, Function, Object, Runtime, Value}; use std::cell::RefCell; use std::rc::Rc; /// Shared handle. All JS closures clone this and `.borrow_mut()` inside. pub type TargetSlot = Rc>>; /// Result of evaluating a script. pub struct EvalOutput { /// Anything the script wrote via `print()`. pub printed: String, /// String form of the final expression's value, or empty if `undefined` / /// the source had no trailing expression. pub result: String, /// Some(msg) if the eval threw. `printed` may still contain partial output. pub error: Option, } pub struct Repl { _rt: Runtime, ctx: Context, target: TargetSlot, /// Accumulates print() output during one eval. Lives in an Rc> /// captured by the `print` closure. print_buf: Rc>, } impl Repl { pub fn new() -> Result { let rt = Runtime::new().map_err(|e| format!("rt: {}", e))?; let ctx = Context::full(&rt).map_err(|e| format!("ctx: {}", e))?; let target: TargetSlot = Rc::new(RefCell::new(None)); let print_buf: Rc> = Rc::new(RefCell::new(String::new())); ctx.with(|c| -> Result<(), String> { install_globals(c, target.clone(), print_buf.clone()) .map_err(|e| format!("install globals: {}", e)) })?; Ok(Repl { _rt: rt, ctx, target, print_buf }) } /// Evaluate one script. Captures `print()` output. Always returns Ok with /// an EvalOutput; JS exceptions become `error: Some(...)`. pub fn eval(&mut self, source: &str) -> EvalOutput { self.print_buf.borrow_mut().clear(); let mut result = String::new(); let mut error = None; self.ctx.with(|c| { match c.eval::(source).catch(&c) { Ok(v) => { if !v.is_undefined() { result = value_to_display(&c, &v).unwrap_or_else(|e| format!("", e)); } } Err(e) => { error = Some(format_caught(&e)); } } }); EvalOutput { printed: std::mem::take(&mut *self.print_buf.borrow_mut()), result, error, } } /// If a target exists, return its PID so we can kill on shutdown. pub fn target_pid(&self) -> Option { self.target.borrow().as_ref().map(|t| t.pid()) } } /// Render a JS value as a one-line string. We delegate to the prelude's /// `repr()` so BigInts (and BigInts nested inside objects) render correctly. fn value_to_display<'js>(c: &Ctx<'js>, v: &Value<'js>) -> Result { let globals = c.globals(); let repr: Function = globals.get("repr").map_err(|e| e.to_string())?; let s: String = repr.call((v.clone(),)).map_err(|e| e.to_string())?; Ok(s) } fn format_caught(e: &CaughtError) -> String { match e { CaughtError::Error(err) => format!("error: {}", err), CaughtError::Exception(exc) => { let msg = exc.message().unwrap_or_else(|| "".into()); if let Some(stack) = exc.stack() { format!("error: {}\n{}", msg, stack) } else { format!("error: {}", msg) } } CaughtError::Value(v) => format!("error: {:?}", v), } } // --------------------------------------------------------------------------- fn install_globals<'js>( c: Ctx<'js>, target: TargetSlot, print_buf: Rc>, ) -> rquickjs::Result<()> { let globals = c.globals(); // ---------- print(...) ---------- { let buf = print_buf.clone(); let f = Function::new(c.clone(), move |rest: Rest>| { let mut s = String::new(); for (i, v) in rest.0.iter().enumerate() { if i > 0 { s.push(' '); } // Best-effort String() coercion using global String. if let Ok(string_fn) = v.ctx().globals().get::<_, Function>("String") { if let Ok(part) = string_fn.call::<_, String>((v.clone(),)) { s.push_str(&part); continue; } } s.push_str(""); } s.push('\n'); buf.borrow_mut().push_str(&s); })?; globals.set("print", f)?; } // ---------- ctx (persistent JS object) ---------- { // Only set if it doesn't already exist (so REPL re-init wouldn't clobber). if globals.get::<_, Value>("ctx").map(|v| v.is_undefined()).unwrap_or(true) { let obj = Object::new(c.clone())?; globals.set("ctx", obj)?; } } // ---------- spawn(path, ...args) ---------- { let target = target.clone(); let f = Function::new(c.clone(), move |c: Ctx<'js>, path: String, rest: Rest| -> rquickjs::Result> { let args: Vec = rest.0; match Target::spawn(&path, &args) { Ok(t) => { let pid = t.pid(); *target.borrow_mut() = Some(t); let obj = Object::new(c.clone())?; obj.set("pid", pid)?; obj.set("status", "stopped_at_entry")?; Ok(obj.into_value()) } Err(e) => Err(throw(&c, &format!("spawn failed: {}", e))), } })?; globals.set("spawn", f)?; } // ---------- __step_native(n=1) ---------- // Raw single-stepper. Steps up to n instructions, stopping early if the // target dies. Returns the last stop-reason string. The public `step(n)` // is defined in prelude.js: it loops one instruction at a time so it can // fire 'step'/'breakpoint' listeners after each stop. We keep the native // n-loop too, for the fast path when no listeners are registered. { let target = target.clone(); let f = Function::new(c.clone(), move |c: Ctx<'js>, n: rquickjs::function::Opt| -> rquickjs::Result { let n = n.0.unwrap_or(1); if n < 1 { return Err(throw(&c, "step(n): n must be >= 1")); } let mut slot = target.borrow_mut(); let t = slot.as_mut().ok_or_else(|| throw(&c, "no target attached"))?; let mut last = None; for _ in 0..n { match t.step() { Ok(r) => { let alive = r.is_alive(); last = Some(r); if !alive { break; } } Err(e) => return Err(throw(&c, &e)), } } Ok(last.map(|r| r.tag()).unwrap_or_else(|| "step".into())) })?; globals.set("__step_native", f)?; } // ---------- __cont_native() ---------- // Raw continue. Resumes until the next stop (breakpoint, signal, exit). // The public `cont()` in prelude.js wraps this to fire listeners on the // resulting stop. { let target = target.clone(); let f = Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result { let mut slot = target.borrow_mut(); let t = slot.as_mut().ok_or_else(|| throw(&c, "no target attached"))?; match t.cont() { Ok(r) => Ok(r.tag()), Err(e) => Err(throw(&c, &e)), } })?; // NOTE: `continue` is a reserved keyword in JS — `continue()` as an // expression is a syntax error. We expose only `cont()`. globals.set("__cont_native", f)?; } // ---------- state ---------- { let target_for_state = target.clone(); let state_obj = build_register_view( &c, move |c| -> rquickjs::Result { let slot = target_for_state.borrow(); let t = slot.as_ref().ok_or_else(|| throw(c, "no target attached"))?; t.regs().map_err(|e| throw(c, &e)) }, )?; // state.memory(addr) → object with u8/u32/u64/i64/bytes { let target = target.clone(); let mem_fn = Function::new(c.clone(), move |c: Ctx<'js>, addr: Value<'js>| -> rquickjs::Result> { let addr = coerce_addr(&c, &addr)?; let target = target.clone(); let obj = Object::new(c.clone())?; // u8() { let target = target.clone(); obj.set("u8", Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result { let slot = target.borrow(); let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?; let b = t.read_mem(addr, 1).map_err(|e| throw(&c, &e))?; Ok(b[0] as u32) })?)?; } // u32() { let target = target.clone(); obj.set("u32", Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result { let slot = target.borrow(); let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?; let b = t.read_mem(addr, 4).map_err(|e| throw(&c, &e))?; Ok(u32::from_le_bytes([b[0], b[1], b[2], b[3]])) })?)?; } // u64() → BigInt { let target = target.clone(); obj.set("u64", Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result> { let slot = target.borrow(); let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?; let b = t.read_mem(addr, 8).map_err(|e| throw(&c, &e))?; let mut a = [0u8; 8]; a.copy_from_slice(&b); let v = u64::from_le_bytes(a); u64_to_bigint(&c, v) })?)?; } // i64() → BigInt { let target = target.clone(); obj.set("i64", Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result> { let slot = target.borrow(); let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?; let b = t.read_mem(addr, 8).map_err(|e| throw(&c, &e))?; let mut a = [0u8; 8]; a.copy_from_slice(&b); let v = i64::from_le_bytes(a); i64_to_bigint(&c, v) })?)?; } // bytes(n) → hex string { let target = target.clone(); obj.set("bytes", Function::new(c.clone(), move |c: Ctx<'js>, n: usize| -> rquickjs::Result { let slot = target.borrow(); let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?; let b = t.read_mem(addr, n).map_err(|e| throw(&c, &e))?; let mut s = String::with_capacity(n * 2); for byte in &b { s.push_str(&format!("{:02x}", byte)); } Ok(s) })?)?; } Ok(obj.into_value()) })?; state_obj.set("memory", mem_fn)?; } // state.disasm(n=8) — disassemble n instructions starting at pc. { let target = target.clone(); let f = Function::new(c.clone(), move |c: Ctx<'js>, n: rquickjs::function::Opt| -> rquickjs::Result> { let n = n.0.unwrap_or(8).max(1); let slot = target.borrow(); let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?; let pc = t.regs().map_err(|e| throw(&c, &e))?.rip; let bytes = t.read_mem_pristine(pc, n.saturating_mul(15)) .map_err(|e| throw(&c, &e))?; let decoded = crate::disasm::decode(pc, &bytes, n); decoded_to_array(&c, &decoded, t.symbols()) })?; state_obj.set("disasm", f)?; } // state.stack — accessor returning {top, usable_base, guard_size} as BigInts { let target = target.clone(); state_obj.prop( "stack", Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result> { let slot = target.borrow(); let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?; let s = t.stack().ok_or_else(|| throw(&c, "no stack info"))?; let obj = Object::new(c.clone())?; obj.set("top", u64_to_bigint(&c, s.high)?)?; obj.set("usable_base", u64_to_bigint(&c, s.low)?)?; obj.set("guard_size", u64_to_bigint(&c, s.guard_size)?)?; Ok(obj.into_value()) }), )?; } globals.set("state", state_obj)?; } // ---------- disasm(addr, n=8) ---------- { let target = target.clone(); let f = Function::new(c.clone(), move |c: Ctx<'js>, addr: Value<'js>, n: rquickjs::function::Opt| -> rquickjs::Result> { let addr = coerce_addr(&c, &addr)?; let n = n.0.unwrap_or(8).max(1); let slot = target.borrow(); let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?; let bytes = t.read_mem_pristine(addr, n.saturating_mul(15)) .map_err(|e| throw(&c, &e))?; let decoded = crate::disasm::decode(addr, &bytes, n); decoded_to_array(&c, &decoded, t.symbols()) })?; globals.set("disasm", f)?; } // ---------- prev ---------- // Mirror of state for register-only data, backed by the pre-resume // snapshot. Property accessors throw if no snapshot is available yet // (i.e. immediately after spawn, before any step/cont). { let target_for_prev = target.clone(); let prev_obj = build_register_view( &c, move |c| -> rquickjs::Result { let slot = target_for_prev.borrow(); let t = slot.as_ref().ok_or_else(|| throw(c, "no target attached"))?; t.prev_regs().ok_or_else(|| throw(c, "no prev snapshot yet (step/cont first)")) }, )?; globals.set("prev", prev_obj)?; } // ---------- sym(addr) -> "name+0xN" | null ---------- { let target = target.clone(); let f = Function::new(c.clone(), move |c: Ctx<'js>, addr: Value<'js>| -> rquickjs::Result> { let addr = coerce_addr(&c, &addr)?; let slot = target.borrow(); let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?; let s = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?; match s.name_for_addr(addr) { Some(name) => Ok(rquickjs::String::from_str(c.clone(), &name)?.into_value()), None => Ok(Value::new_null(c.clone())), } })?; globals.set("sym", f)?; } // ---------- addr(name) -> BigInt | null ---------- { let target = target.clone(); let f = Function::new(c.clone(), move |c: Ctx<'js>, name: String| -> rquickjs::Result> { let slot = target.borrow(); let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?; let s = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?; match s.addr_for_name(&name) { Some(a) => u64_to_bigint(&c, a), None => Ok(Value::new_null(c.clone())), } })?; globals.set("addr", f)?; } // ---------- symbols() -> { count, load_base, path } ---------- { let target = target.clone(); let f = Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result> { let slot = target.borrow(); let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?; let s = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?; let obj = Object::new(c.clone())?; obj.set("count", s.len() as u32)?; obj.set("load_base", u64_to_bigint(&c, s.load_base)?)?; obj.set("path", s.path.to_string_lossy().into_owned())?; Ok(obj.into_value()) })?; globals.set("symbols", f)?; } // ---------- breakpoint / bp --------------------------------------------- // breakpoint.set(target, name?) → id (target = address number/BigInt or symbol name) // breakpoint.remove(id_or_name) // breakpoint.list() → [{id, addr, name, hits, enabled}] { let bp_obj = Object::new(c.clone())?; // set(target, name?) { let target = target.clone(); let f = Function::new(c.clone(), move |c: Ctx<'js>, location: Value<'js>, name: rquickjs::function::Opt| -> rquickjs::Result { let mut slot = target.borrow_mut(); let t = slot.as_mut().ok_or_else(|| throw(&c, "no target attached"))?; // location: string → resolve via symbols; number/BigInt → coerce_addr. let addr = if let Some(s) = location.clone().into_string() { let s = s.to_string()?; let sym = t.symbols().ok_or_else(|| throw(&c, "no symbol table"))?; sym.addr_for_name(&s).ok_or_else(|| throw(&c, &format!("unknown symbol: {}", s)))? } else { coerce_addr(&c, &location)? }; let name = name.0; t.breakpoint_set(addr, name).map_err(|e| throw(&c, &e)) })?; bp_obj.set("set", f)?; } // remove(id_or_name) { let target = target.clone(); let f = Function::new(c.clone(), move |c: Ctx<'js>, id_or_name: Value<'js>| -> rquickjs::Result<()> { let mut slot = target.borrow_mut(); let t = slot.as_mut().ok_or_else(|| throw(&c, "no target attached"))?; let id = if let Some(s) = id_or_name.clone().into_string() { let s = s.to_string()?; t.breakpoint_id_by_name(&s).ok_or_else(|| throw(&c, &format!("no breakpoint named '{}'", s)))? } else if let Some(n) = id_or_name.as_number() { if n < 0.0 || n.fract() != 0.0 { return Err(throw(&c, "bp id must be a non-negative integer")); } n as u32 } else if id_or_name.is_int() { let i: i32 = id_or_name.get()?; if i < 0 { return Err(throw(&c, "bp id must be non-negative")); } i as u32 } else { return Err(throw(&c, "bp.remove expects id (number) or name (string)")); }; t.breakpoint_remove(id).map_err(|e| throw(&c, &e)) })?; bp_obj.set("remove", f)?; } // list() → array of breakpoint records { let target = target.clone(); let f = Function::new(c.clone(), move |c: Ctx<'js>| -> rquickjs::Result> { let slot = target.borrow(); let t = slot.as_ref().ok_or_else(|| throw(&c, "no target attached"))?; let arr = rquickjs::Array::new(c.clone())?; for (i, bp) in t.breakpoints().iter().enumerate() { let rec = Object::new(c.clone())?; rec.set("id", bp.id)?; rec.set("addr", u64_to_bigint(&c, bp.addr)?)?; rec.set("hits", u64_to_bigint(&c, bp.hits)?)?; rec.set("enabled", bp.enabled)?; match &bp.name { Some(n) => rec.set("name", n.as_str())?, None => rec.set("name", Value::new_null(c.clone()))?, }; arr.set(i, rec)?; } Ok(arr.into_value()) })?; bp_obj.set("list", f)?; } globals.set("breakpoint", bp_obj.clone())?; globals.set("bp", bp_obj)?; } // ---------- JS-side prelude --------------------------------------------- // Loaded last, so it can reference any of the native globals above. // Lives in src/prelude.js, embedded at compile time. const PRELUDE: &str = include_str!("prelude.js"); c.eval::<(), _>(PRELUDE)?; Ok(()) } // --- helpers -------------------------------------------------------------- /// Convert a Vec from the disassembler into a JS Array of records: /// [{addr: BigInt, bytes: "ffce..", text: "mov rax, ...", target?: BigInt, target_sym?: "name"}] /// `bytes` is a hex string for compactness; `target` and `target_sym` are /// only present for branch/call instructions. fn decoded_to_array<'js>( c: &Ctx<'js>, decoded: &[crate::disasm::Decoded], symbols: Option<&crate::symbols::SymbolTable>, ) -> rquickjs::Result> { let arr = rquickjs::Array::new(c.clone())?; for (i, d) in decoded.iter().enumerate() { let obj = Object::new(c.clone())?; obj.set("addr", u64_to_bigint(c, d.addr)?)?; let mut bytes_hex = String::with_capacity(d.bytes.len() * 2); for b in &d.bytes { bytes_hex.push_str(&format!("{:02x}", b)); } obj.set("bytes", bytes_hex)?; obj.set("text", d.text.as_str())?; if let Some(t) = d.branch_target { obj.set("target", u64_to_bigint(c, t)?)?; if let Some(s) = symbols.and_then(|s| s.name_for_addr(t)) { obj.set("target_sym", s)?; } } arr.set(i, obj)?; } Ok(arr.into_value()) } /// Build a JS object with `pc`, `sp`, and `registers` accessor properties, /// each backed by a closure that fetches a `user_regs_struct` on demand. /// Used for both `state` (live regs via ptrace) and `prev` (snapshot). fn build_register_view<'js, F>(c: &Ctx<'js>, fetch: F) -> rquickjs::Result> where F: Fn(&Ctx<'js>) -> rquickjs::Result + Clone + 'static, { let obj = Object::new(c.clone())?; { let fetch = fetch.clone(); obj.prop( "pc", Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result> { let r = fetch(&c)?; u64_to_bigint(&c, r.rip) }), )?; } { let fetch = fetch.clone(); obj.prop( "sp", Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result> { let r = fetch(&c)?; u64_to_bigint(&c, r.rsp) }), )?; } { let fetch = fetch; obj.prop( "registers", Accessor::from(move |c: Ctx<'js>| -> rquickjs::Result> { let r = fetch(&c)?; let inner = Object::new(c.clone())?; let set = |name: &str, val: u64| -> rquickjs::Result<()> { inner.set(name, u64_to_bigint(&c, val)?) }; set("rax", r.rax)?; set("rbx", r.rbx)?; set("rcx", r.rcx)?; set("rdx", r.rdx)?; set("rsi", r.rsi)?; set("rdi", r.rdi)?; set("rbp", r.rbp)?; set("rsp", r.rsp)?; set("rip", r.rip)?; set("r8", r.r8)?; set("r9", r.r9)?; set("r10", r.r10)?; set("r11", r.r11)?; set("r12", r.r12)?; set("r13", r.r13)?; set("r14", r.r14)?; set("r15", r.r15)?; set("eflags", r.eflags)?; Ok(inner.into_value()) }), )?; } Ok(obj) } /// Create a JS Error and turn it into an rquickjs::Error::Exception. The idiom /// in rquickjs for "throw an error from a Rust callback" is to call /// `Exception::throw_*(ctx, msg)`. fn throw(c: &Ctx<'_>, msg: &str) -> rquickjs::Error { rquickjs::Exception::throw_message(c, msg) } /// Accept either a JS Number or BigInt and produce a u64 address. fn coerce_addr<'js>(c: &Ctx<'js>, v: &Value<'js>) -> rquickjs::Result { if let Some(n) = v.as_number() { if n < 0.0 || !n.is_finite() { return Err(throw(c, "address must be a non-negative finite number")); } return Ok(n as u64); } if v.is_int() { let i: i32 = v.get()?; if i < 0 { return Err(throw(c, "address must be non-negative")); } return Ok(i as u64); } // Try BigInt: route through global BigInt -> toString(10) -> parse. let globals = c.globals(); let string_fn: Function = globals.get("String")?; let s: String = string_fn.call((v.clone(),)).map_err(|_| throw(c, "bad address"))?; // BigInt String() form is like "1234567" (no n suffix). s.parse::().map_err(|_| throw(c, &format!("bad address: {}", s))) } /// Convert a u64 to a JS BigInt by evaluating `BigInt("")` in the context. /// Not the fastest, but completely robust and avoids hunting the rquickjs API /// surface in v0.1. fn u64_to_bigint<'js>(c: &Ctx<'js>, v: u64) -> rquickjs::Result> { let globals = c.globals(); let bi: Function = globals.get("BigInt")?; let s = v.to_string(); bi.call((s,)) } fn i64_to_bigint<'js>(c: &Ctx<'js>, v: i64) -> rquickjs::Result> { let globals = c.globals(); let bi: Function = globals.get("BigInt")?; let s = v.to_string(); bi.call((s,)) }