use onerom_config::chip::ChipType;
use onerom_config::mcu::{RP235X_BASE_FLASH, RP235X_BASE_SRAM, RP235X_END_SRAM};
use onerom_metadata::{
BitModes, CURRENT_METADATA_VERSION, DeviceMemoryView, FireVreg, GPIO_NONE, OneromAlgAddrConfig,
OneromAlgConfig, OneromAlgCsConfig, OneromAlgDataConfig, OneromAlgDmaConfig,
OneromAlgOverrideConfig, OneromAlgPullConfig, OneromFirmwareConfig, OneromFirmwareOverrides,
OneromHardwareInfo, OneromMetadataHeader, OneromRomInfo, OneromRomPinMap, OneromRomSlot,
Pointer, RomSlotType, Rp235xVariant, generate_host_metadata_c,
};
use onerom_metadata::{METADATA_BASE, METADATA_SIZE, SerializeError, serialize};
fn read_u32_le(buf: &[u8], offset: usize) -> u32 {
u32::from_le_bytes(buf[offset..offset + 4].try_into().unwrap())
}
fn ptr_to_off(ptr: u32, base: u32) -> usize {
(ptr - base) as usize
}
fn default_pin_map() -> Option<OneromRomPinMap> {
Some(OneromRomPinMap {
addr: [GPIO_NONE; 24],
data: [GPIO_NONE; 16],
})
}
fn make_rom_info(rom_type: &str) -> OneromRomInfo {
let chip =
ChipType::try_from_str(rom_type).unwrap_or_else(|| panic!("unknown chip type: {rom_type}"));
OneromRomInfo {
rom_type: rom_type.into(),
filename: None,
pin_map: default_pin_map(),
chip_size: chip.size_bytes() as u32,
rbcp_rom_type: chip.rbcp_chip_type(),
}
}
fn default_alg() -> Option<OneromAlgConfig> {
Some(OneromAlgConfig {
alg_cs: OneromAlgCsConfig::AlgCs0 {
clkdiv_int: 1,
clkdiv_frac: 0,
gpio_base: 0,
base_cs_pin: 0,
num_cs_pins: 1,
base_data_pin: 8,
num_data_pins: 8,
cs_active_delay: 0,
cs_inactive_delay: 0,
serve_cs_low_0: 1,
byte_pin: GPIO_NONE,
first_rom_cs_base: 0,
first_rom_num_cs_pins: 1,
},
alg_addr: OneromAlgAddrConfig::AlgAddr0 {
clkdiv_int: 1,
clkdiv_frac: 0,
gpio_base: 0,
num_delay_cycles: 2,
base_addr_pin: 0,
num_addr_pins: 13,
num_rom_table_bits: 13,
},
alg_data: OneromAlgDataConfig::AlgData0 {
clkdiv_int: 1,
clkdiv_frac: 0,
gpio_base: 0,
base_data_pin: 8,
word_size: 8,
},
alg_dma: OneromAlgDmaConfig::AlgDma0 {
bit_mode: BitModes::BitMode8,
continuous: 1,
},
gpio_pull_config: None,
gpio_override_config: None,
})
}
fn make_slot(roms: Vec<OneromRomInfo>) -> OneromRomSlot {
OneromRomSlot {
data: Pointer::Null,
size: 8192,
roms,
rom_count: 0,
slot_type: RomSlotType::RomSlotTypeSingleRom,
alg: default_alg(),
firmware_overrides: None,
}
}
fn minimal_header() -> OneromMetadataHeader {
let pin_map = OneromRomPinMap {
addr: [GPIO_NONE; 24],
data: [GPIO_NONE; 16],
};
let chip_2364 = ChipType::Chip2364;
let rom_info = OneromRomInfo {
rom_type: "2364".into(),
filename: None,
pin_map: Some(pin_map),
chip_size: chip_2364.size_bytes() as u32,
rbcp_rom_type: chip_2364.rbcp_chip_type(),
};
let alg = OneromAlgConfig {
alg_cs: OneromAlgCsConfig::AlgCs0 {
clkdiv_int: 1,
clkdiv_frac: 0,
gpio_base: 0,
base_cs_pin: 0,
num_cs_pins: 1,
base_data_pin: 8,
num_data_pins: 8,
cs_active_delay: 0,
cs_inactive_delay: 0,
serve_cs_low_0: 1,
byte_pin: GPIO_NONE,
first_rom_cs_base: 0,
first_rom_num_cs_pins: 1,
},
alg_addr: OneromAlgAddrConfig::AlgAddr0 {
clkdiv_int: 1,
clkdiv_frac: 0,
gpio_base: 0,
num_delay_cycles: 2,
base_addr_pin: 0,
num_addr_pins: 13,
num_rom_table_bits: 13,
},
alg_data: OneromAlgDataConfig::AlgData0 {
clkdiv_int: 1,
clkdiv_frac: 0,
gpio_base: 0,
base_data_pin: 8,
word_size: 8,
},
alg_dma: OneromAlgDmaConfig::AlgDma0 {
bit_mode: BitModes::BitMode8,
continuous: 1,
},
gpio_pull_config: None,
gpio_override_config: None,
};
let slot = OneromRomSlot {
data: Pointer::Null,
size: 8192,
roms: vec![rom_info],
rom_count: 0,
slot_type: RomSlotType::RomSlotTypeSingleRom,
alg: Some(alg),
firmware_overrides: None,
};
let hw = OneromHardwareInfo {
hw_rev: "1.0".into(),
rp235x: Rp235xVariant::Rp235xb,
num_phys_pins: 28,
usb_capable: 1,
gpio_vbus: GPIO_NONE,
gpio_ext_flash_cs: GPIO_NONE,
gpio_status: 25,
gpio_neopixel: GPIO_NONE,
gpio_swdio: GPIO_NONE,
gpio_swclk: GPIO_NONE,
gpio_sel: [GPIO_NONE; 7],
sel_jumper_pull: 0,
gpio_from_phys_pin: [[GPIO_NONE; 2]; 40],
gpio_x1: [GPIO_NONE; 2],
gpio_x2: [GPIO_NONE; 2],
};
let fw = OneromFirmwareConfig {
name: None,
serial_number: None,
};
OneromMetadataHeader {
magic: *b"ONEROM_METADATA\0",
version: CURRENT_METADATA_VERSION,
hw,
fw,
rom_slot_count: 0,
boot_logging: 0,
swd_enabled: 0,
turbo_boot: 0,
rom_slots: vec![slot],
}
}
fn dummy_rom_data(n_slots: usize) -> Vec<Vec<u8>> {
(0..n_slots).map(|_| vec![0xAAu8; 8]).collect()
}
fn do_serialize(header: &OneromMetadataHeader) -> Vec<u8> {
let mut buf = vec![0u8; METADATA_SIZE];
serialize(header, METADATA_BASE, &mut buf).expect("serialize failed");
buf
}
fn round_trip(header: &OneromMetadataHeader) -> OneromMetadataHeader {
let buf = do_serialize(header);
let view = DeviceMemoryView::new(&buf, METADATA_BASE);
OneromMetadataHeader::parse(&view, METADATA_BASE).expect("parse failed")
}
fn normalize_counts(h: &mut OneromMetadataHeader) {
h.rom_slot_count = h.rom_slots.len() as u8;
for slot in &mut h.rom_slots {
slot.rom_count = slot.roms.len() as u8;
}
}
fn assert_round_trips(original: &OneromMetadataHeader) {
let mut expected = original.clone();
normalize_counts(&mut expected);
assert_eq!(expected, round_trip(original));
}
#[test]
fn round_trip_minimal() {
let original = minimal_header();
assert_round_trips(&original);
}
#[test]
fn round_trip_optional_fields() {
let fw = OneromFirmwareConfig {
name: Some("MyUnit".into()),
serial_number: Some("SN-0042".into()),
};
let chip_2364 = ChipType::Chip2364;
let rom = OneromRomInfo {
rom_type: "2364".into(),
filename: Some("basic.rom".into()),
pin_map: default_pin_map(),
chip_size: chip_2364.size_bytes() as u32,
rbcp_rom_type: chip_2364.rbcp_chip_type(),
};
let fw_overrides = OneromFirmwareOverrides {
override_present: [0x04, 0, 0, 0, 0, 0, 0, 0],
ice_freq: 0,
fire_freq: 0,
fire_vreg: FireVreg::FireVregNone,
override_value: [0; 8],
};
let slot = OneromRomSlot {
firmware_overrides: Some(fw_overrides),
..make_slot(vec![rom])
};
let original = OneromMetadataHeader {
fw,
rom_slots: vec![slot],
..minimal_header()
};
assert_round_trips(&original);
}
#[test]
fn round_trip_multiple_slots() {
let slot_a = make_slot(vec![make_rom_info("2364")]);
let slot_b = OneromRomSlot {
size: 16384,
slot_type: RomSlotType::RomSlotTypeMultiRom,
..make_slot(vec![make_rom_info("27128")])
};
let slot_c = OneromRomSlot {
size: 32768,
slot_type: RomSlotType::RomSlotTypeBankedRom,
..make_slot(vec![make_rom_info("27256")])
};
let original = OneromMetadataHeader {
rom_slots: vec![slot_a, slot_b, slot_c],
..minimal_header()
};
assert_round_trips(&original);
}
#[test]
fn round_trip_multiple_roms_per_slot() {
let roms = vec![make_rom_info("2364"), make_rom_info("27128")];
let original = OneromMetadataHeader {
rom_slots: vec![make_slot(roms)],
..minimal_header()
};
assert_round_trips(&original);
}
#[test]
fn round_trip_alg_cs1() {
let alg = OneromAlgConfig {
alg_cs: OneromAlgCsConfig::AlgCs1 {
clkdiv_int: 2,
clkdiv_frac: 0,
gpio_base: 0,
base_cs_pin: 0,
num_cs_pins: 2,
base_data_pin: 8,
num_data_pins: 8,
cs_active_delay: 1,
cs_inactive_delay: 1,
cs_ignore_index: 1,
},
..default_alg().unwrap()
};
let original = OneromMetadataHeader {
rom_slots: vec![OneromRomSlot {
alg: Some(alg),
..make_slot(vec![make_rom_info("2364")])
}],
..minimal_header()
};
assert_round_trips(&original);
}
#[test]
fn round_trip_alg_cs2() {
let alg = OneromAlgConfig {
alg_cs: OneromAlgCsConfig::AlgCs2 {
clkdiv_int: 1,
clkdiv_frac: 0,
gpio_base: 0,
base_cs_pin: 0,
num_cs_pins: 1,
base_data_pin: 8,
num_data_pins: 8,
cs_active_delay: 0,
cs_inactive_delay: 0,
base_qualifier_pin: 2,
num_qualifier_pins: 2,
qualifier_inactive_pattern: 0b11,
},
..default_alg().unwrap()
};
let original = OneromMetadataHeader {
rom_slots: vec![OneromRomSlot {
alg: Some(alg),
..make_slot(vec![make_rom_info("2364")])
}],
..minimal_header()
};
assert_round_trips(&original);
}
#[test]
fn round_trip_alg_data1() {
let alg = OneromAlgConfig {
alg_data: OneromAlgDataConfig::AlgData1 {
clkdiv_int: 1,
clkdiv_frac: 0,
gpio_base: 0,
base_data_pin: 0,
word_size: 16,
byte_pin: 5,
a_minus_1_pin: 6,
},
..default_alg().unwrap()
};
let original = OneromMetadataHeader {
rom_slots: vec![OneromRomSlot {
alg: Some(alg),
..make_slot(vec![make_rom_info("2364")])
}],
..minimal_header()
};
assert_round_trips(&original);
}
#[test]
fn round_trip_simple_fams() {
let alg = OneromAlgConfig {
gpio_pull_config: Some(OneromAlgPullConfig {
params: vec![0x85, 0x86], }),
gpio_override_config: Some(OneromAlgOverrideConfig {
params: vec![0x47], }),
..default_alg().unwrap()
};
let original = OneromMetadataHeader {
rom_slots: vec![OneromRomSlot {
alg: Some(alg),
..make_slot(vec![make_rom_info("2364")])
}],
..minimal_header()
};
assert_round_trips(&original);
}
#[test]
fn round_trip_string_reuse() {
let original = OneromMetadataHeader {
rom_slots: vec![
make_slot(vec![make_rom_info("2364")]),
make_slot(vec![make_rom_info("2364")]),
],
..minimal_header()
};
assert_round_trips(&original);
}
#[test]
fn byte_check_count_fields_derived_from_vec() {
let slot = OneromRomSlot {
rom_count: 99, ..make_slot(vec![make_rom_info("2364")])
};
let header = OneromMetadataHeader {
rom_slot_count: 42, rom_slots: vec![slot],
..minimal_header()
};
let buf = do_serialize(&header);
assert_eq!(
buf[28], 1,
"rom_slot_count should equal rom_slots.len() (1), not 42",
);
let slots_ptr = read_u32_le(&buf, 32);
let slots_off = ptr_to_off(slots_ptr, METADATA_BASE);
assert_eq!(
buf[slots_off + 12],
1,
"rom_count should equal roms.len() (1), not 99",
);
}
#[test]
fn byte_check_opaque_ptr_verbatim() {
let slot = OneromRomSlot {
data: Pointer::Addr32(0xDEAD_BEEF),
..make_slot(vec![make_rom_info("2364")])
};
let header = OneromMetadataHeader {
rom_slots: vec![slot],
..minimal_header()
};
let buf = do_serialize(&header);
let slots_ptr = read_u32_le(&buf, 32);
let slots_off = ptr_to_off(slots_ptr, METADATA_BASE);
let written = read_u32_le(&buf, slots_off);
assert_eq!(
written, 0xDEAD_BEEF,
"opaque_ptr data field must be written verbatim"
);
}
#[test]
fn byte_check_object_dedup() {
let alg = default_alg();
let slot_a = OneromRomSlot {
alg: alg.clone(),
..make_slot(vec![make_rom_info("2364")])
};
let slot_b = OneromRomSlot {
alg,
..make_slot(vec![make_rom_info("27128")])
};
let header = OneromMetadataHeader {
rom_slots: vec![slot_a, slot_b],
..minimal_header()
};
let buf = do_serialize(&header);
let slots_ptr = read_u32_le(&buf, 32);
let slots_off = ptr_to_off(slots_ptr, METADATA_BASE);
let alg_ptr_a = read_u32_le(&buf, slots_off + 16);
let alg_ptr_b = read_u32_le(&buf, slots_off + 32 + 16);
assert_eq!(
alg_ptr_a, alg_ptr_b,
"identical alg configs must share one object: \
slot_a.alg → 0x{alg_ptr_a:08X}, slot_b.alg → 0x{alg_ptr_b:08X}",
);
}
#[test]
fn byte_check_header_padding_is_ff() {
let buf = do_serialize(&minimal_header());
#[allow(clippy::needless_range_loop)]
for offset in 36..256 {
assert_eq!(
buf[offset], 0xFF,
"header reserved byte at offset {offset} should be 0xFF, got 0x{:02X}",
buf[offset],
);
}
}
#[test]
fn byte_check_pointer_alignment() {
let buf = do_serialize(&minimal_header());
let is_valid_ptr = |ptr: u32| ptr != 0 && ptr != 0xFFFF_FFFF;
let check_aligned = |label: &str, ptr: u32| {
if is_valid_ptr(ptr) {
assert_eq!(
ptr % 4,
0,
"{label}: flash pointer 0x{ptr:08X} is not 4-byte aligned",
);
}
};
let hw_ptr = read_u32_le(&buf, 20);
let fw_ptr = read_u32_le(&buf, 24);
let slots_ptr = read_u32_le(&buf, 32);
check_aligned("hw", hw_ptr);
check_aligned("fw", fw_ptr);
check_aligned("rom_slots", slots_ptr);
let slots_off = ptr_to_off(slots_ptr, METADATA_BASE);
let roms_ptr = read_u32_le(&buf, slots_off + 8);
let alg_ptr = read_u32_le(&buf, slots_off + 16);
let fw_ovr_ptr = read_u32_le(&buf, slots_off + 20);
check_aligned("slot.roms", roms_ptr);
check_aligned("slot.alg", alg_ptr);
check_aligned("slot.firmware_overrides", fw_ovr_ptr);
let alg_off = ptr_to_off(alg_ptr, METADATA_BASE);
check_aligned("alg.alg_cs", read_u32_le(&buf, alg_off));
check_aligned("alg.alg_addr", read_u32_le(&buf, alg_off + 4));
check_aligned("alg.alg_data", read_u32_le(&buf, alg_off + 8));
check_aligned("alg.alg_dma", read_u32_le(&buf, alg_off + 12));
check_aligned("alg.gpio_pull_config", read_u32_le(&buf, alg_off + 16));
check_aligned("alg.gpio_override_config", read_u32_le(&buf, alg_off + 20));
}
#[test]
fn error_overflow() {
let header = minimal_header();
let mut buf = vec![0u8; 64];
assert_eq!(
serialize(&header, METADATA_BASE, &mut buf),
Err(SerializeError::Overflow),
);
}
#[test]
fn error_count_overflow() {
let mut header = minimal_header();
header.rom_slots = (0..256)
.map(|_| make_slot(vec![make_rom_info("2364")]))
.collect();
let mut buf = vec![0u8; METADATA_SIZE];
assert_eq!(
serialize(&header, METADATA_BASE, &mut buf),
Err(SerializeError::CountOverflow {
field: "rom_slot_count"
}),
);
}
#[test]
fn host_c_gen_smoke() {
let header = minimal_header();
let c_src = generate_host_metadata_c(&header, dummy_rom_data(1));
assert!(!c_src.is_empty(), "generated C source should not be empty");
}
#[test]
fn host_c_gen_structural() {
let header = minimal_header();
let c_src = generate_host_metadata_c(&header, dummy_rom_data(1));
assert!(
c_src.contains("_metadata_start"),
"missing `_metadata_start` definition"
);
assert!(
c_src.contains("extern const"),
"missing `extern const` forward declarations"
);
assert!(
c_src.contains("\"2364\""),
"missing rom_type string literal \"2364\""
);
assert!(
c_src.contains("\"1.0\""),
"missing hw_rev string literal \"1.0\""
);
assert!(
c_src.contains("= NULL"),
"missing `= NULL` for null pointer fields"
);
assert!(
c_src.contains("#pragma GCC diagnostic push"),
"missing `#pragma GCC diagnostic push` for FAM structs"
);
assert!(
c_src.contains("-Wpedantic"),
"missing `-Wpedantic` in FAM pragma"
);
assert!(
c_src.contains("ROM_SLOT_TYPE_SINGLE_ROM"),
"missing `ROM_SLOT_TYPE_SINGLE_ROM` enum constant"
);
}
#[cfg(any(target_os = "linux", target_os = "macos"))]
#[test]
fn host_c_gen_compiles() {
use std::process::Command;
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let project_root = manifest_dir
.parent()
.and_then(|p| p.parent())
.expect("CARGO_MANIFEST_DIR should be two levels below the project root");
let include_dir = project_root.join("firmware").join("generated");
let firmware_include_dir = project_root.join("firmware").join("include");
if !include_dir.join("onerom_metadata.h").exists() {
eprintln!(
"host_c_gen_compiles: skipping — onerom_metadata.h not found at {}",
include_dir.display()
);
return;
}
let header = minimal_header();
let c_src = generate_host_metadata_c(&header, dummy_rom_data(1));
let pid = std::process::id();
let tmp = std::env::temp_dir();
let c_path = tmp.join(format!("onerom_host_gen_{pid}.c"));
let o_path = tmp.join(format!("onerom_host_gen_{pid}.o"));
std::fs::write(&c_path, &c_src).expect("failed to write temp C source file");
let result = Command::new("gcc")
.args([
"-std=c99",
"-Wall",
"-Wextra",
"-Wpedantic",
"-c",
"-I",
&include_dir.to_string_lossy(),
"-I",
&firmware_include_dir.to_string_lossy(),
"-D",
"TEST_BUILD",
"-o",
&o_path.to_string_lossy(),
&c_path.to_string_lossy(),
])
.output();
let _ = std::fs::remove_file(&c_path);
let _ = std::fs::remove_file(&o_path);
match result {
Err(e) => {
eprintln!("host_c_gen_compiles: skipping — gcc not available: {e}");
}
Ok(output) => {
assert!(
output.status.success(),
"generated C failed to compile:\n{}",
String::from_utf8_lossy(&output.stderr)
);
}
}
}
#[test]
fn pointer_new_sentinels() {
assert_eq!(Pointer::new(0), Pointer::Null);
assert_eq!(Pointer::new(0xFFFF_FFFF), Pointer::Null);
assert_eq!(Pointer::new(0x1000_8000), Pointer::Addr32(0x1000_8000));
assert_eq!(Pointer::new(0x0000_0001), Pointer::Addr32(0x0000_0001));
}
#[test]
fn pointer_is_null() {
assert!(Pointer::Null.is_null());
assert!(!Pointer::Addr32(0x1000_0000).is_null());
}
#[test]
fn pointer_addr() {
assert_eq!(Pointer::Null.addr(), None);
assert_eq!(Pointer::Addr32(0xABCD_1234).addr(), Some(0xABCD_1234));
}
#[test]
fn pointer_raw() {
assert_eq!(Pointer::Null.raw(), 0);
assert_eq!(Pointer::Addr32(0xDEAD_BEEF).raw(), 0xDEAD_BEEF);
}
#[test]
fn pointer_is_flash() {
assert!(Pointer::Addr32(RP235X_BASE_FLASH).is_flash());
assert!(Pointer::Addr32(0x1000_C000).is_flash());
assert!(Pointer::Addr32(0x1FFF_FFFF).is_flash());
assert!(!Pointer::Addr32(0x2000_0000).is_flash());
assert!(!Pointer::Addr32(RP235X_BASE_SRAM).is_flash());
assert!(!Pointer::Null.is_flash());
}
#[test]
fn pointer_is_sram() {
assert!(Pointer::Addr32(RP235X_BASE_SRAM).is_sram());
assert!(Pointer::Addr32(RP235X_END_SRAM).is_sram());
assert!(!Pointer::Addr32(RP235X_END_SRAM + 1).is_sram());
assert!(!Pointer::Addr32(RP235X_BASE_FLASH).is_sram());
assert!(!Pointer::Null.is_sram());
}
#[test]
fn round_trip_opaque_ptr_addr32() {
let slot = OneromRomSlot {
data: Pointer::Addr32(0x1001_0000),
..make_slot(vec![make_rom_info("2364")])
};
let original = OneromMetadataHeader {
rom_slots: vec![slot],
..minimal_header()
};
assert_round_trips(&original);
}