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 crate::syscalls::CapFlags;
|
||||||
use serde::{Deserialize, Serialize};
|
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)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Deserialize, Serialize)]
|
||||||
pub enum AppMode {
|
pub enum AppMode {
|
||||||
Game,
|
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)]
|
#[derive(Debug)]
|
||||||
pub enum CartridgeError {
|
pub enum CartridgeError {
|
||||||
NotFound,
|
NotFound,
|
||||||
@ -62,6 +118,7 @@ pub enum CartridgeError {
|
|||||||
InvalidManifest,
|
InvalidManifest,
|
||||||
UnsupportedVersion,
|
UnsupportedVersion,
|
||||||
MissingProgram,
|
MissingProgram,
|
||||||
|
MissingAssets,
|
||||||
IoError,
|
IoError,
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -90,8 +147,4 @@ pub struct CartridgeManifest {
|
|||||||
pub entrypoint: String,
|
pub entrypoint: String,
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub capabilities: Vec<Capability>,
|
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 crate::syscalls::{CapFlags, caps};
|
||||||
use std::collections::HashSet;
|
use std::collections::HashSet;
|
||||||
use std::fs;
|
use std::fs;
|
||||||
@ -55,10 +58,16 @@ impl DirectoryCartridgeLoader {
|
|||||||
let program = fs::read(program_path).map_err(|_| CartridgeError::IoError)?;
|
let program = fs::read(program_path).map_err(|_| CartridgeError::IoError)?;
|
||||||
|
|
||||||
let assets_pa_path = path.join("assets.pa");
|
let assets_pa_path = path.join("assets.pa");
|
||||||
let assets = if assets_pa_path.exists() {
|
let (assets, asset_table, preload) = if assets_pa_path.exists() {
|
||||||
fs::read(assets_pa_path).map_err(|_| CartridgeError::IoError)?
|
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 {
|
} else {
|
||||||
Vec::new()
|
if capabilities & caps::ASSET != 0 {
|
||||||
|
return Err(CartridgeError::MissingAssets);
|
||||||
|
}
|
||||||
|
|
||||||
|
(Vec::new(), Vec::new(), Vec::new())
|
||||||
};
|
};
|
||||||
|
|
||||||
let dto = CartridgeDTO {
|
let dto = CartridgeDTO {
|
||||||
@ -70,8 +79,8 @@ impl DirectoryCartridgeLoader {
|
|||||||
capabilities,
|
capabilities,
|
||||||
program,
|
program,
|
||||||
assets,
|
assets,
|
||||||
asset_table: manifest.asset_table,
|
asset_table,
|
||||||
preload: manifest.preload,
|
preload,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok(Cartridge::from(dto))
|
Ok(Cartridge::from(dto))
|
||||||
@ -111,9 +120,44 @@ fn normalize_capabilities(capabilities: &[Capability]) -> Result<CapFlags, Cartr
|
|||||||
Ok(normalized)
|
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)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
use crate::asset::{AssetEntry, BankType, PreloadEntry};
|
||||||
|
use crate::cartridge::{ASSETS_PA_MAGIC, ASSETS_PA_SCHEMA_VERSION, AssetsPackPrelude};
|
||||||
use serde_json::json;
|
use serde_json::json;
|
||||||
use std::path::{Path, PathBuf};
|
use std::path::{Path, PathBuf};
|
||||||
use std::sync::atomic::{AtomicU64, Ordering};
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
@ -149,6 +193,31 @@ mod tests {
|
|||||||
fn path(&self) -> &Path {
|
fn path(&self) -> &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 {
|
impl Drop for TestCartridgeDir {
|
||||||
@ -205,6 +274,7 @@ mod tests {
|
|||||||
#[test]
|
#[test]
|
||||||
fn load_with_all_capability_normalizes_to_all_flags() {
|
fn load_with_all_capability_normalizes_to_all_flags() {
|
||||||
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["all"])));
|
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");
|
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
|
||||||
|
|
||||||
@ -246,4 +316,91 @@ mod tests {
|
|||||||
|
|
||||||
assert!(matches!(error, CartridgeError::InvalidManifest));
|
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