use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ContentEncoding {
Identity,
Brotli,
}
impl ContentEncoding {
pub const fn token(self) -> Option<&'static str> {
match self {
Self::Identity => None,
Self::Brotli => Some("br"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct EmbeddedAsset {
pub path: &'static str,
pub bytes: &'static [u8],
pub raw_len: usize,
pub encoding: ContentEncoding,
pub content_type: &'static str,
pub etag: &'static str,
}
impl EmbeddedAsset {
pub fn decode(&self) -> Option<Vec<u8>> {
match self.encoding {
ContentEncoding::Identity => Some(self.bytes.to_vec()),
ContentEncoding::Brotli => decode_brotli(self.bytes, self.raw_len),
}
}
pub fn is_encoded(&self) -> bool {
matches!(self.encoding, ContentEncoding::Brotli)
}
pub fn etag_for(&self, encoded: bool) -> String {
if !encoded {
return self.etag.to_string();
}
match self.etag.strip_suffix('"') {
Some(head) => format!("{head}-br\""),
None => format!("{}-br", self.etag),
}
}
}
fn decode_brotli(payload: &[u8], raw_len: usize) -> Option<Vec<u8>> {
use std::io::Read as _;
let mut out = Vec::with_capacity(raw_len);
let mut decoder = brotli_decompressor::Decompressor::new(payload, 4096);
decoder.read_to_end(&mut out).ok()?;
Some(out)
}
#[derive(Debug, Clone, Copy)]
pub struct EmbeddedAssets {
root: &'static str,
files: &'static [EmbeddedAsset],
}
impl EmbeddedAssets {
pub const fn new(root: &'static str, files: &'static [EmbeddedAsset]) -> Self {
Self { root, files }
}
pub const fn root(&self) -> &'static str {
self.root
}
pub const fn files(&self) -> &'static [EmbeddedAsset] {
self.files
}
pub fn len(&self) -> usize {
self.files.len()
}
pub fn is_empty(&self) -> bool {
self.files.is_empty()
}
pub fn total_bytes(&self) -> u64 {
self.files.iter().map(|file| file.bytes.len() as u64).sum()
}
pub fn total_raw_bytes(&self) -> u64 {
self.files.iter().map(|file| file.raw_len as u64).sum()
}
pub fn get(&self, path: &str) -> Option<&'static EmbeddedAsset> {
self.files
.binary_search_by(|file| file.path.cmp(path))
.ok()
.map(|index| &self.files[index])
}
pub fn debug_assert_sorted(&self) {
debug_assert!(
self.files
.windows(2)
.all(|pair| pair[0].path < pair[1].path),
"EmbeddedAssets must be sorted by path, with no duplicates"
);
}
pub fn shadowed_by(&self, disk_root: &Path) -> Vec<&'static str> {
let mut shadowed = Vec::new();
collect_shadowed(self, disk_root, "", &mut shadowed);
shadowed
}
}
fn collect_shadowed(
assets: &EmbeddedAssets,
dir: &Path,
prefix: &str,
shadowed: &mut Vec<&'static str>,
) {
let Ok(entries) = std::fs::read_dir(dir) else {
return;
};
for entry in entries.flatten() {
let name = entry.file_name();
let Some(name) = name.to_str() else {
continue;
};
let key = if prefix.is_empty() {
name.to_string()
} else {
format!("{prefix}/{name}")
};
let path = entry.path();
if path.is_dir() {
collect_shadowed(assets, &path, &key, shadowed);
} else if let Some(asset) = assets.get(&key) {
shadowed.push(asset.path);
}
}
}
inventory::collect!(EmbeddedAssets);
#[cfg(test)]
mod tests {
use super::{ContentEncoding, EmbeddedAsset, EmbeddedAssets};
const FILES: &[EmbeddedAsset] = &[
EmbeddedAsset {
path: "assets/app.css",
bytes: b"a{}",
raw_len: 3,
encoding: ContentEncoding::Identity,
content_type: "text/css",
etag: "\"sha256-1\"",
},
EmbeddedAsset {
path: "index.html",
bytes: b"index",
raw_len: 5,
encoding: ContentEncoding::Identity,
content_type: "text/html",
etag: "\"sha256-2\"",
},
EmbeddedAsset {
path: "only-embedded.txt",
bytes: b"embedded",
raw_len: 8,
encoding: ContentEncoding::Identity,
content_type: "text/plain",
etag: "\"sha256-3\"",
},
];
fn brotli(source: &str) -> Vec<u8> {
use std::io::Write as _;
let mut out = Vec::new();
let mut encoder = brotli::CompressorWriter::new(&mut out, 4096, 11, 22);
encoder.write_all(source.as_bytes()).unwrap();
encoder.flush().unwrap();
drop(encoder);
out
}
#[test]
fn get_finds_every_entry() {
let assets = EmbeddedAssets::new("wwwroot", FILES);
assets.debug_assert_sorted();
for file in FILES {
assert_eq!(assets.get(file.path).unwrap().bytes, file.bytes);
}
assert_eq!(assets.len(), 3);
assert_eq!(assets.total_bytes(), 16);
assert_eq!(assets.total_raw_bytes(), 16);
}
#[test]
fn an_encoded_entry_decodes_back_to_the_original_file() {
let source = "body { color: red; }".repeat(32);
let payload = brotli(&source);
assert!(
payload.len() < source.len(),
"the fixture must actually compress"
);
let encoded = EmbeddedAsset {
path: "app.css",
bytes: Box::leak(payload.into_boxed_slice()),
raw_len: source.len(),
encoding: ContentEncoding::Brotli,
content_type: "text/css",
etag: "\"sha256-x\"",
};
assert!(encoded.is_encoded());
assert_eq!(encoded.decode().unwrap(), source.as_bytes());
assert_eq!(FILES[0].decode().unwrap(), b"a{}");
assert!(!FILES[0].is_encoded());
}
#[test]
fn totals_separate_the_stored_payload_from_the_original_size() {
let source = "x".repeat(4096);
let payload = brotli(&source);
let encoded: &'static EmbeddedAsset = Box::leak(Box::new(EmbeddedAsset {
path: "big.txt",
bytes: Box::leak(payload.into_boxed_slice()),
raw_len: 4096,
encoding: ContentEncoding::Brotli,
content_type: "text/plain",
etag: "\"sha256-y\"",
}));
let assets = EmbeddedAssets::new("wwwroot", std::slice::from_ref(encoded));
assert!(assets.total_bytes() < assets.total_raw_bytes());
assert_eq!(assets.total_raw_bytes(), 4096);
}
#[test]
fn each_representation_gets_its_own_validator() {
let plain = &FILES[0];
assert_eq!(plain.etag_for(false), "\"sha256-1\"");
assert_eq!(plain.etag_for(true), "\"sha256-1-br\"");
assert_ne!(plain.etag_for(false), plain.etag_for(true));
let bare = EmbeddedAsset {
etag: "sha256-z",
..*plain
};
assert_eq!(bare.etag_for(true), "sha256-z-br");
}
#[test]
fn encoding_tokens_are_the_http_ones() {
assert_eq!(ContentEncoding::Identity.token(), None);
assert_eq!(ContentEncoding::Brotli.token(), Some("br"));
}
#[test]
fn get_misses_unknown_and_prefix_paths() {
let assets = EmbeddedAssets::new("wwwroot", FILES);
assert!(assets.get("assets").is_none());
assert!(assets.get("assets/nope.css").is_none());
assert!(assets.get("index.htm").is_none());
assert!(assets.get("").is_none());
}
#[test]
fn root_is_reported_as_written() {
assert_eq!(EmbeddedAssets::new("wwwroot", FILES).root(), "wwwroot");
assert_eq!(
EmbeddedAssets::new("../wwwroot", FILES).root(),
"../wwwroot"
);
}
#[test]
fn shadowed_by_reports_same_named_disk_files() {
let dir = tempfile::tempdir().unwrap();
std::fs::create_dir_all(dir.path().join("assets")).unwrap();
std::fs::write(dir.path().join("assets/app.css"), b"disk").unwrap();
std::fs::write(dir.path().join("only-embedded.txt"), b"disk").unwrap();
std::fs::write(dir.path().join("only-disk.txt"), b"disk").unwrap();
let mut shadowed = EmbeddedAssets::new("wwwroot", FILES).shadowed_by(dir.path());
shadowed.sort_unstable();
assert_eq!(shadowed, vec!["assets/app.css", "only-embedded.txt"]);
}
#[test]
fn shadowed_by_missing_directory_is_empty() {
let dir = tempfile::tempdir().unwrap();
let assets = EmbeddedAssets::new("wwwroot", FILES);
assert!(assets
.shadowed_by(&dir.path().join("does-not-exist"))
.is_empty());
}
}