Skip to main content

ud_format/
raw.rs

1//! Raw flat-binary container.
2//!
3//! A `RawImage` is a contiguous byte buffer mapped at a fixed
4//! virtual address. The address space outside the buffer is not
5//! materialised. This is the format the 6502 stack uses — a 256-byte
6//! WozMon ROM at $FF00 is just `RawImage::new(bytes, 0xFF00)`.
7//!
8//! Vectors aren't part of the container itself; they live in the
9//! `.ud` `@module` block and are resolved by reading specific bytes
10//! out of the image (e.g. `read_u16_le(0xFFFC)` for the 6502 reset
11//! vector). The `read_*` helpers below are the convenience accessors.
12
13#![allow(clippy::cast_possible_truncation)]
14
15use ud_core::VAddr;
16
17/// Errors returned by `RawImage` accessors.
18#[derive(Debug, thiserror::Error, Clone, Copy, PartialEq, Eq)]
19pub enum Error {
20    #[error("address {addr:#06x} is outside the image (load=[{load:#06x}, {end:#06x}))")]
21    OutOfRange { addr: u64, load: u64, end: u64 },
22    #[error("multi-byte read at {addr:#06x} ({len} bytes) crosses the end of the image")]
23    Truncated { addr: u64, len: usize },
24}
25
26pub type Result<T, E = Error> = std::result::Result<T, E>;
27
28/// A flat byte image mapped at `load_addr`.
29#[derive(Debug, Clone)]
30pub struct RawImage {
31    pub bytes: Vec<u8>,
32    pub load_addr: u64,
33}
34
35impl RawImage {
36    /// Construct an image from `bytes` mapped at `load_addr`.
37    #[must_use]
38    pub fn new(bytes: Vec<u8>, load_addr: u64) -> Self {
39        Self { bytes, load_addr }
40    }
41
42    /// First valid address.
43    #[must_use]
44    pub fn start(&self) -> u64 {
45        self.load_addr
46    }
47
48    /// One past the last valid address.
49    #[must_use]
50    pub fn end(&self) -> u64 {
51        self.load_addr + self.bytes.len() as u64
52    }
53
54    /// Whether `addr` lies inside the image.
55    #[must_use]
56    pub fn contains(&self, addr: u64) -> bool {
57        addr >= self.start() && addr < self.end()
58    }
59
60    /// Translate a virtual address into a byte offset, or `None` if
61    /// the address is outside the image.
62    #[must_use]
63    pub fn offset_of(&self, addr: u64) -> Option<usize> {
64        if self.contains(addr) {
65            Some((addr - self.load_addr) as usize)
66        } else {
67            None
68        }
69    }
70
71    /// Read one byte at `addr`.
72    pub fn read_u8(&self, addr: u64) -> Result<u8> {
73        let off = self.offset_of(addr).ok_or(Error::OutOfRange {
74            addr,
75            load: self.start(),
76            end: self.end(),
77        })?;
78        Ok(self.bytes[off])
79    }
80
81    /// Read a little-endian 16-bit word at `addr`. Used to read 6502
82    /// vectors (`read_u16_le(0xFFFC)` is the reset vector).
83    pub fn read_u16_le(&self, addr: u64) -> Result<u16> {
84        let off = self.offset_of(addr).ok_or(Error::OutOfRange {
85            addr,
86            load: self.start(),
87            end: self.end(),
88        })?;
89        if off + 2 > self.bytes.len() {
90            return Err(Error::Truncated { addr, len: 2 });
91        }
92        Ok(u16::from_le_bytes([self.bytes[off], self.bytes[off + 1]]))
93    }
94
95    /// Borrow a slice starting at `addr`. Returns `None` if `addr` is
96    /// outside the image.
97    #[must_use]
98    pub fn slice_at(&self, addr: u64) -> Option<&[u8]> {
99        let off = self.offset_of(addr)?;
100        Some(&self.bytes[off..])
101    }
102
103    /// Bounds of the image as a `VAddr` pair, in (start, end) order.
104    #[must_use]
105    pub fn bounds(&self) -> (VAddr, VAddr) {
106        (VAddr(self.start()), VAddr(self.end()))
107    }
108}
109
110#[cfg(test)]
111mod tests {
112    use super::*;
113
114    #[test]
115    fn read_and_slice() {
116        let img = RawImage::new(vec![0xAA, 0xBB, 0xCC, 0xDD], 0xFF00);
117        assert_eq!(img.read_u8(0xFF00).unwrap(), 0xAA);
118        assert_eq!(img.read_u16_le(0xFF00).unwrap(), 0xBBAA);
119        assert_eq!(img.slice_at(0xFF02), Some(&[0xCC, 0xDD][..]));
120        assert!(matches!(img.read_u8(0xFEFF), Err(Error::OutOfRange { .. })));
121    }
122
123    /// Apple I reset vector: $FFFC reads the low byte of the WozMon
124    /// entry address ($FF00). This is the canonical sanity check.
125    #[test]
126    fn reset_vector_layout() {
127        // 256-byte buffer; $FFFC/$FFFD = $00 $FF.
128        let mut bytes = vec![0; 256];
129        bytes[0xFC] = 0x00; // LSB
130        bytes[0xFD] = 0xFF; // MSB
131        let img = RawImage::new(bytes, 0xFF00);
132        assert_eq!(img.read_u16_le(0xFFFC).unwrap(), 0xFF00);
133    }
134}