use crate::game::RomIdentity;
use crate::rom_header::RomHeader;
use std::cell::RefCell;
use std::ffi::{c_char, c_int, CStr, CString};
use std::path::PathBuf;
pub const UXIE_OK: c_int = 0;
pub const UXIE_ERR_INVALID_INPUT: c_int = -1;
pub const UXIE_ERR_IO: c_int = -2;
pub const UXIE_ERR_PARSE: c_int = -3;
pub const UXIE_ERR_NOT_FOUND: c_int = -4;
pub const UXIE_ERR_UNKNOWN: c_int = -99;
thread_local! {
static LAST_ERROR: RefCell<Option<CString>> = const { RefCell::new(None) };
}
pub(crate) fn set_last_error(msg: impl Into<String>) {
let text = msg.into();
let c = CString::new(text)
.unwrap_or_else(|_| CString::new("error message contained interior NUL").unwrap());
LAST_ERROR.with(|slot| *slot.borrow_mut() = Some(c));
}
pub(crate) fn clear_last_error() {
LAST_ERROR.with(|slot| *slot.borrow_mut() = None);
}
#[unsafe(no_mangle)]
pub extern "C" fn uxie_last_error() -> *const c_char {
LAST_ERROR.with(|slot| {
slot.borrow()
.as_ref()
.map_or(std::ptr::null(), |c| c.as_ptr())
})
}
pub(crate) fn string_to_c(s: String) -> *mut c_char {
CString::new(s).map_or_else(
|_| {
set_last_error("returned string contained an interior NUL byte");
std::ptr::null_mut()
},
|c| c.into_raw(),
)
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn uxie_free_string(ptr: *mut c_char) {
if !ptr.is_null() {
unsafe {
let _ = CString::from_raw(ptr);
}
}
}
pub(crate) unsafe fn c_str_to_path(ptr: *const c_char) -> Option<PathBuf> {
if ptr.is_null() {
return None;
}
unsafe { CStr::from_ptr(ptr).to_str().ok().map(PathBuf::from) }
}
#[unsafe(no_mangle)]
pub extern "C" fn uxie_version() -> *mut c_char {
clear_last_error();
string_to_c(env!("CARGO_PKG_VERSION").to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use std::ffi::CString;
fn write_header_yaml(dir: &std::path::Path, gamecode: &str) -> CString {
let yaml = dir.join("header.yaml");
std::fs::write(
&yaml,
format!("title: TESTROM\ngamecode: {gamecode}\nmakercode: '01'\nrom_version: 0\n"),
)
.unwrap();
CString::new(yaml.to_str().unwrap()).unwrap()
}
#[test]
fn identify_rom_returns_identity_json() {
let dir = tempfile::tempdir().unwrap();
let path = write_header_yaml(dir.path(), "CPUE");
let ptr = unsafe { uxie_identify_rom(path.as_ptr()) };
assert!(!ptr.is_null());
let json = unsafe { CStr::from_ptr(ptr) }.to_str().unwrap().to_string();
unsafe { uxie_free_string(ptr) };
let v: serde_json::Value = serde_json::from_str(&json).unwrap();
assert_eq!(v["game_code"], "CPUE");
assert_eq!(v["game"], "Platinum");
assert_eq!(v["family"], "Platinum");
assert_eq!(v["language"], "English");
}
#[test]
fn identify_rom_unknown_code_returns_null_with_error() {
let dir = tempfile::tempdir().unwrap();
let path = write_header_yaml(dir.path(), "ZZZZ");
let ptr = unsafe { uxie_identify_rom(path.as_ptr()) };
assert!(ptr.is_null());
let err = uxie_last_error();
assert!(!err.is_null());
let msg = unsafe { CStr::from_ptr(err) }.to_str().unwrap();
assert!(msg.contains("ZZZZ"), "error should mention the bad code: {msg}");
}
#[test]
fn identify_rom_null_path_returns_null_with_error() {
let ptr = unsafe { uxie_identify_rom(std::ptr::null()) };
assert!(ptr.is_null());
assert!(!uxie_last_error().is_null());
}
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn uxie_identify_rom(path: *const c_char) -> *mut c_char {
clear_last_error();
let Some(path) = (unsafe { c_str_to_path(path) }) else {
set_last_error("uxie_identify_rom: path was null or not valid UTF-8");
return std::ptr::null_mut();
};
let header = match RomHeader::open(&path) {
Ok(header) => header,
Err(e) => {
set_last_error(format!(
"uxie_identify_rom: failed to read ROM header from {}: {e}",
path.display()
));
return std::ptr::null_mut();
}
};
let Some(identity) = RomIdentity::from_header(&header) else {
set_last_error(format!(
"uxie_identify_rom: '{}' is not a recognized Gen 4 Pokémon game code",
header.game_code
));
return std::ptr::null_mut();
};
match serde_json::to_string(&identity) {
Ok(json) => string_to_c(json),
Err(e) => {
set_last_error(format!("uxie_identify_rom: failed to serialize identity: {e}"));
std::ptr::null_mut()
}
}
}