All checks were successful
Intrepid/Prometeu/Runtime/pipeline/head This commit looks good
Reviewed-on: #12 Co-authored-by: bQUARKz <bquarkz@gmail.com> Co-committed-by: bQUARKz <bquarkz@gmail.com>
587 lines
20 KiB
Rust
587 lines
20 KiB
Rust
use crate::asset::{AssetEntry, BankType};
|
|
use crate::cartridge::{
|
|
ASSETS_PA_MAGIC, ASSETS_PA_PRELUDE_SIZE, ASSETS_PA_SCHEMA_VERSION, AssetsPackHeader,
|
|
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;
|
|
|
|
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, asset_table, preload) = if assets_pa_path.exists() {
|
|
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);
|
|
}
|
|
|
|
(AssetsPayloadSource::empty(), Vec::new(), Vec::new())
|
|
};
|
|
|
|
let dto = CartridgeDTO {
|
|
app_id: manifest.app_id,
|
|
title: manifest.title,
|
|
app_version: manifest.app_version,
|
|
app_mode: manifest.app_mode,
|
|
capabilities,
|
|
program,
|
|
assets,
|
|
asset_table,
|
|
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)
|
|
}
|
|
|
|
struct ParsedAssetsPack {
|
|
header: AssetsPackHeader,
|
|
payload_offset: u64,
|
|
payload_len: u64,
|
|
}
|
|
|
|
fn parse_assets_pack(path: &Path) -> Result<ParsedAssetsPack, CartridgeError> {
|
|
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(&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(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 > file_len || payload_offset > file_len {
|
|
return Err(CartridgeError::InvalidFormat);
|
|
}
|
|
if header_end != payload_offset {
|
|
return Err(CartridgeError::InvalidFormat);
|
|
}
|
|
|
|
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)?;
|
|
validate_preload(&header.asset_table, &header.preload)?;
|
|
let payload_len =
|
|
u64::try_from(file_len - payload_offset).map_err(|_| CartridgeError::InvalidFormat)?;
|
|
|
|
Ok(ParsedAssetsPack { header, payload_offset: prelude.payload_offset, payload_len })
|
|
}
|
|
|
|
fn validate_preload(
|
|
asset_table: &[AssetEntry],
|
|
preload: &[crate::asset::PreloadEntry],
|
|
) -> Result<(), CartridgeError> {
|
|
let mut claimed_slots = HashSet::<(BankType, usize)>::new();
|
|
|
|
for item in preload {
|
|
let entry = asset_table
|
|
.iter()
|
|
.find(|entry| entry.asset_id == item.asset_id)
|
|
.ok_or(CartridgeError::InvalidFormat)?;
|
|
|
|
if !claimed_slots.insert((entry.bank_type, item.slot)) {
|
|
return Err(CartridgeError::InvalidFormat);
|
|
}
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use crate::asset::{AssetCodec, AssetEntry, BankType, PreloadEntry};
|
|
use crate::cartridge::{ASSETS_PA_MAGIC, ASSETS_PA_SCHEMA_VERSION, AssetsPackPrelude};
|
|
use crate::glyph_bank::GLYPH_BANK_PALETTE_COUNT_V1;
|
|
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
|
|
}
|
|
|
|
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 {
|
|
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"
|
|
});
|
|
|
|
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"])));
|
|
dir.write_assets_pa(vec![], vec![], &[]);
|
|
|
|
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));
|
|
}
|
|
|
|
fn test_asset_entry(offset: u64, size: u64) -> AssetEntry {
|
|
AssetEntry {
|
|
asset_id: 7,
|
|
asset_name: "tiles".to_string(),
|
|
bank_type: BankType::GLYPH,
|
|
offset,
|
|
size,
|
|
decoded_size: 16 * 16 + (GLYPH_BANK_PALETTE_COUNT_V1 as u64 * 16 * 2),
|
|
codec: AssetCodec::None,
|
|
metadata: json!({
|
|
"tile_size": 16,
|
|
"width": 16,
|
|
"height": 16,
|
|
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
|
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
|
}),
|
|
}
|
|
}
|
|
|
|
#[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_id: 7, slot: 2 }],
|
|
&payload,
|
|
);
|
|
|
|
let cartridge = DirectoryCartridgeLoader::load(dir.path()).expect("cartridge must load");
|
|
|
|
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);
|
|
assert_eq!(cartridge.preload[0].asset_id, 7);
|
|
assert_eq!(cartridge.preload[0].slot, 2);
|
|
}
|
|
|
|
#[test]
|
|
fn load_rejects_preload_with_missing_asset_id() {
|
|
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
|
dir.write_assets_pa(
|
|
vec![test_asset_entry(0, 4)],
|
|
vec![PreloadEntry { asset_id: 999, slot: 2 }],
|
|
&[1_u8, 2, 3, 4],
|
|
);
|
|
|
|
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
|
|
|
assert!(matches!(error, CartridgeError::InvalidFormat));
|
|
}
|
|
|
|
#[test]
|
|
fn load_rejects_preload_slot_clash_per_bank_type() {
|
|
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
|
let asset_table = vec![
|
|
test_asset_entry(0, 4),
|
|
AssetEntry {
|
|
asset_id: 8,
|
|
asset_name: "other_tiles".to_string(),
|
|
bank_type: BankType::GLYPH,
|
|
offset: 4,
|
|
size: 4,
|
|
decoded_size: 16 * 16 + (GLYPH_BANK_PALETTE_COUNT_V1 as u64 * 16 * 2),
|
|
codec: AssetCodec::None,
|
|
metadata: json!({
|
|
"tile_size": 16,
|
|
"width": 16,
|
|
"height": 16,
|
|
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
|
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
|
}),
|
|
},
|
|
];
|
|
let preload =
|
|
vec![PreloadEntry { asset_id: 7, slot: 2 }, PreloadEntry { asset_id: 8, slot: 2 }];
|
|
dir.write_assets_pa(asset_table, preload, &[1_u8, 2, 3, 4, 5, 6, 7, 8]);
|
|
|
|
let error = DirectoryCartridgeLoader::load(dir.path()).unwrap_err();
|
|
|
|
assert!(matches!(error, CartridgeError::InvalidFormat));
|
|
}
|
|
|
|
#[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));
|
|
}
|
|
|
|
#[test]
|
|
fn load_rejects_unknown_codec_string_in_assets_pa_header() {
|
|
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
|
let header = serde_json::json!({
|
|
"asset_table": [{
|
|
"asset_id": 7,
|
|
"asset_name": "tiles",
|
|
"bank_type": "GLYPH",
|
|
"offset": 0,
|
|
"size": 4,
|
|
"decoded_size": 768,
|
|
"codec": "LZ4",
|
|
"metadata": {
|
|
"tile_size": 16,
|
|
"width": 16,
|
|
"height": 16,
|
|
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
|
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
|
}
|
|
}],
|
|
"preload": []
|
|
});
|
|
let header_bytes = serde_json::to_vec(&header).expect("header must serialize");
|
|
let prelude = AssetsPackPrelude {
|
|
magic: ASSETS_PA_MAGIC,
|
|
schema_version: ASSETS_PA_SCHEMA_VERSION,
|
|
header_len: header_bytes.len() as u32,
|
|
payload_offset: (ASSETS_PA_PRELUDE_SIZE + header_bytes.len()) as u64,
|
|
flags: 0,
|
|
reserved: 0,
|
|
header_checksum: 0,
|
|
};
|
|
let mut bytes = prelude.to_bytes().to_vec();
|
|
bytes.extend_from_slice(&header_bytes);
|
|
bytes.extend_from_slice(&[1_u8, 2, 3, 4]);
|
|
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));
|
|
}
|
|
|
|
#[test]
|
|
fn load_rejects_legacy_raw_codec_string_in_assets_pa_header() {
|
|
let dir = TestCartridgeDir::new(manifest_with_capabilities(Some(vec!["asset"])));
|
|
let header = serde_json::json!({
|
|
"asset_table": [{
|
|
"asset_id": 7,
|
|
"asset_name": "tiles",
|
|
"bank_type": "GLYPH",
|
|
"offset": 0,
|
|
"size": 4,
|
|
"decoded_size": 768,
|
|
"codec": "RAW",
|
|
"metadata": {
|
|
"tile_size": 16,
|
|
"width": 16,
|
|
"height": 16,
|
|
"palette_count": GLYPH_BANK_PALETTE_COUNT_V1,
|
|
"palette_authored": GLYPH_BANK_PALETTE_COUNT_V1
|
|
}
|
|
}],
|
|
"preload": []
|
|
});
|
|
let header_bytes = serde_json::to_vec(&header).expect("header must serialize");
|
|
let prelude = AssetsPackPrelude {
|
|
magic: ASSETS_PA_MAGIC,
|
|
schema_version: ASSETS_PA_SCHEMA_VERSION,
|
|
header_len: header_bytes.len() as u32,
|
|
payload_offset: (ASSETS_PA_PRELUDE_SIZE + header_bytes.len()) as u64,
|
|
flags: 0,
|
|
reserved: 0,
|
|
header_checksum: 0,
|
|
};
|
|
let mut bytes = prelude.to_bytes().to_vec();
|
|
bytes.extend_from_slice(&header_bytes);
|
|
bytes.extend_from_slice(&[1_u8, 2, 3, 4]);
|
|
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));
|
|
}
|
|
}
|