char/avril

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

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

Charlotte Somsnapshot: capture and restore extension layouts in one artifactd71572b

main
18.3 KiB547 linesraw
1use std::cell::RefCell;
2use std::collections::HashMap;
3use std::rc::Rc;
4
5use crate::{Error, Extension};
6use v8::MapFnTo;
7
8type Exports = Vec<(String, v8::Global<v8::Value>)>;
9
10struct Modules {
11    sources: HashMap<&'static str, &'static str>,
12    builtins: HashMap<String, Exports>,
13    modules: RefCell<HashMap<String, v8::Global<v8::Module>>>,
14    // Identity hashes collide, so each bucket holds candidates confirmed by Local == Global.
15    by_identity: RefCell<HashMap<i32, Vec<String>>>,
16    origin: v8::Global<v8::PrimitiveArray>,
17}
18
19fn valid_name(name: &str) -> bool {
20    name.starts_with("ext:")
21        && name != "ext:core"
22        && name.len() > 4
23        && name[4..]
24            .bytes()
25            .all(|b| b.is_ascii_alphanumeric() || b == b'_' || b == b'-')
26}
27
28#[derive(Clone, Copy, PartialEq)]
29enum Mark {
30    Unvisited,
31    Visiting,
32    Done,
33}
34
35fn append_extension_in_dependency_order(
36    index: usize,
37    extensions: &[Extension],
38    indices: &HashMap<&'static str, usize>,
39    marks: &mut [Mark],
40    stack: &mut Vec<usize>,
41    order: &mut Vec<usize>,
42) -> Result<(), Error> {
43    match marks[index] {
44        Mark::Done => return Ok(()),
45        Mark::Visiting => {
46            let start = stack.iter().position(|item| *item == index).unwrap();
47            let mut cycle = stack[start..]
48                .iter()
49                .map(|item| extensions[*item].name)
50                .collect::<Vec<_>>();
51            cycle.push(extensions[index].name);
52            return Err(Error::Module(format!(
53                "Extension dependency cycle: {}",
54                cycle.join(" -> ")
55            )));
56        }
57        Mark::Unvisited => {}
58    }
59    marks[index] = Mark::Visiting;
60    stack.push(index);
61    for dependency in extensions[index].dependencies {
62        if !valid_name(dependency) {
63            return Err(Error::Module(format!(
64                "Invalid extension dependency: {dependency}"
65            )));
66        }
67        let Some(dependency) = indices.get(dependency) else {
68            return Err(Error::Module(format!(
69                "Extension {} depends on missing extension {dependency}",
70                extensions[index].name
71            )));
72        };
73        append_extension_in_dependency_order(
74            *dependency,
75            extensions,
76            indices,
77            marks,
78            stack,
79            order,
80        )?;
81    }
82    stack.pop();
83    marks[index] = Mark::Done;
84    order.push(index);
85    Ok(())
86}
87
88pub(crate) fn validate(extensions: &[Extension]) -> Result<Vec<usize>, Error> {
89    let mut names = std::collections::HashSet::new();
90    let mut sources = std::collections::HashSet::new();
91    for extension in extensions {
92        let name = extension.name;
93        if !valid_name(name) {
94            return Err(Error::Module(format!("Invalid extension name: {name}")));
95        }
96        if !names.insert(name) {
97            return Err(Error::Module(format!("Duplicate extension: {name}")));
98        }
99        for &(specifier, _) in extension.modules {
100            if (specifier != name && !specifier.starts_with(&format!("{name}/")))
101                || normalize(specifier).as_deref() != Some(specifier)
102            {
103                return Err(Error::Module(format!(
104                    "Invalid extension module: {specifier}"
105                )));
106            }
107            if !sources.insert(specifier) {
108                return Err(Error::Module(format!(
109                    "Duplicate extension module: {specifier}"
110                )));
111            }
112        }
113        if let Some(entry) = extension.entry_point
114            && !extension.modules.iter().any(|(name, _)| *name == entry)
115        {
116            return Err(Error::Module(format!(
117                "Missing extension entry point: {entry}"
118            )));
119        }
120    }
121    let indices = extensions
122        .iter()
123        .enumerate()
124        .map(|(index, extension)| (extension.name, index))
125        .collect::<HashMap<_, _>>();
126    let mut marks = vec![Mark::Unvisited; extensions.len()];
127    let mut stack = Vec::new();
128    let mut order = Vec::with_capacity(extensions.len());
129    for index in 0..extensions.len() {
130        append_extension_in_dependency_order(
131            index, extensions, &indices, &mut marks, &mut stack, &mut order,
132        )?;
133    }
134    Ok(order)
135}
136
137pub(crate) fn clear(isolate: &mut v8::Isolate) {
138    isolate.remove_slot::<Rc<Modules>>();
139}
140
141pub(crate) const SOURCES: &[(&str, &str)] = &[
142    ("ext:core/promises", include_str!("promises.js")),
143    ("ext:core/bootstrap", include_str!("bootstrap.js")),
144    (
145        "ext:core/shared_array_buffer",
146        include_str!("shared_array_buffer.js"),
147    ),
148    ("ext:core/stack_trace.js", include_str!("stack_trace.js")),
149];
150
151type NativeModule = (&'static str, Vec<(&'static str, v8::FunctionCallback)>);
152
153pub(crate) fn native_modules() -> Vec<NativeModule> {
154    vec![
155        (
156            "ext:core/tasks",
157            vec![("enqueueTask", crate::tasks::enqueue.map_fn_to())],
158        ),
159        (
160            "ext:core/microtasks",
161            vec![("enqueueMicrotask", crate::microtasks::enqueue.map_fn_to())],
162        ),
163        (
164            "ext:core/promise_hooks",
165            vec![
166                (
167                    "markPromiseHandled",
168                    crate::op::mark_promise_handled.map_fn_to(),
169                ),
170                (
171                    "performPromiseThen",
172                    crate::op::perform_promise_then.map_fn_to(),
173                ),
174                ("promiseState", crate::op::promise_state.map_fn_to()),
175            ],
176        ),
177        (
178            "ext:core/event_hooks",
179            vec![
180                (
181                    "setEventHandlers",
182                    crate::reporting::set_event_handlers.map_fn_to(),
183                ),
184                ("isPromise", crate::reporting::is_promise.map_fn_to()),
185                (
186                    "reportNonfatalException",
187                    crate::reporting::report_nonfatal_exception.map_fn_to(),
188                ),
189            ],
190        ),
191        (
192            "ext:core/structured_clone",
193            vec![("clone", crate::structured_clone::clone.map_fn_to())],
194        ),
195        (
196            "ext:core/source_map",
197            vec![(
198                "mapSourceLocation",
199                crate::modules::map_source_location_op.map_fn_to(),
200            )],
201        ),
202    ]
203}
204
205pub(crate) fn external_references() -> Vec<v8::ExternalReference> {
206    let evaluate: v8::SyntheticModuleEvaluationSteps = evaluate_builtin.map_fn_to();
207    let mut references = vec![v8::ExternalReference {
208        pointer: evaluate as *mut std::ffi::c_void,
209    }];
210    for (_, exports) in native_modules() {
211        references.extend(
212            exports
213                .into_iter()
214                .map(|(_, function)| v8::ExternalReference { function }),
215        );
216    }
217    references
218}
219
220pub(crate) fn init(
221    scope: &mut v8::PinScope,
222    sources: HashMap<&'static str, &'static str>,
223    primordials: v8::Local<v8::Object>,
224    ops: v8::Local<v8::Object>,
225) -> Option<()> {
226    let exports = |scope: &mut v8::PinScope, object: v8::Local<v8::Object>| -> Option<Exports> {
227        let keys = object.get_own_property_names(scope, Default::default())?;
228        let mut exports = Vec::new();
229        for index in 0..keys.length() {
230            let key = keys.get_index(scope, index)?;
231            let value = object.get(scope, key)?;
232            exports.push((
233                key.to_rust_string_lossy(scope),
234                v8::Global::new(scope, value),
235            ));
236        }
237        Some(exports)
238    };
239    let mut builtins = HashMap::new();
240    builtins.insert("ext:core/primordials".into(), exports(scope, primordials)?);
241    builtins.insert("ext:core/ops".into(), exports(scope, ops)?);
242    builtins.insert(
243        "ext:core".into(),
244        vec![
245            (
246                "primordials".into(),
247                v8::Global::new(scope, v8::Local::<v8::Value>::from(primordials)),
248            ),
249            (
250                "ops".into(),
251                v8::Global::new(scope, v8::Local::<v8::Value>::from(ops)),
252            ),
253        ],
254    );
255    for (module, functions) in native_modules() {
256        let mut exports = Vec::new();
257        for (name, callback) in functions {
258            let function = v8::Function::builder_raw(callback)
259                .constructor_behavior(v8::ConstructorBehavior::Throw)
260                .build(scope)?;
261            exports.push((
262                name.into(),
263                v8::Global::new(scope, v8::Local::<v8::Value>::from(function)),
264            ));
265        }
266        builtins.insert(module.into(), exports);
267    }
268    let origin = v8::PrimitiveArray::new(scope, 1);
269    let token = v8::Symbol::new(scope, None);
270    origin.set(scope, 0, token.into());
271    let origin = v8::Global::new(scope, origin);
272    scope.set_slot(Rc::new(Modules {
273        sources,
274        builtins,
275        modules: RefCell::new(HashMap::new()),
276        by_identity: RefCell::new(HashMap::new()),
277        origin,
278    }));
279    Some(())
280}
281
282#[derive(Clone, serde::Serialize, serde::Deserialize)]
283pub(crate) struct SnapshotState {
284    origin: usize,
285    builtins: std::collections::BTreeMap<String, Vec<(String, usize)>>,
286    modules: std::collections::BTreeMap<String, usize>,
287}
288
289/// Records handles in name order: context data indices are part of the artifact bytes.
290pub(crate) fn capture(scope: &mut v8::PinScope) -> SnapshotState {
291    let map = scope.get_slot::<Rc<Modules>>().unwrap().clone();
292    let context = scope.get_current_context();
293    let marker = v8::Local::new(scope, &map.origin).get(scope, 0);
294    let origin = scope.add_context_data(context, marker);
295    let mut builtins = std::collections::BTreeMap::new();
296    for (name, exports) in map
297        .builtins
298        .iter()
299        .collect::<std::collections::BTreeMap<_, _>>()
300    {
301        let exports = exports
302            .iter()
303            .map(|(key, value)| {
304                let value = v8::Local::new(scope, value);
305                (key.clone(), scope.add_context_data(context, value))
306            })
307            .collect();
308        builtins.insert(name.clone(), exports);
309    }
310    let modules = map
311        .modules
312        .borrow()
313        .iter()
314        .map(|(name, module)| (name.clone(), module.clone()))
315        .collect::<std::collections::BTreeMap<_, _>>()
316        .into_iter()
317        .map(|(name, module)| {
318            let module = v8::Local::new(scope, module);
319            (name, scope.add_context_data(context, module))
320        })
321        .collect();
322    SnapshotState { origin, builtins, modules }
323}
324
325pub(crate) fn restore(
326    scope: &mut v8::PinScope,
327    sources: HashMap<&'static str, &'static str>,
328    state: &SnapshotState,
329) -> Result<(), crate::SnapshotError> {
330    let marker = scope.get_context_data_from_snapshot_once::<v8::Symbol>(state.origin)?;
331    let origin = v8::PrimitiveArray::new(scope, 1);
332    origin.set(scope, 0, marker.into());
333    let origin = v8::Global::new(scope, origin);
334    let mut builtins = HashMap::new();
335    for (name, exports) in &state.builtins {
336        let mut values = Vec::new();
337        for (key, index) in exports {
338            let value = scope.get_context_data_from_snapshot_once::<v8::Value>(*index)?;
339            values.push((key.clone(), v8::Global::new(scope, value)));
340        }
341        builtins.insert(name.clone(), values);
342    }
343    let mut modules = HashMap::new();
344    let mut by_identity: HashMap<i32, Vec<String>> = HashMap::new();
345    for (name, &index) in &state.modules {
346        let module = scope.get_context_data_from_snapshot_once::<v8::Module>(index)?;
347        // Identity hashes belong to the restored heap, not the snapshot-producing isolate.
348        by_identity
349            .entry(module.get_identity_hash().get())
350            .or_default()
351            .push(name.clone());
352        modules.insert(name.clone(), v8::Global::new(scope, module));
353    }
354    scope.set_slot(Rc::new(Modules {
355        sources,
356        builtins,
357        modules: RefCell::new(modules),
358        by_identity: RefCell::new(by_identity),
359        origin,
360    }));
361    Ok(())
362}
363
364pub(crate) fn dynamic_imports_disabled(
365    scope: &mut v8::PinScope,
366    origin: v8::Local<v8::Data>,
367) -> bool {
368    let Some(map) = scope.get_slot::<Rc<Modules>>().cloned() else {
369        return false;
370    };
371    let Ok(options) = v8::Local::<v8::FixedArray>::try_from(origin) else {
372        return false;
373    };
374    if options.length() != 1 {
375        return false;
376    }
377    // Snapshot restoration can copy the options array; the private symbol retains identity.
378    let marker = v8::Local::new(scope, &map.origin).get(scope, 0);
379    options
380        .get(scope, 0)
381        .and_then(|token| v8::Local::<v8::Value>::try_from(token).ok())
382        .is_some_and(|token| token.strict_equals(marker.into()))
383}
384
385pub(crate) fn module_name(
386    scope: &mut v8::PinScope,
387    module: v8::Local<v8::Module>,
388) -> Option<String> {
389    let map = scope.get_slot::<Rc<Modules>>()?;
390    let hash = module.get_identity_hash().get();
391    let by_identity = map.by_identity.borrow();
392    let modules = map.modules.borrow();
393    for name in by_identity.get(&hash).into_iter().flatten() {
394        if let Some(global) = modules.get(name)
395            && v8::Local::new(scope, global) == module
396        {
397            return Some(name.clone());
398        }
399    }
400    None
401}
402
403fn normalize(name: &str) -> Option<String> {
404    let (root, path) = name.split_once('/').unwrap_or((name, ""));
405    if !root.starts_with("ext:") || root.len() == 4 || name.contains(['?', '#', '\\', '%']) {
406        return None;
407    }
408    let mut components = Vec::new();
409    if !path.is_empty() {
410        for part in path.split('/') {
411            match part {
412                "" => return None,
413                "." => {}
414                ".." => {
415                    components.pop()?;
416                }
417                part => components.push(part),
418            }
419        }
420    }
421    if components.is_empty() {
422        Some(root.into())
423    } else {
424        Some(format!("{root}/{}", components.join("/")))
425    }
426}
427
428fn fail<T>(scope: &mut v8::PinScope, message: impl AsRef<str>) -> Option<T> {
429    let message = v8::String::new(scope, message.as_ref())?;
430    let error = v8::Exception::error(scope, message);
431    scope.throw_exception(error);
432    None
433}
434
435fn load<'s>(scope: &mut v8::PinScope<'s, '_>, name: &str) -> Option<v8::Local<'s, v8::Module>> {
436    let map = scope.get_slot::<Rc<Modules>>().unwrap().clone();
437    {
438        let modules = map.modules.borrow();
439        if let Some(cached) = modules.get(name) {
440            return Some(v8::Local::new(scope, cached));
441        }
442    }
443    let module = if let Some(exports) = map.builtins.get(name) {
444        let name = v8::String::new(scope, name)?;
445        let keys = exports
446            .iter()
447            .map(|(name, _)| v8::String::new(scope, name))
448            .collect::<Option<Vec<_>>>()?;
449        v8::Module::create_synthetic_module(scope, name, &keys, evaluate_builtin)
450    } else {
451        let Some(code) = map.sources.get(name) else {
452            return fail(scope, format!("Unknown extension module: {name}"));
453        };
454        let origin = v8::Local::new(scope, &map.origin);
455        let mut source = crate::source(scope, name, code, true, Some(origin.into()));
456        v8::script_compiler::compile_module(scope, &mut source)?
457    };
458    map.modules
459        .borrow_mut()
460        .insert(name.to_owned(), v8::Global::new(scope, module));
461    let hash = module.get_identity_hash().get();
462    let mut by_identity = map.by_identity.borrow_mut();
463    let bucket = by_identity.entry(hash).or_default();
464    if !bucket.iter().any(|candidate| candidate == name) {
465        bucket.push(name.to_owned());
466    }
467    Some(module)
468}
469
470fn evaluate_builtin<'s>(
471    context: v8::Local<'s, v8::Context>,
472    module: v8::Local<'s, v8::Module>,
473) -> Option<v8::Local<'s, v8::Value>> {
474    // SAFETY: V8 supplies this module's entered context.
475    v8::callback_scope!(unsafe scope, context);
476    let name = module_name(scope, module)?;
477    let map = scope.get_slot::<Rc<Modules>>().unwrap().clone();
478    for (key, value) in &map.builtins[&*name] {
479        let key = v8::String::new(scope, key)?;
480        let value = v8::Local::new(scope, value);
481        module.set_synthetic_module_export(scope, key, value)?;
482    }
483    let resolver = v8::PromiseResolver::new(scope)?;
484    resolver.resolve(scope, v8::undefined(scope).into())?;
485    Some(resolver.get_promise(scope).into())
486}
487
488fn resolve<'s>(
489    context: v8::Local<'s, v8::Context>,
490    specifier: v8::Local<'s, v8::String>,
491    attributes: v8::Local<'s, v8::FixedArray>,
492    referrer: v8::Local<'s, v8::Module>,
493) -> Option<v8::Local<'s, v8::Module>> {
494    // Trust module identity, never the origin string supplied by a guest loader.
495    // SAFETY: V8 invokes module resolution with the supplied context entered.
496    v8::callback_scope!(unsafe scope, context);
497    let Some(referrer) = module_name(scope, referrer) else {
498        return fail(scope, "Not an extension module");
499    };
500    if attributes.length() != 0 {
501        return fail(scope, "not yet implemented: extension import attributes");
502    }
503    let specifier = specifier.to_rust_string_lossy(scope);
504    let name = resolve_name(scope, &specifier, &referrer)?;
505    load(scope, &name)
506}
507
508pub(crate) fn resolve_name(
509    scope: &mut v8::PinScope,
510    specifier: &str,
511    referrer: &str,
512) -> Option<String> {
513    let candidate = if specifier.starts_with("ext:") {
514        specifier.to_owned()
515    } else if specifier.starts_with("./") || specifier.starts_with("../") {
516        let Some((parent, _)) = referrer.rsplit_once('/') else {
517            return fail(scope, "Relative import from an extension package root");
518        };
519        format!("{parent}/{specifier}")
520    } else {
521        return fail(
522            scope,
523            "Extensions can only import embedded extension modules",
524        );
525    };
526    let Some(name) = normalize(&candidate) else {
527        return fail(scope, format!("Invalid extension import: {candidate}"));
528    };
529    Some(name)
530}
531
532pub(crate) fn execute<'s>(
533    scope: &mut v8::PinScope<'s, '_>,
534    entry: &str,
535) -> Option<v8::Local<'s, v8::Module>> {
536    let module = load(scope, entry)?;
537    if module.get_status() == v8::ModuleStatus::Uninstantiated
538        && module.instantiate_module(scope, resolve) != Some(true)
539    {
540        return None;
541    }
542    let evaluation = module.evaluate(scope)?;
543    let promise = v8::Local::<v8::Promise>::try_from(evaluation).ok()?;
544    promise.mark_as_handled();
545    crate::op::run_event_loop(scope, Some(promise), false)?;
546    Some(module)
547}