Skip to main content

pfx_mod/
host.rs

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}