use std::process::{Command, Stdio};
use std::time::Duration;
fn ae() -> std::path::PathBuf {
let mut p = std::env::current_exe().expect("test exe");
p.pop();
if p.ends_with("deps") {
p.pop();
}
p.join(format!("ae{}", std::env::consts::EXE_SUFFIX))
}
#[derive(Debug, PartialEq)]
enum Outcome {
Handled,
Crashed(String),
HungOrKilled,
}
static SERIAL: std::sync::Mutex<()> = std::sync::Mutex::new(());
fn feed(source: &str) -> Outcome {
let _serial = SERIAL.lock().unwrap_or_else(|e| e.into_inner());
let dir = std::env::temp_dir().join(format!(
"ae_hostile_{}_{}",
std::process::id(),
std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.map(|d| d.as_nanos())
.unwrap_or(0)
));
std::fs::create_dir_all(&dir).expect("temp dir");
let path = dir.join("input.ae");
std::fs::write(&path, source).expect("write input");
let out_path = dir.join("stdout");
let err_path = dir.join("stderr");
let mut child = Command::new(ae())
.arg("--deterministic")
.arg(&path)
.stdout(Stdio::from(
std::fs::File::create(&out_path).expect("stdout file"),
))
.stderr(Stdio::from(
std::fs::File::create(&err_path).expect("stderr file"),
))
.spawn()
.expect("spawn ae");
let deadline = std::time::Instant::now() + Duration::from_secs(60);
let status = loop {
match child.try_wait().expect("try_wait") {
Some(s) => break Some(s),
None if std::time::Instant::now() > deadline => {
let _ = child.kill();
let _ = child.wait();
break None;
}
None => std::thread::sleep(Duration::from_millis(25)),
}
};
let text = format!(
"{}{}",
std::fs::read_to_string(&out_path).unwrap_or_default(),
std::fs::read_to_string(&err_path).unwrap_or_default()
);
let _ = std::fs::remove_dir_all(&dir);
match status {
None => Outcome::HungOrKilled,
Some(_) if text.contains("overflowed its stack") => {
Outcome::Crashed("stack overflow".into())
}
Some(_) if text.contains("panicked at") => Outcome::Crashed(format!(
"panic: {}",
text.lines()
.find(|l| l.contains("panicked at"))
.unwrap_or_default()
)),
Some(_) => Outcome::Handled,
}
}
fn assert_handled(label: &str, source: &str) {
match feed(source) {
Outcome::Handled => {}
other => panic!("{label}: {other:?}"),
}
}
#[test]
fn deeply_nested_brackets_do_not_crash() {
for (label, open, close) in [("parentheses", "(", ")"), ("arrays", "[", "]")] {
for depth in [1_000, 20_000, 60_000] {
let src = format!("{}1{}", open.repeat(depth), close.repeat(depth));
assert_handled(&format!("{depth} nested {label}"), &src);
}
}
}
#[test]
fn deeply_nested_records_do_not_crash() {
for depth in [1_000, 20_000, 60_000] {
let src = format!("{}1{}", "{a:".repeat(depth), "}".repeat(depth));
assert_handled(&format!("{depth} nested records"), &src);
}
}
#[test]
fn long_prefix_operator_runs_do_not_crash() {
for (label, prefix, tail) in [
("minus", "-", "1"),
("not", "!", "true"),
("await", "await ", "1"),
("throw", "throw ", "1"),
] {
for depth in [1_000, 50_000] {
let src = format!("{}{}", prefix.repeat(depth), tail);
assert_handled(&format!("{depth} leading {label}"), &src);
}
}
}
#[test]
fn long_operator_and_postfix_chains_do_not_crash() {
let n = 40_000;
let cases = [
("member access", format!("x{}", ".f".repeat(n))),
("call chain", format!("f{}", "()".repeat(n))),
("pipe chain", format!("1{}", " | f".repeat(n))),
("addition", format!("1{}", " + 1".repeat(n))),
("logical and", format!("true{}", " && true".repeat(n))),
("comparison", format!("1{}", " < 1".repeat(n))),
];
for (label, src) in cases {
assert_handled(&format!("{n}-long {label}"), &src);
}
}
#[test]
fn unbalanced_brackets_do_not_crash() {
for depth in [1_000, 60_000] {
assert_handled(&format!("{depth} unclosed parens"), &"(".repeat(depth));
assert_handled(&format!("{depth} unopened parens"), &")".repeat(depth));
assert_handled(&format!("{depth} unclosed arrays"), &"[".repeat(depth));
assert_handled(&format!("{depth} unclosed braces"), &"{".repeat(depth));
}
}
#[test]
fn realistic_programs_still_run() {
for (label, src) in [
(
"pipeline",
"print([1,2,3] | map(fn(x) => x * 2) | where(fn(x) => x > 2))",
),
("arithmetic", "print(1 + 2 * 3 - 4 / 2)"),
("member access", "let r = {a: {b: {c: 1}}}\nprint(r.a.b.c)"),
("nested calls", "print(len(str(len([1,2,3]))))"),
("moderate nesting", "print(((((((((((1))))))))))) "),
(
"currying",
"let mk = fn(a) => fn(b) => a + b\nprint(mk(1)(2))",
),
("try/catch", "print(try { throw \"x\" } catch e { e })"),
("match", "print(match 2 { 1 => \"one\", _ => \"other\" })"),
] {
assert_handled(label, src);
}
}
#[test]
fn moderately_nested_code_is_not_rejected() {
let src = format!("print({}1{})", "(".repeat(100), ")".repeat(100));
let out = Command::new(ae())
.args(["--deterministic", "-c", &src])
.output()
.expect("run");
let text = String::from_utf8_lossy(&out.stdout);
assert!(
text.contains('1'),
"100 levels of nesting should evaluate, got: {text:?}"
);
}
#[test]
fn malformed_sources_do_not_crash() {
let cases: [(&str, String); 12] = [
("empty", String::new()),
("only whitespace", " \n\t\r\n ".into()),
("lone quote", "\"".into()),
("unterminated string", "let a = \"abc".into()),
("unterminated interpolation", "\"${".into()),
("nul byte", "let a = 1\u{0}let b = 2".into()),
("control characters", (1u8..32).map(|b| b as char).collect()),
("lone surrogate-ish bytes", "\u{fffd}\u{fffd}".into()),
("rtl override", "let \u{202e}abc = 1".into()),
("bom then code", "\u{feff}print(1)".into()),
(
"very long identifier",
format!("let {} = 1", "a".repeat(200_000)),
),
(
"very long string",
format!("let a = \"{}\"", "x".repeat(500_000)),
),
];
for (label, src) in cases {
assert_handled(label, &src);
}
}
#[test]
fn pathological_interpolation_does_not_crash() {
for (label, src) in [
(
"many holes",
format!("print(\"{}\")", "${a}".repeat(20_000)),
),
(
"nested braces in a hole",
format!("print(\"${{{}}}\")", "{".repeat(5_000)),
),
("unclosed hole", "print(\"${a\")".to_string()),
("hole containing a quote", "print(\"${\"}\")".to_string()),
] {
assert_handled(label, &src);
}
}
#[test]
fn truncated_utf8_in_source_does_not_crash() {
for take in 1..=3 {
let mut src = String::from("print(\"");
src.push_str(&"a".repeat(80));
let snowman = "☃";
src.push_str(&snowman.chars().next().unwrap().to_string()[..]);
let _ = take;
src.push_str("\")");
assert_handled("multibyte near a boundary", &src);
}
}