use std::{
io::{self, Read, Seek, SeekFrom},
ops::Range,
path::{Path, PathBuf},
};
use bevy_asset::{
Asset,
io::{AssetReader, AssetReaderError, PathStream, Reader, SliceReader},
};
use bevy_reflect::TypePath;
use byteorder::{LittleEndian, ReadBytesExt as _};
use hashbrown::HashMap;
use memmap2::Mmap;
use thiserror::Error;
const PAK_MAGIC: &[u8; 4] = b"PACK";
const PAK_ENTRY_SIZE: u32 = 64;
#[derive(Error, Debug)]
pub enum PakError {
#[error("I/O error: {0}")]
Io(#[from] io::Error),
#[error("Invalid magic number: {0:?}")]
InvalidMagicNumber([u8; 4]),
#[error("Invalid file table offset: {0}")]
InvalidTableOffset(i32),
#[error("Invalid file table size: {0}")]
InvalidTableSize(u32),
#[error("Invalid file offset: {0}")]
InvalidFileOffset(i32),
#[error("Invalid file size: {0}")]
InvalidFileSize(i32),
#[error("File name too long: {0}")]
FileNameTooLong(String),
#[error("Non-UTF-8 file name: {0}")]
NonUtf8FileName(#[from] std::string::FromUtf8Error),
}
impl From<PakError> for AssetReaderError {
fn from(value: PakError) -> Self {
match value {
PakError::Io(error) if error.kind() == std::io::ErrorKind::NotFound => {
AssetReaderError::NotFound(PathBuf::new())
}
PakError::Io(error) => AssetReaderError::Io(error.into()),
other => AssetReaderError::Io(std::io::Error::other(other).into()),
}
}
}
#[derive(Debug, Clone)]
enum PakEntry {
File(Range<usize>),
Directory(Box<[PathBuf]>),
}
#[derive(Debug)]
pub enum PakBacking {
Mmap(Mmap),
Memory(Box<[u8]>),
}
impl From<Mmap> for PakBacking {
fn from(value: Mmap) -> Self {
Self::Mmap(value)
}
}
impl From<Box<[u8]>> for PakBacking {
fn from(value: Box<[u8]>) -> Self {
Self::Memory(value)
}
}
impl AsRef<[u8]> for PakBacking {
fn as_ref(&self) -> &[u8] {
match self {
Self::Mmap(mmap) => mmap.as_ref(),
Self::Memory(mem) => mem.as_ref(),
}
}
}
#[derive(Asset, TypePath, Debug)]
pub struct Pak {
name: String,
memory: PakBacking,
entries: HashMap<PathBuf, PakEntry>,
}
fn get_meta_path(path: &Path) -> PathBuf {
let mut meta_path = path.to_path_buf();
let mut extension = path.extension().unwrap_or_default().to_os_string();
if !extension.is_empty() {
extension.push(".");
}
extension.push("meta");
meta_path.set_extension(extension);
meta_path
}
impl AssetReader for Pak {
async fn read<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
match self.entries.get(path) {
Some(PakEntry::File(range)) => {
Ok(SliceReader::new(&self.memory.as_ref()[range.clone()]))
}
None | Some(PakEntry::Directory(..)) => {
Err(AssetReaderError::NotFound(path.to_path_buf()))
}
}
}
async fn read_meta<'a>(&'a self, path: &'a Path) -> Result<impl Reader + 'a, AssetReaderError> {
let path = get_meta_path(path);
match self.entries.get(&path) {
Some(PakEntry::File(range)) => {
Ok(SliceReader::new(&self.memory.as_ref()[range.clone()]))
}
None | Some(PakEntry::Directory(..)) => {
Err(AssetReaderError::NotFound(path.to_path_buf()))
}
}
}
async fn read_directory<'a>(
&'a self,
path: &'a Path,
) -> Result<Box<PathStream>, AssetReaderError> {
let entry = self
.entries
.get(path)
.ok_or_else(|| AssetReaderError::NotFound(path.to_owned()))?;
let dir_entries = if let PakEntry::Directory(entries) = entry {
Some(entries)
} else {
None
};
let iter = dir_entries
.into_iter()
.flat_map(AsRef::as_ref)
.cloned()
.collect::<Vec<_>>()
.into_iter();
Ok(Box::new(futures::stream::iter(iter)))
}
async fn is_directory<'a>(&'a self, path: &'a Path) -> Result<bool, AssetReaderError> {
match self.entries.get(path) {
Some(PakEntry::Directory(..)) => Ok(true),
Some(PakEntry::File(..)) => Ok(false),
None => Err(AssetReaderError::NotFound(path.to_path_buf())),
}
}
}
impl Pak {
pub fn name(&self) -> &str {
&self.name
}
pub fn from_backing<S: ToString, B: Into<PakBacking>>(
name: S,
bytes: B,
) -> Result<Self, PakError> {
let bytes = bytes.into();
let mut reader = io::Cursor::new(bytes.as_ref());
let mut magic = [0u8; 4];
reader.read_exact(&mut magic)?;
if &magic != PAK_MAGIC {
Err(PakError::InvalidMagicNumber(magic))?;
}
let table_offset = match reader.read_i32::<LittleEndian>()? {
o if o <= 0 => Err(PakError::InvalidTableOffset(o))?,
o => o as u32,
};
let table_size = reader.read_u32::<LittleEndian>()?;
if !table_size.is_multiple_of(PAK_ENTRY_SIZE) {
return Err(PakError::InvalidTableSize(table_size));
}
let mut map = HashMap::default();
for i in 0..(table_size / PAK_ENTRY_SIZE) {
let entry_offset = table_offset as u64 + (i * PAK_ENTRY_SIZE) as u64;
reader.seek(SeekFrom::Start(entry_offset))?;
let mut path_bytes = [0u8; 56];
reader.read_exact(&mut path_bytes)?;
let file_offset = match reader.read_i32::<LittleEndian>()? {
o if o <= 0 => Err(PakError::InvalidFileOffset(o))?,
o => o as u32,
};
let file_size = match reader.read_i32::<LittleEndian>()? {
s if s <= 0 => Err(PakError::InvalidFileSize(s))?,
s => s as u32,
};
let last = path_bytes
.iter()
.position(|b| *b == 0)
.ok_or(PakError::FileNameTooLong(
String::from_utf8_lossy(&path_bytes).into_owned(),
))?;
let path = String::from_utf8(path_bytes[0..last].to_vec())?;
map.insert(
PathBuf::from(path),
PakEntry::File(file_offset as usize..(file_offset + file_size) as usize),
);
}
let keys = map.keys().cloned().collect::<Box<[_]>>();
for path in &keys[..] {
let mut path: &Path = path.as_ref();
while let Some(parent) = path.parent() {
map.insert(parent.to_owned(), PakEntry::Directory(Default::default()));
path = parent;
}
}
let keys = map.keys().cloned().collect::<Box<[_]>>();
for (path, entry) in &mut map {
if let PakEntry::Directory(inner) = entry {
*inner = keys
.iter()
.filter(|k| path != *k && k.starts_with(path))
.map(|p| p.to_path_buf())
.collect();
}
}
map.shrink_to_fit();
let name = name.to_string();
Ok(Pak {
name,
memory: bytes,
entries: map,
})
}
}