use std::path::Path;
use std::sync::LazyLock;
use sha2::{Digest, Sha256};
pub struct Asset {
pub name: &'static str,
pub hashed: &'static str,
pub hash: &'static str,
pub mime: &'static str,
pub identity: &'static [u8],
pub brotli: Option<&'static [u8]>,
pub gzip: Option<&'static [u8]>,
}
include!(concat!(env!("OUT_DIR"), "/manifest.rs"));
#[derive(Clone, Copy, Debug)]
pub enum Owned<'a> {
Asset(&'a str),
SystemWidget(&'a str),
Module {
snapshot: &'a crate::widgets::Snapshot,
file: &'a Path,
known: &'a [String],
},
}
pub fn address(owned: Owned) -> String {
match owned {
Owned::Asset(name) => {
let asset = ASSETS
.iter()
.find(|asset| asset.name == name)
.unwrap_or_else(|| panic!("{name} is not a built-in asset"));
format!("/__devpage/{}", asset.hashed)
}
Owned::SystemWidget(name) => format!(
"/__devpage/system-widgets/{name}.tsx?v={}",
system_widget_hash(name).unwrap_or_default()
),
Owned::Module {
snapshot,
file,
known,
} => format!(
"/__devpage/widgets/{}?v={}",
file.strip_prefix(snapshot.served())
.unwrap_or(file)
.display(),
snapshot.hash(file, known)
),
}
}
pub fn asset(requested: &str) -> Option<(&'static Asset, bool)> {
if let Some(asset) = ASSETS.iter().find(|asset| asset.hashed == requested) {
return Some((asset, true));
}
let (stem, rest) = requested.split_once('.')?;
let ext = rest.rsplit_once('.').map_or(rest, |(_, ext)| ext);
ASSETS
.iter()
.find(|asset| asset.name == format!("{stem}.{ext}"))
.map(|asset| (asset, false))
}
pub fn system_widget_hash(name: &str) -> Option<&'static str> {
static HASHES: LazyLock<Vec<(&'static str, String)>> = LazyLock::new(|| {
let names: Vec<&str> = crate::widgets::SYSTEM.iter().map(|(n, _)| *n).collect();
crate::widgets::SYSTEM
.iter()
.map(|(name, source)| {
let mut hash = Sha256::new();
hash.update(BUILD);
hash.update(names.join("\0"));
hash.update(name);
hash.update(source);
(*name, short(&hash.finalize()))
})
.collect()
});
HASHES
.iter()
.find(|(n, _)| *n == name)
.map(|(_, hash)| hash.as_str())
}
pub fn short(digest: &[u8]) -> String {
digest[..8].iter().map(|b| format!("{b:02x}")).collect()
}
pub fn hash(bytes: &[u8]) -> String {
short(&Sha256::digest(bytes))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn every_asset_has_a_content_address_and_old_names_still_find_it() {
for asset in ASSETS {
let address = address(Owned::Asset(asset.name));
assert_eq!(address, format!("/__devpage/{}", asset.hashed));
assert_eq!(asset.hash, hash(asset.identity), "{}", asset.name);
let (found, current) = super::asset(asset.hashed).unwrap();
assert!(current && found.name == asset.name);
let (found, current) = super::asset(asset.name).unwrap();
assert!(!current && found.name == asset.name);
let (stem, ext) = asset.name.split_once('.').unwrap();
let (found, current) = super::asset(&format!("{stem}.0123456789abcdef.{ext}")).unwrap();
assert!(!current && found.name == asset.name);
}
assert!(super::asset("nope.js").is_none());
}
#[test]
fn an_asset_names_the_others_by_their_content_addresses() {
let text = |name: &str| {
let asset = ASSETS.iter().find(|a| a.name == name).unwrap();
String::from_utf8(asset.identity.to_vec()).unwrap()
};
assert!(text("devpage.js").contains(&address(Owned::Asset("design.js"))));
let design = text("design.js");
assert!(design.contains(&address(Owned::Asset("design-engine.js"))));
assert!(design.contains(&address(Owned::Asset("design-engine.wasm"))));
}
#[test]
fn a_system_widget_address_carries_its_version() {
let callout = address(Owned::SystemWidget("Callout"));
assert_eq!(
callout,
format!(
"/__devpage/system-widgets/Callout.tsx?v={}",
system_widget_hash("Callout").unwrap()
)
);
assert_ne!(
system_widget_hash("Callout"),
system_widget_hash("Tabs"),
"each system widget has its own version"
);
}
}