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;
#[derive(Debug, Clone)]
pub struct FasFile {
pub header: Header,
pub symbols: Vec<Symbol>,
pub prep: PrepSource,
pub dump: AssemblyDump,
pub sections: Option<Vec<SectionName>>,
pub references: Option<Vec<SymbolReference>>,
}
impl FasFile {
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,
})
}
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(§ions_offset.to_le_bytes());
data[52..56].copy_from_slice(§ion_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);
}
}