Reviewed-on: #10 Co-authored-by: bQUARKz <bquarkz@gmail.com> Co-committed-by: bQUARKz <bquarkz@gmail.com>
313 lines
8.9 KiB
Rust
313 lines
8.9 KiB
Rust
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<u8>,
|
|
pub assets: AssetsPayloadSource,
|
|
pub asset_table: Vec<AssetEntry>,
|
|
pub preload: Vec<PreloadEntry>,
|
|
}
|
|
|
|
#[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<u8>,
|
|
pub assets: AssetsPayloadSource,
|
|
pub asset_table: Vec<AssetEntry>,
|
|
pub preload: Vec<PreloadEntry>,
|
|
}
|
|
|
|
impl From<CartridgeDTO> 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<FileAssetsPayloadSource>),
|
|
}
|
|
|
|
impl AssetsPayloadSource {
|
|
pub fn empty() -> Self {
|
|
Self::Memory(Arc::<[u8]>::from(Vec::<u8>::new()))
|
|
}
|
|
|
|
pub fn from_bytes(bytes: Vec<u8>) -> 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<AssetsPayloadSlice> {
|
|
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<FileAssetsPayloadSource>, offset: u64, size: u64 },
|
|
}
|
|
|
|
impl AssetsPayloadSlice {
|
|
pub fn open_reader(&self) -> io::Result<AssetsPayloadReader> {
|
|
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<Vec<u8>> {
|
|
let mut reader = self.open_reader()?;
|
|
let mut buffer = Vec::new();
|
|
reader.read_to_end(&mut buffer)?;
|
|
Ok(buffer)
|
|
}
|
|
}
|
|
|
|
pub enum AssetsPayloadReader {
|
|
Memory(Cursor<Arc<[u8]>>),
|
|
File(FileSliceReader),
|
|
}
|
|
|
|
impl Read for AssetsPayloadReader {
|
|
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
|
|
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<u64> {
|
|
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<usize> {
|
|
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<u64> {
|
|
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<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,
|
|
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<Capability>,
|
|
}
|