implements PR-011b assets.pa bootstrap and header loading
This commit is contained in:
parent
9b65005ffb
commit
d3321c4400
@ -2,6 +2,10 @@ use crate::asset::{AssetEntry, PreloadEntry};
|
||||
use crate::syscalls::CapFlags;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
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,
|
||||
@ -55,6 +59,58 @@ impl From<CartridgeDTO> for Cartridge {
|
||||
}
|
||||
}
|
||||
|
||||
#[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<Self> {
|
||||
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<AssetEntry>,
|
||||
#[serde(default)]
|
||||
pub preload: Vec<PreloadEntry>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum CartridgeError {
|
||||
NotFound,
|
||||
@ -62,6 +118,7 @@ pub enum CartridgeError {
|
||||
InvalidManifest,
|
||||
UnsupportedVersion,
|
||||
MissingProgram,
|
||||
MissingAssets,
|
||||
IoError,
|
||||
}
|
||||
|
||||
@ -90,8 +147,4 @@ pub struct CartridgeManifest {
|
||||
pub entrypoint: String,
|
||||
#[serde(default)]
|
||||
pub capabilities: Vec<Capability>,
|
||||
#[serde(default)]
|
||||
pub asset_table: Vec<AssetEntry>,
|
||||
#[serde(default)]
|
||||
pub preload: Vec<PreloadEntry>,
|
||||
}
|
||||
|
||||
@ -1,4 +1,7 @@
|
||||
use crate::cartridge::{Capability, Cartridge, CartridgeDTO, CartridgeError, CartridgeManifest};
|
||||
use crate::cartridge::{
|
||||
ASSETS_PA_MAGIC, ASSETS_PA_PRELUDE_SIZE, ASSETS_PA_SCHEMA_VERSION, AssetsPackHeader,
|
||||
AssetsPackPrelude, Capability, Cartridge, CartridgeDTO, CartridgeError, CartridgeManifest,
|
||||
};
|
||||
use crate::syscalls::{CapFlags, caps};
|
||||
use std::collections::HashSet;
|
||||
use std::fs;
|
||||
@ -55,10 +58,16 @@ impl DirectoryCartridgeLoader {
|
||||
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)?
|
||||
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)
|
||||
} else {
|
||||
Vec::new()
|
||||
if capabilities & caps::ASSET != 0 {
|
||||
return Err(CartridgeError::MissingAssets);
|
||||
}
|
||||
|
||||
(Vec::new(), Vec::new(), Vec::new())
|
||||
};
|
||||
|
||||
let dto = CartridgeDTO {
|
||||
@ -70,8 +79,8 @@ impl DirectoryCartridgeLoader {
|
||||
capabilities,
|
||||
program,
|
||||
assets,
|
||||
asset_table: manifest.asset_table,
|
||||
preload: manifest.preload,
|
||||
asset_table,
|
||||
preload,
|
||||
};
|
||||
|
||||
Ok(Cartridge::from(dto))
|
||||
@ -111,9 +120,44 @@ fn normalize_capabilities(capabilities: &[Capability]) -> Result<CapFlags, Cartr
|
||||
Ok(normalized)
|
||||
}
|
||||
|
||||
struct ParsedAssetsPack<'a> {
|
||||
header: AssetsPackHeader,
|
||||
payload: &'a [u8],
|
||||
}
|
||||
|
||||
fn parse_assets_pack(bytes: &[u8]) -> Result<ParsedAssetsPack<'_>, CartridgeError> {
|
||||
let prelude =
|
||||
AssetsPackPrelude::from_bytes(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_end = header_start
|
||||
.checked_add(prelude.header_len as usize)
|
||||
.ok_or(CartridgeError::InvalidFormat)?;
|
||||
let payload_offset = usize::try_from(prelude.payload_offset).map_err(|_| CartridgeError::InvalidFormat)?;
|
||||
|
||||
if payload_offset < header_start || header_end > bytes.len() || payload_offset > bytes.len() {
|
||||
return Err(CartridgeError::InvalidFormat);
|
||||
}
|
||||
if header_end != payload_offset {
|
||||
return Err(CartridgeError::InvalidFormat);
|
||||
}
|
||||
|
||||
let header_bytes = &bytes[header_start..header_end];
|
||||
let header: AssetsPackHeader =
|
||||
serde_json::from_slice(header_bytes).map_err(|_| CartridgeError::InvalidFormat)?;
|
||||
|
||||
Ok(ParsedAssetsPack { header, payload: &bytes[payload_offset..] })
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::asset::{AssetEntry, BankType, PreloadEntry};
|
||||
use crate::cartridge::{ASSETS_PA_MAGIC, ASSETS_PA_SCHEMA_VERSION, AssetsPackPrelude};
|
||||
use serde_json::json;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
@ -149,6 +193,31 @@ mod tests {
|
||||
fn path(&self) -> &Path {
|
||||
&self.path
|
||||
}
|
||||
|
||||
fn write_assets_pa(
|
||||
&self,
|
||||
asset_table: Vec<AssetEntry>,
|
||||
preload: Vec<PreloadEntry>,
|
||||
payload: &[u8],
|
||||
) {
|
||||
let header = serde_json::to_vec(&AssetsPackHeader { asset_table, preload })
|
||||
.expect("assets header must serialize");
|
||||
let payload_offset = (ASSETS_PA_PRELUDE_SIZE + header.len()) as u64;
|
||||
let prelude = AssetsPackPrelude {
|
||||
magic: ASSETS_PA_MAGIC,
|
||||
schema_version: ASSETS_PA_SCHEMA_VERSION,
|
||||
header_len: header.len() as u32,
|
||||
payload_offset,
|
||||
flags: 0,
|
||||
reserved: 0,
|
||||
header_checksum: 0,
|
||||
};
|
||||
|
||||
let mut bytes = prelude.to_bytes().to_vec();
|
||||
bytes.extend_from_slice(&header);
|
||||
bytes.extend_from_slice(payload);
|
||||
fs::write(self.path.join("assets.pa"), bytes).expect("must write assets.pa");
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for TestCartridgeDir {
|
||||
@ -205,6 +274,7 @@ mod tests {
|
||||
#[test]
|
||||
fn load_with_all_capability_normalizes_to_all_flags() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["all"])));
|
||||
dir.write_assets_pa(vec![], vec![], &[]);
|
||||
|
||||
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
|
||||
|
||||
@ -246,4 +316,91 @@ mod tests {
|
||||
|
||||
assert!(matches!(error, CartridgeError::InvalidManifest));
|
||||
}
|
||||
|
||||
fn test_asset_entry(offset: u64, size: u64) -> AssetEntry {
|
||||
AssetEntry {
|
||||
asset_id: 7,
|
||||
asset_name: "tiles".to_string(),
|
||||
bank_type: BankType::TILES,
|
||||
offset,
|
||||
size,
|
||||
decoded_size: size,
|
||||
codec: "RAW".to_string(),
|
||||
metadata: json!({
|
||||
"tile_size": 16,
|
||||
"width": 16,
|
||||
"height": 16
|
||||
}),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_with_asset_capability_requires_assets_pa() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
||||
|
||||
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, CartridgeError::MissingAssets));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_without_asset_capability_accepts_missing_assets_pa() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["gfx"])));
|
||||
|
||||
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
|
||||
|
||||
assert!(cartridge.assets.is_empty());
|
||||
assert!(cartridge.asset_table.is_empty());
|
||||
assert!(cartridge.preload.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_reads_asset_table_and_preload_from_assets_pa_header() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
||||
let payload = vec![1_u8, 2, 3, 4];
|
||||
dir.write_assets_pa(
|
||||
vec![test_asset_entry(0, payload.len() as u64)],
|
||||
vec![PreloadEntry { asset_name: "tiles".to_string(), slot: 2 }],
|
||||
&payload,
|
||||
);
|
||||
|
||||
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
|
||||
|
||||
assert_eq!(cartridge.assets, payload);
|
||||
assert_eq!(cartridge.asset_table.len(), 1);
|
||||
assert_eq!(cartridge.asset_table[0].asset_name, "tiles");
|
||||
assert_eq!(cartridge.preload.len(), 1);
|
||||
assert_eq!(cartridge.preload[0].slot, 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_rejects_invalid_assets_pa_prelude() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
||||
fs::write(dir.path().join("assets.pa"), b"not-a-valid-pack").expect("must write assets.pa");
|
||||
|
||||
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, CartridgeError::InvalidFormat));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn load_rejects_invalid_assets_pa_header_json() {
|
||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
||||
let prelude = AssetsPackPrelude {
|
||||
magic: ASSETS_PA_MAGIC,
|
||||
schema_version: ASSETS_PA_SCHEMA_VERSION,
|
||||
header_len: 4,
|
||||
payload_offset: (ASSETS_PA_PRELUDE_SIZE + 4) as u64,
|
||||
flags: 0,
|
||||
reserved: 0,
|
||||
header_checksum: 0,
|
||||
};
|
||||
let mut bytes = prelude.to_bytes().to_vec();
|
||||
bytes.extend_from_slice(b"{no}");
|
||||
fs::write(dir.path().join("assets.pa"), bytes).expect("must write assets.pa");
|
||||
|
||||
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
||||
|
||||
assert!(matches!(error, CartridgeError::InvalidFormat));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user