use crate::Error;
use crate::plugin::{ResolvedPlugin, plugin_to_chip_set_config};
use onerom_config::chip::{CHIP_TYPE_NAMES_PLUGINS, ChipFunction, ChipType, ControlLineType};
use onerom_config::fw::FirmwareVersion;
use onerom_config::hw::{Board, Model};
use onerom_gen::MIN_SUPPORTED_FIRMWARE_VERSION_V2;
use onerom_gen::compat::{check_chip_set_on_board, default_cs_config, supported_chips};
use onerom_gen::{
ChipConfig, ChipSetConfig, ChipSetType, ChipTypeSpec, Config, CsLogic, FileFormat, FireConfig,
FireCpuFreq, FireVreg, FirmwareConfig, LedConfig, LoadAddress, SizeHandling, Transform,
parse_transform_list, requires_half_select_cs1,
};
const DEFAULT_CONFIG_DESCRIPTION: &str = "Created by the One ROM CLI";
pub struct GlobalConfig {
pub config_name: Option<String>,
pub config_description: Option<String>,
pub instance_name: Option<String>,
pub serial_override: Option<String>,
pub boot_logging: Option<bool>,
pub disable_swd: Option<bool>,
pub turbo_boot: Option<bool>,
}
pub struct ConfirmationsRequired {
pub cpu_freq: bool,
pub vreg: bool,
}
pub fn check_confirmations(slots: &[SlotSpec]) -> ConfirmationsRequired {
ConfirmationsRequired {
cpu_freq: slots.iter().any(|s| {
s.cpu_freq
.map(|f| f > FireCpuFreq::stock_value())
.unwrap_or(false)
}),
vreg: slots.iter().any(|s| {
s.vreg
.as_ref()
.map(|v| *v > FireVreg::stock_value())
.unwrap_or(false)
}),
}
}
pub fn check_slot_confirmations(
slots: &[String],
board: &Board,
) -> Result<ConfirmationsRequired, Error> {
Ok(check_confirmations(&parse_slots(slots, board)?))
}
fn expand_tilde(path: &str) -> std::borrow::Cow<'_, str> {
if let Some(rest) = path.strip_prefix("~/")
&& let Some(home) = std::env::var_os("HOME")
{
format!("{}/{}", home.to_string_lossy(), rest).into()
} else {
path.into()
}
}
#[derive(Debug)]
pub struct SlotSpec {
pub file: Option<String>,
pub label: Option<String>,
pub chip_type: ChipTypeSpec,
pub cs1: Option<CsLogic>,
pub cs2: Option<CsLogic>,
pub cs3: Option<CsLogic>,
pub cs4: Option<CsLogic>,
size_handling: Option<SizeHandling>,
pub cpu_freq: Option<FireCpuFreq>,
pub vreg: Option<FireVreg>,
pub led: Option<bool>,
pub force_16bit: Option<bool>,
pub format: Option<FileFormat>,
pub load_address: Option<LoadAddress>,
pub transform: Vec<Transform>,
}
fn parse_cs_logic(slot: &str, key: &str, value: &str) -> Result<CsLogic, Error> {
CsLogic::try_from_str(value).ok_or_else(|| {
let supported = CsLogic::supported_values()
.iter()
.map(|v| v.name())
.collect::<Vec<_>>()
.join("|");
Error::InvalidArgument(
"--slot".to_string(),
format!(
"Invalid CS logic '{value}': expected {key}={supported}|0|1\n --slot '{slot}'"
),
)
})
}
fn parse_size_handling(slot: &str, _key: &str, value: &str) -> Result<SizeHandling, Error> {
serde_json::from_str::<SizeHandling>(&format!("\"{value}\"")).map_err(|_| {
let supported_variants = SizeHandling::supported_values()
.iter()
.map(|v| {
serde_json::to_string(v)
.unwrap()
.trim_matches('"')
.to_string()
})
.collect::<Vec<_>>()
.join(", ");
Error::InvalidArgument(
"--slot".to_string(),
format!(
"Invalid size-handling '{value}'\n --slot '{slot}'\n Supported values: {supported_variants}"
),
)
})
}
fn parse_format(slot: &str, value: &str) -> Result<FileFormat, Error> {
FileFormat::try_from_str(value).ok_or_else(|| {
Error::InvalidArgument(
"--slot".to_string(),
format!(
"Invalid format '{value}'\n --slot '{slot}'\n Supported values: binary, ihex"
),
)
})
}
fn parse_load_address(slot: &str, value: &str) -> Result<LoadAddress, Error> {
LoadAddress::parse_str(value).map_err(|e| {
Error::InvalidArgument("--slot".to_string(), format!("{e}\n --slot '{slot}'"))
})
}
fn parse_transform(slot: &str, value: &str) -> Result<Vec<Transform>, Error> {
parse_transform_list(value).map_err(|e| {
Error::InvalidArgument("--slot".to_string(), format!("{e}\n --slot '{slot}'"))
})
}
fn parse_bool(slot: &str, key: &str, value: &str) -> Result<bool, Error> {
match value.to_lowercase().as_str() {
"true" | "on" | "1" => Ok(true),
"false" | "off" | "0" => Ok(false),
other => Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Invalid boolean '{other}': expected {key}=true|false|on|off|1|0\n --slot '{slot}'"
),
)),
}
}
fn parse_cpu_freq(slot: &str, key: &str, value: &str) -> Result<FireCpuFreq, Error> {
let digits = if value.to_lowercase().ends_with("mhz") {
&value[..value.len() - 3]
} else {
value
};
let mhz = digits.parse::<u16>().map_err(|_| {
Error::InvalidArgument(
"--slot".to_string(),
format!("Invalid CPU frequency '{value}': expected formats {key}=150|150MHz\n --slot '{slot}'"),
)
})?;
FireCpuFreq::mhz(mhz).map_err(|_| {
Error::InvalidArgument(
"--slot".to_string(),
format!(
"CPU frequency {mhz}MHz out of range ({}-{}MHz)\n --slot '{slot}'",
FireCpuFreq::MIN_MHZ,
FireCpuFreq::MAX_MHZ,
),
)
})
}
fn parse_vreg(slot: &str, key: &str, value: &str) -> Result<FireVreg, Error> {
let stripped = if value.ends_with('v') || value.ends_with('V') {
&value[..value.len() - 1]
} else {
value
};
let canonical = match stripped.split_once('.') {
Some((int, frac)) => {
let padded = format!("{frac:0<2}");
if padded.len() > 2 {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Invalid VReg '{value}': too many decimal places, max 2\n --slot '{slot}'"
),
));
}
format!("{int}.{padded}V")
}
None => {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Invalid VReg '{value}': expected format {key}=1.1|1.10|1.10V\n --slot '{slot}'"
),
));
}
};
serde_json::from_str::<FireVreg>(&format!("\"{canonical}\"")).map_err(|_| {
let levels = FireVreg::supported_levels()
.iter()
.map(|v| {
serde_json::to_string(v)
.unwrap()
.trim_matches('"')
.to_string()
})
.collect::<Vec<_>>()
.join(", ");
Error::InvalidArgument(
"--slot".to_string(),
format!(
"Unsupported VReg '{value}'\n --slot '{slot}'\n Supported levels: {levels}"
),
)
})
}
const SLOT_KEYS: &[&str] = &[
"file",
"label",
"type",
"cs1",
"cs2",
"cs3",
"cs4",
"size-handling",
"size",
"cpu-freq",
"cpu-vreg",
"led",
"force-16-bit",
"format",
"load-address",
"transform",
];
fn parse_slot(slot: &str, board: &Board) -> Result<SlotSpec, Error> {
let mut file = None;
let mut label = None;
let mut chip_type_str = None;
let mut cs1 = None;
let mut cs2 = None;
let mut cs3 = None;
let mut cs4 = None;
let mut size_handling = None;
let mut cpu_freq = None;
let mut vreg = None;
let mut led = None;
let mut force_16bit = None;
let mut format = None;
let mut load_address = None;
let mut transform = Vec::new();
let mut seen = std::collections::HashSet::new();
for part in slot.split(',') {
let (key, value) = part.split_once('=').ok_or_else(|| {
Error::InvalidArgument("--slot".to_string(), format!("Slot key '{part}' is missing a value - expected '{part}=<value>'\n --slot '{slot}'"))
})?;
let key = key.trim();
if !seen.insert(key) {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!("Duplicate slot key '{key}' found.\n --slot '{slot}'"),
));
}
match key {
"file" | "path" | "url" => file = Some(expand_tilde(value).into_owned()),
"label" | "name" => label = Some(value.to_string()),
"type" | "rom-type" | "rom_type" | "chip_type" | "chip-type" => {
chip_type_str = Some(value.to_string())
}
"cs1" => cs1 = Some(parse_cs_logic(slot, key, value)?),
"cs2" => cs2 = Some(parse_cs_logic(slot, key, value)?),
"cs3" => cs3 = Some(parse_cs_logic(slot, key, value)?),
"cs4" => cs4 = Some(parse_cs_logic(slot, key, value)?),
"size-handling" | "size_handling" | "size" => {
size_handling = Some(parse_size_handling(slot, key, value)?)
}
"cpu" | "freq" | "frequency" | "cpu-freq" | "cpu_freq" | "cpu_frequency"
| "cpu-frequency" => cpu_freq = Some(parse_cpu_freq(slot, key, value)?),
"vreg" | "cpu-vreg" | "cpu_vreg" => vreg = Some(parse_vreg(slot, key, value)?),
"led" | "status_led" | "status-led" => led = Some(parse_bool(slot, key, value)?),
"16bit" | "force_16bit" | "force_16_bit" | "force-16bit" | "force-16-bit" => {
force_16bit = Some(parse_bool(slot, key, value)?)
}
"format" => format = Some(parse_format(slot, value)?),
"load_address" | "load-address" | "load_addr" => {
load_address = Some(parse_load_address(slot, value)?)
}
"transform" | "trans" => transform = parse_transform(slot, value)?,
other => {
let supported_keys = SLOT_KEYS.join(", ");
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Unrecognised slot key '{other}'\n --slot '{slot}'\n Supported keys: {supported_keys}"
),
));
}
}
}
let chip_type_str = chip_type_str.ok_or_else(|| {
Error::InvalidArgument(
"--slot".to_string(),
format!("slot missing 'type' key\n --slot '{slot}'"),
)
})?;
let chip_type = ChipType::try_from_str(&chip_type_str).ok_or_else(|| {
let supported = candidate_chip_names_for_board(board);
Error::UnsupportedChipType(chip_type_str.clone(), supported)
})?;
if chip_type.chip_function() != ChipFunction::Ram && file.is_none() {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!("Missing 'file' key for ROM chip.\n --slot '{slot}'"),
));
}
validate_cs_lines(slot, &chip_type, cs1, cs2, cs3, cs4)?;
if force_16bit.is_some() && board.chip_pins() != 40 {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!("force-16-bit is only valid on 40-pin boards\n --slot '{slot}'"),
));
}
if load_address.is_some() && format != Some(FileFormat::IntelHex) {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!("load-address is only valid with format=ihex\n --slot '{slot}'"),
));
}
Ok(SlotSpec {
file,
label,
chip_type: ChipTypeSpec::new(chip_type_str, chip_type),
cs1,
cs2,
cs3,
cs4,
size_handling,
cpu_freq,
vreg,
led,
force_16bit,
format,
load_address,
transform,
})
}
fn validate_cs_lines(
slot: &str,
chip_type: &ChipType,
cs1: Option<CsLogic>,
cs2: Option<CsLogic>,
cs3: Option<CsLogic>,
cs4: Option<CsLogic>,
) -> Result<(), Error> {
let cs_values = [("cs1", cs1), ("cs2", cs2), ("cs3", cs3), ("cs4", cs4)];
for line in chip_type.control_lines() {
let supplied = cs_values
.iter()
.find(|(name, _)| *name == line.name)
.and_then(|(_, v)| *v);
match line.line_type {
ControlLineType::Configurable if supplied.is_none() => {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Chip type {} requires {} to be specified\n --slot '{slot}'",
chip_type.name(),
line.name
),
));
}
ControlLineType::FixedActiveLow | ControlLineType::FixedActiveHigh if matches!(supplied, Some(logic) if logic != CsLogic::Ignore) =>
{
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Chip type {} has fixed {} {}, do not specify it\n --slot '{slot}'",
chip_type.name(),
if line.line_type == ControlLineType::FixedActiveHigh {
"active-high"
} else {
"active-low"
},
line.name
),
));
}
ControlLineType::Configurable
| ControlLineType::FixedActiveLow
| ControlLineType::FixedActiveHigh => {}
}
}
for (cs_name, user) in &cs_values {
if *cs_name == "cs1" && requires_half_select_cs1(chip_type) {
continue;
}
if user.is_some() && !chip_type.control_lines().iter().any(|l| l.name == *cs_name) {
return Err(Error::InvalidArgument(
"--slot".to_string(),
format!(
"Chip type {} has no {} line\n --slot '{slot}'",
chip_type.name(),
cs_name
),
));
}
}
Ok(())
}
pub fn emulatable_chip_names(board: &Board) -> Vec<&'static str> {
supported_chips(*board, ChipSetType::Single, 1)
.iter()
.map(|e| e.alias)
.collect()
}
fn candidate_chip_names_for_board(board: &Board) -> String {
let mut names: Vec<&'static str> = board.supported_chip_type_names().to_vec();
names.extend(board.extra_chip_types().iter().map(|c| c.name()));
if board.model() == Model::Fire {
names.extend(emulatable_chip_names(board));
}
names.extend_from_slice(CHIP_TYPE_NAMES_PLUGINS);
names.sort_unstable();
names.dedup();
names.join(", ")
}
pub fn supported_chip_names_for_board(board: &Board, version: &FirmwareVersion) -> String {
let mut names = if serves_v2(version) {
emulatable_chip_names(board)
} else {
let mut v1: Vec<&'static str> = board.supported_chip_type_names().to_vec();
v1.extend(board.extra_chip_types().iter().map(|c| c.name()));
v1.sort_unstable();
v1.dedup();
v1
};
names.extend_from_slice(CHIP_TYPE_NAMES_PLUGINS);
names.join(", ")
}
fn serves_v2(version: &FirmwareVersion) -> bool {
*version >= MIN_SUPPORTED_FIRMWARE_VERSION_V2
}
pub fn check_slot_chip_types(
slots: &[SlotSpec],
board: &Board,
version: &FirmwareVersion,
) -> Result<(), Error> {
if serves_v2(version) && board.model() != Model::Fire {
return Err(Error::IceBoardUnsupported(board.name().to_string()));
}
for slot in slots {
let chip_type = slot.chip_type.resolved();
if chip_type.is_plugin() {
continue;
}
let servable = if serves_v2(version) {
check_chip_set_on_board(
*board,
chip_type,
ChipSetType::Single,
1,
default_cs_config(chip_type),
)
.is_ok()
} else {
board.allows_chip_type(chip_type)
};
if !servable {
return Err(Error::UnsupportedBoardChipType(
chip_type.name().to_string(),
chip_type.aliases().join(", "),
supported_chip_names_for_board(board, version),
));
}
}
Ok(())
}
pub fn parse_slots(slots: &[String], board: &Board) -> Result<Vec<SlotSpec>, Error> {
slots.iter().map(|s| parse_slot(s, board)).collect()
}
fn slot_to_chip_config(slot: &SlotSpec) -> ChipConfig {
let mut chip = ChipConfig::new(
slot.file.clone().unwrap_or_default(),
slot.chip_type.clone(),
);
chip.cs1 = slot.cs1;
chip.cs2 = slot.cs2;
chip.cs3 = slot.cs3;
chip.cs4 = slot.cs4;
chip.size_handling = slot.size_handling.clone().unwrap_or_default();
chip.label = slot.label.clone();
chip.format = slot.format.unwrap_or_default();
chip.load_address = slot.load_address.unwrap_or_default();
chip.transform = slot.transform.clone();
chip
}
fn slot_to_firmware_overrides(slot: &SlotSpec) -> Option<FirmwareConfig> {
let has_fire = slot.cpu_freq.is_some() || slot.vreg.is_some() || slot.force_16bit.is_some();
let has_led = slot.led.is_some();
if !has_fire && !has_led {
return None;
}
let fire = has_fire.then(|| FireConfig {
cpu_freq: slot.cpu_freq,
overclock: slot.cpu_freq.map(|f| f > FireCpuFreq::stock_value()),
vreg: slot.vreg.clone(),
force_16_bit: slot.force_16bit.unwrap_or(false),
..Default::default()
});
Some(FirmwareConfig {
ice: None,
fire,
led: slot.led.map(|enabled| LedConfig { enabled }),
swd: None,
serve_alg_params: None,
})
}
pub fn slots_to_config_json(
plugins: &[ResolvedPlugin],
slots: &[SlotSpec],
global_config: Option<&GlobalConfig>,
) -> Result<String, Error> {
let mut sorted_plugins: Vec<&ResolvedPlugin> = plugins.iter().collect();
sorted_plugins.sort_by_key(|p| p.plugin_type.slot_index());
let mut chip_sets: Vec<ChipSetConfig> = sorted_plugins
.iter()
.map(|p| plugin_to_chip_set_config(&p.file(), p.plugin_type, p.size))
.collect::<Result<Vec<_>, _>>()?;
for slot in slots {
let mut chip_set = ChipSetConfig::new(ChipSetType::Single, vec![slot_to_chip_config(slot)]);
chip_set.firmware_overrides = slot_to_firmware_overrides(slot);
chip_sets.push(chip_set);
}
let description = global_config
.and_then(|c| c.config_description.clone())
.unwrap_or(DEFAULT_CONFIG_DESCRIPTION.to_string());
let mut config = Config::new(description, chip_sets);
config.name = global_config.and_then(|c| c.config_name.clone());
config.instance_name = global_config.and_then(|c| c.instance_name.clone());
config.serial_override = global_config.and_then(|c| c.serial_override.clone());
config.boot_logging = global_config.is_some_and(|c| c.boot_logging.unwrap_or(false));
config.swd_enabled = !global_config.is_some_and(|c| c.disable_swd.unwrap_or(false));
config.turbo_boot = global_config.is_some_and(|c| c.turbo_boot.unwrap_or(false));
serde_json::to_string_pretty(&config).map_err(|e| Error::Other(e.to_string()))
}
pub fn inject_plugins_into_config(
json: String,
plugins: &[ResolvedPlugin],
) -> Result<String, Error> {
if plugins.is_empty() {
return Ok(json);
}
let mut config: Config = serde_json::from_str(&json)
.map_err(|e| Error::Other(format!("Failed to parse config JSON: {e}")))?;
if config
.chip_sets
.iter()
.flat_map(|cs| cs.chips.iter())
.any(|c| c.chip_type.resolved().is_plugin())
{
return Err(Error::Other(
"The provided config file already defines a plugin; remove it from \
the config, or drop --plugin."
.to_string(),
));
}
let mut sorted_plugins: Vec<&ResolvedPlugin> = plugins.iter().collect();
sorted_plugins.sort_by_key(|p| p.plugin_type.slot_index());
let mut chip_sets: Vec<ChipSetConfig> = sorted_plugins
.iter()
.map(|p| plugin_to_chip_set_config(&p.file(), p.plugin_type, p.size))
.collect::<Result<Vec<_>, _>>()?;
chip_sets.append(&mut config.chip_sets);
config.chip_sets = chip_sets;
serde_json::to_string_pretty(&config).map_err(|e| Error::Other(e.to_string()))
}
pub fn save_config(path: &str, json: &str) -> Result<(), Error> {
std::fs::write(path, json).map_err(|e| Error::io(path, e))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::plugin::{PluginType, PluginVersion, ResolvedSource};
const ROM_ONLY: &str = r#"{
"version": 1,
"name": "t",
"description": "d",
"chip_sets": [
{ "type": "single", "chips": [
{ "file": "http://x/rom.bin", "type": "23128",
"cs1": "active_low", "cs2": "active_low", "cs3": "active_high" }
] }
]
}"#;
fn plugin(plugin_type: PluginType) -> ResolvedPlugin {
ResolvedPlugin {
plugin_type,
name: "p".to_string(),
version: PluginVersion::new(0, 1, 0, 0),
size: 1024,
source: ResolvedSource::File {
path: "/tmp/p.bin".to_string(),
},
}
}
fn chip_types(json: &str) -> Vec<ChipType> {
let config: Config = serde_json::from_str(json).expect("valid config");
config
.chip_sets
.iter()
.flat_map(|cs| cs.chips.iter())
.map(|c| c.chip_type.resolved())
.collect()
}
const V1: FirmwareVersion = FirmwareVersion::new(0, 6, 14, 0);
const V2: FirmwareVersion = MIN_SUPPORTED_FIRMWARE_VERSION_V2;
fn gate(board: &str, slot: &str, version: &FirmwareVersion) -> Result<(), Error> {
let board = Board::try_from_str(board).unwrap();
let parsed = parse_slots(&[slot.to_string()], &board).expect("slot parses");
check_slot_chip_types(&parsed, &board, version)
}
#[test]
fn a_fly_lead_chip_is_servable_only_under_v2() {
assert!(gate("fire-24-a", "file=rom.bin,type=2764", &V2).is_ok());
assert!(gate("fire-24-a", "file=rom.bin,type=2764", &V1).is_err());
}
#[test]
fn an_overhang_chip_outside_the_extras_list_is_servable_only_under_v2() {
assert!(gate("fire-28-a", "file=rom.bin,type=28C16", &V2).is_ok());
assert!(gate("fire-28-a", "file=rom.bin,type=28C16", &V1).is_err());
}
#[test]
fn sram_is_servable_under_v1_and_tracks_the_v2_chip_list() {
assert!(gate("fire-24-a", "type=6116", &V1).is_ok());
assert_eq!(
gate("fire-24-a", "type=6116", &V2).is_ok(),
onerom_gen::SUPPORTED_CHIP_TYPES_V2.contains(&ChipType::Chip6116),
);
}
#[test]
fn an_unservable_chip_is_refused_with_the_right_alternatives() {
let err = gate("fire-24-a", "file=rom.bin,type=27C400", &V2).unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("cannot serve chip types 27C400"), "{msg}");
assert!(msg.contains("2364"), "{msg}");
assert!(msg.contains("2764"), "{msg}");
let msg = format!(
"{}",
gate("fire-24-a", "file=rom.bin,type=27C400", &V1).unwrap_err()
);
assert!(msg.contains("2364"), "{msg}");
assert!(!msg.contains("2764"), "{msg}");
}
#[test]
fn a_plugin_chip_type_passes_both_gates() {
for version in [&V1, &V2] {
assert!(gate("fire-24-a", "file=plugin.bin,type=SystemPlugin", version).is_ok());
}
}
#[test]
fn an_ice_board_is_refused_by_the_v2_gate_but_served_by_v1() {
let err = gate("ice-24-e", "file=rom.bin,type=2364,cs1=active_low", &V2).unwrap_err();
assert!(matches!(err, Error::IceBoardUnsupported(_)), "{err}");
assert!(gate("ice-24-e", "file=rom.bin,type=2364,cs1=active_low", &V1).is_ok());
}
#[test]
fn slot_parses_ihex_format_and_load_address() {
let board = Board::try_from_str("fire-24-e").unwrap();
let slot = parse_slot(
"file=rom.hex,type=2364,cs1=active_low,format=ihex,load_address=$E000",
&board,
)
.unwrap();
assert_eq!(slot.format, Some(FileFormat::IntelHex));
assert_eq!(slot.load_address, Some(LoadAddress(0xE000)));
let chip = slot_to_chip_config(&slot);
assert_eq!(chip.format, FileFormat::IntelHex);
assert_eq!(chip.load_address, LoadAddress(0xE000));
}
#[test]
fn slot_defaults_to_binary_format() {
let board = Board::try_from_str("fire-24-e").unwrap();
let slot = parse_slot("file=rom.bin,type=2364,cs1=active_low", &board).unwrap();
assert_eq!(slot.format, None);
assert_eq!(slot.load_address, None);
assert_eq!(slot_to_chip_config(&slot).format, FileFormat::Binary);
}
#[test]
fn slot_accepts_both_spellings_of_a_cs_value() {
let board = Board::try_from_str("fire-24-e").unwrap();
for (spelling, expected) in [
("active_low", CsLogic::ActiveLow),
("active-low", CsLogic::ActiveLow),
("ACTIVE-LOW", CsLogic::ActiveLow),
("0", CsLogic::ActiveLow),
("active_high", CsLogic::ActiveHigh),
("active-high", CsLogic::ActiveHigh),
("1", CsLogic::ActiveHigh),
] {
let slot = parse_slot(&format!("file=rom.bin,type=2364,cs1={spelling}"), &board)
.unwrap_or_else(|e| panic!("cs1={spelling} rejected: {e}"));
assert_eq!(slot.cs1, Some(expected), "cs1={spelling}");
}
}
#[test]
fn slot_accepts_the_ignore_cs_value() {
let board = Board::try_from_str("fire-24-e").unwrap();
let slot = parse_slot("file=rom.bin,type=2332,cs1=active-low,cs2=ignore", &board)
.expect("cs2=ignore should parse");
assert_eq!(slot.cs2, Some(CsLogic::Ignore));
}
#[test]
fn slot_rejects_an_unknown_cs_value_and_names_the_alternatives() {
let board = Board::try_from_str("fire-24-e").unwrap();
let err = parse_slot("file=rom.bin,type=2364,cs1=sideways", &board).unwrap_err();
let msg = err.to_string();
assert!(msg.contains("sideways"), "{msg}");
for value in CsLogic::supported_values() {
assert!(
msg.contains(value.name()),
"{} missing from: {msg}",
value.name()
);
}
}
#[test]
fn slot_parses_a_single_transform() {
let board = Board::try_from_str("fire-24-e").unwrap();
let slot = parse_slot(
"file=rom.bin,type=2364,cs1=active_low,transform=swap_bytes",
&board,
)
.unwrap();
assert_eq!(slot.transform, vec![Transform::SwapBytes]);
assert_eq!(
slot_to_chip_config(&slot).transform,
vec![Transform::SwapBytes]
);
}
#[test]
fn slot_accepts_the_trans_key_alias() {
let board = Board::try_from_str("fire-24-e").unwrap();
for key in ["transform", "trans"] {
let slot = parse_slot(
&format!("file=rom.bin,type=2364,cs1=active_low,{key}=swap_bytes"),
&board,
)
.unwrap_or_else(|e| panic!("slot key '{key}' rejected: {e}"));
assert_eq!(slot.transform, vec![Transform::SwapBytes]);
}
}
#[test]
fn slot_parses_a_transform_list_in_order() {
let board = Board::try_from_str("fire-24-e").unwrap();
let slot = parse_slot(
"file=rom.bin,type=2364,cs1=active_low,transform=deinterleave:1/2/2+swap_bytes",
&board,
)
.unwrap();
assert_eq!(
slot.transform,
vec![
Transform::Deinterleave {
offset: 1,
stride: 2,
bytes: 2
},
Transform::SwapBytes,
]
);
}
#[test]
fn slot_transform_unit_defaults_to_one() {
let board = Board::try_from_str("fire-24-e").unwrap();
let slot = parse_slot(
"file=rom.bin,type=2364,cs1=active_low,transform=deinterleave:0/4",
&board,
)
.unwrap();
assert_eq!(
slot.transform,
vec![Transform::Deinterleave {
offset: 0,
stride: 4,
bytes: 1
}]
);
}
#[test]
fn slot_defaults_to_no_transform() {
let board = Board::try_from_str("fire-24-e").unwrap();
let slot = parse_slot("file=rom.bin,type=2364,cs1=active_low", &board).unwrap();
assert!(slot.transform.is_empty());
assert!(slot_to_chip_config(&slot).transform.is_empty());
}
#[test]
fn slot_rejects_a_bad_transform() {
let board = Board::try_from_str("fire-24-e").unwrap();
let err = parse_slot(
"file=rom.bin,type=2364,cs1=active_low,transform=nonsense",
&board,
)
.unwrap_err();
let msg = format!("{err}");
assert!(msg.contains("unknown transform 'nonsense'"), "{msg}");
assert!(msg.contains("swap_bytes"), "{msg}");
assert!(msg.contains("--slot 'file=rom.bin"), "{msg}");
}
#[test]
fn slot_rejects_a_duplicate_transform_key() {
let board = Board::try_from_str("fire-24-e").unwrap();
let err = parse_slot(
"file=rom.bin,type=2364,cs1=active_low,transform=swap_bytes,transform=swap_bytes",
&board,
)
.unwrap_err();
assert!(format!("{err}").contains("Duplicate slot key 'transform'"));
}
#[test]
fn slot_load_address_requires_ihex() {
let board = Board::try_from_str("fire-24-e").unwrap();
let err = parse_slot(
"file=rom.bin,type=2364,cs1=active_low,load_address=0x100",
&board,
)
.unwrap_err();
assert!(format!("{err}").contains("load-address is only valid with format=ihex"));
}
#[test]
fn empty_plugins_is_passthrough() {
let out = inject_plugins_into_config(ROM_ONLY.to_string(), &[]).unwrap();
assert_eq!(out, ROM_ONLY);
}
#[test]
fn plugins_are_prepended_system_then_user() {
let out = inject_plugins_into_config(
ROM_ONLY.to_string(),
&[plugin(PluginType::User), plugin(PluginType::System)],
)
.unwrap();
assert_eq!(
chip_types(&out),
vec![
ChipType::SystemPlugin,
ChipType::UserPlugin,
ChipType::Chip23128,
]
);
}
#[test]
fn system_only_prepends_before_rom() {
let out = inject_plugins_into_config(ROM_ONLY.to_string(), &[plugin(PluginType::System)])
.unwrap();
assert_eq!(
chip_types(&out),
vec![ChipType::SystemPlugin, ChipType::Chip23128]
);
}
#[test]
fn config_already_defining_a_plugin_is_rejected() {
let with_plugin = r#"{
"version": 1,
"name": "t",
"description": "d",
"chip_sets": [
{ "type": "single", "chips": [
{ "file": "http://x/usb.bin", "type": "system_plugin" } ] },
{ "type": "single", "chips": [
{ "file": "http://x/rom.bin", "type": "23128",
"cs1": "active_low", "cs2": "active_low", "cs3": "active_high" } ] }
]
}"#;
let err =
inject_plugins_into_config(with_plugin.to_string(), &[plugin(PluginType::System)])
.unwrap_err();
assert!(err.to_string().contains("already defines a plugin"));
}
}