use wasm_bindgen::prelude::*;
use crate::ast::{Module, Value};
use crate::format::solana::LoaderKind;
use crate::translate::compile::AsmWarning;
#[wasm_bindgen]
pub fn decompile(bytes: &[u8], format: &str) -> Result<String, JsError> {
set_panic_hook();
match format {
"auto" => decompile_auto(bytes),
"elf" => decompile_as_elf(bytes),
"pe" => decompile_as_pe(bytes),
"macho" => decompile_as_macho(bytes),
"wasm" => decompile_as_wasm(bytes),
"raw-6502" => decompile_as_raw_6502(bytes),
other => Err(JsError::new(&format!(
"unsupported format hint {other:?} (expected \"auto\", \"elf\", \"pe\", \"macho\", \"wasm\", or \"raw-6502\")"
))),
}
}
fn decompile_auto(bytes: &[u8]) -> Result<String, JsError> {
if crate::format::elf::is_elf64_le(bytes) {
return decompile_as_elf(bytes);
}
if crate::format::pe::is_pe(bytes) {
return decompile_as_pe(bytes);
}
if crate::format::macho::is_macho64(bytes) {
return decompile_as_macho(bytes);
}
if crate::format::wasm::is_wasm(bytes) {
return decompile_as_wasm(bytes);
}
Err(JsError::new(
"auto-detect found no ELF / PE / Mach-O / WASM header. Pick an explicit format if this is a raw image (e.g. 6502).",
))
}
fn decompile_as_elf(bytes: &[u8]) -> Result<String, JsError> {
let elf = crate::format::elf::Elf64File::parse(bytes)
.map_err(|e| JsError::new(&format!("parse ELF: {e}")))?;
crate::translate::decompile::decompile_to_text(&elf)
.map_err(|e| JsError::new(&format!("decompile ELF: {e}")))
}
fn decompile_as_pe(bytes: &[u8]) -> Result<String, JsError> {
let pe = crate::format::pe::PeFile::parse(bytes)
.map_err(|e| JsError::new(&format!("parse PE: {e}")))?;
Ok(crate::translate::decompile::decompile_pe_to_text(&pe))
}
fn decompile_as_macho(bytes: &[u8]) -> Result<String, JsError> {
let macho = crate::format::macho::MachoFile::parse(bytes)
.map_err(|e| JsError::new(&format!("parse Mach-O: {e}")))?;
Ok(crate::translate::decompile::decompile_macho_to_text(&macho))
}
fn decompile_as_wasm(bytes: &[u8]) -> Result<String, JsError> {
let wasm = crate::format::wasm::WasmFile::parse(bytes)
.map_err(|e| JsError::new(&format!("parse WASM: {e}")))?;
Ok(crate::translate::decompile::decompile_wasm_to_text(&wasm))
}
fn decompile_as_raw_6502(bytes: &[u8]) -> Result<String, JsError> {
let load_addr = raw_6502_load_addr(bytes).ok_or_else(|| {
JsError::new(
"6502 raw image: file must be 6..=65536 bytes and the reset vector at $FFFC must point inside the image",
)
})?;
let image = crate::format::raw::RawImage::new(bytes.to_vec(), load_addr);
crate::translate::decompile::decompile_raw_6502_to_text(&image)
.map_err(|e| JsError::new(&format!("decompile 6502 raw: {e}")))
}
#[wasm_bindgen]
pub fn compile(source: &str) -> Result<Vec<u8>, JsError> {
set_panic_hook();
let ast = crate::translate::compile::parse(source)
.map_err(|e| JsError::new(&format!("parse .ud: {e}")))?;
let format = read_string(&ast.module, "format").ok_or_else(|| {
JsError::new(
"missing `@module.format` (expected \"elf\", \"pe\", \"macho\", \"wasm\", or \"raw\")",
)
})?;
let warnings = crate::translate::compile::verify_asm(&ast);
let bytes = match format.as_str() {
"elf" => crate::translate::compile::lower_to_elf(&ast)
.map_err(|e| JsError::new(&with_warnings(&format!("lower to ELF: {e}"), &warnings))),
"pe" => crate::translate::compile::lower_to_pe(&ast)
.map_err(|e| JsError::new(&with_warnings(&format!("lower to PE: {e}"), &warnings))),
"macho" => crate::translate::compile::lower_to_macho(&ast)
.map_err(|e| JsError::new(&with_warnings(&format!("lower to Mach-O: {e}"), &warnings))),
"wasm" => crate::translate::compile::lower_to_wasm(&ast)
.map_err(|e| JsError::new(&with_warnings(&format!("lower to WASM: {e}"), &warnings))),
"raw" => crate::translate::compile::lower_to_raw(&ast)
.map_err(|e| JsError::new(&with_warnings(&format!("lower to raw: {e}"), &warnings))),
other => Err(JsError::new(&format!(
"unsupported `@module.format` value {other:?} (expected \"elf\", \"pe\", \"macho\", \"wasm\", or \"raw\")"
))),
}?;
Ok(bytes)
}
#[wasm_bindgen]
pub fn verify(source: &str) -> Result<String, JsError> {
set_panic_hook();
let ast = crate::translate::compile::parse(source)
.map_err(|e| JsError::new(&format!("parse .ud: {e}")))?;
let warnings = crate::translate::compile::verify_asm(&ast);
Ok(format_warnings(&warnings))
}
#[wasm_bindgen]
#[must_use]
pub fn solana_classify_loader(owner: &str) -> String {
set_panic_hook();
match crate::format::solana::classify_loader(owner) {
LoaderKind::BpfLoader2 => "bpf_loader_2",
LoaderKind::Upgradeable => "upgradeable",
LoaderKind::LoaderV4 => "loader_v4",
LoaderKind::Unknown => "unknown",
}
.to_string()
}
#[wasm_bindgen]
pub fn solana_programdata_pubkey_base58(program_data: &[u8]) -> Result<String, JsError> {
set_panic_hook();
let pk = crate::format::solana::programdata_pubkey(program_data)
.map_err(|e| JsError::new(&format!("Solana Program account: {e}")))?;
Ok(bs58::encode(pk).into_string())
}
#[wasm_bindgen]
pub fn solana_strip_elf(account_data: &[u8], loader: &str) -> Result<Vec<u8>, JsError> {
set_panic_hook();
let stripped: &[u8] = match loader {
"bpf_loader_2" => crate::format::solana::strip_bpf_loader_v2(account_data)
.map_err(|e| JsError::new(&format!("BPFLoader2: {e}")))?,
"upgradeable" => crate::format::solana::strip_bpf_loader_upgradeable(account_data)
.map_err(|e| JsError::new(&format!("BPFLoaderUpgradeable: {e}")))?,
"loader_v4" => crate::format::solana::strip_loader_v4(account_data)
.map_err(|e| JsError::new(&format!("LoaderV4: {e}")))?,
other => {
return Err(JsError::new(&format!(
"unknown loader hint {other:?} (expected \"bpf_loader_2\", \"upgradeable\", \"loader_v4\")"
)));
}
};
Ok(stripped.to_vec())
}
fn with_warnings(msg: &str, warnings: &[AsmWarning]) -> String {
if warnings.is_empty() {
msg.into()
} else {
let mut out = String::from(msg);
out.push('\n');
out.push_str(&format_warnings(warnings));
out
}
}
fn format_warnings(warnings: &[AsmWarning]) -> String {
use std::fmt::Write as _;
let mut s = String::new();
for (i, w) in warnings.iter().enumerate() {
if i > 0 {
s.push('\n');
}
let _ = write!(s, "{w:?}");
}
s
}
fn read_string(module: &Module, name: &str) -> Option<String> {
module.fields.iter().find_map(|f| {
if f.name != name {
return None;
}
let Value::String(s) = &f.value else {
return None;
};
Some(s.clone())
})
}
fn raw_6502_load_addr(bytes: &[u8]) -> Option<u64> {
let len = bytes.len();
if !(6..=0x10000).contains(&len) {
return None;
}
let load_addr = 0x10000u64 - len as u64;
let end = 0x10000u64;
let reset_lo_off = usize::try_from(0xFFFCu64 - load_addr).ok()?;
let reset_hi_off = reset_lo_off.checked_add(1)?;
let lo = u64::from(*bytes.get(reset_lo_off)?);
let hi = u64::from(*bytes.get(reset_hi_off)?);
let reset = (hi << 8) | lo;
if reset < load_addr || reset >= end {
None
} else {
Some(load_addr)
}
}
fn set_panic_hook() {
use std::sync::Once;
static ONCE: Once = Once::new();
ONCE.call_once(|| {
console_error_panic_hook::set_once();
crate::translate::register_all_arches();
});
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn decompile_then_compile_elf_round_trips() {
let path = concat!(env!("CARGO_MANIFEST_DIR"), "/testdata/hello-gcc13-O0");
let Ok(bytes) = std::fs::read(path) else {
return; };
let text = decompile(&bytes, "auto").expect("decompile");
let rebuilt = compile(&text).expect("compile");
assert_eq!(
rebuilt,
bytes,
"round-trip diverged: input {} bytes, rebuilt {} bytes",
bytes.len(),
rebuilt.len()
);
}
}