diff --git a/crates/console/prometeu-drivers/src/asset.rs b/crates/console/prometeu-drivers/src/asset.rs index 1027a131..9521d95e 100644 --- a/crates/console/prometeu-drivers/src/asset.rs +++ b/crates/console/prometeu-drivers/src/asset.rs @@ -5,6 +5,7 @@ use prometeu_hal::asset::{ AssetEntry, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats, }; +use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::color::Color; use prometeu_hal::sample::Sample; use prometeu_hal::sound_bank::SoundBank; @@ -106,7 +107,7 @@ pub struct AssetManager { name_to_id: Arc>>, handles: Arc>>, next_handle_id: Mutex, - assets_data: Arc>>, + assets_data: Arc>, /// Narrow hardware interfaces gfx_installer: Arc, @@ -136,7 +137,7 @@ impl AssetBridge for AssetManager { &self, assets: Vec, preload: Vec, - assets_data: Vec, + assets_data: AssetsPayloadSource, ) { self.initialize_for_cartridge(assets, preload, assets_data) } @@ -172,7 +173,7 @@ impl AssetBridge for AssetManager { impl AssetManager { pub fn new( assets: Vec, - assets_data: Vec, + assets_data: AssetsPayloadSource, gfx_installer: Arc, sound_installer: Arc, ) -> Self { @@ -203,7 +204,7 @@ impl AssetManager { &self, assets: Vec, preload: Vec, - assets_data: Vec, + assets_data: AssetsPayloadSource, ) { self.shutdown(); { @@ -450,22 +451,20 @@ impl AssetManager { fn perform_load_tile_bank( entry: &AssetEntry, - assets_data: Arc>>, + assets_data: Arc>, ) -> Result { if entry.codec != "RAW" { return Err(format!("Unsupported codec: {}", entry.codec)); } - let assets_data = assets_data.read().unwrap(); - - let start = entry.offset as usize; - let end = start + entry.size as usize; - - if end > assets_data.len() { - return Err("Asset offset/size out of bounds".to_string()); - } - - let buffer = &assets_data[start..end]; + let buffer = { + let assets_data = assets_data.read().unwrap(); + assets_data + .open_slice(entry.offset, entry.size) + .map_err(|_| "Asset offset/size out of bounds".to_string())? + .read_all() + .map_err(|_| "Asset payload read failed".to_string())? + }; // Decode TILEBANK metadata let tile_size_val = @@ -505,22 +504,20 @@ impl AssetManager { fn perform_load_sound_bank( entry: &AssetEntry, - assets_data: Arc>>, + assets_data: Arc>, ) -> Result { if entry.codec != "RAW" { return Err(format!("Unsupported codec: {}", entry.codec)); } - let assets_data = assets_data.read().unwrap(); - - let start = entry.offset as usize; - let end = start + entry.size as usize; - - if end > assets_data.len() { - return Err("Asset offset/size out of bounds".to_string()); - } - - let buffer = &assets_data[start..end]; + let buffer = { + let assets_data = assets_data.read().unwrap(); + assets_data + .open_slice(entry.offset, entry.size) + .map_err(|_| "Asset offset/size out of bounds".to_string())? + .read_all() + .map_err(|_| "Asset payload read failed".to_string())? + }; let sample_rate = entry.metadata.get("sample_rate").and_then(|v| v.as_u64()).unwrap_or(44100) as u32; @@ -818,7 +815,12 @@ mod tests { let data = test_tile_asset_data(); let asset_entry = test_tile_asset_entry("test_tiles", data.len()); - let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer); + let am = AssetManager::new( + vec![asset_entry], + AssetsPayloadSource::from_bytes(data), + gfx_installer, + sound_installer, + ); let slot = SlotRef::gfx(0); let handle = am.load("test_tiles", slot).expect("Should start loading"); @@ -853,7 +855,12 @@ mod tests { let data = test_tile_asset_data(); let asset_entry = test_tile_asset_entry("test_tiles", data.len()); - let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer); + let am = AssetManager::new( + vec![asset_entry], + AssetsPayloadSource::from_bytes(data), + gfx_installer, + sound_installer, + ); let handle1 = am.load("test_tiles", SlotRef::gfx(0)).unwrap(); let start = Instant::now(); @@ -892,7 +899,12 @@ mod tests { }), }; - let am = AssetManager::new(vec![asset_entry], data, gfx_installer, sound_installer); + let am = AssetManager::new( + vec![asset_entry], + AssetsPayloadSource::from_bytes(data), + gfx_installer, + sound_installer, + ); let slot = SlotRef::audio(0); let handle = am.load("test_sound", slot).expect("Should start loading"); @@ -934,12 +946,17 @@ mod tests { let preload = vec![PreloadEntry { asset_name: "preload_sound".to_string(), slot: 5 }]; - let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer); + let am = AssetManager::new( + vec![], + AssetsPayloadSource::empty(), + gfx_installer, + sound_installer, + ); // Before init, slot 5 is empty assert!(banks.sound_bank_slot(5).is_none()); - am.initialize_for_cartridge(vec![asset_entry], preload, data); + am.initialize_for_cartridge(vec![asset_entry], preload, AssetsPayloadSource::from_bytes(data)); // After init, slot 5 should be occupied because of preload assert!(banks.sound_bank_slot(5).is_some()); @@ -958,8 +975,13 @@ mod tests { let preload = vec![PreloadEntry { asset_name: "my_tiles".to_string(), slot: 3 }]; - let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer); - am.initialize_for_cartridge(vec![asset_entry], preload, data); + let am = AssetManager::new( + vec![], + AssetsPayloadSource::empty(), + gfx_installer, + sound_installer, + ); + am.initialize_for_cartridge(vec![asset_entry], preload, AssetsPayloadSource::from_bytes(data)); assert_eq!(am.find_slot_by_name("my_tiles", BankType::TILES), Some(3)); assert_eq!(am.find_slot_by_name("unknown", BankType::TILES), None); @@ -971,7 +993,12 @@ mod tests { let banks = Arc::new(MemoryBanks::new()); let gfx_installer = Arc::clone(&banks) as Arc; let sound_installer = Arc::clone(&banks) as Arc; - let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer); + let am = AssetManager::new( + vec![], + AssetsPayloadSource::empty(), + gfx_installer, + sound_installer, + ); let result = am.load("missing", SlotRef::gfx(0)); @@ -986,7 +1013,7 @@ mod tests { let data = test_tile_asset_data(); let am = AssetManager::new( vec![test_tile_asset_entry("test_tiles", data.len())], - data, + AssetsPayloadSource::from_bytes(data), gfx_installer, sound_installer, ); @@ -1004,7 +1031,7 @@ mod tests { let data = test_tile_asset_data(); let am = AssetManager::new( vec![test_tile_asset_entry("test_tiles", data.len())], - data, + AssetsPayloadSource::from_bytes(data), gfx_installer, sound_installer, ); @@ -1019,7 +1046,12 @@ mod tests { let banks = Arc::new(MemoryBanks::new()); let gfx_installer = Arc::clone(&banks) as Arc; let sound_installer = Arc::clone(&banks) as Arc; - let am = AssetManager::new(vec![], vec![], gfx_installer, sound_installer); + let am = AssetManager::new( + vec![], + AssetsPayloadSource::empty(), + gfx_installer, + sound_installer, + ); assert_eq!(am.status(999), LoadStatus::UnknownHandle); } @@ -1032,7 +1064,7 @@ mod tests { let data = test_tile_asset_data(); let am = AssetManager::new( vec![test_tile_asset_entry("test_tiles", data.len())], - data, + AssetsPayloadSource::from_bytes(data), gfx_installer, sound_installer, ); diff --git a/crates/console/prometeu-drivers/src/hardware.rs b/crates/console/prometeu-drivers/src/hardware.rs index eadd0952..21834223 100644 --- a/crates/console/prometeu-drivers/src/hardware.rs +++ b/crates/console/prometeu-drivers/src/hardware.rs @@ -7,6 +7,7 @@ use crate::memory_banks::{ }; use crate::pad::Pad; use crate::touch::Touch; +use prometeu_hal::cartridge::AssetsPayloadSource; use prometeu_hal::{AssetBridge, AudioBridge, GfxBridge, HardwareBridge, PadBridge, TouchBridge}; use std::sync::Arc; @@ -98,7 +99,7 @@ impl Hardware { touch: Touch::default(), assets: AssetManager::new( vec![], - vec![], + AssetsPayloadSource::empty(), Arc::clone(&memory_banks) as Arc, Arc::clone(&memory_banks) as Arc, ), diff --git a/crates/console/prometeu-firmware/src/firmware/firmware.rs b/crates/console/prometeu-firmware/src/firmware/firmware.rs index 8d33e88f..8860f7cc 100644 --- a/crates/console/prometeu-firmware/src/firmware/firmware.rs +++ b/crates/console/prometeu-firmware/src/firmware/firmware.rs @@ -171,7 +171,7 @@ mod tests { use prometeu_bytecode::assembler::assemble; use prometeu_bytecode::model::{BytecodeModule, FunctionMeta, SyscallDecl}; use prometeu_drivers::hardware::Hardware; - use prometeu_hal::cartridge::AppMode; + use prometeu_hal::cartridge::{AppMode, AssetsPayloadSource}; use prometeu_hal::syscalls::caps; use prometeu_system::CrashReport; @@ -184,7 +184,7 @@ mod tests { entrypoint: "".into(), capabilities: 0, program: vec![0, 0, 0, 0], - assets: vec![], + assets: AssetsPayloadSource::empty(), asset_table: vec![], preload: vec![], } @@ -221,7 +221,7 @@ mod tests { entrypoint: "".into(), capabilities: caps::GFX, program, - assets: vec![], + assets: AssetsPayloadSource::empty(), asset_table: vec![], preload: vec![], } diff --git a/crates/console/prometeu-hal/src/asset_bridge.rs b/crates/console/prometeu-hal/src/asset_bridge.rs index 21e62f03..1ed7acaa 100644 --- a/crates/console/prometeu-hal/src/asset_bridge.rs +++ b/crates/console/prometeu-hal/src/asset_bridge.rs @@ -2,13 +2,14 @@ use crate::asset::{ AssetEntry, AssetLoadError, AssetOpStatus, BankStats, BankType, HandleId, LoadStatus, PreloadEntry, SlotRef, SlotStats, }; +use crate::cartridge::AssetsPayloadSource; pub trait AssetBridge { fn initialize_for_cartridge( &self, assets: Vec, preload: Vec, - assets_data: Vec, + assets_data: AssetsPayloadSource, ); fn load(&self, asset_name: &str, slot: SlotRef) -> Result; fn status(&self, handle: HandleId) -> LoadStatus; diff --git a/crates/console/prometeu-hal/src/cartridge.rs b/crates/console/prometeu-hal/src/cartridge.rs index 35e1d198..19a5a247 100644 --- a/crates/console/prometeu-hal/src/cartridge.rs +++ b/crates/console/prometeu-hal/src/cartridge.rs @@ -1,6 +1,10 @@ use crate::asset::{AssetEntry, PreloadEntry}; use crate::syscalls::CapFlags; use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io::{self, Read, Seek, SeekFrom}; +use std::path::PathBuf; +use std::sync::Arc; pub const ASSETS_PA_MAGIC: [u8; 4] = *b"ASPA"; pub const ASSETS_PA_SCHEMA_VERSION: u32 = 1; @@ -21,12 +25,12 @@ pub struct Cartridge { pub entrypoint: String, pub capabilities: CapFlags, pub program: Vec, - pub assets: Vec, + pub assets: AssetsPayloadSource, pub asset_table: Vec, pub preload: Vec, } -#[derive(Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone)] pub struct CartridgeDTO { pub app_id: u32, pub title: String, @@ -35,10 +39,8 @@ pub struct CartridgeDTO { pub entrypoint: String, pub capabilities: CapFlags, pub program: Vec, - pub assets: Vec, - #[serde(default)] + pub assets: AssetsPayloadSource, pub asset_table: Vec, - #[serde(default)] pub preload: Vec, } @@ -59,6 +61,88 @@ impl From for Cartridge { } } +#[derive(Debug, Clone)] +pub enum AssetsPayloadSource { + Memory(Arc<[u8]>), + File(Arc), +} + +impl AssetsPayloadSource { + pub fn empty() -> Self { + Self::Memory(Arc::<[u8]>::from(Vec::::new())) + } + + pub fn from_bytes(bytes: Vec) -> Self { + Self::Memory(Arc::<[u8]>::from(bytes)) + } + + pub fn from_file(path: PathBuf, payload_offset: u64, payload_len: u64) -> Self { + Self::File(Arc::new(FileAssetsPayloadSource { path, payload_offset, payload_len })) + } + + pub fn is_empty(&self) -> bool { + match self { + Self::Memory(bytes) => bytes.is_empty(), + Self::File(source) => source.payload_len == 0, + } + } + + pub fn open_slice(&self, offset: u64, size: u64) -> io::Result { + match self { + Self::Memory(bytes) => { + let start = usize::try_from(offset).map_err(|_| invalid_input("asset offset overflow"))?; + let len = usize::try_from(size).map_err(|_| invalid_input("asset size overflow"))?; + let end = start.checked_add(len).ok_or_else(|| invalid_input("asset range overflow"))?; + if end > bytes.len() { + return Err(invalid_input("asset range out of bounds")); + } + + Ok(AssetsPayloadSlice::Memory { bytes: Arc::clone(bytes), start, len }) + } + Self::File(source) => { + let end = offset.checked_add(size).ok_or_else(|| invalid_input("asset range overflow"))?; + if end > source.payload_len { + return Err(invalid_input("asset range out of bounds")); + } + + Ok(AssetsPayloadSlice::File { source: Arc::clone(source), offset, size }) + } + } + } +} + +#[derive(Debug)] +pub struct FileAssetsPayloadSource { + pub path: PathBuf, + pub payload_offset: u64, + pub payload_len: u64, +} + +#[derive(Debug, Clone)] +pub enum AssetsPayloadSlice { + Memory { bytes: Arc<[u8]>, start: usize, len: usize }, + File { source: Arc, offset: u64, size: u64 }, +} + +impl AssetsPayloadSlice { + pub fn read_all(&self) -> io::Result> { + match self { + Self::Memory { bytes, start, len } => Ok(bytes[*start..*start + *len].to_vec()), + Self::File { source, offset, size } => { + let mut file = File::open(&source.path)?; + file.seek(SeekFrom::Start(source.payload_offset + offset))?; + let mut buffer = vec![0_u8; usize::try_from(*size).map_err(|_| invalid_input("asset size overflow"))?]; + file.read_exact(&mut buffer)?; + Ok(buffer) + } + } + } +} + +fn invalid_input(message: &'static str) -> io::Error { + io::Error::new(io::ErrorKind::InvalidInput, message) +} + #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub struct AssetsPackPrelude { pub magic: [u8; 4], diff --git a/crates/console/prometeu-hal/src/cartridge_loader.rs b/crates/console/prometeu-hal/src/cartridge_loader.rs index d0ae94f1..b99c10bf 100644 --- a/crates/console/prometeu-hal/src/cartridge_loader.rs +++ b/crates/console/prometeu-hal/src/cartridge_loader.rs @@ -1,10 +1,12 @@ use crate::cartridge::{ ASSETS_PA_MAGIC, ASSETS_PA_PRELUDE_SIZE, ASSETS_PA_SCHEMA_VERSION, AssetsPackHeader, - AssetsPackPrelude, Capability, Cartridge, CartridgeDTO, CartridgeError, CartridgeManifest, + AssetsPackPrelude, AssetsPayloadSource, Capability, Cartridge, CartridgeDTO, CartridgeError, + CartridgeManifest, }; use crate::syscalls::{CapFlags, caps}; use std::collections::HashSet; use std::fs; +use std::io::{Read, Seek, SeekFrom}; use std::path::Path; pub struct CartridgeLoader; @@ -59,15 +61,22 @@ impl DirectoryCartridgeLoader { let assets_pa_path = path.join("assets.pa"); let (assets, asset_table, preload) = if assets_pa_path.exists() { - let assets_pa = fs::read(assets_pa_path).map_err(|_| CartridgeError::IoError)?; - let parsed = parse_assets_pack(&assets_pa)?; - (parsed.payload.to_vec(), parsed.header.asset_table, parsed.header.preload) + let parsed = parse_assets_pack(&assets_pa_path)?; + ( + AssetsPayloadSource::from_file( + assets_pa_path.clone(), + parsed.payload_offset, + parsed.payload_len, + ), + parsed.header.asset_table, + parsed.header.preload, + ) } else { if capabilities & caps::ASSET != 0 { return Err(CartridgeError::MissingAssets); } - (Vec::new(), Vec::new(), Vec::new()) + (AssetsPayloadSource::empty(), Vec::new(), Vec::new()) }; let dto = CartridgeDTO { @@ -120,37 +129,48 @@ fn normalize_capabilities(capabilities: &[Capability]) -> Result { +struct ParsedAssetsPack { header: AssetsPackHeader, - payload: &'a [u8], + payload_offset: u64, + payload_len: u64, } -fn parse_assets_pack(bytes: &[u8]) -> Result, CartridgeError> { +fn parse_assets_pack(path: &Path) -> Result { + let mut file = fs::File::open(path).map_err(|_| CartridgeError::IoError)?; + let mut prelude_bytes = [0_u8; ASSETS_PA_PRELUDE_SIZE]; + file.read_exact(&mut prelude_bytes).map_err(|_| CartridgeError::InvalidFormat)?; + let prelude = - AssetsPackPrelude::from_bytes(bytes).ok_or(CartridgeError::InvalidFormat)?; + AssetsPackPrelude::from_bytes(&prelude_bytes).ok_or(CartridgeError::InvalidFormat)?; if prelude.magic != ASSETS_PA_MAGIC || prelude.schema_version != ASSETS_PA_SCHEMA_VERSION { return Err(CartridgeError::InvalidFormat); } let header_start = ASSETS_PA_PRELUDE_SIZE; + let header_len = usize::try_from(prelude.header_len).map_err(|_| CartridgeError::InvalidFormat)?; let header_end = header_start - .checked_add(prelude.header_len as usize) + .checked_add(header_len) .ok_or(CartridgeError::InvalidFormat)?; let payload_offset = usize::try_from(prelude.payload_offset).map_err(|_| CartridgeError::InvalidFormat)?; + let file_len = usize::try_from(file.metadata().map_err(|_| CartridgeError::IoError)?.len()) + .map_err(|_| CartridgeError::InvalidFormat)?; - if payload_offset < header_start || header_end > bytes.len() || payload_offset > bytes.len() { + if payload_offset < header_start || header_end > file_len || payload_offset > file_len { return Err(CartridgeError::InvalidFormat); } if header_end != payload_offset { return Err(CartridgeError::InvalidFormat); } - let header_bytes = &bytes[header_start..header_end]; + file.seek(SeekFrom::Start(header_start as u64)).map_err(|_| CartridgeError::IoError)?; + let mut header_bytes = vec![0_u8; header_len]; + file.read_exact(&mut header_bytes).map_err(|_| CartridgeError::InvalidFormat)?; let header: AssetsPackHeader = - serde_json::from_slice(header_bytes).map_err(|_| CartridgeError::InvalidFormat)?; + serde_json::from_slice(&header_bytes).map_err(|_| CartridgeError::InvalidFormat)?; + let payload_len = u64::try_from(file_len - payload_offset).map_err(|_| CartridgeError::InvalidFormat)?; - Ok(ParsedAssetsPack { header, payload: &bytes[payload_offset..] }) + Ok(ParsedAssetsPack { header, payload_offset: prelude.payload_offset, payload_len }) } #[cfg(test)] @@ -366,7 +386,13 @@ mod tests { let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load"); - assert_eq!(cartridge.assets, payload); + let slice = cartridge + .assets + .open_slice(0, payload.len() as u64) + .expect("payload slice must open") + .read_all() + .expect("payload slice must read"); + assert_eq!(slice, payload); assert_eq!(cartridge.asset_table.len(), 1); assert_eq!(cartridge.asset_table[0].asset_name, "tiles"); assert_eq!(cartridge.preload.len(), 1); diff --git a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs index b76fdb95..aaf9e4f0 100644 --- a/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs +++ b/crates/console/prometeu-system/src/virtual_machine_runtime/tests.rs @@ -9,7 +9,7 @@ use prometeu_hal::AudioOpStatus; use prometeu_hal::asset::{AssetEntry, AssetLoadError, AssetOpStatus, BankType, LoadStatus}; use prometeu_hal::GfxOpStatus; use prometeu_hal::InputSignals; -use prometeu_hal::cartridge::Cartridge; +use prometeu_hal::cartridge::{AssetsPayloadSource, Cartridge}; use prometeu_hal::syscalls::caps; use prometeu_vm::VmInitError; use std::collections::HashMap; @@ -61,7 +61,7 @@ fn cartridge_with_program(program: Vec, capabilities: u64) -> Cartridge { entrypoint: "".into(), capabilities, program, - assets: vec![], + assets: AssetsPayloadSource::empty(), asset_table: vec![], preload: vec![], } @@ -632,7 +632,7 @@ fn tick_asset_load_invalid_slot_returns_status_and_zero_handle() { hardware.assets.initialize_for_cartridge( vec![test_tile_asset_entry("tile_asset", asset_data.len())], vec![], - asset_data, + AssetsPayloadSource::from_bytes(asset_data), ); let code = assemble("PUSH_CONST 0\nPUSH_I32 0\nPUSH_I32 16\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module_with_consts( @@ -668,7 +668,7 @@ fn tick_asset_load_kind_mismatch_returns_status_and_zero_handle() { hardware.assets.initialize_for_cartridge( vec![test_tile_asset_entry("tile_asset", asset_data.len())], vec![], - asset_data, + AssetsPayloadSource::from_bytes(asset_data), ); let code = assemble("PUSH_CONST 0\nPUSH_I32 1\nPUSH_I32 0\nHOSTCALL 0\nHALT").expect("assemble"); let program = serialized_single_function_module_with_consts( @@ -755,7 +755,7 @@ fn tick_asset_commit_invalid_transition_returns_status_not_crash() { hardware.assets.initialize_for_cartridge( vec![test_tile_asset_entry("tile_asset", asset_data.len())], vec![], - asset_data, + AssetsPayloadSource::from_bytes(asset_data), ); let handle = hardware .assets @@ -819,7 +819,7 @@ fn tick_asset_cancel_invalid_transition_returns_status_not_crash() { hardware.assets.initialize_for_cartridge( vec![test_tile_asset_entry("tile_asset", asset_data.len())], vec![], - asset_data, + AssetsPayloadSource::from_bytes(asset_data), ); let handle = hardware .assets @@ -1025,7 +1025,7 @@ fn tick_memcard_slot_roundtrip_for_game_profile() { entrypoint: "".into(), capabilities: caps::FS, program, - assets: vec![], + assets: AssetsPayloadSource::empty(), asset_table: vec![], preload: vec![], }; @@ -1065,7 +1065,7 @@ fn tick_memcard_access_is_denied_for_non_game_profile() { entrypoint: "".into(), capabilities: caps::FS, program, - assets: vec![], + assets: AssetsPayloadSource::empty(), asset_table: vec![], preload: vec![], };