use bytes::Bytes;
use dashmap::DashMap;
use ferro_rs::{HttpResponse, Request};
use sha2::{Digest, Sha256};
use std::sync::OnceLock;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error("bundle not found at path: {0}")]
NotFound(String),
#[error("duplicate bundle name: {0} already registered")]
DuplicateName(String),
}
pub type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, Clone)]
struct BundleEntry {
name: String,
bytes: &'static [u8],
content_type: String,
sha256_full_hex: String,
sha256_short_hex: String,
ext: String,
hashed_url: String,
}
static BUNDLE_REGISTRY: OnceLock<DashMap<String, BundleEntry>> = OnceLock::new();
static ALIAS_REGISTRY: OnceLock<DashMap<String, String>> = OnceLock::new();
static NAME_INDEX: OnceLock<DashMap<String, String>> = OnceLock::new();
fn bundle_registry() -> &'static DashMap<String, BundleEntry> {
BUNDLE_REGISTRY.get_or_init(DashMap::new)
}
fn alias_registry() -> &'static DashMap<String, String> {
ALIAS_REGISTRY.get_or_init(DashMap::new)
}
fn name_index() -> &'static DashMap<String, String> {
NAME_INDEX.get_or_init(DashMap::new)
}
fn ext_from_content_type(ct: &str) -> &'static str {
match ct.split(';').next().unwrap_or(ct).trim() {
"application/javascript" | "text/javascript" => "js",
"text/css" => "css",
"text/html" => "html",
"text/plain" => "txt",
"application/json" => "json",
"image/png" => "png",
"image/jpeg" => "jpg",
"image/svg+xml" => "svg",
"image/gif" => "gif",
"image/webp" => "webp",
"font/woff2" => "woff2",
"font/woff" => "woff",
"application/wasm" => "wasm",
_ => "",
}
}
fn hashed_url_for(name: &str, sha8: &str, ext: &str) -> String {
if ext.is_empty() {
format!("/bundles/{name}.{sha8}")
} else {
format!("/bundles/{name}.{sha8}.{ext}")
}
}
pub struct Bundle {
name: String,
}
impl Bundle {
pub fn new(name: &str, bytes: &'static [u8]) -> Self {
if name_index().contains_key(name) {
panic!("ferro-bundle: duplicate registration for bundle name {name:?}");
}
let digest = Sha256::digest(bytes);
let sha256_full_hex = hex::encode(digest);
let sha256_short_hex = sha256_full_hex[..8].to_string();
let content_type = "application/octet-stream".to_string();
let ext = ext_from_content_type(&content_type).to_string();
let hashed_url = hashed_url_for(name, &sha256_short_hex, &ext);
let entry = BundleEntry {
name: name.to_string(),
bytes,
content_type,
sha256_full_hex,
sha256_short_hex,
ext,
hashed_url: hashed_url.clone(),
};
bundle_registry().insert(hashed_url.clone(), entry);
name_index().insert(name.to_string(), hashed_url);
Bundle {
name: name.to_string(),
}
}
pub fn content_type(self, ct: &str) -> Self {
let ext = ext_from_content_type(ct).to_string();
let old_url = match name_index().get(&self.name) {
Some(v) => v.value().clone(),
None => return self, };
let mut entry = match bundle_registry().remove(&old_url) {
Some((_, e)) => e,
None => return self, };
entry.content_type = ct.to_string();
entry.ext = ext.clone();
let new_url = hashed_url_for(&entry.name, &entry.sha256_short_hex, &ext);
entry.hashed_url = new_url.clone();
bundle_registry().insert(new_url.clone(), entry);
name_index().insert(self.name.clone(), new_url);
self
}
pub fn with_alias(self, alias_path: &str) -> Self {
let target = match name_index().get(&self.name) {
Some(v) => v.value().clone(),
None => return self, };
alias_registry().insert(alias_path.to_string(), target);
self
}
pub fn hashed_url(&self) -> String {
name_index()
.get(&self.name)
.map(|v| v.value().clone())
.unwrap_or_default()
}
pub fn serve(req: Request) -> HttpResponse {
let path = req.path().to_string();
let if_none_match = req.header("if-none-match").map(|s| s.to_string());
serve_inner(&path, if_none_match.as_deref())
}
}
pub(crate) fn serve_inner(path: &str, if_none_match: Option<&str>) -> HttpResponse {
if let Some(target) = alias_registry().get(path) {
return HttpResponse::new()
.status(301)
.header("Location", target.value().clone());
}
if let Some(entry) = bundle_registry().get(path) {
let etag = format!("\"{}\"", entry.sha256_full_hex);
if let Some(inm) = if_none_match {
if inm == etag {
return HttpResponse::new()
.status(304)
.header("ETag", etag)
.header("Cache-Control", "public, max-age=31536000, immutable");
}
}
return HttpResponse::bytes(Bytes::from_static(entry.bytes))
.header("Content-Type", entry.content_type.clone())
.header("Cache-Control", "public, max-age=31536000, immutable")
.header("ETag", etag);
}
HttpResponse::new()
.status(404)
.header("Content-Type", "text/plain")
}
#[doc(hidden)]
pub mod __test_internals {
use ferro_rs::HttpResponse;
#[inline]
pub fn serve_inner(path: &str, if_none_match: Option<&str>) -> HttpResponse {
crate::serve_inner(path, if_none_match)
}
}
#[cfg(test)]
pub(crate) fn reset() {
if let Some(r) = BUNDLE_REGISTRY.get() {
r.clear();
}
if let Some(r) = ALIAS_REGISTRY.get() {
r.clear();
}
if let Some(r) = NAME_INDEX.get() {
r.clear();
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hash_is_deterministic() {
reset();
let b = Bundle::new("test1", b"hello").content_type("text/plain");
assert_eq!(b.hashed_url(), "/bundles/test1.2cf24dba.txt");
}
#[test]
fn default_content_type_is_octet_stream() {
reset();
let b = Bundle::new("test2", b"x");
let url = b.hashed_url();
assert!(
url.starts_with("/bundles/test2."),
"expected /bundles/test2. prefix, got {url}"
);
assert!(
!url.ends_with(".txt") && !url.ends_with(".js") && !url.ends_with(".css"),
"default URL should not have a known extension; got {url}"
);
let suffix = url.strip_prefix("/bundles/test2.").unwrap();
assert_eq!(suffix.len(), 8, "expected 8-char short hash; got {suffix}");
}
#[test]
#[should_panic(expected = "duplicate")]
fn duplicate_name_panics() {
reset();
Bundle::new("dup", b"a");
Bundle::new("dup", b"a");
}
#[test]
fn error_not_found_displays_message() {
let e = Error::NotFound("/x".to_string());
assert_eq!(e.to_string(), "bundle not found at path: /x");
}
#[test]
fn error_duplicate_name_displays_message() {
let e = Error::DuplicateName("dup".to_string());
assert_eq!(
e.to_string(),
"duplicate bundle name: dup already registered"
);
}
}