250 lines
7.8 KiB
Rust

use crate::cartridge::{Capability, Cartridge, CartridgeDTO, CartridgeError, CartridgeManifest};
use crate::syscalls::{CapFlags, caps};
use std::collections::HashSet;
use std::fs;
use std::path::Path;
pub struct CartridgeLoader;
impl CartridgeLoader {
pub fn load(path: impl AsRef<Path>) -> Result<Cartridge, CartridgeError> {
let path = path.as_ref();
if !path.exists() {
return Err(CartridgeError::NotFound);
}
if path.is_dir() {
DirectoryCartridgeLoader::load(path)
} else if path.extension().is_some_and(|ext| ext == "pmc") {
PackedCartridgeLoader::load(path)
} else {
Err(CartridgeError::InvalidFormat)
}
}
}
pub struct DirectoryCartridgeLoader;
impl DirectoryCartridgeLoader {
pub fn load(path: &Path) -> Result<Cartridge, CartridgeError> {
let manifest_path = path.join("manifest.json");
if !manifest_path.exists() {
return Err(CartridgeError::InvalidManifest);
}
let manifest_content =
fs::read_to_string(manifest_path).map_err(|_| CartridgeError::IoError)?;
let manifest: CartridgeManifest =
serde_json::from_str(&manifest_content).map_err(|_| CartridgeError::InvalidManifest)?;
// Additional validation as per requirements
if manifest.magic != "PMTU" {
return Err(CartridgeError::InvalidManifest);
}
if manifest.cartridge_version != 1 {
return Err(CartridgeError::UnsupportedVersion);
}
let capabilities = normalize_capabilities(&manifest.capabilities)?;
let program_path = path.join("program.pbx");
if !program_path.exists() {
return Err(CartridgeError::MissingProgram);
}
let program = fs::read(program_path).map_err(|_| CartridgeError::IoError)?;
let assets_pa_path = path.join("assets.pa");
let assets = if assets_pa_path.exists() {
fs::read(assets_pa_path).map_err(|_| CartridgeError::IoError)?
} else {
Vec::new()
};
let dto = CartridgeDTO {
app_id: manifest.app_id,
title: manifest.title,
app_version: manifest.app_version,
app_mode: manifest.app_mode,
entrypoint: manifest.entrypoint,
capabilities,
program,
assets,
asset_table: manifest.asset_table,
preload: manifest.preload,
};
Ok(Cartridge::from(dto))
}
}
pub struct PackedCartridgeLoader;
impl PackedCartridgeLoader {
pub fn load(_path: &Path) -> Result<Cartridge, CartridgeError> {
Err(CartridgeError::InvalidFormat)
}
}
fn normalize_capabilities(capabilities: &[Capability]) -> Result<CapFlags, CartridgeError> {
let mut seen = HashSet::new();
let mut normalized = caps::NONE;
for capability in capabilities {
if !seen.insert(*capability) {
return Err(CartridgeError::InvalidManifest);
}
normalized |= match capability {
Capability::None => caps::NONE,
Capability::System => caps::SYSTEM,
Capability::Gfx => caps::GFX,
Capability::Audio => caps::AUDIO,
Capability::Fs => caps::FS,
Capability::Log => caps::LOG,
Capability::Asset => caps::ASSET,
Capability::Bank => caps::BANK,
Capability::All => caps::ALL,
};
}
Ok(normalized)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
use std::path::{Path, PathBuf};
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{SystemTime, UNIX_EPOCH};
static TEST_DIR_COUNTER: AtomicU64 = AtomicU64::new(0);
struct TestCartridgeDir {
path: PathBuf,
}
impl TestCartridgeDir {
fn new(manifest: serde_json::Value) -> Self {
let unique = TEST_DIR_COUNTER.fetch_add(1, Ordering::Relaxed);
let timestamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system time must be after unix epoch")
.as_nanos();
let path = std::env::temp_dir()
.join(format!("prometeu-hal-cartridge-loader-{}-{}", timestamp, unique));
fs::create_dir_all(&path).expect("must create temporary cartridge directory");
fs::write(
path.join("manifest.json"),
serde_json::to_vec_pretty(&manifest).expect("manifest must serialize"),
)
.expect("must write manifest.json");
fs::write(path.join("program.pbx"), [0x01_u8, 0x02, 0x03]).expect("must write program");
Self { path }
}
fn path(&self) -> &Path {
&self.path
}
}
impl Drop for TestCartridgeDir {
fn drop(&mut self) {
let _ = fs::remove_dir_all(&self.path);
}
}
fn manifest_with_capabilities(capabilities: Option<Vec<&str>>) -> serde_json::Value {
let mut manifest = json!({
"magic": "PMTU",
"cartridge_version": 1,
"app_id": 1001,
"title": "Example",
"app_version": "1.0.0",
"app_mode": "Game",
"entrypoint": "main"
});
if let Some(capabilities) = capabilities {
manifest["capabilities"] = json!(capabilities);
}
manifest
}
#[test]
fn load_without_capabilities_defaults_to_none() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(None));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::NONE);
}
#[test]
fn load_with_single_capability_normalizes_to_flag() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx"])));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::GFX);
}
#[test]
fn load_with_multiple_capabilities_combines_flags() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx", "audio"])));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::GFX | caps::AUDIO);
}
#[test]
fn load_with_all_capability_normalizes_to_all_flags() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["all"])));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::ALL);
}
#[test]
fn load_with_none_capability_keeps_zero_flags() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["none"])));
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
assert_eq!(cartridge.capabilities, caps::NONE);
}
#[test]
fn load_with_duplicate_capabilities_fails() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx", "gfx"])));
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidManifest));
}
#[test]
fn load_with_unknown_capability_fails() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["network"])));
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidManifest));
}
#[test]
fn load_with_legacy_input_capability_fails() {
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["input"])));
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
assert!(matches!(error, CartridgeError::InvalidManifest));
}
}