use crate::asset::{AssetEntry, PreloadEntry}; use crate::syscalls::CapFlags; use serde::{Deserialize, Serialize}; use std::fs::File; use std::io::{self, Cursor, 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; pub const ASSETS_PA_PRELUDE_SIZE: usize = 32; #[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)] pub enum AppMode { Game, System, } #[derive(Debug, Clone)] pub struct Cartridge { pub app_id: u32, pub title: String, pub app_version: String, pub app_mode: AppMode, pub capabilities: CapFlags, pub program: Vec, pub assets: AssetsPayloadSource, pub asset_table: Vec, pub preload: Vec, } #[derive(Debug, Clone)] pub struct CartridgeDTO { pub app_id: u32, pub title: String, pub app_version: String, pub app_mode: AppMode, pub capabilities: CapFlags, pub program: Vec, pub assets: AssetsPayloadSource, pub asset_table: Vec, pub preload: Vec, } impl From for Cartridge { fn from(dto: CartridgeDTO) -> Self { Self { app_id: dto.app_id, title: dto.title, app_version: dto.app_version, app_mode: dto.app_mode, capabilities: dto.capabilities, program: dto.program, assets: dto.assets, asset_table: dto.asset_table, preload: dto.preload, } } } #[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 open_reader(&self) -> io::Result { match self { Self::Memory { bytes, start, len } => { let data = Arc::<[u8]>::from(bytes[*start..*start + *len].to_vec()); Ok(AssetsPayloadReader::Memory(Cursor::new(data))) } Self::File { source, offset, size } => { let mut file = File::open(&source.path)?; let absolute_start = source.payload_offset + offset; file.seek(SeekFrom::Start(absolute_start))?; Ok(AssetsPayloadReader::File(FileSliceReader { file, start: absolute_start, len: *size, position: 0, })) } } } pub fn read_all(&self) -> io::Result> { let mut reader = self.open_reader()?; let mut buffer = Vec::new(); reader.read_to_end(&mut buffer)?; Ok(buffer) } } pub enum AssetsPayloadReader { Memory(Cursor>), File(FileSliceReader), } impl Read for AssetsPayloadReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { match self { Self::Memory(reader) => reader.read(buf), Self::File(reader) => reader.read(buf), } } } impl Seek for AssetsPayloadReader { fn seek(&mut self, pos: SeekFrom) -> io::Result { match self { Self::Memory(reader) => reader.seek(pos), Self::File(reader) => reader.seek(pos), } } } pub struct FileSliceReader { file: File, start: u64, len: u64, position: u64, } impl Read for FileSliceReader { fn read(&mut self, buf: &mut [u8]) -> io::Result { if self.position >= self.len { return Ok(0); } let remaining = (self.len - self.position) as usize; let to_read = remaining.min(buf.len()); let read = self.file.read(&mut buf[..to_read])?; self.position += read as u64; Ok(read) } } impl Seek for FileSliceReader { fn seek(&mut self, pos: SeekFrom) -> io::Result { let next = match pos { SeekFrom::Start(offset) => offset as i128, SeekFrom::Current(delta) => self.position as i128 + delta as i128, SeekFrom::End(delta) => self.len as i128 + delta as i128, }; if next < 0 || next as u64 > self.len { return Err(invalid_input("slice seek out of bounds")); } let next = next as u64; self.file.seek(SeekFrom::Start(self.start + next))?; self.position = next; Ok(self.position) } } 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], pub schema_version: u32, pub header_len: u32, pub payload_offset: u64, pub flags: u32, pub reserved: u32, pub header_checksum: u32, } impl AssetsPackPrelude { pub fn from_bytes(bytes: &[u8]) -> Option { if bytes.len() < ASSETS_PA_PRELUDE_SIZE { return None; } let mut magic = [0_u8; 4]; magic.copy_from_slice(&bytes[0..4]); Some(Self { magic, schema_version: u32::from_le_bytes(bytes[4..8].try_into().ok()?), header_len: u32::from_le_bytes(bytes[8..12].try_into().ok()?), payload_offset: u64::from_le_bytes(bytes[12..20].try_into().ok()?), flags: u32::from_le_bytes(bytes[20..24].try_into().ok()?), reserved: u32::from_le_bytes(bytes[24..28].try_into().ok()?), header_checksum: u32::from_le_bytes(bytes[28..32].try_into().ok()?), }) } pub fn to_bytes(self) -> [u8; ASSETS_PA_PRELUDE_SIZE] { let mut bytes = [0_u8; ASSETS_PA_PRELUDE_SIZE]; bytes[0..4].copy_from_slice(&self.magic); bytes[4..8].copy_from_slice(&self.schema_version.to_le_bytes()); bytes[8..12].copy_from_slice(&self.header_len.to_le_bytes()); bytes[12..20].copy_from_slice(&self.payload_offset.to_le_bytes()); bytes[20..24].copy_from_slice(&self.flags.to_le_bytes()); bytes[24..28].copy_from_slice(&self.reserved.to_le_bytes()); bytes[28..32].copy_from_slice(&self.header_checksum.to_le_bytes()); bytes } } #[derive(Debug, Clone, Deserialize, Serialize)] pub struct AssetsPackHeader { #[serde(default)] pub asset_table: Vec, #[serde(default)] pub preload: Vec, } #[derive(Debug)] pub enum CartridgeError { NotFound, InvalidFormat, InvalidManifest, UnsupportedVersion, MissingProgram, MissingAssets, IoError, } #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Deserialize, Serialize)] #[serde(rename_all = "snake_case")] pub enum Capability { None, System, Gfx, Audio, Fs, Log, Asset, Bank, All, } #[derive(Deserialize)] pub struct CartridgeManifest { pub magic: String, pub cartridge_version: u32, pub app_id: u32, pub title: String, pub app_version: String, pub app_mode: AppMode, #[serde(default)] pub capabilities: Vec, }