hyperlight_js/
resolver.rs1use std::path::{Path, PathBuf};
22
23pub use oxc_resolver::{FileMetadata, FileSystem, ResolveError};
24use phf::Map;
25
26#[derive(Clone, Copy)]
45pub struct FileSystemEmbedded {
46 modules: &'static Map<&'static str, &'static str>,
47}
48
49impl FileSystemEmbedded {
50 pub const fn new(modules: &'static Map<&'static str, &'static str>) -> Self {
54 Self { modules }
55 }
56
57 fn normalize_path<'a>(&self, path: &'a Path) -> Option<std::borrow::Cow<'a, str>> {
59 let s = path.to_str()?;
60
61 if s.contains('\\') || s.starts_with("./") || s.starts_with('/') {
62 Some(std::borrow::Cow::Owned(
63 s.replace('\\', "/")
64 .trim_start_matches("./")
65 .trim_start_matches('/')
66 .to_string(),
67 ))
68 } else {
69 Some(std::borrow::Cow::Borrowed(s))
70 }
71 }
72
73 fn is_directory(&self, normalized: &str) -> bool {
76 if normalized.is_empty() {
77 return !self.modules.is_empty();
78 }
79
80 let prefix = format!("{}/", normalized);
81 self.modules.keys().any(|key| key.starts_with(&prefix))
82 }
83}
84
85impl FileSystem for FileSystemEmbedded {
86 fn new() -> Self {
87 unreachable!("Use embed_modules! macro to create FileSystemEmbedded");
88 }
89
90 fn read(&self, path: &Path) -> std::io::Result<Vec<u8>> {
91 self.read_to_string(path).map(|s| s.into_bytes())
92 }
93
94 fn read_to_string(&self, path: &Path) -> std::io::Result<String> {
95 let normalized = self.normalize_path(path).ok_or_else(|| {
96 std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid UTF-8 in path")
97 })?;
98
99 self.modules
100 .get(&normalized)
101 .map(|&content| content.to_string())
102 .ok_or_else(|| {
103 std::io::Error::new(
104 std::io::ErrorKind::NotFound,
105 format!("Module '{}' not found", normalized),
106 )
107 })
108 }
109
110 fn metadata(&self, path: &Path) -> std::io::Result<FileMetadata> {
111 let normalized = self.normalize_path(path).ok_or_else(|| {
112 std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid UTF-8 in path")
113 })?;
114
115 let is_file = self.modules.contains_key(normalized.as_ref());
116 let is_dir = self.is_directory(normalized.as_ref());
117
118 if !is_file && !is_dir {
119 return Err(std::io::Error::new(
120 std::io::ErrorKind::NotFound,
121 format!("Path '{}' not found", normalized),
122 ));
123 }
124
125 Ok(FileMetadata::new(
126 is_file, is_dir, false, ))
128 }
129
130 fn symlink_metadata(&self, path: &Path) -> std::io::Result<FileMetadata> {
131 self.metadata(path)
132 }
133
134 fn read_link(&self, _path: &Path) -> Result<PathBuf, ResolveError> {
135 Err(std::io::Error::new(
136 std::io::ErrorKind::InvalidInput,
137 "symlinks are not supported in embedded file system",
138 )
139 .into())
140 }
141
142 fn canonicalize(&self, path: &Path) -> std::io::Result<PathBuf> {
143 self.normalize_path(path)
144 .ok_or_else(|| {
145 std::io::Error::new(std::io::ErrorKind::InvalidInput, "Invalid UTF-8 in path")
146 })
147 .map(|v| PathBuf::from(v.into_owned()))
148 }
149}
150
151#[macro_export]
192macro_rules! embed_modules {
193 ($($key:expr => $file:expr),* $(,)?) => {{
195 use $crate::FileSystemEmbedded;
196 use ::phf::{phf_map, Map};
197
198 static EMBEDDED_MODULES: Map<&'static str, &'static str> = phf_map! {
199 $(
200 $key => include_str!($file),
201 )*
202 };
203
204 FileSystemEmbedded::new(&EMBEDDED_MODULES)
205 }};
206
207 ($($key:expr => @inline $content:expr),* $(,)?) => {{
209 use $crate::FileSystemEmbedded;
210 use ::phf::{phf_map, Map};
211
212 static EMBEDDED_MODULES: Map<&'static str, &'static str> = phf_map! {
213 $(
214 $key => $content,
215 )*
216 };
217
218 FileSystemEmbedded::new(&EMBEDDED_MODULES)
219 }};
220}
221
222#[cfg(test)]
223mod tests {
224 use std::path::Path;
225
226 use super::*;
227
228 #[test]
229 fn test_file_read() {
230 let fs = embed_modules! {
231 "test.js" => @inline "console.log('hello');",
232 };
233
234 let content = fs.read_to_string(Path::new("test.js")).unwrap();
235 assert_eq!(content, "console.log('hello');");
236 }
237
238 #[test]
239 fn test_directory_detection() {
240 let fs = embed_modules! {
241 "foo/bar.js" => @inline "content",
242 };
243
244 let metadata = fs.metadata(Path::new("foo")).unwrap();
245 assert!(metadata.is_dir());
246 assert!(!metadata.is_file());
247 }
248
249 #[test]
250 fn test_file_metadata() {
251 let fs = embed_modules! {
252 "test.js" => @inline "content",
253 };
254
255 let metadata = fs.metadata(Path::new("test.js")).unwrap();
256 assert!(metadata.is_file());
257 assert!(!metadata.is_dir());
258 }
259
260 #[test]
261 fn test_prefix_collision() {
262 let fs = embed_modules! {
263 "foo.js" => @inline "content1",
264 "foobar.js" => @inline "content2",
265 };
266
267 assert!(fs.metadata(Path::new("foo")).is_err());
268 assert!(fs.metadata(Path::new("foo.js")).unwrap().is_file());
269 }
270
271 #[test]
272 fn test_not_found() {
273 let fs = embed_modules! {
274 "exists.js" => @inline "content",
275 };
276
277 let result = fs.read_to_string(Path::new("missing.js"));
278 assert!(result.is_err());
279 }
280}