kr580 1.0.0

Desktop KR580VM80 / Intel 8080 emulator.
Documentation
use k580_core::{Cpu8080State, Memory64K};

pub const LEGACY_LENGTH: usize = Memory64K::SIZE + 13;

#[derive(Debug)]
pub enum ProgramError {
    NotA580File,
    EmptyFile,
    WrongSize { size: usize },
    InvalidLegacyTrailer,
    Io(std::io::Error),
}

impl std::fmt::Display for ProgramError {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        match self {
            ProgramError::NotA580File => write!(f, "not a .580 file"),
            ProgramError::EmptyFile => write!(f, "file is empty"),
            ProgramError::WrongSize { size } => {
                write!(f, "expected {LEGACY_LENGTH} bytes, got {size}")
            }
            ProgramError::InvalidLegacyTrailer => {
                write!(f, "legacy .580 trailer is missing the FF FF end marker")
            }
            ProgramError::Io(err) => write!(f, "I/O error: {err}"),
        }
    }
}

impl std::error::Error for ProgramError {
    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
        match self {
            ProgramError::Io(err) => Some(err),
            _ => None,
        }
    }
}

impl From<std::io::Error> for ProgramError {
    fn from(err: std::io::Error) -> Self {
        ProgramError::Io(err)
    }
}

pub struct ProgramSerializer;

impl ProgramSerializer {
    pub fn save_file(
        path: impl AsRef<std::path::Path>,
        state: &Cpu8080State,
    ) -> Result<(), ProgramError> {
        let mut out = Vec::with_capacity(LEGACY_LENGTH);
        out.extend_from_slice(state.memory.as_slice());
        out.resize(out.len() + 9, 0);
        out.extend_from_slice(&state.pc.to_le_bytes());
        out.push(0xFF);
        out.push(0xFF);
        std::fs::write(path, out)?;
        Ok(())
    }

    pub fn load_file(path: impl AsRef<std::path::Path>) -> Result<Cpu8080State, ProgramError> {
        validate_path(path.as_ref())?;
        let bytes = std::fs::read(path)?;
        if bytes.is_empty() {
            return Err(ProgramError::EmptyFile);
        }
        if bytes.len() != LEGACY_LENGTH {
            return Err(ProgramError::WrongSize { size: bytes.len() });
        }
        Self::from_legacy_bytes(&bytes)
    }

    fn from_legacy_bytes(bytes: &[u8]) -> Result<Cpu8080State, ProgramError> {
        let trailer_start = Memory64K::SIZE;
        let trailer = &bytes[trailer_start..];
        if trailer[11] != 0xFF || trailer[12] != 0xFF {
            return Err(ProgramError::InvalidLegacyTrailer);
        }
        let mut state = Cpu8080State::default();
        state
            .memory
            .as_mut_slice()
            .copy_from_slice(&bytes[..trailer_start]);
        state.pc = u16::from_le_bytes([trailer[9], trailer[10]]);
        Ok(state)
    }
}

fn validate_path(path: &std::path::Path) -> Result<(), ProgramError> {
    match path.extension().and_then(|e| e.to_str()) {
        Some(ext) if ext.eq_ignore_ascii_case("580") => Ok(()),
        _ => Err(ProgramError::NotA580File),
    }
}