r2fas 0.2.1

radare2 core plugin that loads FASM -s symbolic dumps for named labels, source lines, and comments
//! Little-endian bounds-checked reads from a FAS byte slice.
//!
//! All helpers return [`FasError::Truncated`] instead of panicking when the
//! requested field would extend past the end of `data`.

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

/// Read a byte at `off`.
pub fn u8_at(data: &[u8], off: usize) -> Result<u8> {
    data.get(off).copied().ok_or(FasError::Truncated("u8"))
}

/// Read a little-endian `u16` at `off`.
pub fn u16_at(data: &[u8], off: usize) -> Result<u16> {
    let b: [u8; 2] = data
        .get(off..off + 2)
        .ok_or(FasError::Truncated("u16"))?
        .try_into()
        .map_err(|_| FasError::Truncated("u16"))?;
    Ok(u16::from_le_bytes(b))
}

/// Read a little-endian `u32` at `off`.
pub fn u32_at(data: &[u8], off: usize) -> Result<u32> {
    let b: [u8; 4] = data
        .get(off..off + 4)
        .ok_or(FasError::Truncated("u32"))?
        .try_into()
        .map_err(|_| FasError::Truncated("u32"))?;
    Ok(u32::from_le_bytes(b))
}

/// Read a little-endian `u64` at `off`.
pub fn u64_at(data: &[u8], off: usize) -> Result<u64> {
    let b: [u8; 8] = data
        .get(off..off + 8)
        .ok_or(FasError::Truncated("u64"))?
        .try_into()
        .map_err(|_| FasError::Truncated("u64"))?;
    Ok(u64::from_le_bytes(b))
}

/// Return `data[off..off + len]`, checking both overflow and length.
pub fn slice_at(data: &[u8], off: u32, len: u32) -> Result<&[u8]> {
    let start = off as usize;
    let end = start
        .checked_add(len as usize)
        .ok_or(FasError::Truncated("slice overflow"))?;
    data.get(start..end).ok_or(FasError::Truncated("slice"))
}

/// Read a NUL-terminated Latin-1/ASCII string starting at `off`.
pub fn cstring_at(data: &[u8], off: usize) -> Result<&str> {
    let tail = data.get(off..).ok_or(FasError::Truncated("cstring"))?;
    let n = tail
        .iter()
        .position(|&b| b == 0)
        .ok_or(FasError::BadString("unterminated"))?;
    std::str::from_utf8(&tail[..n]).map_err(|_| FasError::BadString("utf8"))
}

/// Read a Pascal string (byte length followed by that many bytes) at `off`.
pub fn pascal_at(data: &[u8], off: usize) -> Result<&str> {
    let len = u8_at(data, off)? as usize;
    let bytes = data
        .get(off + 1..off + 1 + len)
        .ok_or(FasError::Truncated("pascal"))?;
    std::str::from_utf8(bytes).map_err(|_| FasError::BadString("pascal utf8"))
}

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

    #[test]
    fn reads_scalars() {
        let d = [0x78, 0x56, 0x34, 0x12, 0x00, 0x00, 0x00, 0x00];
        assert_eq!(u8_at(&d, 0).unwrap(), 0x78);
        assert_eq!(u16_at(&d, 0).unwrap(), 0x5678);
        assert_eq!(u32_at(&d, 0).unwrap(), 0x12345678);
    }

    #[test]
    fn truncated_is_err() {
        assert!(u32_at(&[1, 2], 0).is_err());
        assert!(cstring_at(b"ab", 0).is_err());
    }
}