use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use bincode::{Decode, Encode};
use crate::class_id::ClassId;
pub const SCHEMA_VERSION: u16 = 6;
pub const MAGIC: [u8; 8] = *b"UADBIN__";
pub const CACHE_MAGIC: [u8; 8] = *b"UADCACHE";
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Encode, Decode)]
pub enum AssetType {
Native(u32),
Script(u32),
}
impl AssetType {
pub fn native(class_id: ClassId) -> Self {
Self::Native(class_id as u32)
}
}
#[derive(Debug, Clone, Encode, Decode)]
pub struct SubAsset {
pub file_id: i64,
pub class_id: u32,
pub name: Box<str>,
}
#[derive(Debug, Clone, Encode, Decode)]
pub struct AssetEntry {
pub guid: u128,
pub asset_type: AssetType,
pub name: Box<str>,
pub sub_assets: Vec<SubAsset>,
pub hint: Box<str>,
}
#[derive(Debug, Clone, Default, Encode, Decode)]
pub struct AssetDb {
pub schema_version: u16,
pub script_types: Vec<u128>,
pub entries: Vec<AssetEntry>,
}
impl AssetDb {
pub fn new() -> Self {
Self {
schema_version: SCHEMA_VERSION,
..Default::default()
}
}
pub fn find_by_guid(&self, guid: u128) -> Option<&AssetEntry> {
let idx = self.entries.binary_search_by_key(&guid, |e| e.guid).ok()?;
Some(&self.entries[idx])
}
pub fn script_guid(&self, idx: u32) -> u128 {
self.script_types[idx as usize]
}
pub fn intern_script(&mut self, guid: u128) -> u32 {
match self.script_types.binary_search(&guid) {
Ok(idx) => idx as u32,
Err(idx) => {
self.script_types.insert(idx, guid);
idx as u32
}
}
}
pub fn sort(&mut self) {
self.entries.sort_by_key(|e| e.guid);
for e in &mut self.entries {
e.sub_assets.sort_by_key(|s| s.file_id);
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Encode, Decode)]
pub enum CachedAssetType {
Native(u32),
Script(u128),
}
#[derive(Debug, Clone, Encode, Decode)]
pub struct CachedEntry {
pub hint: Box<str>,
pub meta_mtime_ns: u64,
pub asset_mtime_ns: u64,
pub guid: u128,
pub asset_type: CachedAssetType,
pub sub_assets: Vec<SubAsset>,
}
#[derive(Debug, Clone, Default, Encode, Decode)]
pub struct BakeCache {
pub schema_version: u16,
pub entries: Vec<CachedEntry>,
}
impl BakeCache {
pub fn new() -> Self {
Self {
schema_version: SCHEMA_VERSION,
..Default::default()
}
}
}
pub const DB_FILENAME: &str = "asset-db.bin";
pub const CACHE_FILENAME: &str = "asset-db.cache.bin";
pub fn db_path(dir: &Path) -> PathBuf {
dir.join(DB_FILENAME)
}
pub fn cache_path(dir: &Path) -> PathBuf {
dir.join(CACHE_FILENAME)
}
pub fn read(path: &Path) -> Result<AssetDb> {
let bytes =
std::fs::read(path).with_context(|| format!("read asset-db: {}", path.display()))?;
decode(&bytes)
}
pub fn decode(bytes: &[u8]) -> Result<AssetDb> {
let body = check_magic(bytes, MAGIC, "asset-db")?;
let cfg = bincode::config::standard();
let (db, _): (AssetDb, _) = bincode::decode_from_slice(body, cfg).context("bincode decode")?;
if db.schema_version != SCHEMA_VERSION {
anyhow::bail!(
"asset-db schema {} expected {}, re-bake required",
db.schema_version,
SCHEMA_VERSION
);
}
Ok(db)
}
pub fn write(path: &Path, db: &AssetDb) -> Result<()> {
write_bytes(path, &encode(db)?)
}
pub fn encode(db: &AssetDb) -> Result<Vec<u8>> {
encode_with_magic(db, MAGIC)
}
pub fn read_cache(path: &Path) -> Result<BakeCache> {
let bytes = std::fs::read(path).with_context(|| format!("read cache: {}", path.display()))?;
decode_cache(&bytes)
}
pub fn decode_cache(bytes: &[u8]) -> Result<BakeCache> {
let body = check_magic(bytes, CACHE_MAGIC, "asset-db.cache")?;
let cfg = bincode::config::standard();
let (cache, _): (BakeCache, _) =
bincode::decode_from_slice(body, cfg).context("bincode decode cache")?;
if cache.schema_version != SCHEMA_VERSION {
anyhow::bail!(
"asset-db cache schema {} expected {}",
cache.schema_version,
SCHEMA_VERSION
);
}
Ok(cache)
}
pub fn write_cache(path: &Path, cache: &BakeCache) -> Result<()> {
write_bytes(path, &encode_cache(cache)?)
}
pub fn encode_cache(cache: &BakeCache) -> Result<Vec<u8>> {
encode_with_magic(cache, CACHE_MAGIC)
}
fn encode_with_magic<T: Encode>(value: &T, magic: [u8; 8]) -> Result<Vec<u8>> {
let cfg = bincode::config::standard();
let body = bincode::encode_to_vec(value, cfg).context("bincode encode")?;
let mut out = Vec::with_capacity(magic.len() + body.len());
out.extend_from_slice(&magic);
out.extend_from_slice(&body);
Ok(out)
}
fn check_magic<'a>(bytes: &'a [u8], magic: [u8; 8], label: &str) -> Result<&'a [u8]> {
if bytes.len() < magic.len() {
anyhow::bail!("{label} too short ({} bytes)", bytes.len());
}
let (head, body) = bytes.split_at(magic.len());
if head != magic {
anyhow::bail!("{label} magic mismatch");
}
Ok(body)
}
fn write_bytes(path: &Path, bytes: &[u8]) -> Result<()> {
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent)
.with_context(|| format!("create dir: {}", parent.display()))?;
}
std::fs::write(path, bytes).with_context(|| format!("write: {}", path.display()))?;
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::class_id::ClassId;
#[test]
fn roundtrip_empty() {
let db = AssetDb::new();
let bytes = encode(&db).unwrap();
let back = decode(&bytes).unwrap();
assert_eq!(back.schema_version, SCHEMA_VERSION);
assert!(back.entries.is_empty());
assert!(back.script_types.is_empty());
}
#[test]
fn roundtrip_with_entries() {
let mut db = AssetDb::new();
let script_guid = 0x1234_5678_9abc_def0_1122_3344_5566_7788_u128;
let idx = db.intern_script(script_guid);
db.entries.push(AssetEntry {
guid: 0xaabb_ccdd_u128,
asset_type: AssetType::native(ClassId::Prefab),
name: "Foo".into(),
sub_assets: vec![],
hint: "Assets/UI/Foo.prefab".into(),
});
db.entries.push(AssetEntry {
guid: 0x1111_2222_u128,
asset_type: AssetType::Script(idx),
name: "Bar".into(),
sub_assets: vec![SubAsset {
file_id: 21300000,
class_id: ClassId::Sprite as u32,
name: "Bar_sub".into(),
}],
hint: "Assets/Tween/Bar.asset".into(),
});
db.sort();
let bytes = encode(&db).unwrap();
let back = decode(&bytes).unwrap();
assert_eq!(back.script_types, vec![script_guid]);
assert_eq!(back.entries.len(), 2);
assert_eq!(back.entries[0].guid, 0x1111_2222_u128);
assert_eq!(&*back.find_by_guid(0xaabb_ccdd_u128).unwrap().name, "Foo");
assert!(back.find_by_guid(0xdead_beef_u128).is_none());
}
#[test]
fn intern_dedups() {
let mut db = AssetDb::new();
let g = 42u128;
let a = db.intern_script(g);
let b = db.intern_script(g);
assert_eq!(a, b);
assert_eq!(db.script_types.len(), 1);
}
#[test]
fn magic_mismatch_errors() {
let bad = b"NOTAPDB!extra".to_vec();
assert!(decode(&bad).is_err());
}
#[test]
fn cache_roundtrip() {
let mut c = BakeCache::new();
c.entries.push(CachedEntry {
hint: "UI/Foo.prefab".into(),
meta_mtime_ns: 1,
asset_mtime_ns: 2,
guid: 0xaa_u128,
asset_type: CachedAssetType::Native(1001),
sub_assets: vec![],
});
c.entries.push(CachedEntry {
hint: "Tween/Bar.asset".into(),
meta_mtime_ns: 3,
asset_mtime_ns: 4,
guid: 0xbb_u128,
asset_type: CachedAssetType::Script(0xcc_u128),
sub_assets: vec![],
});
let bytes = encode_cache(&c).unwrap();
let back = decode_cache(&bytes).unwrap();
assert_eq!(back.entries.len(), 2);
assert_eq!(
back.entries[1].asset_type,
CachedAssetType::Script(0xcc_u128)
);
}
#[test]
fn cache_magic_distinct_from_db() {
let c = BakeCache::new();
let cache_bytes = encode_cache(&c).unwrap();
assert!(decode(&cache_bytes).is_err());
let db = AssetDb::new();
let db_bytes = encode(&db).unwrap();
assert!(decode_cache(&db_bytes).is_err());
}
}