ocserve_http/
embedded_ui.rs1use std::collections::HashMap;
23use std::sync::OnceLock;
24
25static PACK_ZST: &[u8] = include_bytes!("../assets/webui/1.18.31.pack.zst");
34
35pub const APP_TAG: &str = "1.18.31";
37
38static FILES: OnceLock<HashMap<String, Vec<u8>>> = OnceLock::new();
39
40fn files() -> &'static HashMap<String, Vec<u8>> {
44 FILES.get_or_init(|| {
45 let raw = match zstd::stream::decode_all(PACK_ZST) {
46 Ok(r) => r,
47 Err(e) => {
48 tracing::warn!("embedded UI pack: zstd decode failed: {e:#}");
49 return HashMap::new();
50 }
51 };
52 parse_pack(&raw).unwrap_or_else(|e| {
53 tracing::warn!("embedded UI pack: parse failed: {e}");
54 HashMap::new()
55 })
56 })
57}
58
59fn parse_pack(raw: &[u8]) -> Result<HashMap<String, Vec<u8>>, String> {
60 if raw.len() < 6 || &raw[..6] != b"OCAP1\n" {
61 return Err("bad magic".into());
62 }
63 let mut i = 6usize;
64 let count = read_u32(raw, &mut i)? as usize;
65 let mut map = HashMap::with_capacity(count);
66 for _ in 0..count {
67 let plen = read_u16(raw, &mut i)? as usize;
68 if i + plen > raw.len() {
69 return Err("path overruns".into());
70 }
71 let path = String::from_utf8(raw[i..i + plen].to_vec()).map_err(|_| "bad path utf8")?;
72 i += plen;
73 let dlen = read_u64(raw, &mut i)? as usize;
74 if i + dlen > raw.len() {
75 return Err("data overruns".into());
76 }
77 let data = raw[i..i + dlen].to_vec();
78 i += dlen;
79 map.insert(path, data);
80 }
81 Ok(map)
82}
83
84fn read_u16(b: &[u8], i: &mut usize) -> Result<u16, String> {
85 if *i + 2 > b.len() {
86 return Err("eof u16".into());
87 }
88 let v = u16::from_le_bytes([b[*i], b[*i + 1]]);
89 *i += 2;
90 Ok(v)
91}
92fn read_u32(b: &[u8], i: &mut usize) -> Result<u32, String> {
93 if *i + 4 > b.len() {
94 return Err("eof u32".into());
95 }
96 let v = u32::from_le_bytes([b[*i], b[*i + 1], b[*i + 2], b[*i + 3]]);
97 *i += 4;
98 Ok(v)
99}
100fn read_u64(b: &[u8], i: &mut usize) -> Result<u64, String> {
101 if *i + 8 > b.len() {
102 return Err("eof u64".into());
103 }
104 let v = u64::from_le_bytes(b[*i..*i + 8].try_into().map_err(|_| "u64")?);
105 *i += 8;
106 Ok(v)
107}
108
109pub fn available() -> bool {
111 !files().is_empty()
112}
113
114pub fn lookup(path: &str) -> Option<(&'static str, &'static [u8])> {
120 let map = files();
121 let key = path.trim_start_matches('/');
122 if let Some((k, data)) = map.get_key_value(key) {
123 return Some((k.as_str(), data));
125 }
126 if !path.starts_with("/assets/")
129 && let Some(data) = map.get("index.html")
130 {
131 return Some(("index.html", data));
132 }
133 None
134}
135
136pub fn mime_for(path: &str) -> &'static str {
138 let p = path.split('?').next().unwrap_or(path);
139 match p.rsplit('.').next().unwrap_or("") {
140 "html" => "text/html",
141 "js" | "mjs" => "text/javascript",
142 "css" => "text/css",
143 "json" | "webmanifest" => "application/json",
144 "svg" => "image/svg+xml",
145 "png" => "image/png",
146 "ico" => "image/x-icon",
147 "ttf" => "font/ttf",
148 "woff" => "font/woff",
149 "woff2" => "font/woff2",
150 "mp4" => "video/mp4",
151 "wasm" => "application/wasm",
152 "txt" => "text/plain",
153 _ => "application/octet-stream",
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160
161 #[test]
162 fn pack_parses_and_has_entrypoints() {
163 assert!(available(), "embedded pack did not parse");
165 let map = files();
166 assert!(map.contains_key("index.html"), "no index.html");
167 assert!(
169 map.keys()
170 .any(|k| k.starts_with("assets/index-") && k.ends_with(".js")),
171 "no entry JS"
172 );
173 }
174
175 #[test]
176 fn spa_fallback_only_for_non_assets() {
177 let (resolved, idx) = lookup("/some/spa/route").expect("spa fallback");
179 assert_eq!(resolved, "index.html");
180 assert!(String::from_utf8_lossy(idx).contains("<"));
181 assert!(lookup("/assets/does-not-exist-xyz.js").is_none());
183 }
184
185 #[test]
186 fn mime_table() {
187 assert_eq!(mime_for("/assets/index-abc.js"), "text/javascript");
188 assert_eq!(mime_for("/assets/index-abc.css"), "text/css");
189 assert_eq!(mime_for("/assets/font.woff2"), "font/woff2");
190 assert_eq!(mime_for("/"), "application/octet-stream");
191 assert_eq!(mime_for("/index.html"), "text/html");
192 }
193}