use serde::{Deserialize, Serialize};
use tsify::Tsify;
use wasm_bindgen::prelude::*;
use airfrog_rpc::io::Reader;
use onerom_config::fw::{FirmwareProperties, FirmwareVersion};
use onerom_config::mcu::Family;
use onerom_fw_parser::{
ParsedDevice, Parser, SlotKind, readers::MemoryReader, readers::RegionKind,
};
use onerom_gen::{Builder as GenBuilder, FileData};
#[wasm_bindgen(start)]
pub fn init() {
console_error_panic_hook::set_once();
console_log::init_with_level(log::Level::Debug).unwrap();
}
#[wasm_bindgen]
pub fn version() -> String {
env!("CARGO_PKG_VERSION").to_string()
}
#[wasm_bindgen]
pub struct VersionInfo {
onerom_wasm: String,
onerom_config: String,
onerom_gen: String,
sdrr_fw_parser: String,
metadata_version: String,
}
#[wasm_bindgen]
impl VersionInfo {
#[wasm_bindgen(getter)]
pub fn onerom_wasm(&self) -> String {
self.onerom_wasm.clone()
}
#[wasm_bindgen(getter)]
pub fn onerom_config(&self) -> String {
self.onerom_config.clone()
}
#[wasm_bindgen(getter)]
pub fn onerom_gen(&self) -> String {
self.onerom_gen.clone()
}
#[wasm_bindgen(getter)]
pub fn sdrr_fw_parser(&self) -> String {
self.sdrr_fw_parser.clone()
}
#[wasm_bindgen(getter)]
pub fn metadata_version(&self) -> String {
self.metadata_version.clone()
}
}
#[wasm_bindgen]
pub fn versions() -> VersionInfo {
VersionInfo {
onerom_wasm: env!("CARGO_PKG_VERSION").to_string(),
onerom_config: onerom_config::crate_version().to_string(),
onerom_gen: onerom_gen::crate_version().to_string(),
sdrr_fw_parser: onerom_fw_parser::crate_version().to_string(),
metadata_version: onerom_gen::metadata_version().to_string(),
}
}
#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct DeviceSummary {
pub version: Option<String>,
pub mcu: Option<String>,
pub model: Option<String>,
pub hw_rev: Option<String>,
pub corrupt: bool,
pub parse_errors: Vec<String>,
pub can_run: bool,
pub running: bool,
pub plugins: Vec<RomSummary>,
pub roms: Vec<RomSummary>,
pub full_reread_size: Option<u32>,
pub dump: String,
}
#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct RomSummary {
pub label: String,
pub active: bool,
pub index: Option<usize>,
}
const RAM_BLOCK_LEN: u32 = 256;
struct CallbackReader {
flash: Vec<u8>,
flash_base: u32,
read_cb: js_sys::Function,
ram_cache: Vec<(u32, Vec<u8>)>,
}
impl CallbackReader {
fn new(flash: Vec<u8>, flash_base: u32, read_cb: js_sys::Function) -> Self {
Self {
flash,
flash_base,
read_cb,
ram_cache: Vec::new(),
}
}
async fn fetch(&self, addr: u32, len: u32) -> Result<Vec<u8>, String> {
let promise = self
.read_cb
.call2(
&JsValue::NULL,
&JsValue::from_f64(addr as f64),
&JsValue::from_f64(len as f64),
)
.map_err(|e| format!("read callback threw: {e:?}"))?;
let resolved = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise))
.await
.map_err(|e| format!("read failed at {addr:#010x}: {e:?}"))?;
Ok(js_sys::Uint8Array::new(&resolved).to_vec())
}
}
impl Reader for CallbackReader {
type Error = String;
async fn read(&mut self, addr: u32, buf: &mut [u8]) -> Result<(), Self::Error> {
let len = buf.len();
let end = addr
.checked_add(len as u32)
.ok_or_else(|| format!("address overflow at {addr:#010x}"))?;
let flash_end = self.flash_base.saturating_add(self.flash.len() as u32);
if addr >= self.flash_base && end <= flash_end {
let off = (addr - self.flash_base) as usize;
buf.copy_from_slice(&self.flash[off..off + len]);
return Ok(());
}
if let Some((base, data)) = self
.ram_cache
.iter()
.find(|(b, d)| addr >= *b && end <= b.saturating_add(d.len() as u32))
{
let off = (addr - *base) as usize;
buf.copy_from_slice(&data[off..off + len]);
return Ok(());
}
let fetch_len = core::cmp::max(RAM_BLOCK_LEN, len as u32);
let block = self.fetch(addr, fetch_len).await?;
if block.len() < len {
return Err(format!(
"short read at {addr:#010x}: got {}, need {len}",
block.len()
));
}
buf.copy_from_slice(&block[..len]);
self.ram_cache.push((addr, block));
Ok(())
}
fn update_base_address(&mut self, new_base: u32) {
self.flash_base = new_base;
}
}
#[wasm_bindgen]
pub async fn parse_firmware(
flash: Vec<u8>,
read_cb: js_sys::Function,
) -> Result<DeviceSummary, JsValue> {
let mut reader = CallbackReader::new(flash, 0x08000000, read_cb);
let mut parser = Parser::new(&mut reader);
let parsed = parser.parse_device().await;
device_summary(&parsed).map_err(|e| JsValue::from_str(&e))
}
fn device_summary(dev: &ParsedDevice) -> Result<DeviceSummary, String> {
let parse_errors: Vec<String> = dev.parse_errors().iter().map(|e| e.to_string()).collect();
let mut plugins = Vec::new();
let mut roms = Vec::new();
for slot in dev.slots() {
let active = slot.active;
let index = slot.user_index;
let kind = slot.kind;
for rom in slot.roms() {
let label = rom
.filename
.map(|s| s.to_string())
.unwrap_or_else(|| rom.rom_type.into_owned());
let entry = RomSummary {
label,
active,
index,
};
match kind {
SlotKind::Plugin => plugins.push(entry),
SlotKind::Rom => roms.push(entry),
}
}
}
let board = dev.get_board();
let dump = serde_json::to_string(dev).map_err(|e| e.to_string())?;
Ok(DeviceSummary {
version: version_string(dev),
mcu: dev.mcu_name(),
model: board.as_ref().map(|b| b.model().to_string()),
hw_rev: board.as_ref().map(|b| b.name().to_string()),
corrupt: !parse_errors.is_empty(),
parse_errors,
can_run: dev.is_usb_run_capable(),
running: dev.is_running(),
plugins,
roms,
full_reread_size: full_reread_size(dev),
dump,
})
}
fn version_string(dev: &ParsedDevice) -> Option<String> {
let (maj, min, pat) = match dev {
ParsedDevice::Original(s) => {
let f = s.flash.as_ref()?;
(f.major_version, f.minor_version, f.patch_version)
}
ParsedDevice::Schema(o) => {
let i = o.info()?;
(i.major_version, i.minor_version, i.patch_version)
}
};
Some(format!("{maj}.{min}.{pat}"))
}
fn full_reread_size(dev: &ParsedDevice) -> Option<u32> {
let f = dev.as_original()?.flash.as_ref()?;
if f.major_version == 0 && f.minor_version < 5 && !f.parse_errors.is_empty() {
Some((f.mcu_variant?.flash_storage_kb() * 1024) as u32)
} else {
None
}
}
pub async fn parse_all(flash_data: Vec<u8>, rom_data: Vec<u8>) -> Result<JsValue, JsValue> {
let mut reader = MemoryReader::new_of_kind(RegionKind::Flash, flash_data, 0x08000000);
reader.add_region(RegionKind::Ram, rom_data, 0x20000000);
let mut parser = Parser::new(&mut reader);
let info = parser.parse().await;
serde_wasm_bindgen::to_value(&info).map_err(|e| JsValue::from_str(&e.to_string()))
}
#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct McuInfo {
name: String,
family: String,
flash_kb: usize,
ram_kb: usize,
ccm_ram_kb: Option<usize>,
max_sysclk_mhz: u32,
supports_usb_dfu: bool,
supports_banked_roms: bool,
supports_multi_rom_sets: bool,
}
#[wasm_bindgen]
pub fn mcus() -> Vec<String> {
onerom_config::mcu::MCU_VARIANTS
.iter()
.map(|t| t.to_string())
.collect()
}
#[wasm_bindgen]
pub fn mcu_info(name: String) -> Result<McuInfo, JsValue> {
let variant = onerom_config::mcu::Variant::try_from_str(&name)
.ok_or_else(|| JsValue::from_str(&format!("Unknown MCU variant: {}", name)))?;
let processor = variant.processor();
let info = McuInfo {
name: variant.to_string(),
family: variant.family().to_string(),
flash_kb: variant.flash_storage_kb(),
ram_kb: variant.ram_kb(),
ccm_ram_kb: variant.ccm_ram_kb(),
max_sysclk_mhz: processor.max_sysclk_mhz(),
supports_usb_dfu: variant.supports_usb_dfu(),
supports_banked_roms: variant.supports_banked_roms(),
supports_multi_rom_sets: variant.supports_multi_rom_sets(),
};
Ok(info)
}
#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct ChipTypeInfo {
name: String,
aliases: Vec<String>,
chip_function: String,
is_plugin: bool,
is_supported: bool,
bit_modes: Vec<u8>,
size_bytes: usize,
chip_pins: u8,
num_addr_lines: usize,
address_pins: Vec<AddressPin>,
data_pins: Vec<DataPin>,
control_lines: Vec<ControlLine>,
programming_pins: Option<Vec<ProgrammingPin>>,
power_pins: Vec<PowerPin>,
}
#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct AddressPin {
line: usize, pin: u8, }
#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct DataPin {
line: usize, pin: u8,
}
#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct ControlLine {
name: String,
pin: u8,
configurable: bool, }
#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct ProgrammingPin {
name: String,
pin: u8,
read_state: String, }
#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct PowerPin {
name: String,
pin: u8,
}
#[wasm_bindgen]
pub fn chip_types() -> Vec<String> {
onerom_config::chip::CHIP_TYPES
.iter()
.filter(|t| !t.is_plugin())
.map(|t| t.name().to_string())
.collect()
}
#[wasm_bindgen]
pub fn supported_chip_types() -> Vec<String> {
onerom_config::chip::CHIP_TYPES
.iter()
.filter(|t| !t.is_plugin() && t.is_supported())
.map(|t| t.name().to_string())
.collect()
}
#[wasm_bindgen]
pub fn chip_type_aliases() -> Vec<String> {
onerom_config::chip::CHIP_TYPES
.iter()
.filter(|t| !t.is_plugin())
.flat_map(|t| t.aliases().iter().map(|s| s.to_string()))
.collect()
}
#[wasm_bindgen]
pub fn supported_chip_type_aliases() -> Vec<String> {
onerom_config::chip::CHIP_TYPES
.iter()
.filter(|t| !t.is_plugin() && t.is_supported())
.flat_map(|t| t.aliases().iter().map(|s| s.to_string()))
.collect()
}
#[wasm_bindgen]
pub fn extra_chip_types_for_board(board_name: String) -> Vec<String> {
if let Some(board) = onerom_config::hw::BOARDS
.iter()
.find(|b| b.name() == board_name)
{
board
.extra_chip_types()
.iter()
.map(|t| t.name().to_string())
.collect()
} else {
vec![]
}
}
#[wasm_bindgen]
pub fn chip_type_info(name: String) -> Result<ChipTypeInfo, JsValue> {
let chip_type = onerom_config::chip::ChipType::try_from_str(&name)
.ok_or_else(|| JsValue::from_str(&format!("Unknown ROM type: {}", name)))?;
let address_pins = chip_type
.address_pins()
.iter()
.enumerate()
.map(|(line, &pin)| AddressPin { line, pin })
.collect();
let data_pins = chip_type
.data_pins()
.iter()
.enumerate()
.map(|(line, &pin)| DataPin { line, pin })
.collect();
let control_lines = chip_type
.control_lines()
.iter()
.map(|cl| ControlLine {
name: cl.name.to_string(),
pin: cl.pin,
configurable: cl.line_type == onerom_config::chip::ControlLineType::Configurable,
})
.collect();
let programming_pins = chip_type.programming_pins().map(|pins| {
pins.iter()
.map(|p| ProgrammingPin {
name: p.name.to_string(),
pin: p.pin,
read_state: match p.read_state {
onerom_config::chip::ProgrammingPinState::Vcc => "Vcc",
onerom_config::chip::ProgrammingPinState::High => "High",
onerom_config::chip::ProgrammingPinState::Low => "Low",
onerom_config::chip::ProgrammingPinState::ChipSelect => "ChipSelect",
onerom_config::chip::ProgrammingPinState::Ignored => "Ignored",
onerom_config::chip::ProgrammingPinState::WordSize => "WordSize",
}
.to_string(),
})
.collect()
});
let power_pins = chip_type
.power_pins()
.iter()
.map(|p| PowerPin {
name: p.name.to_string(),
pin: p.pin,
})
.collect();
let info = ChipTypeInfo {
name: chip_type.name().to_string(),
aliases: chip_type.aliases().iter().map(|s| s.to_string()).collect(),
chip_function: format!("{:?}", chip_type.chip_function()),
is_plugin: chip_type.is_plugin(),
is_supported: chip_type.is_supported(),
bit_modes: chip_type.bit_modes().to_vec(),
size_bytes: chip_type.size_bytes(),
chip_pins: chip_type.chip_pins(),
num_addr_lines: chip_type.num_addr_lines(),
address_pins,
data_pins,
control_lines,
programming_pins,
power_pins,
};
Ok(info)
}
#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct BoardInfo {
name: String,
description: String,
mcu_family: String,
chip_pins: u8,
data_pins: Vec<u8>,
addr_pins: Vec<u8>,
sel_pins: Vec<u8>,
pin_status: u8,
pin_x1: Option<u8>, pin_x2: Option<u8>,
port_data: String,
port_addr: String,
port_cs: String,
port_sel: String,
port_status: String,
sel_jumper_pulls: Vec<u8>, x_jumper_pull: u8,
has_usb: bool,
supports_multi_chip_sets: bool,
}
#[wasm_bindgen]
pub fn boards() -> Result<Vec<String>, JsValue> {
let boards: Vec<String> = onerom_config::hw::BOARDS
.iter()
.map(|b| b.name().to_string())
.collect();
Ok(boards)
}
#[wasm_bindgen]
pub fn mcu_flash_base(name: &str) -> Result<u32, JsValue> {
let family = onerom_config::mcu::Family::try_from_str(name)
.ok_or_else(|| JsValue::from_str(&format!("Unknown MCU family: {}", name)))?;
Ok(family.get_flash_base())
}
#[wasm_bindgen]
pub fn board_info(name: String) -> Result<BoardInfo, JsValue> {
let board = onerom_config::hw::Board::try_from_str(&name)
.ok_or_else(|| JsValue::from_str(&format!("Unknown board: {}", name)))?;
let pin_x1 = board.pin_x1();
let pin_x2 = board.pin_x2();
let info = BoardInfo {
name: board.name().to_string(),
description: board.description().to_string(),
mcu_family: board.mcu_family().to_string(),
chip_pins: board.chip_pins(),
data_pins: board.data_pins().to_vec(),
addr_pins: board.addr_pins().to_vec(),
sel_pins: board.sel_pins().to_vec(),
pin_status: board.pin_status(),
pin_x1: if pin_x1 == 255 { None } else { Some(pin_x1) },
pin_x2: if pin_x2 == 255 { None } else { Some(pin_x2) },
port_data: board.port_data().to_string(),
port_addr: board.port_addr().to_string(),
port_cs: board.port_cs().to_string(),
port_sel: board.port_sel().to_string(),
port_status: board.port_status().to_string(),
sel_jumper_pulls: board.sel_jumper_pulls().to_vec(),
x_jumper_pull: board.x_jumper_pull(),
has_usb: board.has_usb(),
supports_multi_chip_sets: board.supports_multi_chip_sets(),
};
Ok(info)
}
#[wasm_bindgen]
pub struct ValuePrettyPair {
value: String,
pretty: String,
}
#[wasm_bindgen]
impl ValuePrettyPair {
#[wasm_bindgen(getter)]
pub fn value(&self) -> String {
self.value.clone()
}
#[wasm_bindgen(getter)]
pub fn pretty(&self) -> String {
self.pretty.clone()
}
}
#[wasm_bindgen]
pub fn boards_for_mcu_family(family_name: String) -> Result<Vec<ValuePrettyPair>, JsValue> {
let family = onerom_config::mcu::Family::try_from_str(&family_name)
.ok_or_else(|| JsValue::from_str(&format!("Unknown MCU family: {}", family_name)))?;
let boards: Vec<ValuePrettyPair> = onerom_config::hw::BOARDS
.iter()
.filter(|b| b.mcu_family() == family)
.map(|b| ValuePrettyPair {
value: b.name().to_string(),
pretty: format_board_name(b.name()),
})
.collect();
Ok(boards)
}
fn format_board_name(name: &str) -> String {
name.split('-')
.map(|part| {
match part.to_uppercase().as_str() {
"USB" => "USB".to_string(),
_ => {
let mut chars = part.chars();
match chars.next() {
None => String::new(),
Some(first) => first.to_uppercase().collect::<String>() + chars.as_str(),
}
}
}
})
.collect::<Vec<_>>()
.join(" ")
}
#[wasm_bindgen]
pub fn mcus_for_mcu_family(family_name: String) -> Result<Vec<ValuePrettyPair>, JsValue> {
let family = onerom_config::mcu::Family::try_from_str(&family_name)
.ok_or_else(|| JsValue::from_str(&format!("Unknown MCU family: {}", family_name)))?;
let mcus: Vec<ValuePrettyPair> = onerom_config::mcu::MCU_VARIANTS
.iter()
.filter(|v| v.family() == family)
.map(|v| ValuePrettyPair {
value: v.to_string(),
pretty: v.to_string(), })
.collect();
Ok(mcus)
}
#[wasm_bindgen]
pub fn mcu_chip_id(variant_name: String) -> Result<String, JsValue> {
let variant = onerom_config::mcu::Variant::try_from_str(&variant_name)
.ok_or_else(|| JsValue::from_str(&format!("Unknown MCU variant: {}", variant_name)))?;
Ok(variant.chip_id().to_string())
}
#[wasm_bindgen]
pub struct WasmGenBuilder(GenBuilder);
#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct WasmFileSpec {
pub id: usize,
pub source: String,
pub extract: Option<String>,
pub size_handling: String,
pub chip_type: String,
pub description: Option<String>,
pub rom_size: usize,
pub set_id: usize,
pub cs1: Option<String>,
pub cs2: Option<String>,
pub cs3: Option<String>,
pub set_type: String,
pub set_description: Option<String>,
}
#[wasm_bindgen]
#[allow(dead_code)]
pub struct WasmImages(Vec<u8>, Vec<u8>);
#[wasm_bindgen]
impl WasmImages {
#[wasm_bindgen(getter)]
pub fn metadata(&self) -> Vec<u8> {
self.0.clone()
}
#[wasm_bindgen(getter)]
pub fn firmware_images(&self) -> Vec<u8> {
self.1.clone()
}
}
#[wasm_bindgen]
pub fn gen_builder_from_json(
version: String,
family: String,
config_json: &str,
) -> Result<WasmGenBuilder, String> {
let version = FirmwareVersion::try_from_str(&version)
.map_err(|_| "Invalid firmware version format".to_string())?;
let family = Family::try_from_str(&family).ok_or("Unknown MCU family".to_string())?;
Ok(WasmGenBuilder(
GenBuilder::from_json(version, family, config_json)
.map_err(|e| format!("Error creating GenBuilder: {e:?}"))?,
))
}
#[wasm_bindgen]
pub fn gen_file_specs(builder: &WasmGenBuilder) -> Vec<WasmFileSpec> {
builder
.0
.file_specs()
.into_iter()
.map(|spec| WasmFileSpec {
id: spec.id,
source: spec.source,
extract: spec.extract,
size_handling: serde_json::to_string(&spec.size_handling)
.unwrap()
.trim_matches('"')
.to_string(),
rom_size: spec.rom_size,
chip_type: serde_json::to_string(&spec.chip_type.name())
.unwrap()
.trim_matches('"')
.to_string(),
description: spec.description,
set_id: spec.set_id,
cs1: serde_json::to_string(&spec.cs1)
.ok()
.map(|s| s.trim_matches('"').to_string()),
cs2: serde_json::to_string(&spec.cs2)
.ok()
.map(|s| s.trim_matches('"').to_string()),
cs3: serde_json::to_string(&spec.cs3)
.ok()
.map(|s| s.trim_matches('"').to_string()),
set_type: serde_json::to_string(&spec.set_type)
.unwrap()
.trim_matches('"')
.to_string(),
set_description: spec.set_description,
})
.collect()
}
#[derive(Serialize, Deserialize, Tsify)]
#[tsify(into_wasm_abi, from_wasm_abi)]
pub struct WasmLicense {
pub id: usize,
pub file_id: usize,
pub url: String,
}
#[wasm_bindgen]
pub fn gen_licenses(builder: &mut WasmGenBuilder) -> Vec<WasmLicense> {
builder
.0
.licenses()
.into_iter()
.map(|license| WasmLicense {
id: license.id,
file_id: license.file_id,
url: license.url,
})
.collect()
}
#[wasm_bindgen]
pub fn accept_license(builder: &mut WasmGenBuilder, license: WasmLicense) -> Result<(), String> {
let license = onerom_gen::License::new(license.id, license.file_id, license.url.clone());
builder
.0
.accept_license(&license)
.map_err(|e| format!("Error accepting license: {e:?}"))
}
#[wasm_bindgen]
pub fn gen_add_file(builder: &mut WasmGenBuilder, id: usize, data: Vec<u8>) -> Result<(), String> {
let file_data = FileData { id, data };
builder
.0
.add_file(file_data)
.map_err(|e| format!("Error adding file: {e:?}"))
}
#[wasm_bindgen]
pub fn gen_build(builder: &WasmGenBuilder, properties: JsValue) -> Result<WasmImages, String> {
let props: FirmwareProperties = serde_wasm_bindgen::from_value(properties)
.map_err(|e| format!("Error deserializing properties: {}", e))?;
builder
.0
.build(props)
.map(|(firmware_image, metadata_json)| WasmImages(firmware_image, metadata_json))
.map_err(|e| format!("Error building firmware image: {e:?}"))
}
#[wasm_bindgen]
pub fn gen_description(builder: &WasmGenBuilder) -> String {
builder.0.description()
}
#[wasm_bindgen]
pub fn gen_categories(builder: &WasmGenBuilder) -> Vec<String> {
builder.0.categories()
}
#[wasm_bindgen]
pub fn gen_build_validation(builder: &WasmGenBuilder, properties: JsValue) -> Result<(), String> {
let props: FirmwareProperties = serde_wasm_bindgen::from_value(properties)
.map_err(|e| format!("Error deserializing properties: {}", e))?;
builder
.0
.build_validation(&props)
.map_err(|e| format!("Not ready to build: {e:?}"))
}
struct JsFetch {
callback: js_sys::Function,
}
impl onerom_app::LocalPluginFetch for JsFetch {
type Error = String;
async fn fetch(&self, source: &str) -> Result<Vec<u8>, Self::Error> {
let promise = self
.callback
.call1(&JsValue::NULL, &JsValue::from_str(source))
.map_err(|e| format!("plugin fetch callback threw: {e:?}"))?;
let resolved = wasm_bindgen_futures::JsFuture::from(js_sys::Promise::from(promise))
.await
.map_err(|e| format!("plugin fetch failed for {source}: {e:?}"))?;
Ok(js_sys::Uint8Array::new(&resolved).to_vec())
}
}
fn plugin_err_to_js(e: onerom_app::Error<String>) -> JsValue {
JsValue::from_str(&e.to_string())
}
#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct WasmPluginRelease {
pub version: String,
pub sha256: String,
pub url: String,
pub min_fw_version: String,
}
#[wasm_bindgen]
pub struct PluginCatalog(onerom_app::Catalogue);
#[wasm_bindgen]
impl PluginCatalog {
pub fn plugins(&self) -> Result<JsValue, JsValue> {
serde_wasm_bindgen::to_value(self.0.plugins())
.map_err(|e| JsValue::from_str(&e.to_string()))
}
pub fn newest_compatible(&self, name: String, fw: String) -> Result<JsValue, JsValue> {
let plugin = self
.0
.plugin_by_name(&name)
.ok_or_else(|| JsValue::from_str(&format!("unknown plugin '{name}'")))?;
let fw = FirmwareVersion::try_from_str(&fw)
.map_err(|_| JsValue::from_str("invalid firmware version format"))?;
match onerom_app::newest_compatible(plugin, &fw) {
Some(release) => {
let out = WasmPluginRelease {
version: release.version.to_string(),
sha256: release.sha256.clone(),
url: plugin.binary_url(release),
min_fw_version: release.min_fw_version.to_string(),
};
serde_wasm_bindgen::to_value(&out).map_err(|e| JsValue::from_str(&e.to_string()))
}
None => Ok(JsValue::NULL),
}
}
}
#[wasm_bindgen]
pub async fn plugin_catalog(fetch_callback: js_sys::Function) -> Result<PluginCatalog, JsValue> {
let fetch = JsFetch {
callback: fetch_callback,
};
let mut catalogue = onerom_app::Catalogue::fetch(&fetch)
.await
.map_err(plugin_err_to_js)?;
let _failures = catalogue.load_all_releases_resilient(&fetch).await;
Ok(PluginCatalog(catalogue))
}
#[derive(Serialize, Tsify)]
#[tsify(into_wasm_abi)]
pub struct WasmPluginLabel {
pub label: String,
pub source: String,
pub official: bool,
pub version: Option<String>,
pub description: Option<String>,
}
#[wasm_bindgen]
pub async fn resolve_plugin_label(
slot_index: usize,
source: String,
fetch_callback: js_sys::Function,
) -> Result<JsValue, JsValue> {
let fetch = JsFetch {
callback: fetch_callback,
};
let Some(display) = onerom_app::resolve_plugin_display(slot_index, &source, &fetch).await
else {
return Ok(JsValue::NULL);
};
let (official, version, description) = match &display.origin {
onerom_app::PluginOrigin::Manifest { plugin, version } => {
(true, Some(version.to_string()), plugin.description.clone())
}
onerom_app::PluginOrigin::Local { .. } => (false, None, None),
};
let out = WasmPluginLabel {
label: display.display_label().to_string(),
source,
official,
version,
description,
};
serde_wasm_bindgen::to_value(&out).map_err(|e| JsValue::from_str(&e.to_string()))
}