use core_fas::session::DebugInfo;
use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
use std::sync::OnceLock;
const HELLO_START: u64 = 0x0804_8074;
const HELLO_START_PADDR: u64 = 0x74;
const HELLO64_MSG: u64 = 0x0040_10d1;
fn fixtures(rel: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures")
.join(rel)
}
fn plugin_so() -> PathBuf {
let exe = std::env::current_exe().expect("test executable");
let deps = exe.parent().expect("deps dir");
let profile_dir = deps.parent().expect("profile dir (debug/release)");
let candidates = [
deps.join("libcore_fas.so"),
profile_dir.join("libcore_fas.so"),
];
for so in &candidates {
if so.is_file() {
return so.clone();
}
}
panic!(
"missing libcore_fas.so (looked in {} and {}); cargo test --features plugin should emit the cdylib",
candidates[0].display(),
candidates[1].display()
);
}
fn ensure_binary(rel: &str) -> PathBuf {
let path = fixtures(rel);
if path.is_file() {
return path;
}
let script = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures/assemble.sh");
let status = Command::new(&script).status().unwrap_or_else(|e| {
panic!(
"run {}: {e} (install fasm, or `make fixtures`)",
script.display()
)
});
assert!(
status.success() && path.is_file(),
"fixture binary {} missing after assemble.sh; install fasm and run `make fixtures`",
path.display()
);
path
}
fn r2_is_available() -> bool {
Command::new("r2").arg("-v").output().is_ok()
}
fn native_debug_binary() -> Option<&'static Path> {
static BINARY: OnceLock<Option<PathBuf>> = OnceLock::new();
BINARY.get_or_init(build_native_debug_binary).as_deref()
}
fn build_native_debug_binary() -> Option<PathBuf> {
if !commands_available(&["as", "ld"]) {
return None;
}
let root = std::env::temp_dir().join(format!("r2fas-native-{}", std::process::id()));
std::fs::create_dir_all(&root).ok()?;
let source = root.join("native.s");
let object = root.join("native.o");
let binary = root.join("native");
std::fs::write(
&source,
r#".global _start
.text
_start:
mov $1, %rax
mov $1, %rdi
mov $msg, %rsi
mov $11, %rdx
syscall
mov $60, %rax
xor %rdi, %rdi
syscall
.data
msg:
.ascii "Hello ASM!\n"
"#,
)
.ok()?;
let assembled = Command::new("as")
.args(["--64", "-g", "-o"])
.arg(&object)
.arg(&source)
.status()
.ok()?;
let linked = Command::new("ld")
.args(["-o"])
.arg(&binary)
.arg(&object)
.status()
.ok()?;
(assembled.success() && linked.success() && binary.is_file()).then_some(binary)
}
fn commands_available(commands: &[&str]) -> bool {
commands.iter().all(|command| {
Command::new(command)
.arg("--version")
.output()
.is_ok_and(|output| output.status.success())
})
}
fn in_ci() -> bool {
matches!(std::env::var("CI").as_deref(), Ok("true") | Ok("1"))
}
fn r2(binary: &Path, script: &str) -> String {
assert!(
binary.is_file(),
"fixture binary {} missing; run `make fixtures` (needs fasm)",
binary.display()
);
assert!(
r2_is_available(),
"r2 is not on PATH; plugin integration tests need radare2"
);
let so = plugin_so();
let output = Command::new("timeout")
.args([
"-s",
"KILL",
"8",
"r2",
"-e",
"scr.color=0",
"-NN",
"-c",
&format!("L {}", so.display()),
"-qc",
&format!("fas;{script}"),
])
.arg(binary)
.output()
.unwrap_or_else(|e| panic!("spawn r2: {e}"));
let text = output_text(&output);
assert!(
output.status.success(),
"r2 failed ({:?}):\n{text}",
output.status.code()
);
text
}
fn output_text(output: &Output) -> String {
let mut s = String::from_utf8_lossy(&output.stdout).into_owned();
s.push_str(&String::from_utf8_lossy(&output.stderr));
s
}
fn r2_plain(binary: &Path, script: &str) -> String {
let output = Command::new("r2")
.args(["-qNN", "-e", "scr.color=0", "-c", script])
.arg(binary)
.output()
.expect("spawn r2");
let text = output_text(&output);
assert!(output.status.success(), "r2 failed:\n{text}");
text
}
fn r2_with_plugin(binary: &Path, script: &str) -> String {
r2_with_plugin_in(binary, script, None)
}
fn r2_with_plugin_in(binary: &Path, script: &str, directory: Option<&Path>) -> String {
let so = plugin_so();
let mut command = Command::new("r2");
command.args([
"-qNN",
"-e",
"scr.color=0",
"-c",
&format!("L {}", so.display()),
"-c",
"fas",
"-c",
script,
]);
if let Some(directory) = directory {
command.current_dir(directory);
}
let output = command.arg(binary).output().expect("spawn r2 with plugin");
let text = output_text(&output);
assert!(output.status.success(), "r2 with plugin failed:\n{text}");
assert!(text.contains("fas: loaded"), "plugin did not load:\n{text}");
text
}
fn json_sections<'a>(output: &'a str, markers: &[(&str, &str)]) -> Vec<&'a str> {
markers
.iter()
.map(|(begin, end)| section(output, begin, end))
.collect()
}
fn section<'a>(out: &'a str, begin: &str, end: &str) -> &'a str {
let start = out
.find(begin)
.unwrap_or_else(|| panic!("missing marker {begin} in r2 output:\n{out}"));
let rest = &out[start + begin.len()..];
let stop = rest.find(end).unwrap_or(rest.len());
rest[..stop].trim()
}
fn parse_fas_flags(block: &str) -> BTreeMap<String, u64> {
let mut flags = BTreeMap::new();
for line in block.lines() {
let line = line.trim();
let mut parts = line.split_whitespace();
let Some(addr_s) = parts.next() else { continue };
let Some(addr) = parse_r2_u64(addr_s) else {
continue;
};
let Some(_size) = parts.next() else { continue };
let Some(name) = parts.next() else { continue };
flags.insert(name.to_string(), addr);
}
flags
}
fn parse_r2_u64(s: &str) -> Option<u64> {
let s = s.trim();
if let Some(hex) = s.strip_prefix("0x").or_else(|| s.strip_prefix("0X")) {
u64::from_str_radix(hex, 16).ok()
} else {
s.parse().ok()
}
}
const HELLO_SCRIPT: &str = "\
?e FAS_FLAGS_BEGIN
fs fas
f
?e FAS_FLAGS_END
?e START_VA_BEGIN
?vi start
?e START_VA_END
?e PD_BEGIN
pd 3 @ start
?e PD_END
?e CL_BEGIN
s start
CL.
?e CL_END
";
const HELLO64_SCRIPT: &str = "\
?e FAS_FLAGS_BEGIN
fs fas
f
?e FAS_FLAGS_END
?e MSG_VA_BEGIN
?vi msg
?e MSG_VA_END
?e HEX_BEGIN
p8 4 @ msg
?e HEX_END
";
fn hello_flags() -> (BTreeMap<String, u64>, String) {
let out = r2(&ensure_binary("elfexe/hello"), HELLO_SCRIPT);
assert!(
out.contains("fas: loaded"),
"plugin did not load dump:\n{out}"
);
let flags = parse_fas_flags(section(&out, "FAS_FLAGS_BEGIN", "FAS_FLAGS_END"));
(flags, out)
}
#[test]
fn r2_hello_flags_use_dump_vas_not_file_offsets() {
let info = DebugInfo::from_path(&fixtures("elfexe/hello.fas")).unwrap();
let dump_start = info
.labels
.iter()
.find(|l| l.name == "start")
.expect("dump has start")
.addr;
let dump_msg = info
.labels
.iter()
.find(|l| l.name == "msg")
.expect("dump has msg")
.addr;
assert_eq!(dump_start, HELLO_START);
assert_eq!(dump_msg, 0x0804_9093);
let (flags, out) = hello_flags();
assert_eq!(
flags.get("start").copied(),
Some(HELLO_START),
"start flag: {flags:?}\n{out}"
);
assert_eq!(
flags.get("msg").copied(),
Some(dump_msg),
"msg flag: {flags:?}"
);
assert_ne!(
flags["start"], HELLO_START_PADDR,
"start must not be the file offset"
);
let va = parse_r2_u64(section(&out, "START_VA_BEGIN", "START_VA_END")).expect("?vi start");
assert_eq!(va, HELLO_START);
let pd = section(&out, "PD_BEGIN", "PD_END");
assert!(
pd.contains("mov eax"),
"pd @ start should be code at the ELF VA:\n{pd}"
);
assert!(
!pd.contains("invalid"),
"pd @ start must not be unmapped file-offset bytes:\n{pd}"
);
assert!(
pd.contains("hello.asm:11"),
"addrline should point at hello.asm:\n{pd}"
);
let cl = section(&out, "CL_BEGIN", "CL_END");
assert!(cl.contains("hello.asm"), "CL. file:\n{cl}");
assert!(
cl.contains("0x08048074") || cl.contains("0x8048074"),
"CL. addr:\n{cl}"
);
}
#[test]
fn r2_hello64_msg_is_at_elf64_va() {
let info = DebugInfo::from_path(&fixtures("elfexe/hello64.fas")).unwrap();
let dump_msg = info
.labels
.iter()
.find(|l| l.name == "msg")
.expect("dump has msg")
.addr;
assert_eq!(dump_msg, HELLO64_MSG);
let out = r2(&ensure_binary("elfexe/hello64"), HELLO64_SCRIPT);
assert!(
out.contains("fas: loaded"),
"plugin did not load dump:\n{out}"
);
let flags = parse_fas_flags(section(&out, "FAS_FLAGS_BEGIN", "FAS_FLAGS_END"));
assert_eq!(
flags.get("msg").copied(),
Some(HELLO64_MSG),
"msg flag: {flags:?}\n{out}"
);
assert_ne!(flags["msg"], 0x10d1, "msg must not be the file offset");
let va = parse_r2_u64(section(&out, "MSG_VA_BEGIN", "MSG_VA_END")).expect("?vi msg");
assert_eq!(va, HELLO64_MSG);
let hex = section(&out, "HEX_BEGIN", "HEX_END").to_ascii_lowercase();
assert!(
hex.contains("48656c6c"),
"bytes at msg should be 'Hell' (Hello 64-bit…):\n{hex}"
);
}
#[test]
fn r2_debuggee_path_resolves_from_forked_child() {
use radare2::io_uri::ptrace_exe;
ensure_binary("elfexe/hello");
let mut child = Command::new("sleep")
.arg("30")
.spawn()
.expect("spawn sleep (needs coreutils)");
let uri = PathBuf::from(format!("ptrace://{}", child.id()));
let exe = ptrace_exe(&uri).expect("read /proc/PID/exe");
assert!(exe.is_absolute());
assert_eq!(
exe.file_name().and_then(|n| n.to_str()),
Some("sleep"),
"ptrace:// URI should resolve through procfs, not ptrace(2)"
);
let _ = child.kill();
let _ = child.wait();
let (flags, out) = hello_flags();
assert_eq!(
flags.get("start").copied(),
Some(HELLO_START),
"start flag with debuggee running: {flags:?}\n{out}"
);
assert_ne!(flags["start"], HELLO_START_PADDR);
}
const DIFFERENTIAL_SCRIPT: &str = "\
?e IS_BEGIN
isj
?e IS_END
?e ID_BEGIN
idj
?e ID_END
?e CLJ_BEGIN
CLj
?e CLJ_END
?e AFL_BEGIN
aflj
?e AFL_END
?e AXL_BEGIN
axlj
?e AXL_END
";
const DIFFERENTIAL_MARKERS: &[(&str, &str)] = &[
("IS_BEGIN", "IS_END"),
("ID_BEGIN", "ID_END"),
("CLJ_BEGIN", "CLJ_END"),
("AFL_BEGIN", "AFL_END"),
("AXL_BEGIN", "AXL_END"),
];
#[test]
fn differential_native_and_fas_debug_surfaces() {
let Some(native) = native_debug_binary() else {
eprintln!("skip differential_native_and_fas_debug_surfaces: GNU as/ld unavailable");
return;
};
let fasm = ensure_binary("elfexe/hello64");
let native_output = r2_plain(native, DIFFERENTIAL_SCRIPT);
let fasm_output = r2_with_plugin(&fasm, DIFFERENTIAL_SCRIPT);
let native_sections = json_sections(&native_output, DIFFERENTIAL_MARKERS);
let fasm_sections = json_sections(&fasm_output, DIFFERENTIAL_MARKERS);
assert!(
native_sections[0].contains("_start"),
"native isj:\n{}",
native_sections[0]
);
assert!(
native_sections[1].contains("native.s"),
"native idj:\n{}",
native_sections[1]
);
assert!(
native_sections[2].contains("native.s"),
"native CLj:\n{}",
native_sections[2]
);
assert!(
fasm_sections[0].contains("msg")
&& fasm_sections[0].contains("\"type\":\"OBJ\"")
&& fasm_sections[0].contains("\"paddr\":209")
&& fasm_sections[0].contains("\"vaddr\":4198609"),
"FAS native isj:\n{}",
fasm_sections[0]
);
assert!(
fasm_sections[1] == "[]" || fasm_sections[1].contains("hello64.asm"),
"unexpected FAS idj behavior:\n{}",
fasm_sections[1]
);
assert!(
fasm_sections[2].contains("hello64.asm"),
"FAS CLj:\n{}",
fasm_sections[2]
);
assert_eq!(
fasm_sections[3], "[]",
"FAS hello64 aflj:\n{}",
fasm_sections[3]
);
assert!(
fasm_sections[4].contains("\"type\":\"DATA\"")
&& fasm_sections[4].contains("\"from\":4194485")
&& fasm_sections[4].contains("\"addr\":4198609"),
"FAS axlj:\n{}",
fasm_sections[4]
);
}
#[test]
fn native_metadata_unload_and_reload_are_reversible() {
let fasm = ensure_binary("elfexe/hello64");
let script = "\
?e FIRST_IS_BEGIN
isj
?e FIRST_IS_END
?e FIRST_CL_BEGIN
CLj
?e FIRST_CL_END
?e FIRST_AX_BEGIN
axlj
?e FIRST_AX_END
fas unload
?e EMPTY_IS_BEGIN
isj
?e EMPTY_IS_END
?e EMPTY_CL_BEGIN
CLj
?e EMPTY_CL_END
?e EMPTY_AX_BEGIN
axlj
?e EMPTY_AX_END
fas load
?e SECOND_IS_BEGIN
isj
?e SECOND_IS_END
?e SECOND_CL_BEGIN
CLj
?e SECOND_CL_END
?e SECOND_AX_BEGIN
axlj
?e SECOND_AX_END
";
let output = r2_with_plugin(&fasm, script);
let first_symbols = section(&output, "FIRST_IS_BEGIN", "FIRST_IS_END");
let first_lines = section(&output, "FIRST_CL_BEGIN", "FIRST_CL_END");
let first_xrefs = section(&output, "FIRST_AX_BEGIN", "FIRST_AX_END");
let empty_symbols = section(&output, "EMPTY_IS_BEGIN", "EMPTY_IS_END");
let empty_lines = section(&output, "EMPTY_CL_BEGIN", "EMPTY_CL_END");
let empty_xrefs = section(&output, "EMPTY_AX_BEGIN", "EMPTY_AX_END");
let second_symbols = section(&output, "SECOND_IS_BEGIN", "SECOND_IS_END");
let second_lines = section(&output, "SECOND_CL_BEGIN", "SECOND_CL_END");
let second_xrefs = section(&output, "SECOND_AX_BEGIN", "SECOND_AX_END");
assert!(first_symbols.contains("msg"), "first isj:\n{first_symbols}");
assert!(
first_lines.contains("hello64.asm"),
"first CLj:\n{first_lines}"
);
assert!(first_xrefs.contains("DATA"), "first axlj:\n{first_xrefs}");
assert_eq!(empty_symbols, "[]", "unload isj:\n{empty_symbols}");
assert_eq!(empty_lines, "[]", "unload CLj:\n{empty_lines}");
assert_eq!(empty_xrefs, "[]", "unload axlj:\n{empty_xrefs}");
assert_eq!(second_symbols, first_symbols, "reload isj changed");
assert_eq!(second_lines, first_lines, "reload CLj changed");
assert_eq!(second_xrefs, first_xrefs, "reload axlj changed");
}
#[test]
fn real_functions_and_fas_xrefs_are_reversible() {
let fasm = ensure_binary("elfexe/hello");
let script = "\
?e FIRST_AF_BEGIN
aflj
?e FIRST_AF_END
?e FIRST_AX_BEGIN
axlj
?e FIRST_AX_END
fas unload
?e EMPTY_AF_BEGIN
aflj
?e EMPTY_AF_END
?e EMPTY_AX_BEGIN
axlj
?e EMPTY_AX_END
fas load
?e SECOND_AF_BEGIN
aflj
?e SECOND_AF_END
?e SECOND_AX_BEGIN
axlj
?e SECOND_AX_END
";
let output = r2_with_plugin(&fasm, script);
let first_functions = section(&output, "FIRST_AF_BEGIN", "FIRST_AF_END");
let first_xrefs = section(&output, "FIRST_AX_BEGIN", "FIRST_AX_END");
let empty_functions = section(&output, "EMPTY_AF_BEGIN", "EMPTY_AF_END");
let empty_xrefs = section(&output, "EMPTY_AX_BEGIN", "EMPTY_AX_END");
let second_functions = section(&output, "SECOND_AF_BEGIN", "SECOND_AF_END");
let second_xrefs = section(&output, "SECOND_AX_BEGIN", "SECOND_AX_END");
assert!(
first_functions.contains("\"name\":\"start\"") && first_functions.contains("\"ninstrs\":8"),
"real FAS function missing:\n{first_functions}"
);
assert!(
first_xrefs.contains("\"type\":\"DATA\"")
&& first_xrefs.contains("\"from\":134512766")
&& first_xrefs.contains("\"addr\":134516883"),
"FAS reference xref missing:\n{first_xrefs}"
);
assert_eq!(empty_functions, "[]", "unload aflj:\n{empty_functions}");
assert_eq!(empty_xrefs, "[]", "unload axlj:\n{empty_xrefs}");
assert_eq!(second_functions, first_functions, "reload aflj changed");
assert_eq!(second_xrefs, first_xrefs, "reload axlj changed");
}
#[test]
fn preexisting_functions_and_xrefs_survive_plugin_unload() {
let fasm = ensure_binary("elfexe/hello");
let script = "\
fas unload
af @ 0x08048074
axd 0x08049093 @ 0x0804807a
fas load
fas unload
?e AF_BEGIN
aflj
?e AF_END
?e AX_BEGIN
axlj
?e AX_END
";
let output = r2_with_plugin(&fasm, script);
let functions = section(&output, "AF_BEGIN", "AF_END");
let xrefs = section(&output, "AX_BEGIN", "AX_END");
assert!(
functions.contains("134512756"),
"pre-existing function removed:\n{functions}"
);
assert!(
xrefs.contains("\"from\":134512762") && xrefs.contains("\"addr\":134516883"),
"pre-existing xref removed:\n{xrefs}"
);
}
#[test]
fn plugin_commands_report_state_and_obey_explicit_load() {
let fasm = ensure_binary("elfexe/hello64");
let explicit = fixtures("elfexe/hello64.fas");
let so = plugin_so();
let output = Command::new("r2")
.args([
"-qNN",
"-e",
"scr.color=0",
"-c",
&format!("L {}", so.display()),
"-c",
"fas unload",
"-c",
"e fas.autoload=false",
"-c",
"fas.",
"-c",
"fas?",
"-c",
"fas info",
"-c",
"fas unknown",
"-c",
&format!("fas load {}", explicit.display()),
"-c",
"fas info",
])
.arg(&fasm)
.output()
.expect("spawn r2 command test");
let text = output_text(&output);
assert!(output.status.success(), "r2 failed:\n{text}");
assert!(text.contains("Usage: fas"), "missing fas help:\n{text}");
assert!(
text.contains("fas: nothing loaded"),
"autoload was not disabled:\n{text}"
);
assert!(
text.contains("unknown fas subcommand"),
"unknown command diagnostic missing:\n{text}"
);
assert!(
text.contains("functions=0") && text.contains("xrefs=1"),
"fas info missing native counts:\n{text}"
);
}
#[test]
fn replacement_load_swaps_transactions_without_duplicates() {
let fasm = ensure_binary("elfexe/hello");
let original = fixtures("elfexe/hello.fas");
let root = std::env::temp_dir().join(format!("r2fas-replacement-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("create replacement fixture directory");
let replacement = root.join("replacement.fas");
std::fs::copy(&original, &replacement).expect("copy replacement FAS dump");
let script = format!(
"\
?e FIRST_AF_BEGIN
aflj
?e FIRST_AF_END
?e FIRST_AX_BEGIN
axlj
?e FIRST_AX_END
fas load {}
?e SECOND_AF_BEGIN
aflj
?e SECOND_AF_END
?e SECOND_AX_BEGIN
axlj
?e SECOND_AX_END
fas unload
?e EMPTY_AF_BEGIN
aflj
?e EMPTY_AF_END
?e EMPTY_AX_BEGIN
axlj
?e EMPTY_AX_END
",
replacement.display()
);
let output = r2_with_plugin(&fasm, &script);
let first_functions = section(&output, "FIRST_AF_BEGIN", "FIRST_AF_END");
let first_xrefs = section(&output, "FIRST_AX_BEGIN", "FIRST_AX_END");
let second_functions = section(&output, "SECOND_AF_BEGIN", "SECOND_AF_END");
let second_xrefs = section(&output, "SECOND_AX_BEGIN", "SECOND_AX_END");
let empty_functions = section(&output, "EMPTY_AF_BEGIN", "EMPTY_AF_END");
let empty_xrefs = section(&output, "EMPTY_AX_BEGIN", "EMPTY_AX_END");
assert_eq!(
second_functions, first_functions,
"replacement changed aflj"
);
assert_eq!(second_xrefs, first_xrefs, "replacement changed axlj");
assert_eq!(empty_functions, "[]", "replacement leaked functions");
assert_eq!(empty_xrefs, "[]", "replacement leaked xrefs");
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn relocatable_object_does_not_invent_zero_valued_runtime_labels() {
let object = ensure_binary("elfobj/msgdemo.o");
let output = r2_with_plugin(&object, "fs fas;fj");
let start = output.find('[').unwrap_or(0);
let flags = &output[start..];
assert!(
!flags.contains("\"name\":\"_start\"") && !flags.contains("\"name\":\"msg\""),
"zero-valued section-relative labels were placed on an arbitrary map:\n{output}"
);
}
#[test]
fn native_addrlines_preserve_source_paths_with_spaces() {
let fixture = ensure_binary("elfexe/hello64");
let source_dir = fixture.parent().expect("fixture directory");
let root = std::env::temp_dir().join(format!("r2fas path spaces {}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("create spaced fixture directory");
for name in ["hello64", "hello64.fas", "hello64.asm"] {
std::fs::copy(source_dir.join(name), root.join(name)).expect("copy spaced fixture");
}
let binary = root.join("hello64");
let output = r2_with_plugin_in(&binary, "CLj", Some(Path::new("/")));
assert!(
output.contains(&root.to_string_lossy().into_owned()),
"native CLj lost spaces in source path:\n{output}"
);
assert!(
!output.contains("r2fas_path_spaces"),
"path was underscored"
);
let _ = std::fs::remove_dir_all(root);
}
#[test]
fn r2_debug_hello_keeps_the_same_vas() {
if in_ci() {
eprintln!("skip r2_debug_hello_keeps_the_same_vas: ptrace unavailable in CI");
return;
}
let hello = ensure_binary("elfexe/hello");
let so = plugin_so();
let output = Command::new("timeout")
.args([
"-s",
"KILL",
"8",
"r2",
"-e",
"scr.color=0",
"-NN",
"-d",
"-c",
&format!("L {}", so.display()),
"-qc",
&format!("fas;{HELLO_SCRIPT}"),
])
.arg(&hello)
.output()
.expect("spawn r2 -d");
let out = output_text(&output);
if !output.status.success() && !out.contains("fas: loaded") && !out.contains("FAS_FLAGS_BEGIN")
{
panic!("r2 -d failed ({:?}):\n{out}", output.status.code());
}
assert!(
out.contains("fas: loaded"),
"plugin did not load dump under r2 -d:\n{out}"
);
let flags = parse_fas_flags(section(&out, "FAS_FLAGS_BEGIN", "FAS_FLAGS_END"));
assert_eq!(
flags.get("start").copied(),
Some(HELLO_START),
"r2 -d start must stay at the ELF VA, not a paddr:\n{flags:?}\n{out}"
);
assert_ne!(flags["start"], HELLO_START_PADDR);
let pd = section(&out, "PD_BEGIN", "PD_END");
assert!(pd.contains("mov eax"), "r2 -d pd @ start:\n{pd}");
assert!(!pd.contains("invalid"), "r2 -d pd @ start:\n{pd}");
assert!(pd.contains("hello.asm:11"), "r2 -d addrline:\n{pd}");
}