-
Notifications
You must be signed in to change notification settings - Fork 3
/
Copy pathmodule_loader.rs
203 lines (179 loc) · 7.14 KB
/
module_loader.rs
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
use std::path::{Path, PathBuf};
use deno_core::anyhow::anyhow;
use deno_core::error::type_error;
use deno_core::futures::FutureExt;
use deno_core::{
resolve_import, ModuleLoader, ModuleSource, ModuleSourceFuture, ModuleSpecifier, ModuleType,
ResolutionKind,
};
use tokio::fs::File;
use tokio::io::AsyncReadExt;
pub type AnyError = deno_core::anyhow::Error;
use deno_core::anyhow::Result;
/// Our custom module loader.
pub struct ZinniaModuleLoader {
module_root: Option<PathBuf>,
}
impl ZinniaModuleLoader {
pub fn build(module_root: Option<PathBuf>) -> Result<Self> {
let module_root = match module_root {
None => None,
// We must canonicalize the module root path too. It's best to do it once at startup.
Some(r) => Some(r.canonicalize()?),
};
Ok(Self { module_root })
}
}
pub fn get_module_root(main_js_module: &ModuleSpecifier) -> Result<PathBuf> {
Ok(main_js_module
.to_file_path()
.map_err(|_| anyhow!("Invalid main module specifier: not a local path."))?
.parent()
.ok_or_else(|| anyhow!("Invalid main module specifier: it has no parent directory!"))?
// Resolve any symlinks inside the path to prevent modules from escaping our sandbox
.canonicalize()?)
}
impl ModuleLoader for ZinniaModuleLoader {
fn resolve(
&self,
specifier: &str,
referrer: &str,
_kind: ResolutionKind,
) -> Result<ModuleSpecifier, AnyError> {
if specifier == "zinnia:test" {
return Ok(ModuleSpecifier::parse("ext:zinnia_runtime/test.js").unwrap());
} else if specifier == "zinnia:assert" {
return Ok(
ModuleSpecifier::parse("ext:zinnia_runtime/vendored/asserts.bundle.js").unwrap(),
);
}
let resolved = resolve_import(specifier, referrer)?;
Ok(resolved)
}
fn load(
&self,
module_specifier: &ModuleSpecifier,
maybe_referrer: Option<&ModuleSpecifier>,
_is_dyn_import: bool,
) -> std::pin::Pin<Box<ModuleSourceFuture>> {
let module_specifier = module_specifier.clone();
let module_root = self.module_root.clone();
let maybe_referrer = maybe_referrer.cloned();
async move {
let spec_str = module_specifier.as_str();
let code = {
let is_module_local = match module_specifier.to_file_path() {
Err(()) => false,
Ok(module_path) => {
match &module_root {
None => true,
Some(root) => module_path
// Resolve any symlinks inside the path to prevent modules from escaping our sandbox
.canonicalize()
// Check that the module path is inside the module root directory
.map(|p| p.starts_with(root))
.unwrap_or(false),
}
},
};
if is_module_local {
read_file_to_string(module_specifier.to_file_path().unwrap()).await?
} else if spec_str == "https://deno.land/[email protected]/testing/asserts.ts" || spec_str == "https://deno.land/[email protected]/testing/asserts.ts" {
return Err(anyhow!(
"Zinnia bundles Deno asserts as 'zinnia:assert`. Please update your imports accordingly.\nModule URL: {spec_str}\nImported from: {}",
maybe_referrer.map(|u| u.to_string()).unwrap_or("(none)".into())
));
} else {
let mut msg = if module_specifier.scheme() == "file" && module_root.is_some() {
format!("Cannot import files outside of module root directory {}. ", module_root.unwrap().display())
} else {
"Zinnia supports importing from relative paths only. ".to_string()
};
msg.push_str(module_specifier.as_str());
if let Some(referrer) = &maybe_referrer {
msg.push_str(" imported from ");
msg.push_str(referrer.as_str());
}
return Err(anyhow!(msg));
}
};
let module = ModuleSource::new(ModuleType::JavaScript, code.into(), &module_specifier);
Ok(module)
}.boxed_local()
}
}
async fn read_file_to_string(path: impl AsRef<Path>) -> Result<String, AnyError> {
let mut f = File::open(&path).await.map_err(|err| {
type_error(format!(
"Module not found: {}. {}",
err,
path.as_ref().display()
))
})?;
// read the whole file
let mut buffer = Vec::new();
f.read_to_end(&mut buffer).await?;
Ok(String::from_utf8_lossy(&buffer).to_string())
}
#[cfg(test)]
mod tests {
use super::*;
use deno_core::anyhow::Context;
use pretty_assertions::assert_eq;
#[tokio::test]
async fn allows_import_of_files_inside_sandbox() {
let mut imported_file = get_js_dir();
imported_file.push("99_main.js");
let loader = ZinniaModuleLoader::build(Some(get_js_dir())).unwrap();
let result = loader
.load(
&ModuleSpecifier::from_file_path(&imported_file).unwrap(),
None,
false,
)
.await
.with_context(|| format!("cannot import {}", imported_file.display()))
.unwrap();
assert_eq!(result.module_type, ModuleType::JavaScript);
}
#[tokio::test]
async fn rejects_import_of_files_outside_sandbox() {
// project_root is `runtime/tests/js`
let mut project_root = get_js_dir().parent().unwrap().to_path_buf();
project_root.push("tests");
project_root.push("js");
// we are importing file `runtime/js/99_main.js` - it's outside for project_root
let mut imported_file = get_js_dir();
imported_file.push("99_main.js");
let loader = ZinniaModuleLoader::build(Some(project_root)).unwrap();
let result = loader
.load(
&ModuleSpecifier::from_file_path(&imported_file).unwrap(),
None,
false,
)
.await;
match result {
Ok(_) => {
assert!(
result.is_err(),
"Expected import from '{}' to fail, it succeeded instead.",
imported_file.display()
);
}
Err(err) => {
let msg = format!("{err}");
assert!(
msg.contains("Cannot import files outside of module root directory"),
"Expected import to fail with the sandboxing error, it failed with a different error instead:\n{}",
msg,
);
}
}
}
fn get_js_dir() -> PathBuf {
let mut base_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
base_dir.push("js");
base_dir
}
}