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")).join("../../../ui/dist")
62}
63
64fn is_safe_relative_path(path: &str) -> bool {
65 let candidate = Path::new(path);
66 !candidate.is_absolute()
67 && candidate
68 .components()
69 .all(|component| component != Component::ParentDir)
70}
71
72fn mime_type_from_path(path: &str) -> &'static str {
74 let extension = path.split('.').next_back().unwrap_or("");
75
76 match extension {
77 "html" => "text/html",
78 "css" => "text/css",
79 "js" | "mjs" => "application/javascript",
80 "json" => "application/json",
81 "png" => "image/png",
82 "jpg" | "jpeg" => "image/jpeg",
83 "svg" => "image/svg+xml",
84 "woff" => "font/woff",
85 "woff2" => "font/woff2",
86 "ttf" => "font/ttf",
87 "ico" => "image/x-icon",
88 _ => "application/octet-stream",
89 }
90}
91
92#[cfg(test)]
93mod tests {
94 use super::*;
95 use include_dir::Dir;
96
97 fn list_assets() -> Vec<String> {
99 let mut paths = Vec::new();
100 collect_paths(&UI_ASSETS, "", &mut paths);
101 paths
102 }
103
104 fn collect_paths(dir: &Dir, prefix: &str, paths: &mut Vec<String>) {
105 for file in dir.files() {
106 paths.push(format!("{}{}", prefix, file.path().display()));
107 }
108 for subdir in dir.dirs() {
109 let subdir_name = subdir.path().file_name().unwrap().to_str().unwrap();
110 collect_paths(subdir, &format!("{}{}/", prefix, subdir_name), paths);
111 }
112 }
113
114 #[test]
115 fn test_mime_type_inference() {
116 assert_eq!(mime_type_from_path("index.html"), "text/html");
117 assert_eq!(mime_type_from_path("style.css"), "text/css");
118 assert_eq!(mime_type_from_path("app.js"), "application/javascript");
119 assert_eq!(mime_type_from_path("data.json"), "application/json");
120 assert_eq!(
121 mime_type_from_path("unknown.xyz"),
122 "application/octet-stream"
123 );
124 }
125
126 #[test]
127 fn test_list_assets() {
128 let assets = list_assets();
131 println!("Found {} embedded assets", assets.len());
134 for asset in assets.iter().take(10) {
135 println!(" - {}", asset);
136 }
137 }
138
139 #[test]
140 fn test_get_index_html() {
141 let (content, mime_type) =
142 get_asset("index.html").expect("index.html should exist after React build");
143
144 assert_eq!(mime_type, "text/html");
145 assert!(!content.is_empty());
146
147 let html = std::str::from_utf8(content.as_ref()).unwrap();
149 assert!(html.contains("<!DOCTYPE html") || html.contains("<!doctype html"));
150 }
151}