char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Sommass: run cargo fmt05d50bd

main
19.0 KiB562 linesraw
1use std::collections::HashSet;
2use std::rc::Rc;
3
4use super::{ModuleFormat, ModuleMap, fail, fail_syntax, module_name};
5use crate::Error;
6
7fn define_data<'s>(
8    scope: &mut v8::PinScope<'s, '_>,
9    object: v8::Local<'s, v8::Object>,
10    key: &str,
11    value: v8::Local<'s, v8::Value>,
12) -> Option<()> {
13    let key_local = v8::String::new(scope, key)?;
14    if object.create_data_property(scope, key_local.into(), value) != Some(true) {
15        return fail(scope, format!("Failed to define {key}"));
16    }
17    Some(())
18}
19
20fn require_arg_string(
21    scope: &mut v8::PinScope,
22    args: &v8::FunctionCallbackArguments,
23) -> Option<String> {
24    let value = args.get(0);
25    let string = match v8::Local::<v8::String>::try_from(value) {
26        Ok(string) => string,
27        Err(_) => {
28            let message = v8::String::new(scope, "require() argument must be a string")?;
29            scope.throw_exception(v8::Exception::type_error(scope, message));
30            return None;
31        }
32    };
33    Some(string.to_rust_string_lossy(scope))
34}
35
36pub(super) fn strip_preamble(source: &str) -> &str {
37    let source = source.strip_prefix('\u{FEFF}').unwrap_or(source);
38    if source.starts_with("#!") {
39        match source.find('\n') {
40            Some(index) => &source[index..],
41            None => "",
42        }
43    } else {
44        source
45    }
46}
47
48pub(super) fn strip_json(source: &str) -> &str {
49    source.strip_prefix('\u{FEFF}').unwrap_or(source)
50}
51
52fn dirname(name: &str) -> String {
53    std::path::Path::new(name)
54        .parent()
55        .and_then(|parent| parent.to_str())
56        .filter(|parent| !parent.is_empty())
57        .unwrap_or(".")
58        .to_owned()
59}
60
61fn make_require<'s>(
62    scope: &mut v8::PinScope<'s, '_>,
63    referrer: &str,
64) -> Option<v8::Local<'s, v8::Function>> {
65    let referrer_value = v8::String::new(scope, referrer)?;
66    let require = v8::Function::builder(require_call)
67        .data(referrer_value.into())
68        .length(1)
69        .constructor_behavior(v8::ConstructorBehavior::Throw)
70        .build(scope)?;
71    let resolve = v8::Function::builder(require_resolve_call)
72        .data(referrer_value.into())
73        .length(1)
74        .constructor_behavior(v8::ConstructorBehavior::Throw)
75        .build(scope)?;
76    let require_value: v8::Local<v8::Value> = require.into();
77    let require_object = v8::Local::<v8::Object>::try_from(require_value).ok()?;
78    let key = v8::String::new(scope, "resolve")?;
79    if require_object.create_data_property(scope, key.into(), resolve.into()) != Some(true) {
80        return None;
81    }
82    Some(require)
83}
84
85fn make_module<'s>(
86    scope: &mut v8::PinScope<'s, '_>,
87    name: &str,
88) -> Option<v8::Local<'s, v8::Object>> {
89    let require = make_require(scope, name)?;
90    let module = v8::Object::new(scope);
91    let id = v8::String::new(scope, name)?;
92    define_data(scope, module, "id", id.into())?;
93    let filename = v8::String::new(scope, name)?;
94    define_data(scope, module, "filename", filename.into())?;
95    define_data(
96        scope,
97        module,
98        "loaded",
99        v8::Boolean::new(scope, false).into(),
100    )?;
101    define_data(scope, module, "exports", v8::Object::new(scope).into())?;
102    define_data(scope, module, "require", require.into())?;
103    Some(module)
104}
105
106fn cloned_module(map: &Rc<ModuleMap>, name: &str) -> Option<v8::Global<v8::Object>> {
107    map.cjs_modules.borrow().get(name).cloned()
108}
109
110pub(super) fn load_synthetic<'s>(
111    scope: &mut v8::PinScope<'s, '_>,
112    map: &Rc<ModuleMap>,
113    loader: &Rc<dyn super::ModuleLoader>,
114    name: &str,
115) -> Option<v8::Local<'s, v8::Module>> {
116    let mut exports = vec!["default".to_owned()];
117    let named = match loader.cjs_named_exports(name) {
118        Ok(named) => named,
119        Err(Error::Syntax(message)) => return fail_syntax(scope, message),
120        Err(error) => return fail(scope, error.to_string()),
121    };
122    for export in named {
123        if export != "default" && !exports.contains(&export) {
124            exports.push(export);
125        }
126    }
127    let module_name = v8::String::new(scope, name)?;
128    let export_names = exports
129        .iter()
130        .map(|export| v8::String::new(scope, export))
131        .collect::<Option<Vec<_>>>()?;
132    let module =
133        v8::Module::create_synthetic_module(scope, module_name, &export_names, evaluate_synthetic);
134    let hash = module.get_identity_hash().get();
135    let id = map.insert(name.into(), v8::Global::new(scope, module), None, hash);
136    {
137        let mut records = map.records.borrow_mut();
138        records[id.0 as usize].cjs_exports = Some(exports);
139    }
140    Some(v8::Local::new(
141        scope,
142        &map.records.borrow()[id.0 as usize].module,
143    ))
144}
145
146fn evaluate_synthetic<'s>(
147    context: v8::Local<'s, v8::Context>,
148    module: v8::Local<'s, v8::Module>,
149) -> Option<v8::Local<'s, v8::Value>> {
150    // SAFETY: V8 invokes synthetic evaluation with this context entered.
151    v8::callback_scope!(unsafe scope, context);
152    let name = module_name(scope, module);
153    let map = scope.get_slot::<Rc<ModuleMap>>().unwrap().clone();
154    let loader = map.loader.clone()?;
155    if let Err(error) = loader.check_access(&name) {
156        return match error {
157            Error::Syntax(message) => fail_syntax(scope, message),
158            error => fail(scope, error.to_string()),
159        };
160    }
161    let stored = {
162        let records = map.records.borrow();
163        let id = map.id_of(&name)?;
164        records[id.0 as usize].cjs_exports.clone()
165    };
166    let Some(stored) = stored else {
167        return fail(scope, format!("Missing CommonJS exports for {name}"));
168    };
169    let exports = ensure_exports(scope, &map, &loader, &name)?;
170    if let Err(error) = loader.check_access(&name) {
171        return match error {
172            Error::Syntax(message) => fail_syntax(scope, message),
173            error => fail(scope, error.to_string()),
174        };
175    }
176    check_closure(scope, &map, &loader, &name)?;
177    let exports_local = v8::Local::new(scope, &exports);
178    let key = v8::String::new(scope, "default")?;
179    module.set_synthetic_module_export(scope, key, exports_local)?;
180    let is_object = exports_local.is_object() || exports_local.is_function();
181    for export in stored {
182        if export == "default" {
183            continue;
184        }
185        let key = v8::String::new(scope, &export)?;
186        let value = if is_object {
187            let object = v8::Local::<v8::Object>::try_from(exports_local).ok();
188            object.and_then(|object| object.get(scope, key.into()))?
189        } else {
190            v8::undefined(scope).into()
191        };
192        if let Err(error) = loader.check_access(&name) {
193            return match error {
194                Error::Syntax(message) => fail_syntax(scope, message),
195                error => fail(scope, error.to_string()),
196            };
197        }
198        check_closure(scope, &map, &loader, &name)?;
199        module.set_synthetic_module_export(scope, key, value)?;
200    }
201    if let Err(error) = loader.check_access(&name) {
202        return match error {
203            Error::Syntax(message) => fail_syntax(scope, message),
204            error => fail(scope, error.to_string()),
205        };
206    }
207    check_closure(scope, &map, &loader, &name)?;
208    let resolver = v8::PromiseResolver::new(scope)?;
209    resolver.resolve(scope, v8::undefined(scope).into())?;
210    Some(resolver.get_promise(scope).into())
211}
212
213pub(super) fn check_closure(
214    scope: &mut v8::PinScope,
215    map: &Rc<ModuleMap>,
216    loader: &Rc<dyn super::ModuleLoader>,
217    root: &str,
218) -> Option<()> {
219    let mut visited = HashSet::new();
220    let mut pending = vec![root.to_owned()];
221    while let Some(name) = pending.pop() {
222        if !visited.insert(name.clone()) {
223            continue;
224        }
225        // Prior ESM semantics skip rechecking already-evaluated ESM deps;
226        // only CJS/JSON closures (which share mutable exports) are rechecked
227        // so cached ESM wrappers cannot bypass a later pledge via lazy deps.
228        let is_cjs = {
229            map.cjs_modules.borrow().contains_key(name.as_str())
230                || map.json_modules.borrow().contains_key(name.as_str())
231                || map.cjs_deps.borrow().contains_key(name.as_str())
232        };
233        if is_cjs && let Err(error) = loader.check_access(&name) {
234            return match error {
235                Error::Syntax(message) => fail_syntax(scope, message),
236                error => fail(scope, error.to_string()),
237            };
238        }
239        pending.extend(
240            map.cjs_deps
241                .borrow()
242                .get(name.as_str())
243                .cloned()
244                .unwrap_or_default(),
245        );
246    }
247    Some(())
248}
249
250fn record_edge(map: &Rc<ModuleMap>, parent: &str, child: &str) {
251    if parent == child {
252        return;
253    }
254    let mut deps = map.cjs_deps.borrow_mut();
255    let entry = deps.entry(parent.into()).or_default();
256    if !entry.iter().any(|existing| existing == child) {
257        entry.push(child.to_owned());
258    }
259}
260
261fn require_call(
262    scope: &mut v8::PinScope,
263    args: v8::FunctionCallbackArguments,
264    mut result: v8::ReturnValue,
265) {
266    let referrer = args.data().to_rust_string_lossy(scope);
267    let Some(specifier) = require_arg_string(scope, &args) else {
268        return;
269    };
270    if let Some(value) = require_module(scope, &specifier, &referrer) {
271        result.set(value);
272    }
273}
274
275fn require_resolve_call(
276    scope: &mut v8::PinScope,
277    args: v8::FunctionCallbackArguments,
278    mut result: v8::ReturnValue,
279) {
280    let referrer = args.data().to_rust_string_lossy(scope);
281    let Some(specifier) = require_arg_string(scope, &args) else {
282        return;
283    };
284    let map = scope.get_slot::<Rc<ModuleMap>>().unwrap().clone();
285    let Some(loader) = map.loader.clone() else {
286        fail::<()>(scope, "No module loader configured");
287        return;
288    };
289    if specifier.starts_with("ext:") {
290        fail::<()>(scope, "Extension modules are private");
291        return;
292    }
293    if let Err(error) = loader.check_access(&referrer) {
294        match error {
295            Error::Syntax(message) => fail_syntax(scope, message),
296            error => fail::<()>(scope, error.to_string()),
297        };
298        return;
299    }
300    let name = match loader.require_resolve(&specifier, &referrer) {
301        Ok(name) => name,
302        Err(Error::Syntax(message)) => {
303            fail_syntax::<()>(scope, message);
304            return;
305        }
306        Err(error) => {
307            fail::<()>(scope, error.to_string());
308            return;
309        }
310    };
311    if name.starts_with("ext:") {
312        fail::<()>(scope, "Extension modules are private");
313        return;
314    }
315    if let Err(error) = loader.check_access(&name) {
316        match error {
317            Error::Syntax(message) => fail_syntax::<()>(scope, message),
318            error => fail::<()>(scope, error.to_string()),
319        };
320        return;
321    }
322    if let Some(value) = v8::String::new(scope, &name) {
323        result.set(value.into());
324    }
325}
326
327fn require_module<'s>(
328    scope: &mut v8::PinScope<'s, '_>,
329    specifier: &str,
330    referrer: &str,
331) -> Option<v8::Local<'s, v8::Value>> {
332    let map = scope.get_slot::<Rc<ModuleMap>>().unwrap().clone();
333    let loader = map.loader.clone()?;
334    if specifier.starts_with("ext:") {
335        return fail(scope, "Extension modules are private");
336    }
337    if let Err(error) = loader.check_access(referrer) {
338        return match error {
339            Error::Syntax(message) => fail_syntax(scope, message),
340            error => fail(scope, error.to_string()),
341        };
342    }
343    let name = match loader.require_resolve(specifier, referrer) {
344        Ok(name) => name,
345        Err(Error::Syntax(message)) => return fail_syntax(scope, message),
346        Err(error) => return fail(scope, error.to_string()),
347    };
348    if name.starts_with("ext:") {
349        return fail(scope, "Extension modules are private");
350    }
351    if let Err(error) = loader.check_access(&name) {
352        return match error {
353            Error::Syntax(message) => fail_syntax(scope, message),
354            error => fail(scope, error.to_string()),
355        };
356    }
357    let format = match loader.module_format(&name) {
358        Ok(format) => format,
359        Err(Error::Syntax(message)) => return fail_syntax(scope, message),
360        Err(error) => return fail(scope, error.to_string()),
361    };
362    match format {
363        ModuleFormat::Esm => fail(
364            scope,
365            format!("require() of ESM {name} is not supported, use import() instead"),
366        ),
367        ModuleFormat::Json => {
368            let value = require_json(scope, &map, &loader, &name)?;
369            if let Err(error) = loader.check_access(referrer) {
370                return match error {
371                    Error::Syntax(message) => fail_syntax(scope, message),
372                    error => fail(scope, error.to_string()),
373                };
374            }
375            if let Err(error) = loader.check_access(&name) {
376                return match error {
377                    Error::Syntax(message) => fail_syntax(scope, message),
378                    error => fail(scope, error.to_string()),
379                };
380            }
381            check_closure(scope, &map, &loader, &name)?;
382            record_edge(&map, referrer, &name);
383            Some(value)
384        }
385        ModuleFormat::CommonJs => {
386            let value = ensure_exports(scope, &map, &loader, &name)?;
387            if let Err(error) = loader.check_access(referrer) {
388                return match error {
389                    Error::Syntax(message) => fail_syntax(scope, message),
390                    error => fail(scope, error.to_string()),
391                };
392            }
393            if let Err(error) = loader.check_access(&name) {
394                return match error {
395                    Error::Syntax(message) => fail_syntax(scope, message),
396                    error => fail(scope, error.to_string()),
397                };
398            }
399            check_closure(scope, &map, &loader, &name)?;
400            record_edge(&map, referrer, &name);
401            Some(v8::Local::new(scope, &value))
402        }
403    }
404}
405
406fn require_json<'s>(
407    scope: &mut v8::PinScope<'s, '_>,
408    map: &Rc<ModuleMap>,
409    loader: &Rc<dyn super::ModuleLoader>,
410    name: &str,
411) -> Option<v8::Local<'s, v8::Value>> {
412    let cached = { map.json_modules.borrow().get(name).cloned() };
413    if let Some(cached) = cached {
414        return Some(v8::Local::new(scope, &cached));
415    }
416    let source = match loader.load(name) {
417        Ok(source) => source,
418        Err(Error::Syntax(message)) => return fail_syntax(scope, message),
419        Err(error) => return fail(scope, error.to_string()),
420    };
421    let stripped = strip_json(&source);
422    let text = v8::String::new(scope, stripped)?;
423    let value = v8::json::parse(scope, text)?;
424    {
425        map.json_modules
426            .borrow_mut()
427            .insert(name.into(), v8::Global::new(scope, value));
428    }
429    Some(value)
430}
431
432fn ensure_exports<'s>(
433    scope: &mut v8::PinScope<'s, '_>,
434    map: &Rc<ModuleMap>,
435    loader: &Rc<dyn super::ModuleLoader>,
436    name: &str,
437) -> Option<v8::Global<v8::Value>> {
438    if let Some(module) = cloned_module(map, name) {
439        check_closure(scope, map, loader, name)?;
440        let module = v8::Local::new(scope, &module);
441        let key = v8::String::new(scope, "exports")?;
442        let current = module.get(scope, key.into())?;
443        return Some(v8::Global::new(scope, current));
444    }
445    execute(scope, map, loader, name)?;
446    check_closure(scope, map, loader, name)?;
447    let module = cloned_module(map, name)?;
448    let module = v8::Local::new(scope, &module);
449    let key = v8::String::new(scope, "exports")?;
450    let current = module.get(scope, key.into())?;
451    Some(v8::Global::new(scope, current))
452}
453
454fn execute(
455    scope: &mut v8::PinScope,
456    map: &Rc<ModuleMap>,
457    loader: &Rc<dyn super::ModuleLoader>,
458    name: &str,
459) -> Option<()> {
460    let (module_global, exports_global, require_global) = {
461        let module = make_module(scope, name)?;
462        let exports_key = v8::String::new(scope, "exports")?;
463        let exports = module.get(scope, exports_key.into())?;
464        let require_key = v8::String::new(scope, "require")?;
465        let require = module.get(scope, require_key.into())?;
466        (
467            v8::Global::new(scope, module),
468            v8::Global::new(scope, exports),
469            v8::Global::new(scope, require),
470        )
471    };
472    {
473        map.cjs_modules
474            .borrow_mut()
475            .insert(name.into(), module_global.clone());
476        map.cjs_deps.borrow_mut().insert(name.into(), Vec::new());
477    }
478    let remove = |map: &Rc<ModuleMap>| {
479        map.cjs_modules.borrow_mut().remove(name);
480        map.cjs_deps.borrow_mut().remove(name);
481    };
482    let source = match loader.load(name) {
483        Ok(source) => source,
484        Err(Error::Syntax(message)) => {
485            remove(map);
486            return fail_syntax(scope, message);
487        }
488        Err(error) => {
489            remove(map);
490            return fail(scope, error.to_string());
491        }
492    };
493    if let Err(error) = loader.check_access(name) {
494        remove(map);
495        return match error {
496            Error::Syntax(message) => fail_syntax(scope, message),
497            error => fail(scope, error.to_string()),
498        };
499    }
500    let stripped = strip_preamble(&source).to_owned();
501    let params = ["exports", "require", "module", "__filename", "__dirname"]
502        .iter()
503        .map(|name| v8::String::new(scope, name))
504        .collect::<Option<Vec<_>>>()?;
505    let mut source_ref = crate::source(scope, name, &stripped, false, None);
506    let function = match v8::script_compiler::compile_function(
507        scope,
508        &mut source_ref,
509        &params,
510        &[],
511        v8::script_compiler::CompileOptions::NoCompileOptions,
512        v8::script_compiler::NoCacheReason::NoReason,
513    ) {
514        Some(function) => function,
515        None => {
516            remove(map);
517            return None;
518        }
519    };
520    let (exports, require, module, filename, dirname) = {
521        let exports = v8::Local::new(scope, &exports_global);
522        let require = v8::Local::new(scope, &require_global);
523        let module = v8::Local::new(scope, &module_global);
524        let filename = v8::String::new(scope, name)?;
525        let dirname = v8::String::new(scope, &dirname(name))?;
526        (exports, require, module, filename, dirname)
527    };
528    let args = [
529        exports,
530        require,
531        module.into(),
532        filename.into(),
533        dirname.into(),
534    ];
535    let receiver: v8::Local<v8::Value> = exports;
536    if function.call(scope, receiver, &args).is_none() {
537        remove(map);
538        return None;
539    }
540    {
541        let module = v8::Local::new(scope, &module_global);
542        let key = v8::String::new(scope, "loaded")?;
543        match module.create_data_property(scope, key.into(), v8::Boolean::new(scope, true).into()) {
544            Some(true) => {}
545            Some(false) => {
546                remove(map);
547                return fail(scope, format!("Failed to mark {name} loaded"));
548            }
549            None => {
550                remove(map);
551                return None;
552            }
553        }
554    }
555    if let Err(error) = loader.check_access(name) {
556        return match error {
557            Error::Syntax(message) => fail_syntax(scope, message),
558            error => fail(scope, error.to_string()),
559        };
560    }
561    Some(())
562}