250 lines
7.8 KiB
Rust
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));
|
|
}
|
|
}
|