tibrs 0.1.3

Provides functionality to compile and decompile tibasic source for TI graphing calculators


use crate::TibToken;
use anyhow::{Context, Result};
use itertools::Itertools;

fn pad_bytes_42(input: &[u8]) -> [u8; 42] {
    let mut buf: [u8; 42] = [0; 42];
    let cpy_len = input.len().min(24);

    buf[..cpy_len].copy_from_slice(&input[..cpy_len]);
    buf
}

fn pad_bytes_8(input: &[u8]) -> [u8; 8] {
    let mut buf: [u8; 8] = [0; 8];
    let cpy_len = input.len().min(8);

    buf[..cpy_len].copy_from_slice(&input[..cpy_len]);
    buf
}

struct TIVariableSection {
    name: [u8; 8],
    data: Box<[u8]>,
}

impl TIVariableSection {
    fn get_checksum(&self) -> u16 {
        let sum: u64 = self.data.iter().fold(0, |sum, cur| sum + (*cur as u64));
        (sum & 0xFFFF).try_into().unwrap() // take only lower 16 bits
    }
}

impl From<TIVariableSection> for Box<[u8]> {
    fn from(value: TIVariableSection) -> Self {
        let mut buf = Vec::<u8>::new();

        buf.extend_from_slice(&[0x0D, 0x0]); // section signature
        buf.extend_from_slice(&(value.data.len() as u16 + 2).to_le_bytes());
        buf.extend_from_slice(&[0x05]); // section type (PRGM)
        buf.extend_from_slice(&value.name);
        buf.extend_from_slice(&[0x0]); // version
        buf.extend_from_slice(&[0x0]); // flag: 0x0 indicates unarchived
        buf.extend_from_slice(&(value.data.len() as u16 + 2).to_le_bytes());

        buf.extend_from_slice(&(value.data.len() as u16).to_le_bytes());
        buf.extend_from_slice(&value.data);

        buf.into_boxed_slice()
    }
}

pub fn compile(tokens: Vec<TibToken>, prgm_name: &[u8]) -> Result<Vec<u8>> {
    // NOTE: TI uses little-endian for all u16's

    let comment: [u8; 42] = pad_bytes_42(b"Generated by https://docs.rs/tibrs");

    let data_section = TIVariableSection {
        name: pad_bytes_8(prgm_name),
        data: {
            let mut buf = Vec::<u8>::new();

            for tok in tokens {
                buf.extend_from_slice(
                    hex::decode(
                        tok.hex_str
                            .strip_prefix("0x")
                            .with_context(|| format!("Malformated hex: '{}'", tok.hex_str))?,
                    )?
                    .into_iter()
                    .rev()
                    .collect_vec()
                    .as_slice(),
                );
            }

            buf.into()
        },
    };

    let mut buf = Vec::<u8>::new();

    // write file header

    buf.extend_from_slice("**TI83F*".as_bytes()); // file format sig
    buf.extend_from_slice(&[0x1A, 0x0A, 0x0]); // ext sig
    buf.extend_from_slice(&comment);

    let checksum = data_section.get_checksum();
    let data_buf: Box<[u8]> = data_section.into();

    buf.extend_from_slice(&(data_buf.len() as u16).to_le_bytes());

    buf.extend_from_slice(&data_buf);

    buf.extend_from_slice(&checksum.to_le_bytes());

    Ok(buf)
}