use std::{fs, path::Path};
use autoit::{AutoItBinary, Encoding, InputKind, ScriptKind};
const DIR: &str = "tests/samples";
const PAYLOAD: &[u8] = b"AutoIt fixture payload v1";
struct Case {
file: &'static str,
kind: InputKind,
encoding: Encoding,
records: usize,
script_kind: ScriptKind,
}
const CASES: &[Case] = &[
Case {
file: "v310_def.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea04,
records: 2,
script_kind: ScriptKind::PlainText,
},
Case {
file: "v310.a3x",
kind: InputKind::Pe,
encoding: Encoding::Ea04,
records: 2,
script_kind: ScriptKind::PlainText,
},
Case {
file: "v311_def.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea04,
records: 2,
script_kind: ScriptKind::PlainText,
},
Case {
file: "v311.a3x",
kind: InputKind::Pe,
encoding: Encoding::Ea04,
records: 2,
script_kind: ScriptKind::PlainText,
},
Case {
file: "v3201_def.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea05,
records: 2,
script_kind: ScriptKind::PlainText,
},
Case {
file: "v3201.a3x",
kind: InputKind::A3x,
encoding: Encoding::Ea05,
records: 2,
script_kind: ScriptKind::PlainText,
},
Case {
file: "v3220_def.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea05,
records: 2,
script_kind: ScriptKind::PlainText,
},
Case {
file: "v3249_ansi.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea05,
records: 2,
script_kind: ScriptKind::PlainText,
},
Case {
file: "v3249_def.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea05,
records: 2,
script_kind: ScriptKind::UnicodeText,
},
Case {
file: "v3249.a3x",
kind: InputKind::A3x,
encoding: Encoding::Ea05,
records: 2,
script_kind: ScriptKind::UnicodeText,
},
Case {
file: "v3260_def.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea06,
records: 2,
script_kind: ScriptKind::Tokenized,
},
Case {
file: "v3260.a3x",
kind: InputKind::A3x,
encoding: Encoding::Ea06,
records: 2,
script_kind: ScriptKind::Tokenized,
},
Case {
file: "v3281_def.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea06,
records: 2,
script_kind: ScriptKind::Tokenized,
},
Case {
file: "v3381_def.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea06,
records: 2,
script_kind: ScriptKind::Tokenized,
},
Case {
file: "v3381_x64.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea06,
records: 2,
script_kind: ScriptKind::Tokenized,
},
Case {
file: "v33102_def.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea06,
records: 3,
script_kind: ScriptKind::Tokenized,
},
Case {
file: "v33102_x64.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea06,
records: 3,
script_kind: ScriptKind::Tokenized,
},
Case {
file: "v33145_def.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea06,
records: 3,
script_kind: ScriptKind::Tokenized,
},
Case {
file: "v33145_x64.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea06,
records: 3,
script_kind: ScriptKind::Tokenized,
},
Case {
file: "v33180_def.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea06,
records: 3,
script_kind: ScriptKind::Tokenized,
},
Case {
file: "v33180_x64.exe",
kind: InputKind::Pe,
encoding: Encoding::Ea06,
records: 3,
script_kind: ScriptKind::Tokenized,
},
Case {
file: "v33180.a3x",
kind: InputKind::A3x,
encoding: Encoding::Ea06,
records: 3,
script_kind: ScriptKind::Tokenized,
},
];
#[test]
fn generated_fixtures_match_ground_truth() -> Result<(), String> {
if !Path::new(DIR).exists() {
return Ok(());
}
for case in CASES {
let path = Path::new(DIR).join(case.file);
if !path.exists() {
continue;
}
let data = fs::read(&path).map_err(|err| format!("{}: {err}", case.file))?;
let binary = AutoItBinary::try_parse(data.as_slice())
.map_err(|err| format!("{}: parse failed: {err}", case.file))?;
eq(case.file, "input_kind", binary.input_kind(), case.kind)?;
eq(
case.file,
"encoding",
binary.encoding(),
Some(case.encoding),
)?;
eq(case.file, "records", binary.records().len(), case.records)?;
for record in binary.records() {
eq(case.file, "checksum_valid", record.checksum_valid(), true)?;
}
eq(case.file, "scripts", binary.scripts().len(), 1)?;
let script = binary.scripts().first().ok_or("missing script")?;
eq(case.file, "script_kind", script.kind(), case.script_kind)?;
eq(
case.file,
"script_text",
script.source_text().is_some(),
true,
)?;
let payload = binary
.artifacts()
.iter()
.find(|a| a.subtype().contains("payload.txt") || a.name().contains("payload.txt"))
.ok_or_else(|| format!("{}: payload.txt artifact missing", case.file))?;
eq(case.file, "payload_bytes", payload.bytes(), PAYLOAD)?;
}
Ok(())
}
#[test]
fn tokenized_fixture_renders_one_line_if() -> Result<(), String> {
let path = Path::new(DIR).join("v33180.a3x");
if !path.exists() {
return Ok(());
}
let data = fs::read(&path).map_err(|err| err.to_string())?;
let binary = AutoItBinary::try_parse(data.as_slice()).map_err(|err| err.to_string())?;
let script = binary.scripts().first().ok_or("missing script")?;
let text = script.source_text().ok_or("missing source text")?;
if !text.contains("If $SWHO = \"\" Then Return") {
return Err(format!("one-line If not rendered as expected:\n{text}"));
}
if !text.contains("If @Compiled Then FileInstall(") {
return Err(format!(
"one-line If FileInstall not rendered as expected:\n{text}"
));
}
let max_indent = text
.lines()
.map(|l| l.len() - l.trim_start_matches('\t').len())
.max()
.unwrap_or(0);
if max_indent > 6 {
return Err(format!("unexpected indentation depth {max_indent}"));
}
Ok(())
}
#[test]
fn jb01_recovers_autohotkey_script() -> Result<(), String> {
let path = Path::new(DIR).join("jb01_ahk10485.exe");
if !path.exists() {
return Ok(());
}
let data = fs::read(&path).map_err(|err| err.to_string())?;
let binary = AutoItBinary::try_parse(data.as_slice()).map_err(|err| err.to_string())?;
eq(
"jb01_ahk10485.exe",
"encoding",
binary.encoding(),
Some(Encoding::Jb01),
)?;
let script = binary.scripts().first().ok_or("missing script")?;
eq(
"jb01_ahk10485.exe",
"script_kind",
script.kind(),
ScriptKind::PlainText,
)?;
for record in binary.records() {
eq(
"jb01_ahk10485.exe",
"checksum_valid",
record.checksum_valid(),
true,
)?;
}
let text = script.source_text().ok_or("missing source text")?;
if !text.contains("COMPILER: v1.0.48") {
return Err(format!("missing AHK compiler banner:\n{text}"));
}
if !text.contains("MsgBox") || !text.contains("Greet(who)") {
return Err(format!("missing fixture body:\n{text}"));
}
Ok(())
}
#[test]
fn jb01_large_script_decompresses_fully() -> Result<(), String> {
let path = Path::new(DIR).join("jb01_ahk10485_big.exe");
if !path.exists() {
return Ok(());
}
let data = fs::read(&path).map_err(|err| err.to_string())?;
let binary = AutoItBinary::try_parse(data.as_slice()).map_err(|err| err.to_string())?;
let script = binary.scripts().first().ok_or("missing script")?;
let text = script.source_text().ok_or("missing source text")?;
if !text.contains("var400 := \"value_400_a_chunk_2800\"") {
return Err("large JB01 script tail not decompressed correctly".to_string());
}
if !text.trim_end().ends_with("Return") {
return Err("large JB01 script did not end cleanly".to_string());
}
Ok(())
}
#[test]
fn ea04_recovers_plaintext_script_source() -> Result<(), String> {
let path = Path::new(DIR).join("v310_def.exe");
if !path.exists() {
return Ok(());
}
let data = fs::read(&path).map_err(|err| err.to_string())?;
let binary = AutoItBinary::try_parse(data.as_slice()).map_err(|err| err.to_string())?;
eq(
"v310_def.exe",
"encoding",
binary.encoding(),
Some(Encoding::Ea04),
)?;
let script = binary.scripts().first().ok_or("missing script")?;
let text = script.source_text().ok_or("missing source text")?;
if !text.contains("AUT2EXE VERSION: 3.1.0") {
return Err(format!("missing 3.1.0 banner:\n{text}"));
}
if !text.contains("FileInstall(") {
return Err(format!("missing fixture body:\n{text}"));
}
Ok(())
}
fn eq<T>(file: &str, what: &str, actual: T, expected: T) -> Result<(), String>
where
T: core::fmt::Debug + PartialEq,
{
if actual == expected {
Ok(())
} else {
Err(format!(
"{file}: {what}: got {actual:?}, expected {expected:?}"
))
}
}