wavecraft_dev_server/
assets.rs1use include_dir::{Dir, include_dir};
8use std::borrow::Cow;
9use std::path::{Component, Path, PathBuf};
10
11static UI_ASSETS: Dir = include_dir!("$CARGO_MANIFEST_DIR/assets/ui-dist");
15
16pub fn get_asset(path: &str) -> Option<(Cow<'static, [u8]>, &'static str)> {
25 let path = path.trim_start_matches('/');
27
28 let path = if path.is_empty() { "index.html" } else { path };
30
31 if let Some(contents) = try_disk_asset(path) {
32 let mime_type = mime_type_from_path(path);
33 return Some((Cow::Owned(contents), mime_type));
34 }
35
36 let file = UI_ASSETS.get_file(path)?;
38
39 let mime_type = mime_type_from_path(path);
41
42 Some((Cow::Borrowed(file.contents()), mime_type))
43}
44
45fn try_disk_asset(path: &str) -> Option<Vec<u8>> {
46 if !is_safe_relative_path(path) {
47 return None;
48 }
49
50 let base_dir = ui_dist_dir();
51 let asset_path = base_dir.join(path);
52
53 if !asset_path.exists() {
54 return None;
55 }
56
57 std::fs::read(asset_path).ok()
58}
59
60fn ui_dist_dir() -> PathBuf {
61 Path::new(env!("CARGO_MANIFEST_DIR"))
62 .join("../../../ui/dist")
63}
64
65fn is_safe_relative_path(path: &str) -> bool {
66 let candidate = Path::new(path);
67 !candidate.is_absolute()
68 && candidate
69 .components()
70 .all(|component| component != Component::ParentDir)
71}
72
73fn mime_type_from_path(path: &str) -> &'static str {
75 let extension = path.split('.').next_back().unwrap_or("");
76
77 match extension {
78 "html" => "text/html",
79 "css" => "text/css",
80 "js" | "mjs" => "application/javascript",
81 "json" => "application/json",
82 "png" => "image/png",
83 "jpg" | "jpeg" => "image/jpeg",
84 "svg" => "image/svg+xml",
85 "woff" => "font/woff",
86 "woff2" => "font/woff2",
87 "ttf" => "font/ttf",
88 "ico" => "image/x-icon",
89 _ => "application/octet-stream",
90 }
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use include_dir::Dir;
97
98 fn list_assets() -> Vec<String> {
100 let mut paths = Vec::new();
101 collect_paths(&UI_ASSETS, "", &mut paths);
102 paths
103 }
104
105 fn collect_paths(dir: &Dir, prefix: &str, paths: &mut Vec<String>) {
106 for file in dir.files() {
107 paths.push(format!("{}{}", prefix, file.path().display()));
108 }
109 for subdir in dir.dirs() {
110 let subdir_name = subdir.path().file_name().unwrap().to_str().unwrap();
111 collect_paths(subdir, &format!("{}{}/", prefix, subdir_name), paths);
112 }
113 }
114
115 #[test]
116 fn test_mime_type_inference() {
117 assert_eq!(mime_type_from_path("index.html"), "text/html");
118 assert_eq!(mime_type_from_path("style.css"), "text/css");
119 assert_eq!(mime_type_from_path("app.js"), "application/javascript");
120 assert_eq!(mime_type_from_path("data.json"), "application/json");
121 assert_eq!(
122 mime_type_from_path("unknown.xyz"),
123 "application/octet-stream"
124 );
125 }
126
127 #[test]
128 fn test_list_assets() {
129 let assets = list_assets();
132 println!("Found {} embedded assets", assets.len());
135 for asset in assets.iter().take(10) {
136 println!(" - {}", asset);
137 }
138 }
139
140 #[test]
141 fn test_get_index_html() {
142 let (content, mime_type) =
143 get_asset("index.html").expect("index.html should exist after React build");
144
145 assert_eq!(mime_type, "text/html");
146 assert!(!content.is_empty());
147
148 let html = std::str::from_utf8(content.as_ref()).unwrap();
150 assert!(html.contains("<!DOCTYPE html") || html.contains("<!doctype html"));
151 }
152}