Skip to main content

hyperlight_js/
resolver.rs

1/*
2Copyright 2026  The Hyperlight Authors.
3
4Licensed under the Apache License, Version 2.0 (the "License");
5you may not use this file except in compliance with the License.
6You may obtain a copy of the License at
7
8    http://www.apache.org/licenses/LICENSE-2.0
9
10Unless required by applicable law or agreed to in writing, software
11distributed under the License is distributed on an "AS IS" BASIS,
12WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13See the License for the specific language governing permissions and
14limitations under the License.
15*/
16//! Module resolution and loading implementations.
17//!
18//! This module provides the core abstractions and implementations for loading
19//! JavaScript modules into the sandbox environment.
20
21use std::path::{Path, PathBuf};
22
23pub use oxc_resolver::{FileMetadata, FileSystem, ResolveError};
24use phf::Map;
25
26/// File system implementation that uses embedded modules compiled into the binary.
27///
28/// This implementation stores all module contents in a compile-time perfect hash map,
29/// eliminating any runtime file system access. This is the basic secure option for
30/// module loading as it provides a completely closed set of available modules without
31/// filesystem access.
32///
33/// # Example
34///
35/// ```no_run
36/// use hyperlight_js::embed_modules;
37///
38/// let fs = embed_modules! {
39///     "math.js" => "../tests/fixtures/math.js",
40///     "strings.js" => "../tests/fixtures/strings.js",
41/// };
42///
43/// ```
44#[derive(Clone, Copy)]
45pub struct FileSystemEmbedded {
46    modules: &'static Map<&'static str, &'static str>,
47}
48
49impl FileSystemEmbedded {
50    /// Create a new embedded file system with the given module map.
51    ///
52    /// See the `embed_modules!` macro for an easier way to create
53    pub const fn new(modules: &'static Map<&'static str, &'static str>) -> Self {
54        Self { modules }
55    }
56
57    /// Normalize a path for consistent lookups.
58    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    /// Check if a normalized path represents a directory by seeing if any
74    /// embedded modules have this path as a prefix.
75    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, /* is_symlink */
127        ))
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 to create an embedded file system with compile-time included modules.
152///
153/// This macro simplifies the creation of an embedded file system by automatically
154/// generating the `phf_map` and wrapping it in a `FileSystemEmbedded`.
155///
156/// # Example
157///
158/// ```text
159/// embed_modules! {
160///     "module_path" => "file_path",
161///     "another_module" => "another_file",
162///     ...
163/// }
164/// ```
165///
166/// ```no_run
167/// use hyperlight_js::SandboxBuilder;
168/// use hyperlight_js::embed_modules;
169///
170/// let fs = embed_modules! {
171///     "math.js" => "../tests/fixtures/math.js",
172///     "strings.js" => "../tests/fixtures/strings.js",
173/// };
174///
175/// let proto_js_sandbox = SandboxBuilder::new().build().unwrap();
176/// let sandbox = proto_js_sandbox
177///     .set_module_loader(fs)
178///     .unwrap()
179///     .load_runtime()
180///     .unwrap();
181/// ```
182///
183/// // With inline content:
184/// let fs = embed_modules! {
185///     "test.js" => @inline "console.log('test');",
186/// };
187///
188/// # Notes
189///
190/// * File paths are relative to the current file
191#[macro_export]
192macro_rules! embed_modules {
193    // Match file: prefix
194    ($($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    // Match @inline prefix
208    ($($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}