1use std::collections::BTreeSet;
2use std::fs;
3use std::io::Read;
4use std::num::NonZeroUsize;
5use std::path::{Path, PathBuf};
6use std::sync::Arc;
7
8use wasmparser::WasmFeatures;
9use wasmtime::component::{Component, InstancePre, Linker};
10use wasmtime::{Config, Engine, OptLevel, Strategy, WasmBacktraceDetails};
11
12use crate::ctx::ModCtx;
13use crate::depth::{self, Site};
14use crate::error::{HostError, LoadError, Refusal};
15use crate::fingerprint::Fingerprint;
16use crate::limits::Limits;
17use crate::manifest::{Api, MANIFEST, Manifest};
18
19const BACKTRACE_FRAMES: usize = 8;
20
21pub const LOG_INTERFACE: &str = "pfx:mods/log@0.1.0";
22pub const LOG_WIT: &str = include_str!("../wit/log.wit");
23
24pub fn disabled_features() -> WasmFeatures {
25 WasmFeatures::THREADS
26 | WasmFeatures::SHARED_EVERYTHING_THREADS
27 | WasmFeatures::RELAXED_SIMD
28 | WasmFeatures::MEMORY64
29 | WasmFeatures::MEMORY_CONTROL
30 | WasmFeatures::CUSTOM_PAGE_SIZES
31 | WasmFeatures::GC
32 | WasmFeatures::FUNCTION_REFERENCES
33 | WasmFeatures::STACK_SWITCHING
34 | WasmFeatures::EXCEPTIONS
35 | WasmFeatures::LEGACY_EXCEPTIONS
36 | WasmFeatures::CUSTOM_DESCRIPTORS
37 | WasmFeatures::CM_ASYNC
38 | WasmFeatures::CM_ASYNC_STACKFUL
39 | WasmFeatures::CM_MORE_ASYNC_BUILTINS
40 | WasmFeatures::CM_THREADING
41 | WasmFeatures::CM_ERROR_CONTEXT
42 | WasmFeatures::CM_GC
43 | WasmFeatures::CM64
44}
45
46pub fn config(limits: &Limits) -> Config {
47 let mut config = Config::new();
48 config
49 .strategy(Strategy::Cranelift)
50 .cranelift_opt_level(OptLevel::Speed)
51 .cranelift_nan_canonicalization(true)
52 .consume_fuel(true)
53 .max_wasm_stack(limits.stack_bytes)
54 .wasm_backtrace_max_frames(NonZeroUsize::new(BACKTRACE_FRAMES))
55 .wasm_backtrace_details(WasmBacktraceDetails::Disable)
56 .debug_info(false)
57 .wasm_component_model(true)
58 .relaxed_simd_deterministic(true)
59 .wasm_features(disabled_features(), false);
60 config
61}
62
63pub struct Host<T: 'static> {
64 engine: Engine,
65 linker: Linker<ModCtx<T>>,
66 api: Api,
67 limits: Limits,
68}
69
70impl<T: 'static> Host<T> {
71 pub fn new(api: Api, limits: Limits) -> Result<Self, HostError> {
72 if semver::Version::parse(&api.version).is_err() {
73 return Err(HostError(format!(
74 "the game's API version `{}` is not a semantic version such as 1.0.0",
75 api.version
76 )));
77 }
78 let engine = Engine::new(&config(&limits)).map_err(|e| HostError(format!("{e:#}")))?;
79 let mut linker = Linker::new(&engine);
80 if limits.log.is_some() {
81 linker
82 .instance(LOG_INTERFACE)
83 .and_then(|mut log| {
84 log.func_wrap(
85 "line",
86 |mut cx: wasmtime::StoreContextMut<'_, ModCtx<T>>, (text,): (String,)| {
87 cx.data_mut().log.push(&text);
88 Ok(())
89 },
90 )
91 })
92 .map_err(|e| HostError(format!("{e:#}")))?;
93 }
94 Ok(Self {
95 engine,
96 linker,
97 api,
98 limits,
99 })
100 }
101
102 pub fn engine(&self) -> &Engine {
103 &self.engine
104 }
105
106 pub fn linker(&mut self) -> &mut Linker<ModCtx<T>> {
107 &mut self.linker
108 }
109
110 pub fn api(&self) -> &Api {
111 &self.api
112 }
113
114 pub fn limits(&self) -> &Limits {
115 &self.limits
116 }
117
118 fn allowed(&self) -> BTreeSet<&str> {
119 let mut allowed: BTreeSet<&str> = self.api.imports.iter().map(String::as_str).collect();
120 if self.limits.log.is_some() {
121 allowed.insert(LOG_INTERFACE);
122 }
123 allowed
124 }
125
126 pub fn load(&self, manifest: &[u8], component: &[u8]) -> Result<Package<T>, Refusal> {
127 capped("manifest", manifest.len(), self.limits.manifest_bytes)?;
128 let parsed = parse_manifest(manifest)?;
129 self.api.accepts(&parsed.api)?;
130 capped("component", component.len(), self.limits.component_bytes)?;
131 self.prepare(manifest, parsed, component)
132 }
133
134 pub fn load_module(&self, id: &str, wasm: &[u8]) -> Result<Package<T>, Refusal> {
135 if !crate::manifest::valid_id(id) {
136 return Err(Refusal::Id {
137 found: id.to_string(),
138 });
139 }
140 capped("component", wasm.len(), self.limits.component_bytes)?;
141 let text = format!(
142 "id = \"{id}\"\nversion = \"0.0.0\"\napi = \"{}\"\nentry = \"{id}.wasm\"\n",
143 self.api.version
144 );
145 let parsed = Manifest::parse(&text)?;
146 if wasmparser::Parser::is_core_wasm(wasm) {
147 let component = componentize(wasm)?;
148 capped("component", component.len(), self.limits.component_bytes)?;
149 let mut package = self.prepare(text.as_bytes(), parsed, &component)?;
150 package.fingerprint = Fingerprint::of_mod(text.as_bytes(), wasm);
151 return Ok(package);
152 }
153 self.prepare(text.as_bytes(), parsed, wasm)
154 }
155
156 fn prepare(
157 &self,
158 manifest: &[u8],
159 parsed: Manifest,
160 component: &[u8],
161 ) -> Result<Package<T>, Refusal> {
162 scan(component)?;
163 let mut features = WasmFeatures::default();
164 features.remove(disabled_features());
165 wasmparser::Validator::new_with_features(features)
166 .validate_all(component)
167 .map_err(|e| Refusal::Invalid(e.to_string()))?;
168 let instrumented =
169 depth::instrument(component, self.limits.depth).map_err(Refusal::Invalid)?;
170 let compiled = Component::new(&self.engine, &instrumented.bytes)
171 .map_err(|e| Refusal::Invalid(format!("{e:#}")))?;
172 let allowed = self.allowed();
173 let ty = compiled.component_type();
174 for (name, _) in ty.imports(&self.engine) {
175 if !allowed.contains(name) {
176 return Err(Refusal::Import {
177 name: name.to_string(),
178 });
179 }
180 }
181 let pre = self
182 .linker
183 .instantiate_pre(&compiled)
184 .map_err(|e| Refusal::Types(format!("{e:#}")))?;
185 Ok(Package {
186 fingerprint: Fingerprint::of_mod(manifest, component),
187 manifest: parsed,
188 pre,
189 sites: instrumented.sites.into(),
190 })
191 }
192
193 pub fn load_folder(&self, folder: &Path) -> Result<Package<T>, LoadError> {
194 let refuse = |id: Option<String>, refusal| LoadError {
195 folder: folder.to_path_buf(),
196 id,
197 refusal: Box::new(refusal),
198 };
199 let manifest = read_capped(
200 &folder.join(MANIFEST),
201 "manifest",
202 self.limits.manifest_bytes,
203 )
204 .map_err(|r| refuse(None, r))?;
205 let parsed = parse_manifest(&manifest).map_err(|r| refuse(None, r))?;
206 let id = Some(parsed.id.clone());
207 let name = folder
208 .file_name()
209 .map(|n| n.to_string_lossy().into_owned())
210 .unwrap_or_default();
211 if name != parsed.id {
212 return Err(refuse(
213 id,
214 Refusal::Folder {
215 id: parsed.id,
216 folder: name,
217 },
218 ));
219 }
220 let component = read_capped(
221 &folder.join(&parsed.entry),
222 "component",
223 self.limits.component_bytes,
224 )
225 .map_err(|r| refuse(id.clone(), r))?;
226 self.load(&manifest, &component).map_err(|r| refuse(id, r))
227 }
228
229 pub fn load_dir(&self, mods: &Path) -> Loaded<T> {
230 let mut loaded = Loaded {
231 packages: Vec::new(),
232 refused: Vec::new(),
233 };
234 let entries = match fs::read_dir(mods) {
235 Ok(entries) => entries,
236 Err(e) if e.kind() == std::io::ErrorKind::NotFound => return loaded,
237 Err(e) => {
238 loaded.refused.push(LoadError {
239 folder: mods.to_path_buf(),
240 id: None,
241 refusal: Box::new(Refusal::Read(e.to_string())),
242 });
243 return loaded;
244 }
245 };
246 let mut folders: Vec<PathBuf> = entries
247 .filter_map(|e| e.ok())
248 .filter(|e| e.file_type().is_ok_and(|t| t.is_dir()))
249 .map(|e| e.path())
250 .collect();
251 folders.sort();
252 for folder in folders {
253 match self.load_folder(&folder) {
254 Ok(package) => loaded.packages.push(package),
255 Err(e) => loaded.refused.push(e),
256 }
257 }
258 loaded
259 }
260}
261
262pub struct Package<T: 'static> {
263 pub manifest: Manifest,
264 pub fingerprint: Fingerprint,
265 pub(crate) pre: InstancePre<ModCtx<T>>,
266 pub(crate) sites: Arc<[Site]>,
267}
268
269impl<T: 'static> Clone for Package<T> {
270 fn clone(&self) -> Self {
271 Self {
272 manifest: self.manifest.clone(),
273 fingerprint: self.fingerprint,
274 pre: self.pre.clone(),
275 sites: self.sites.clone(),
276 }
277 }
278}
279
280pub struct Loaded<T: 'static> {
281 pub packages: Vec<Package<T>>,
282 pub refused: Vec<LoadError>,
283}
284
285fn capped(what: &'static str, len: usize, cap: usize) -> Result<(), Refusal> {
286 if len > cap {
287 return Err(Refusal::Size {
288 what,
289 bytes: len as u64,
290 cap,
291 });
292 }
293 Ok(())
294}
295
296fn parse_manifest(bytes: &[u8]) -> Result<Manifest, Refusal> {
297 let text = std::str::from_utf8(bytes)
298 .map_err(|_| Refusal::Manifest("it is not UTF-8 text".to_string()))?;
299 Manifest::parse(text)
300}
301
302pub(crate) fn read_capped(path: &Path, what: &'static str, cap: usize) -> Result<Vec<u8>, Refusal> {
303 let file =
304 fs::File::open(path).map_err(|e| Refusal::Read(format!("{}: {e}", path.display())))?;
305 let mut bytes = Vec::new();
306 file.take(cap as u64 + 1)
307 .read_to_end(&mut bytes)
308 .map_err(|e| Refusal::Read(format!("{}: {e}", path.display())))?;
309 if bytes.len() > cap {
310 let bytes = fs::metadata(path).map_or(bytes.len() as u64, |m| m.len());
311 return Err(Refusal::Size { what, bytes, cap });
312 }
313 Ok(bytes)
314}
315
316fn scan(component: &[u8]) -> Result<(), Refusal> {
317 if !wasmparser::Parser::is_component(component) {
318 return Err(Refusal::NotComponent);
319 }
320 for payload in wasmparser::Parser::new(0).parse_all(component) {
321 match payload.map_err(|e| Refusal::Invalid(e.to_string()))? {
322 wasmparser::Payload::ComponentStartSection { .. } => return Err(Refusal::Start),
323 _ => continue,
324 }
325 }
326 Ok(())
327}
328
329fn componentize(module: &[u8]) -> Result<Vec<u8>, Refusal> {
330 let typed = wasmparser::Parser::new(0)
331 .parse_all(module)
332 .filter_map(Result::ok)
333 .any(|payload| {
334 matches!(payload, wasmparser::Payload::CustomSection(section) if section.name().starts_with("component-type"))
335 });
336 if !typed {
337 return Err(Refusal::Module(
338 "it carries no component type, so its world is unknown".to_string(),
339 ));
340 }
341 let mut encoder = wit_component::ComponentEncoder::default();
342 encoder
343 .module(module)
344 .map_err(|e| Refusal::Module(format!("{e:#}")))?;
345 encoder
346 .validate(true)
347 .encode()
348 .map_err(|e| Refusal::Module(format!("{e:#}")))
349}