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) -> Result { 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 { 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 { Err(CartridgeError::InvalidFormat) } } fn normalize_capabilities(capabilities: &[Capability]) -> Result { 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>) -> 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)); } }