use alloc::boxed::Box;
use alloc::format;
use alloc::sync::Arc;
use crate::core::device::{Device, DeviceClass, PropertySpec, RealizeCtx, ResetKind};
use crate::core::error::{Error, Result};
use crate::core::props::{Props, ValueKind};
use crate::core::space::{Region, RegionRef, RomStore, RomWrite};
use crate::machine::realize::Instance;
use crate::machine::validate::{ClassSchema, PropSchema};
pub const CLASS_NAME: &str = "pc.rom";
const ERASED: u8 = 0xff;
const DEFAULT_SIZE: u64 = 128 * 1024;
const MAX_SIZE: u64 = 16 * 1024 * 1024;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Align {
Top,
Bottom,
}
#[derive(Debug)]
pub struct FirmwareRom {
store: Arc<RomStore>,
region: RegionRef,
image_len: u64,
}
impl FirmwareRom {
pub fn new(props: &Props) -> Result<FirmwareRom> {
let mut r = props.reader();
let image = r.require_media("image")?.to_bytes();
let size = r.or_size("size", DEFAULT_SIZE)?;
let align = r.or_enum("align", "top", &["top", "bottom"])?;
let align = if align == "bottom" {
Align::Bottom
} else {
Align::Top
};
r.finish()?;
FirmwareRom::from_image(&image, size, align)
}
pub fn from_image(image: &[u8], size: u64, align: Align) -> Result<FirmwareRom> {
if size == 0 || size > MAX_SIZE {
return Err(Error::Property(format!(
"property `size`: a firmware socket holds between 1 and {MAX_SIZE} bytes, not \
{size}"
)));
}
let len = image.len() as u64;
if len > size {
return Err(Error::Property(format!(
"property `image`: this socket is {size} bytes and the image is {len}; give the \
object a larger `size`, and map it over a larger window"
)));
}
let mut bytes = alloc::vec![ERASED; size as usize];
let at = match align {
Align::Top => (size - len) as usize,
Align::Bottom => 0,
};
bytes[at..at + len as usize].copy_from_slice(image);
let store = Arc::new(RomStore::new(bytes));
let region: RegionRef = Arc::new(Region::rom(
CLASS_NAME,
Arc::clone(&store),
RomWrite::Ignore,
));
Ok(FirmwareRom {
store,
region,
image_len: len,
})
}
#[must_use]
pub fn len(&self) -> u64 {
self.store.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.store.len() == 0
}
#[must_use]
pub fn image_len(&self) -> u64 {
self.image_len
}
#[must_use]
pub fn store(&self) -> &Arc<RomStore> {
&self.store
}
}
pub static CLASS: DeviceClass = DeviceClass {
name: CLASS_NAME,
version: 1,
summary: "a firmware ROM socket: a user-supplied BIOS or option ROM image",
properties: &[
PropertySpec {
name: "image",
kind: ValueKind::Media,
required: true,
summary: "the media slot the image is bound to (`--bios`, `--media vgabios=…`)",
},
PropertySpec {
name: "size",
kind: ValueKind::Size,
required: false,
summary: "how many bytes the socket decodes (default 128K)",
},
PropertySpec {
name: "align",
kind: ValueKind::Str,
required: false,
summary: "\"top\" for a system BIOS under the reset vector, \"bottom\" for an option ROM",
},
],
construct: |props| Ok(Box::new(FirmwareRom::new(props)?)),
};
impl Device for FirmwareRom {
fn class(&self) -> &'static DeviceClass {
&CLASS
}
fn realize(&self, _ctx: &mut RealizeCtx<'_>) -> Result<()> {
Ok(())
}
fn reset(&self, _kind: ResetKind) {
}
fn region(&self, name: &str) -> Option<RegionRef> {
matches!(name, "" | "rom").then(|| Arc::clone(&self.region))
}
}
impl Instance for FirmwareRom {}
pub fn register(registry: &mut crate::core::Registry) -> Result<()> {
registry.add(&CLASS)
}
pub fn bind(bindings: &mut crate::machine::Bindings) -> Result<()> {
bindings.bind(CLASS_NAME, |props| Ok(Arc::new(FirmwareRom::new(props)?)))
}
#[must_use]
pub fn schema() -> ClassSchema {
ClassSchema::new(CLASS_NAME)
.prop(PropSchema::new("image", ValueKind::Media).required())
.prop(PropSchema::new("size", ValueKind::Size))
.prop(PropSchema::new("align", ValueKind::Str).values(&["top", "bottom"]))
.region("")
.region("rom")
}
#[cfg(test)]
mod tests {
use super::*;
use alloc::string::ToString;
use alloc::vec::Vec;
fn socket_bytes(rom: &FirmwareRom) -> Vec<u8> {
let mut out = alloc::vec![0u8; rom.len() as usize];
rom.store.read_at(0, &mut out).expect("the whole socket");
out
}
use crate::core::space::{AddressSpace, MemAttrs};
use crate::core::value::Width;
#[test]
fn a_full_image_fills_the_socket() {
let image: Vec<u8> = (0..256u32).map(|i| i as u8).collect();
let rom = FirmwareRom::from_image(&image, 256, Align::Top).expect("it fits exactly");
assert_eq!(socket_bytes(&rom), image);
assert_eq!(rom.image_len(), 256);
}
#[test]
fn a_short_image_lands_at_the_top_where_the_reset_vector_is() {
let image = [0xeau8; 16];
let rom = FirmwareRom::from_image(&image, 64, Align::Top).expect("it fits");
let bytes = socket_bytes(&rom);
assert_eq!(&bytes[..48], &[ERASED; 48], "the unprogrammed half");
assert_eq!(&bytes[48..], &image, "the image, ending at the top");
}
#[test]
fn a_bottom_aligned_image_starts_where_an_option_rom_scan_looks() {
let image = [0x55u8, 0xaa, 0x4c];
let rom = FirmwareRom::from_image(&image, 64, Align::Bottom).expect("it fits");
let bytes = socket_bytes(&rom);
assert_eq!(&bytes[..3], &image, "the signature is at the bottom");
assert_eq!(&bytes[3..], &[ERASED; 61], "the rest is unprogrammed");
}
#[test]
fn an_image_larger_than_the_socket_is_refused_by_name() {
let e = FirmwareRom::from_image(&[0u8; 128], 64, Align::Top)
.expect_err("128 bytes do not fit in 64")
.to_string();
assert!(e.contains("image"), "{e}");
assert!(e.contains("64"), "{e}");
}
#[test]
fn an_implausible_socket_is_refused_by_name() {
let e = FirmwareRom::from_image(&[], 1 << 40, Align::Top)
.expect_err("a terabyte of ROM")
.to_string();
assert!(e.contains("size"), "{e}");
}
#[test]
fn a_write_to_rom_is_ignored_rather_than_faulted() {
let rom = FirmwareRom::from_image(&[0x55, 0xaa], 2, Align::Top).expect("it fits");
let space = AddressSpace::new("mem", 20);
space
.topology()
.map(rom.region("").expect("the socket's region"), 0)
.expect("nothing else is mapped");
space
.write(0, Width::U8, 0x00, MemAttrs::DEFAULT)
.expect("a write to ROM is swallowed, not refused");
assert_eq!(
space.read(0, Width::U8, MemAttrs::DEFAULT),
Ok(0x55),
"the ROM is unchanged"
);
}
#[test]
fn the_class_constructs_from_properties() {
let mut props = Props::new();
props.insert(
"image",
crate::core::props::Media::new("bios", alloc::vec![0x90u8; 4]),
);
props.insert("size", crate::core::props::Value::Size(16));
let dev = (CLASS.construct)(&props).expect("a socket");
assert_eq!(dev.class().name, CLASS_NAME);
}
}