r2fas 0.2.1

radare2 core plugin that loads FASM -s symbolic dumps for named labels, source lines, and comments
//! FAS file header (FAS.TXT table 1).
//!
//! Offsets in the header are file positions except input/output names, which
//! are offsets into the strings table.

use crate::error::{FasError, Result};
use crate::fas::bytes;

/// Magic stored at byte 0: `1A736166h` as little-endian (`fas` + `0x1a`).
pub const SIGNATURE: u32 = 0x1A73_6166;

/// Parsed header. Missing trailing fields (old FASM versions) stay `None`.
#[derive(Debug, Clone)]
pub struct Header {
    /// FASM major version that wrote the dump.
    pub major: u8,
    /// FASM minor version that wrote the dump.
    pub minor: u8,
    /// Size of this header in bytes (at least 16, typically 64).
    pub header_len: u16,
    /// Offset of the main input file name inside the strings table.
    pub input_name_off: u32,
    /// Offset of the output binary name inside the strings table.
    pub output_name_off: u32,
    /// File offset of the strings table.
    pub strings_off: u32,
    /// Byte length of the strings table.
    pub strings_len: u32,
    /// File offset of the 32-byte symbol array.
    pub symbols_off: u32,
    /// Byte length of the symbol array (multiple of 32).
    pub symbols_len: u32,
    /// File offset of the preprocessed source blob.
    pub preprocessed_off: u32,
    /// Byte length of the preprocessed source blob.
    pub preprocessed_len: u32,
    /// File offset of the assembly dump, if present.
    pub dump_off: Option<u32>,
    /// Byte length of the assembly dump (rows + trailing end offset).
    pub dump_len: Option<u32>,
    /// Section-name table descriptor when this FASM version supports it.
    ///
    /// A zero length means the table is supported but empty. `None` means the
    /// header predates this table and cannot provide it.
    pub sections: Option<TableRange>,
    /// Symbol-reference table descriptor when this FASM version supports it.
    ///
    /// A zero length means the table is supported but empty. `None` means the
    /// header predates this table and cannot provide it.
    pub references: Option<TableRange>,
}

/// Offset and byte length of an optional FAS table.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TableRange {
    /// File offset at which the table begins.
    pub offset: u32,
    /// Table length in bytes.
    pub length: u32,
}

impl Header {
    /// Parse table 1 from the start of `data`.
    pub fn parse(data: &[u8]) -> Result<Self> {
        if bytes::u32_at(data, 0)? != SIGNATURE {
            return Err(FasError::BadSignature);
        }
        let header_len = bytes::u16_at(data, 6)?;
        if header_len < 40 || data.len() < header_len as usize {
            return Err(FasError::Truncated("header"));
        }
        let mut h = Self {
            major: bytes::u8_at(data, 4)?,
            minor: bytes::u8_at(data, 5)?,
            header_len,
            input_name_off: bytes::u32_at(data, 8)?,
            output_name_off: bytes::u32_at(data, 12)?,
            strings_off: bytes::u32_at(data, 16)?,
            strings_len: bytes::u32_at(data, 20)?,
            symbols_off: bytes::u32_at(data, 24)?,
            symbols_len: bytes::u32_at(data, 28)?,
            preprocessed_off: bytes::u32_at(data, 32)?,
            preprocessed_len: bytes::u32_at(data, 36)?,
            dump_off: None,
            dump_len: None,
            sections: None,
            references: None,
        };
        if header_len as usize >= 48 {
            let dump_len = bytes::u32_at(data, 44)?;
            // Length 0 means assembly failed after preprocessing (FAS.TXT).
            if dump_len != 0 {
                h.dump_off = Some(bytes::u32_at(data, 40)?);
                h.dump_len = Some(dump_len);
            }
        }
        if header_len as usize >= 56 {
            h.sections = Some(TableRange {
                offset: bytes::u32_at(data, 48)?,
                length: bytes::u32_at(data, 52)?,
            });
        }
        if header_len as usize >= 64 {
            h.references = Some(TableRange {
                offset: bytes::u32_at(data, 56)?,
                length: bytes::u32_at(data, 60)?,
            });
        }
        h.validate(data)?;
        Ok(h)
    }

    /// Strings table slice.
    pub fn strings<'a>(&self, data: &'a [u8]) -> Result<&'a [u8]> {
        bytes::slice_at(data, self.strings_off, self.strings_len)
    }

    /// Symbol table slice.
    pub fn symbols<'a>(&self, data: &'a [u8]) -> Result<&'a [u8]> {
        bytes::slice_at(data, self.symbols_off, self.symbols_len)
    }

    /// Preprocessed-source slice.
    pub fn preprocessed<'a>(&self, data: &'a [u8]) -> Result<&'a [u8]> {
        bytes::slice_at(data, self.preprocessed_off, self.preprocessed_len)
    }

    /// Assembly dump slice, if the file contains one.
    pub fn dump<'a>(&self, data: &'a [u8]) -> Result<Option<&'a [u8]>> {
        match (self.dump_off, self.dump_len) {
            (Some(off), Some(len)) => Ok(Some(bytes::slice_at(data, off, len)?)),
            _ => Ok(None),
        }
    }

    /// Section-name table slice when supported by this header version.
    pub fn section_names<'a>(&self, data: &'a [u8]) -> Result<Option<&'a [u8]>> {
        self.optional_table(data, self.sections)
    }

    /// Symbol-reference table slice when supported by this header version.
    pub fn symbol_references<'a>(&self, data: &'a [u8]) -> Result<Option<&'a [u8]>> {
        self.optional_table(data, self.references)
    }

    /// Main input file name (usually relative, e.g. `hello.asm`).
    pub fn input_name<'a>(&self, data: &'a [u8]) -> Result<&'a str> {
        self.string_at(data, self.input_name_off)
    }

    /// Output binary name recorded by FASM (e.g. `hello`).
    pub fn output_name<'a>(&self, data: &'a [u8]) -> Result<&'a str> {
        self.string_at(data, self.output_name_off)
    }

    /// Resolve a strings-table offset to a C string.
    pub fn string_at<'a>(&self, data: &'a [u8], off: u32) -> Result<&'a str> {
        let table = self.strings(data)?;
        bytes::cstring_at(table, off as usize)
    }

    fn optional_table<'a>(
        &self,
        data: &'a [u8],
        table: Option<TableRange>,
    ) -> Result<Option<&'a [u8]>> {
        table
            .map(|range| bytes::slice_at(data, range.offset, range.length))
            .transpose()
    }

    fn validate(&self, data: &[u8]) -> Result<()> {
        self.strings(data)?;
        self.symbols(data)?;
        self.preprocessed(data)?;
        self.dump(data)?;
        self.section_names(data)?;
        self.symbol_references(data)?;
        Ok(())
    }
}

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

    fn header(length: u16) -> Vec<u8> {
        let mut data = vec![0u8; 96];
        data[0..4].copy_from_slice(&SIGNATURE.to_le_bytes());
        data[4] = 1;
        data[5] = 73;
        data[6..8].copy_from_slice(&length.to_le_bytes());
        data[16..20].copy_from_slice(&64u32.to_le_bytes());
        data[20..24].copy_from_slice(&2u32.to_le_bytes());
        data[24..28].copy_from_slice(&66u32.to_le_bytes());
        data[32..36].copy_from_slice(&66u32.to_le_bytes());
        data[64] = 0;
        data[65] = 0;
        data
    }

    #[test]
    fn distinguishes_missing_and_empty_optional_tables() {
        let old = Header::parse(&header(48)).unwrap();
        assert_eq!(old.sections, None);
        assert_eq!(old.references, None);

        let current = Header::parse(&header(64)).unwrap();
        assert_eq!(
            current.sections,
            Some(TableRange {
                offset: 0,
                length: 0
            })
        );
        assert_eq!(
            current.references,
            Some(TableRange {
                offset: 0,
                length: 0
            })
        );
    }

    #[test]
    fn rejects_optional_table_outside_file() {
        let mut data = header(64);
        data[48..52].copy_from_slice(&95u32.to_le_bytes());
        data[52..56].copy_from_slice(&4u32.to_le_bytes());
        assert!(matches!(
            Header::parse(&data),
            Err(FasError::Truncated("slice"))
        ));
    }
}