r2fas 0.2.1

radare2 core plugin that loads FASM -s symbolic dumps for named labels, source lines, and comments
//! FASM symbolic-information (`.fas`) file parser.
//!
//! The on-disk layout is specified in [`TOOLS/FAS.TXT`](https://github.com/tgrysztar/fasm/blob/master/TOOLS/FAS.TXT)
//! from the official FASM tree. This module maps that document onto safe
//! Rust types without depending on radare2.

mod bytes;
mod dump;
mod header;
mod line;
mod reference;
mod section;
mod symbol;

pub use dump::{AssemblyDump, DumpRow, EmittedSpan};
pub use header::{Header, TableRange};
pub use line::{MacroFrame, PrepLine, PrepSource, Provenance, ProvenanceDiagnostic, SourceToken};
pub use reference::{DumpRowId, SymbolId, SymbolReference};
pub use section::SectionName;
pub use symbol::Symbol;

use crate::error::Result;
use std::fs;
use std::path::Path;

/// A fully parsed FAS dump, borrowing the original byte buffer.
#[derive(Debug, Clone)]
pub struct FasFile {
    /// Table 1.
    pub header: Header,
    /// Table 2 records.
    pub symbols: Vec<Symbol>,
    /// Table 3 index.
    pub prep: PrepSource,
    /// Table 4 (empty if assembly did not complete).
    pub dump: AssemblyDump,
    /// Optional object-file section names. `None` means unsupported by the
    /// producing FASM version; `Some([])` means supported but empty.
    pub sections: Option<Vec<SectionName>>,
    /// Optional symbol-use events, with the same support/empty distinction.
    pub references: Option<Vec<SymbolReference>>,
}

impl FasFile {
    /// Parse `data` as a FAS file.
    pub fn parse(data: &[u8]) -> Result<Self> {
        let header = Header::parse(data)?;
        let symbols = symbol::parse_symbols(&header, data)?;
        let prep = line::parse_preprocessed(&header, data)?;
        let dump = dump::parse_dump(&header, data)?;
        let sections = section::parse_sections(&header, data)?;
        let references =
            reference::parse_references(&header, data, symbols.len(), dump.rows.len())?;
        Ok(Self {
            header,
            symbols,
            prep,
            dump,
            sections,
            references,
        })
    }

    /// Read a path and parse it.
    pub fn from_path(path: &Path) -> Result<(Vec<u8>, Self)> {
        let bytes = fs::read(path)?;
        let parsed = Self::parse(&bytes)?;
        Ok((bytes, parsed))
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use std::path::{Path, PathBuf};

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

    fn fixture(rel: &str) -> PathBuf {
        fixtures_dir().join(rel)
    }

    fn all_fas() -> Vec<PathBuf> {
        let mut out = Vec::new();
        visit(&fixtures_dir(), &mut out);
        out.sort();
        out
    }

    fn visit(dir: &Path, out: &mut Vec<PathBuf>) {
        for entry in std::fs::read_dir(dir).unwrap() {
            let path = entry.unwrap().path();
            if path.is_dir() {
                visit(&path, out);
            } else if path.extension().and_then(|s| s.to_str()) == Some("fas") {
                out.push(path);
            }
        }
    }

    fn parse_rel(rel: &str) -> (Vec<u8>, FasFile) {
        let bytes = std::fs::read(fixture(rel)).unwrap();
        let fas = FasFile::parse(&bytes).unwrap();
        (bytes, fas)
    }

    fn symbol_names(fas: &FasFile, bytes: &[u8]) -> Vec<String> {
        fas.symbols
            .iter()
            .filter_map(|s| {
                s.name(&fas.header, bytes)
                    .ok()
                    .flatten()
                    .map(str::to_string)
            })
            .collect()
    }

    fn minimal_fas(section_len: u32, reference_len: u32) -> Vec<u8> {
        let strings_offset = 64u32;
        let symbols_offset = 66u32;
        let prep_offset = symbols_offset + 32;
        let dump_offset = prep_offset + 17;
        let sections_offset = dump_offset + 32;
        let references_offset = sections_offset + section_len;
        let mut data = vec![0u8; (references_offset + reference_len) as usize];
        data[0..4].copy_from_slice(&header::SIGNATURE.to_le_bytes());
        data[4] = 1;
        data[5] = 73;
        data[6..8].copy_from_slice(&64u16.to_le_bytes());
        data[16..20].copy_from_slice(&strings_offset.to_le_bytes());
        data[20..24].copy_from_slice(&2u32.to_le_bytes());
        data[24..28].copy_from_slice(&symbols_offset.to_le_bytes());
        data[28..32].copy_from_slice(&32u32.to_le_bytes());
        data[32..36].copy_from_slice(&prep_offset.to_le_bytes());
        data[36..40].copy_from_slice(&17u32.to_le_bytes());
        data[40..44].copy_from_slice(&dump_offset.to_le_bytes());
        data[44..48].copy_from_slice(&32u32.to_le_bytes());
        data[48..52].copy_from_slice(&sections_offset.to_le_bytes());
        data[52..56].copy_from_slice(&section_len.to_le_bytes());
        data[56..60].copy_from_slice(&references_offset.to_le_bytes());
        data[60..64].copy_from_slice(&reference_len.to_le_bytes());
        data[64] = 0;
        data[65] = 0;
        data[prep_offset as usize + 4..prep_offset as usize + 8]
            .copy_from_slice(&1u32.to_le_bytes());
        data[prep_offset as usize + 16] = 0;
        data[dump_offset as usize + 4..dump_offset as usize + 8]
            .copy_from_slice(&0u32.to_le_bytes());
        data[dump_offset as usize + 8..dump_offset as usize + 16]
            .copy_from_slice(&0x1000u64.to_le_bytes());
        data[dump_offset as usize + 25] = 64;
        data[dump_offset as usize + 28..dump_offset as usize + 32]
            .copy_from_slice(&1u32.to_le_bytes());
        data
    }

    #[test]
    fn rejects_optional_table_geometry_and_reference_ids() {
        assert!(matches!(
            FasFile::parse(&minimal_fas(2, 0)),
            Err(crate::FasError::Geometry(
                "section names not a multiple of 4"
            ))
        ));
        assert!(matches!(
            FasFile::parse(&minimal_fas(0, 4)),
            Err(crate::FasError::Geometry("references not a multiple of 8"))
        ));

        let mut bad_symbol = minimal_fas(0, 8);
        let reference_offset = u32::from_le_bytes(bad_symbol[56..60].try_into().unwrap()) as usize;
        bad_symbol[reference_offset..reference_offset + 4].copy_from_slice(&32u32.to_le_bytes());
        assert!(matches!(
            FasFile::parse(&bad_symbol),
            Err(crate::FasError::Geometry("symbol reference"))
        ));

        let mut bad_row = minimal_fas(0, 8);
        let reference_offset = u32::from_le_bytes(bad_row[56..60].try_into().unwrap()) as usize;
        bad_row[reference_offset + 4..reference_offset + 8].copy_from_slice(&28u32.to_le_bytes());
        assert!(matches!(
            FasFile::parse(&bad_row),
            Err(crate::FasError::Geometry("dump reference"))
        ));
    }

    #[test]
    fn parses_every_website_fixture() {
        let files = all_fas();
        assert!(
            files.len() >= 8,
            "expected the FASM website/distribution examples, got {files:?}"
        );
        for path in files {
            let bytes = std::fs::read(&path).unwrap();
            let fas = FasFile::parse(&bytes).unwrap_or_else(|e| panic!("{}: {e}", path.display()));
            assert_eq!(fas.header.major, 1, "{}", path.display());
            assert!(!fas.symbols.is_empty(), "{}", path.display());
            assert!(!fas.dump.rows.is_empty(), "{}", path.display());
            assert!(fas.sections.is_some(), "{}", path.display());
            assert!(fas.references.is_some(), "{}", path.display());
            assert!(!fas.header.input_name(&bytes).unwrap().is_empty());
            assert!(!fas.header.output_name(&bytes).unwrap().is_empty());
        }
    }

    #[test]
    fn hello_elf32_executable() {
        let (bytes, fas) = parse_rel("elfexe/hello.fas");
        assert_eq!(fas.header.input_name(&bytes).unwrap(), "hello.asm");
        assert_eq!(fas.header.output_name(&bytes).unwrap(), "hello");
        assert_eq!(fas.dump.inferred_baddr(), Some(0x0804_8000));
        let names = symbol_names(&fas, &bytes);
        assert!(names.iter().any(|n| n == "start"));
        assert!(names.iter().any(|n| n == "msg"));
    }

    #[test]
    fn hello64_elf64_executable() {
        let (bytes, fas) = parse_rel("elfexe/hello64.fas");
        assert_eq!(fas.header.input_name(&bytes).unwrap(), "hello64.asm");
        assert_eq!(fas.header.output_name(&bytes).unwrap(), "hello64");
        assert_eq!(fas.dump.inferred_baddr(), Some(0x400000));
        let names = symbol_names(&fas, &bytes);
        assert!(names.iter().any(|n| n == "msg"));
        assert!(fas.dump.rows.iter().any(|r| r.code_type == 64));
    }

    #[test]
    fn msgdemo_and_writemsg_elf_objects() {
        let (bytes, fas) = parse_rel("elfobj/msgdemo.fas");
        assert_eq!(fas.header.output_name(&bytes).unwrap(), "msgdemo.o");
        let names = symbol_names(&fas, &bytes);
        assert!(names.iter().any(|n| n == "_start"));
        assert!(names.iter().any(|n| n == "msg"));
        assert_eq!(
            fas.sections
                .as_ref()
                .unwrap()
                .iter()
                .map(|section| section.name.as_str())
                .collect::<Vec<_>>(),
            [".text", ".data"]
        );
        assert!(!fas.references.as_ref().unwrap().is_empty());

        let (bytes, fas) = parse_rel("elfobj/writemsg.fas");
        assert_eq!(fas.header.output_name(&bytes).unwrap(), "writemsg.o");
        let names = symbol_names(&fas, &bytes);
        assert!(names.iter().any(|n| n == "writemsg"));
        assert!(names.iter().any(|n| n == "find_end"));
    }

    #[test]
    fn libcdemo_uses_ccall_macro() {
        let (bytes, fas) = parse_rel("libcdemo/libcdemo.fas");
        assert_eq!(fas.header.input_name(&bytes).unwrap(), "libcdemo.asm");
        let names = symbol_names(&fas, &bytes);
        assert!(names.iter().any(|n| n == "main"));
        assert!(names.iter().any(|n| n == "msg"));
        assert!(fas.dump.rows.len() >= 10);
    }

    #[test]
    fn longmode_examples_from_website() {
        let (bytes, fas) = parse_rel("longmode/simple.fas");
        assert_eq!(fas.header.input_name(&bytes).unwrap(), "simple.asm");
        assert_eq!(fas.dump.inferred_baddr(), Some(0x1600));
        let names = symbol_names(&fas, &bytes);
        for want in ["pm_start", "long_start", "make_page_entries", "GDTR", "GDT"] {
            assert!(names.iter().any(|n| n == want), "missing {want}");
        }

        let (bytes, fas) = parse_rel("longmode/basecode.fas");
        assert_eq!(fas.dump.inferred_baddr(), Some(0x1600));
        let names = symbol_names(&fas, &bytes);
        for want in ["main_loop", "clock", "keyboard", "reboot", "IDTR"] {
            assert!(names.iter().any(|n| n == want), "missing {want}");
        }
        assert!(fas.dump.rows.len() >= 50);
    }

    #[test]
    fn gtk_examples_from_website() {
        let (bytes, fas) = parse_rel("gtk-window/window.fas");
        assert_eq!(fas.header.output_name(&bytes).unwrap(), "window.o");
        let names = symbol_names(&fas, &bytes);
        assert!(names.iter().any(|n| n == "main"));
        assert!(names.iter().any(|n| n == "hWindow"));

        let (bytes, fas) = parse_rel("gtk-button/button.fas");
        assert_eq!(fas.header.output_name(&bytes).unwrap(), "button.o");
        let names = symbol_names(&fas, &bytes);
        for want in ["main", "DeleteEvent", "ButtonClick", "hButton", "szClickMe"] {
            assert!(names.iter().any(|n| n == want), "missing {want}");
        }
        assert!(fas.dump.rows.len() >= 50);
    }

    #[test]
    fn fat12_boot_loader_from_website() {
        let (bytes, fas) = parse_rel("phboot/BOOT.fas");
        assert_eq!(fas.header.input_name(&bytes).unwrap(), "BOOT.ASM");
        assert_eq!(fas.header.output_name(&bytes).unwrap(), "BOOT.BIN");
        assert_eq!(fas.dump.inferred_baddr(), Some(0x7c00));
        let names = symbol_names(&fas, &bytes);
        for want in [
            "start",
            "boot_code",
            "read_root",
            "read_linear",
            "sFileName",
        ] {
            assert!(names.iter().any(|n| n == want), "missing {want}");
        }
        assert!(fas.dump.rows.len() >= 100);
    }
}