use std::path::{Component, Path, PathBuf};
use http::Uri;
use memchr::memmem;
use serde::{Deserialize, Serialize};
use topcoat_core::fnv1a::Fnv1a;
use crate::{AssetOptions, ConstReader, ConstWriter, Source};
#[derive(Clone, Copy, PartialEq, Eq, Hash)]
pub struct Asset(&'static [u8]);
impl Asset {
#[doc(hidden)]
#[must_use]
pub const fn new(inner: &'static [u8]) -> Self {
Self(inner)
}
#[must_use]
#[track_caller]
pub fn id(&self) -> AssetId {
let bytes = core::hint::black_box(self.0);
let mut reader = ConstReader::new(bytes);
reader.skip(SCRAMBLED_PREFIX.len());
AssetId(
reader
.read_u64_le()
.expect("raw asset does not have a valid ID"),
)
}
}
impl std::fmt::Debug for Asset {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct(stringify!(Asset))
.field("id", &self.id())
.finish()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(transparent)]
pub struct AssetId(u64);
impl AssetId {
#[must_use]
pub const fn new(
crate_name: &str,
source_file: &str,
path: &str,
options: &AssetOptions,
) -> Self {
let h = Fnv1a::<u64>::new()
.write(crate_name.as_bytes())
.write(b"\0")
.write(source_file.as_bytes())
.write(b"\0")
.write(path.as_bytes());
Self(options.hash_into(h).finish())
}
#[must_use]
pub const fn as_u64(self) -> u64 {
self.0
}
}
#[derive(Debug, Clone, PartialEq)]
pub struct RawAsset {
id: AssetId,
path: String,
crate_name: String,
manifest_dir: String,
source_file: String,
options: AssetOptions,
}
pub const ENCODED_ASSET_SIZE: usize = 2048;
impl RawAsset {
#[must_use]
pub const fn encode(
id: AssetId,
path: &str,
crate_name: &str,
manifest_dir: &str,
source_file: &str,
options: &AssetOptions,
) -> [u8; ENCODED_ASSET_SIZE] {
let mut out = [0u8; ENCODED_ASSET_SIZE];
let mut w = ConstWriter::new(&mut out);
w.write_bytes(&asset_prefix());
w.write_u64_le(id.0);
w.write_str(path);
w.write_str(crate_name);
w.write_str(manifest_dir);
w.write_str(source_file);
options.encode_into(&mut w);
out
}
#[must_use]
pub fn decode(buffer: &[u8]) -> Option<Self> {
let mut r = ConstReader::new(buffer);
r.skip(asset_prefix().len())?;
Some(Self {
id: AssetId(r.read_u64_le()?),
path: r.read_str()?.to_owned(),
crate_name: r.read_str()?.to_owned(),
manifest_dir: r.read_str()?.to_owned(),
source_file: r.read_str()?.to_owned(),
options: AssetOptions::decode_from(&mut r)?,
})
}
#[must_use]
pub fn options(&self) -> &AssetOptions {
&self.options
}
#[must_use]
pub fn find_in_binary(binary: &[u8]) -> Vec<Self> {
let prefix = asset_prefix();
let finder = memmem::Finder::new(&prefix);
finder
.find_iter(binary)
.filter_map(|index| Self::decode(&binary[index..]))
.filter(|asset| !asset.path.is_empty())
.collect()
}
#[must_use]
pub fn id(&self) -> AssetId {
self.id
}
#[must_use]
pub fn path(&self) -> &str {
&self.path
}
#[must_use]
pub fn source(&self) -> Source {
if let Ok(uri) = self.path.parse::<Uri>()
&& matches!(uri.scheme_str(), Some("http" | "https"))
{
return Source::Url(uri);
}
Source::Path(self.resolved_path())
}
#[must_use]
pub fn crate_name(&self) -> &str {
&self.crate_name
}
pub fn resolved_path(&self) -> PathBuf {
let path = Path::new(&self.path);
if path.is_absolute() {
return normalize(path);
}
let anchor = match path.components().next() {
Some(Component::CurDir | Component::ParentDir) => {
let source = anchor_source_file(&self.manifest_dir, &self.source_file);
source
.parent()
.map_or_else(|| PathBuf::from(&self.manifest_dir), Path::to_path_buf)
}
_ => PathBuf::from(&self.manifest_dir),
};
normalize(&anchor.join(path))
}
}
fn anchor_source_file(manifest_dir: &str, source_file: &str) -> PathBuf {
let source = Path::new(source_file);
if source.is_absolute() {
return source.to_path_buf();
}
let mut cur = Path::new(manifest_dir);
loop {
let candidate = cur.join(source);
if candidate.exists() {
return candidate;
}
match cur.parent() {
Some(parent) => cur = parent,
None => return Path::new(manifest_dir).join(source),
}
}
}
fn normalize(path: &Path) -> PathBuf {
let mut out = PathBuf::new();
for comp in path.components() {
match comp {
Component::CurDir => {}
Component::ParentDir => {
if !out.pop() {
out.push(comp);
}
}
other => out.push(other),
}
}
out
}
#[macro_export]
macro_rules! asset {
($path:expr $(, $($ao:tt)*)?) => {{
const PATH: &str = $path;
const CRATE_NAME: &str = env!("CARGO_CRATE_NAME");
const MANIFEST_DIR: &str = env!("CARGO_MANIFEST_DIR");
const SOURCE_FILE: &str = file!();
const OPTIONS: $crate::AssetOptions = $crate::asset_options!($($($ao)*)?);
const ID: $crate::AssetId = $crate::AssetId::new(CRATE_NAME, SOURCE_FILE, PATH, &OPTIONS);
pub static ENCODED_ASSET: [u8; $crate::ENCODED_ASSET_SIZE] = $crate::RawAsset::encode(
ID,
PATH,
CRATE_NAME,
MANIFEST_DIR,
SOURCE_FILE,
&OPTIONS,
);
$crate::Asset::new(ENCODED_ASSET.as_slice())
}};
}
const PREFIX_KEY: u8 = 0xA7;
const SCRAMBLED_PREFIX: [u8; 13] = [
b'T' ^ PREFIX_KEY,
b'O' ^ PREFIX_KEY,
b'P' ^ PREFIX_KEY,
b'C' ^ PREFIX_KEY,
b'O' ^ PREFIX_KEY,
b'A' ^ PREFIX_KEY,
b'T' ^ PREFIX_KEY,
b'_' ^ PREFIX_KEY,
b'A' ^ PREFIX_KEY,
b'S' ^ PREFIX_KEY,
b'S' ^ PREFIX_KEY,
b'E' ^ PREFIX_KEY,
b'T' ^ PREFIX_KEY,
];
const fn asset_prefix() -> [u8; 13] {
let mut out = [0u8; 13];
let mut i = 0;
while i < SCRAMBLED_PREFIX.len() {
out[i] = SCRAMBLED_PREFIX[i] ^ PREFIX_KEY;
i += 1;
}
out
}