The files a site embeds in its binary and serves from itself, each named by its content so a long cache never serves an old copy.
A site lists its files once, in a [Bundle], and mounts [Bundle::find] as a fallback route. A page
links to a file with [Asset::href] (/ui.css?v=1a2b3c4d); the route answers with the asset's
content_type and [Asset::cache_control]. No runtime and no I/O: a framework adapter turns these
into a response.
One embedded file.
Where it is served from, with no query: /ui.css.
FNV-1a over bytes, as eight hex digits: a name for the content, not a secret.
24impl Asset {
What a page links to: /ui.css?v=1a2b3c4d.
?v= names exactly these bytes, so a request that carries it never changes; one without is
kept for a day.
The address of a file a site makes itself at run time (a stylesheet it builds), named by its content.
Every asset a site serves. Building one refuses a path that is not absolute, carries a query, or is
listed twice, and in a const it does so when the site is compiled.
61const fn same(a: &str, b: &str) -> bool { 62 let (a, b) = (a.as_bytes(), b.as_bytes()); 63 if a.len() != b.len() { 64 return false; 65 } 66 let mut i = 0; 67 while i < a.len() { 68 if a[i] != b[i] { 69 return false; 70 } 71 i += 1; 72 } 73 true 74} 75 76const fn has_query(path: &str) -> bool { 77 let path = path.as_bytes(); 78 let mut i = 0; 79 while i < path.len() { 80 if path[i] == b'?' { 81 return true; 82 } 83 i += 1; 84 } 85 false 86} 87 88impl Bundle { 89 pub const fn new(assets: &'static [Asset]) -> Bundle { 90 let mut i = 0; 91 while i < assets.len() { 92 let path = assets[i].path.as_bytes(); 93 assert!(!path.is_empty() && path[0] == b'/', "an asset's path starts with a slash"); 94 assert!(!has_query(assets[i].path), "an asset's path has no query"); 95 assert!(!assets[i].body.is_empty(), "an asset has bytes"); 96 let mut j = i + 1; 97 while j < assets.len() { 98 assert!(!same(assets[i].path, assets[j].path), "two assets share a path"); 99 j += 1; 100 } 101 i += 1; 102 } 103 Bundle(assets) 104 }
The asset served at path (no query), if there is one.
111 pub fn all(&self) -> &'static [Asset] { 112 self.0 113 } 114} 115 116#[cfg(test)] 117mod tests { 118 use super::*; 119 120 const CSS: Asset = Asset { path: "/a.css", content_type: "text/css; charset=utf-8", body: b"a{}" }; 121 const JS: Asset = Asset { path: "/a.js", content_type: "text/javascript; charset=utf-8", body: b"1;" }; 122 static BUNDLE: Bundle = Bundle::new(&[CSS, JS]); 123 124 #[test] 125 fn find_answers_by_path_without_a_query() { 126 assert_eq!(BUNDLE.find("/a.css").unwrap().path, "/a.css"); 127 assert!(BUNDLE.find("/a.css?v=1").is_none()); 128 assert!(BUNDLE.find("/nope").is_none()); 129 } 130 131 #[test] 132 fn the_version_names_the_bytes() { 133 assert_eq!(hash(b""), "811c9dc5"); 134 assert_eq!(CSS.version().len(), 8); 135 assert_ne!(CSS.version(), JS.version()); 136 assert_eq!(CSS.href(), format!("/a.css?v={}", CSS.version())); 137 assert_eq!(versioned_href("/x.css", "a{}"), format!("/x.css?v={}", CSS.version())); 138 } 139 140 #[test] 141 fn a_matching_version_is_cached_for_good_and_a_bare_path_for_a_day() { 142 let own = format!("x=1&v={}", CSS.version()); 143 assert!(CSS.cache_control(Some(&own)).contains("immutable")); 144 assert!(!CSS.cache_control(Some("v=00000000")).contains("immutable")); 145 assert!(!CSS.cache_control(None).contains("immutable")); 146 } 147 148 #[test] 149 #[should_panic(expected = "two assets share a path")] 150 fn a_repeated_path_is_refused() { 151 Bundle::new(&[CSS, CSS]); 152 } 153 154 #[test] 155 #[should_panic(expected = "starts with a slash")] 156 fn a_relative_path_is_refused() { 157 Bundle::new(&[Asset { path: "a.css", content_type: "text/css", body: b"x" }]); 158 } 159 160 #[test] 161 #[should_panic(expected = "no query")] 162 fn a_path_with_a_query_is_refused() { 163 Bundle::new(&[Asset { path: "/a.css?v=1", content_type: "text/css", body: b"x" }]); 164 } 165}