r2fas 0.2.1

radare2 core plugin that loads FASM -s symbolic dumps for named labels, source lines, and comments
//! Locate a FAS dump that belongs to the binary currently opened in radare2.
//!
//! Search is silent when nothing matches: missing debug info is normal.

use crate::fas::Header;
use radare2::io_uri::on_disk_path;
use std::fs;
use std::path::{Path, PathBuf};

/// Candidate paths beside `binary`, in preference order.
///
/// `binary` may be an r2 IO URI (`dbg://./hello`); those are stripped first.
///
/// 1. `<name>.fas` (`hello` → `hello.fas`)
/// 2. `<name>` with a `.fas` extension if it had another suffix (`msgdemo.o` → `msgdemo.fas`)
pub fn candidates(binary: &Path) -> Vec<PathBuf> {
    let mut out = Vec::new();
    let Some(binary) = on_disk_path(binary) else {
        return out;
    };
    let Some(dir) = binary.parent() else {
        return out;
    };
    let Some(name) = binary.file_name().and_then(|n| n.to_str()) else {
        return out;
    };
    out.push(dir.join(format!("{name}.fas")));
    let as_fas = dir.join(name).with_extension("fas");
    if !out.contains(&as_fas) {
        out.push(as_fas);
    }
    out
}

/// First existing candidate whose FAS header output name matches `binary`
/// (or has no output name). `None` if nothing usable is found.
pub fn find_for_binary(binary: &Path) -> Option<PathBuf> {
    let binary = on_disk_path(binary)?;
    let basename = binary.file_name()?.to_str()?;
    for path in candidates(&binary) {
        if !path.is_file() {
            continue;
        }
        if header_matches_binary(&path, basename) {
            return Some(path);
        }
    }
    None
}

/// True when the dump's recorded output name is empty or equals `basename`.
fn header_matches_binary(path: &Path, basename: &str) -> bool {
    let Ok(bytes) = fs::read(path) else {
        return false;
    };
    let Ok(header) = Header::parse(&bytes) else {
        return false;
    };
    match header.output_name(&bytes) {
        Ok("") => true,
        Ok(name) => Path::new(name)
            .file_name()
            .and_then(|n| n.to_str())
            .map(|n| n == basename)
            .unwrap_or(false),
        Err(_) => false,
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    fn fixtures_dir() -> PathBuf {
        PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
    }

    #[test]
    fn prefers_named_fas() {
        let p = PathBuf::from("/tmp/hello");
        let c = candidates(&p);
        assert_eq!(c, [PathBuf::from("/tmp/hello.fas")]);
    }

    #[test]
    fn also_tries_replacing_a_suffix() {
        let p = PathBuf::from("/tmp/msgdemo.o");
        let c = candidates(&p);
        assert_eq!(
            c,
            [
                PathBuf::from("/tmp/msgdemo.o.fas"),
                PathBuf::from("/tmp/msgdemo.fas"),
            ]
        );
    }

    #[test]
    fn finds_sibling_dump_for_elf_executable() {
        let binary = fixtures_dir().join("elfexe/hello");
        let found = find_for_binary(&binary).expect("hello.fas next to hello");
        assert_eq!(found, fixtures_dir().join("elfexe/hello.fas"));
    }

    #[test]
    fn finds_sibling_dump_for_elf_object() {
        let binary = fixtures_dir().join("elfobj/msgdemo.o");
        let found = find_for_binary(&binary).expect("msgdemo.fas next to msgdemo.o");
        assert_eq!(found, fixtures_dir().join("elfobj/msgdemo.fas"));
    }

    #[test]
    fn strips_dbg_uri_to_on_disk_path() {
        let hello = fixtures_dir().join("elfexe/hello");
        let uri = PathBuf::from(format!("dbg://{}", hello.display()));
        assert_eq!(
            radare2::io_uri::on_disk_path(&uri).as_deref(),
            Some(hello.as_path())
        );
        let found = find_for_binary(&uri).expect("dbg:// URI still finds hello.fas");
        assert_eq!(found, fixtures_dir().join("elfexe/hello.fas"));
    }

    #[test]
    fn ignores_ptrace_pid_uri() {
        assert_eq!(
            radare2::io_uri::on_disk_path(Path::new("ptrace://103133")),
            None
        );
        assert_eq!(
            radare2::io_uri::ptrace_pid("ptrace://103133"),
            Some("103133")
        );
        assert_eq!(radare2::io_uri::ptrace_pid("dbg://./hello"), None);
        assert!(candidates(Path::new("ptrace://103133")).is_empty());
        assert_eq!(find_for_binary(Path::new("ptrace://103133")), None);
    }

    #[test]
    fn ptrace_exe_reads_proc() {
        let pid = std::process::id();
        let uri = PathBuf::from(format!("ptrace://{pid}"));
        let exe = radare2::io_uri::ptrace_exe(&uri).expect("/proc/PID/exe");
        assert!(exe.is_absolute());
    }
}