use std::io::Read;
use anyhow::Context as _;
use ff_rdp_core::{Grip, ObjectActor, WebConsoleActor};
use serde_json::json;
use crate::cli::args::Cli;
use crate::error::AppError;
use crate::hints::{HintContext, HintSource};
use crate::output;
use crate::output_pipeline::OutputPipeline;
use super::connect_tab::connect_and_get_target;
pub(crate) fn load_script(
script: Option<&str>,
file: Option<&str>,
use_stdin: bool,
) -> Result<String, AppError> {
let sources =
usize::from(script.is_some()) + usize::from(file.is_some()) + usize::from(use_stdin);
if sources == 0 {
return Err(AppError::User(
"eval requires a script (positional), --file <PATH>, or --stdin".to_owned(),
));
}
if sources > 1 {
return Err(AppError::User(
"eval accepts only one of: positional <SCRIPT>, --file, --stdin".to_owned(),
));
}
if let Some(s) = script {
return Ok(s.to_owned());
}
if let Some(path) = file {
return std::fs::read_to_string(path).map_err(|e| {
AppError::User(format!("eval: could not read script file '{path}': {e}"))
});
}
let mut buf = String::new();
std::io::stdin()
.read_to_string(&mut buf)
.context("eval: failed to read script from stdin")
.map_err(AppError::from)?;
Ok(buf)
}
pub(crate) fn build_script(user_script: &str, stringify: bool, isolate: bool) -> String {
let encoded = serde_json::to_string(user_script).unwrap_or_else(|e| {
unreachable!("serde_json::to_string(&str) is infallible: {e}")
});
match (isolate, stringify) {
(false, false) => user_script.to_owned(),
(false, true) => format!(
"(function() {{ try {{ return JSON.stringify({user_script}); }} catch(e) {{ if (e instanceof TypeError && e.message.includes('circular')) return '{{\"error\":\"circular reference detected\"}}'; throw e; }} }})()"
),
(true, false) => format!("(function() {{ \"use strict\"; return eval({encoded}); }})()"),
(true, true) => format!(
"(function() {{ \"use strict\"; try {{ return JSON.stringify(eval({encoded})); }} catch(e) {{ if (e instanceof TypeError && e.message.includes('circular')) return '{{\"error\":\"circular reference detected\"}}'; throw e; }} }})()"
),
}
}
pub fn run(
cli: &Cli,
script: Option<&str>,
file: Option<&str>,
use_stdin: bool,
stringify: bool,
no_isolate: bool,
) -> Result<(), AppError> {
let script = load_script(script, file, use_stdin)?;
let isolate = !no_isolate;
let final_script = build_script(&script, stringify, isolate);
let mut ctx = connect_and_get_target(cli)?;
let console_actor = ctx.target.console_actor.clone();
let eval_result =
WebConsoleActor::evaluate_js_async(ctx.transport_mut(), &console_actor, &final_script)
.map_err(AppError::from)?;
if let Some(ref exc) = eval_result.exception {
let msg = exc
.message
.as_deref()
.unwrap_or("evaluation threw an exception");
let detail = exc.value.to_json();
eprintln!("error: {msg}");
eprintln!(
"{}",
serde_json::to_string_pretty(&detail).unwrap_or_default()
);
return Err(AppError::Exit(1));
}
let mut result_json = eval_result.result.to_json();
if let Grip::Object { ref actor, .. } = eval_result.result {
match ObjectActor::prototype_and_properties(ctx.transport_mut(), actor.as_ref()) {
Ok(pap) => {
let names: Vec<&str> = pap.own_properties.keys().map(String::as_str).collect();
result_json["propertyNames"] = json!(names);
}
Err(e) => {
eprintln!("warning: could not fetch property names: {e}");
}
}
}
let mut meta = json!({"host": cli.host, "port": cli.port});
crate::connection_meta::merge_into(&mut meta, &cli.host, cli.port, None);
let envelope = output::envelope(&result_json, 1, &meta);
let hint_ctx = HintContext::new(HintSource::Eval);
OutputPipeline::from_cli(cli)?
.finalize_with_hints(&envelope, Some(&hint_ctx))
.map_err(AppError::from)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn load_script_positional_passthrough() {
let s = load_script(Some("document.title"), None, false).unwrap();
assert_eq!(s, "document.title");
}
#[test]
fn load_script_from_file() {
let tmp = std::env::temp_dir().join(format!(
"ff_rdp_eval_{}.js",
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_nanos()
));
std::fs::write(&tmp, "1 + 2").unwrap();
let s = load_script(None, Some(tmp.to_str().unwrap()), false).unwrap();
assert_eq!(s, "1 + 2");
let _ = std::fs::remove_file(&tmp);
}
#[test]
fn load_script_missing_file_is_user_error() {
let err = load_script(None, Some("/nonexistent/path/xyz.js"), false).unwrap_err();
let msg = format!("{err:?}");
assert!(
msg.contains("could not read script file") || msg.contains("xyz.js"),
"unexpected error: {msg}"
);
}
#[test]
fn load_script_no_source_errors() {
let err = load_script(None, None, false).unwrap_err();
assert!(matches!(err, AppError::User(_)));
}
#[test]
fn build_script_no_isolate_no_stringify_passthrough() {
let s = build_script("document.title", false, false);
assert_eq!(s, "document.title");
}
#[test]
fn build_script_stringify_only_wraps_in_json_stringify() {
let s = build_script("document.querySelectorAll('a')", true, false);
assert!(s.contains("JSON.stringify("));
assert!(s.contains("document.querySelectorAll('a')"));
assert!(s.contains("circular"));
assert!(!s.contains("eval("));
}
#[test]
fn build_script_isolate_only_wraps_in_strict_eval_iife() {
let s = build_script("const x = 1; x", false, true);
assert!(s.starts_with("(function()"));
assert!(s.contains("\"use strict\""));
assert!(s.contains("return eval("));
assert!(s.contains(r#""const x = 1; x""#));
}
#[test]
fn build_script_isolate_preserves_single_expression() {
let s = build_script("1 + 1", false, true);
assert!(s.contains("return eval("));
assert!(s.contains(r#""1 + 1""#));
}
#[test]
fn build_script_isolate_and_stringify_combine() {
let s = build_script("document.querySelectorAll('a')", true, true);
assert!(s.contains("\"use strict\""));
assert!(s.contains("JSON.stringify(eval("));
assert!(s.contains("circular"));
}
#[test]
fn build_script_handles_special_chars() {
let s = build_script("'a' + \"b\" + `c\nd`", false, true);
assert!(s.starts_with("(function()"));
assert!(s.ends_with(")()"));
assert!(s.contains(r"\n"));
}
}