Skip to main content

script/scripting/
script_execution.rs

1/* This Source Code Form is subject to the terms of the Mozilla Public
2 * License, v. 2.0. If a copy of the MPL was not distributed with this
3 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
4
5use std::borrow::Cow;
6use std::ffi::CStr;
7use std::rc::Rc;
8
9use bitflags::bitflags;
10use content_security_policy::sandboxing_directive::SandboxingFlagSet;
11use js::context::JSContext;
12use js::gc::Handle;
13use js::jsapi::{
14    ExceptionStackBehavior, HadFrontendErrors, Heap, JSObject, JSScript, ModuleErrorBehaviour,
15    SetScriptPrivate,
16};
17use js::jsval::{JSVal, PrivateValue, UndefinedValue};
18use js::offthread::CompilationResult;
19use js::panic::maybe_resume_unwind;
20use js::rust::wrappers2::{
21    Compile1, ConvertFrontendErrorsToRuntimeErrors, InstantiateGlobalStencil,
22    JS_ClearPendingException, JS_ExecuteScript, JS_GetScriptPrivate, ModuleEvaluate,
23    ThrowOnModuleEvaluationFailure,
24};
25use js::rust::{
26    CompileOptionsWrapper, HandleValue, MutableHandleValue, transform_str_to_source_text,
27};
28use script_bindings::cformat;
29use script_bindings::js::{JS_IsExceptionPending, JS_SetPendingException};
30use script_bindings::settings_stack::run_a_script;
31use script_bindings::trace::RootedTraceableBox;
32use servo_url::ServoUrl;
33
34use crate::DomTypeHolder;
35use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
36use crate::dom::bindings::error::{Error, ErrorInfo, ErrorResult, report_pending_exception};
37use crate::dom::bindings::inheritance::Castable;
38use crate::dom::globalscope::GlobalScope;
39use crate::dom::window::Window;
40use crate::realms::enter_auto_realm;
41use crate::scripting::fetching_options::{BaseScript, ScriptFetchOptions};
42use crate::scripting::script_module::{ModuleTree, RethrowError};
43use crate::unminify::{ScriptSource, unminify_js};
44
45/// Options that were used when creating a script.
46#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
47pub(crate) struct ScriptOptions(u8);
48
49bitflags! {
50    impl ScriptOptions: u8 {
51        /// <https://html.spec.whatwg.org/multipage/#muted-errors>
52        const MutedErrors = 1 << 0;
53        /// Whether or not this script was compiled to return a value.
54        const ReturnsAValue = 1 << 1;
55        /// Whether or not this script was external.
56        const External = 1 << 2;
57    }
58}
59
60/// <https://html.spec.whatwg.org/multipage/#classic-script>
61#[derive(JSTraceable, MallocSizeOf)]
62#[cfg_attr(crown, crown::unrooted_must_root_lint::must_root)]
63pub(crate) struct ClassicScript {
64    #[ignore_malloc_size_of = "mozjs"]
65    /// <https://html.spec.whatwg.org/multipage/#concept-script-record>
66    record: Option<Heap<*mut JSScript>>,
67    #[ignore_malloc_size_of = "mozjs"]
68    /// <https://html.spec.whatwg.org/multipage/#concept-script-error-to-rethrow>
69    /// For classic scripts the error to rethrow is always the parse error
70    error_to_rethrow: Option<Heap<JSVal>>,
71    /// The options that were used when compiling this script.
72    options: ScriptOptions,
73}
74
75impl ClassicScript {
76    #[expect(unsafe_code)]
77    fn record<'a>(&'a self) -> Option<Handle<'a, *mut JSScript>> {
78        self.record
79            .as_ref()
80            .map(|value| unsafe { Handle::from_raw(value.handle()) })
81    }
82
83    #[expect(unsafe_code)]
84    fn error_to_rethrow<'a>(&'a self) -> Option<HandleValue<'a>> {
85        self.error_to_rethrow
86            .as_ref()
87            .map(|value| unsafe { Handle::from_raw(value.handle()) })
88    }
89
90    pub(crate) fn has_rethrow_error(&self) -> bool {
91        self.error_to_rethrow.is_some()
92    }
93
94    #[expect(unsafe_code)]
95    pub(crate) fn from_stencil(
96        cx: &mut JSContext,
97        compilation_result: CompilationResult,
98        url: ServoUrl,
99        script_options: ScriptOptions,
100        fetch_options: ScriptFetchOptions,
101    ) -> RootedTraceableBox<ClassicScript> {
102        let mut classic_script = RootedTraceableBox::new(ClassicScript {
103            record: None,
104            error_to_rethrow: None,
105            options: script_options,
106        });
107
108        let CompilationResult {
109            stencil,
110            mut storage,
111            fc,
112            compile_options,
113        } = compilation_result;
114
115        match stencil {
116            Some(stencil) => {
117                debug_assert!(!stencil.is_null());
118                let instantiate_options = (&compile_options).into();
119
120                rooted!(&in(cx) let script = unsafe { InstantiateGlobalStencil(
121                    cx,
122                    &instantiate_options,
123                    *stencil,
124                    storage.as_mut_ptr(),
125                )});
126                maybe_associate_with_script(cx, script.handle(), url, fetch_options);
127
128                classic_script.record = Some(Heap::default());
129                classic_script.record.as_ref().unwrap().set(script.get());
130            },
131            None => {
132                debug_assert!(unsafe { HadFrontendErrors(*fc) });
133
134                let options = compile_options.read_only();
135                unsafe { ConvertFrontendErrorsToRuntimeErrors(cx, *fc, options) };
136
137                let error = RethrowError::from_pending_exception(cx);
138
139                classic_script.error_to_rethrow = Some(Heap::default());
140                classic_script
141                    .error_to_rethrow
142                    .as_ref()
143                    .unwrap()
144                    .set(error.handle().get());
145            },
146        }
147
148        classic_script
149    }
150}
151
152pub(crate) enum RethrowErrors {
153    Yes,
154    No,
155}
156
157impl GlobalScope {
158    /// <https://html.spec.whatwg.org/multipage/#creating-a-classic-script>
159    #[expect(clippy::too_many_arguments)]
160    #[expect(unsafe_code)]
161    pub(crate) fn create_a_classic_script(
162        &self,
163        cx: &mut JSContext,
164        source: Cow<'_, str>,
165        url: ServoUrl,
166        options: ScriptOptions,
167        fetch_options: ScriptFetchOptions,
168        introduction_type: Option<&'static CStr>,
169        line_number: u32,
170    ) -> RootedTraceableBox<ClassicScript> {
171        let mut script_source = ScriptSource {
172            source,
173            external: options.contains(ScriptOptions::External),
174            url: &url,
175        };
176        if let Some(unminified_js_dir) = self.unminified_js_dir() {
177            unminify_js(&mut script_source, unminified_js_dir);
178        }
179        let mut source = transform_str_to_source_text(&script_source.source);
180
181        // TODO Step 1. If mutedErrors is true, then set baseURL to about:blank.
182
183        // TODO Step 2. If scripting is disabled for settings, then set source to the empty string.
184
185        // Step 3. Let script be a new classic script that this algorithm will subsequently initialize.
186        // Step 7. Set script's muted errors to mutedErrors.
187        // Step 8. Set script's parse error and error to rethrow to null.
188        let mut script = RootedTraceableBox::new(ClassicScript {
189            record: None,
190            error_to_rethrow: None,
191            options,
192        });
193
194        // TODO Step 4. Set script's settings object to settings.
195
196        // TODO Step 9. Record classic script creation time given script and sourceURLForWindowScripts.
197
198        let compilation_options =
199            fill_compile_options(cx, url.as_str(), options, introduction_type, line_number);
200
201        // Step 10. Let result be ParseScript(source, settings's realm, script).
202        rooted!(&in(cx) let result = unsafe { Compile1(cx, compilation_options.ptr, &mut source) });
203
204        // Step 11. If result is a list of errors, then:
205        if result.is_null() {
206            let error = RethrowError::from_pending_exception(cx);
207
208            // Step 11.1. Set script's parse error and its error to rethrow to result[0].
209            script.error_to_rethrow = Some(Heap::default());
210            script
211                .error_to_rethrow
212                .as_ref()
213                .unwrap()
214                .set(error.handle().get());
215
216            // Step 11.2. Return script.
217            return script;
218        }
219
220        // Step 5. Set script's base URL to baseURL.
221        // Step 6. Set script's fetch options to options.
222        maybe_associate_with_script(cx, result.handle(), url, fetch_options);
223
224        // Step 12. Set script's record to result.
225        script.record = Some(Heap::default());
226        script.record.as_ref().unwrap().set(result.get());
227
228        // Step 13. Return script.
229        script
230    }
231
232    /// <https://html.spec.whatwg.org/multipage/#run-a-classic-script>
233    #[expect(unsafe_code)]
234    pub(crate) fn run_a_classic_script(
235        &self,
236        cx: &mut JSContext,
237        script: RootedTraceableBox<ClassicScript>,
238        rethrow_errors: RethrowErrors,
239        return_value: Option<MutableHandleValue>,
240    ) -> ErrorResult {
241        assert_eq!(
242            return_value.is_some(),
243            script.options.contains(ScriptOptions::ReturnsAValue),
244            "Classic script returns a value option does not match `return_value` argument"
245        );
246
247        // TODO Step 1. Let settings be the settings object of script.
248
249        // Step 2. Check if we can run script with settings. If this returns "do not run", then
250        // return NormalCompletion(empty).
251        if !self.can_run_script() {
252            return Ok(());
253        }
254
255        // TODO Step 3. Record classic script execution start time given script.
256
257        let mut realm = enter_auto_realm(cx, self);
258        let cx = &mut realm.current_realm();
259
260        // Step 4. Prepare to run script given settings.
261        // Once dropped this will run "Step 9. Clean up after running script" steps
262        run_a_script::<DomTypeHolder, _, _>(cx, self, |cx| {
263            // Step 5. Let evaluationStatus be null.
264            let mut result = false;
265
266            // Step 6. If script's error to rethrow is not null, then set evaluationStatus
267            // to ThrowCompletion(script's error to rethrow).
268            if let Some(error) = script.error_to_rethrow() {
269                JS_SetPendingException(cx, error, ExceptionStackBehavior::Capture);
270            } else if let Some(record) = script.record() {
271                // Step 7. Otherwise, set evaluationStatus to ScriptEvaluation(script's record).
272                rooted!(&in(cx) let mut fallback_value = UndefinedValue());
273                let return_value = return_value.unwrap_or_else(|| fallback_value.handle_mut());
274                result = unsafe { JS_ExecuteScript(cx, record, return_value) };
275            }
276
277            // Step 8. If evaluationStatus is an abrupt completion, then:
278            if JS_IsExceptionPending(cx) {
279                warn!("Error evaluating script");
280
281                match (
282                    rethrow_errors,
283                    script.options.contains(ScriptOptions::MutedErrors),
284                ) {
285                    (RethrowErrors::Yes, false) => {
286                        // Step 8.1. If rethrow errors is true and script's muted errors is false, then:
287                        // Rethrow evaluationStatus.[[Value]].
288                        return Err(Error::JSFailed);
289                    },
290                    (RethrowErrors::Yes, true) => {
291                        // Step 8.2. If rethrow errors is true and script's muted errors is true, then:
292                        unsafe { JS_ClearPendingException(cx) };
293                        // Throw a "NetworkError" DOMException.
294                        return Err(Error::Network(None));
295                    },
296                    // Step 8.3. Otherwise, rethrow errors is false. Perform the following steps:
297                    // Report an exception given by evaluationStatus.[[Value]] for script's
298                    // settings object's global object.
299                    (RethrowErrors::No, false) => {
300                        report_pending_exception(cx);
301                        return Err(Error::JSFailed);
302                    },
303                    (RethrowErrors::No, true) => {
304                        unsafe { JS_ClearPendingException(cx) };
305                        self.report_an_error(
306                            cx,
307                            ErrorInfo {
308                                message: String::from("Script error."),
309                                ..Default::default()
310                            },
311                            HandleValue::null(),
312                        );
313                        return Err(Error::JSFailed);
314                    },
315                }
316            }
317
318            maybe_resume_unwind();
319
320            // Step 10. If evaluationStatus is a normal completion, then return evaluationStatus.
321            if result {
322                return Ok(());
323            }
324
325            // Step 11. If we've reached this point, evaluationStatus was left as null because the script
326            // was aborted prematurely during evaluation. Return ThrowCompletion(a new QuotaExceededError).
327            Err(Error::QuotaExceeded {
328                quota: None,
329                requested: None,
330            })
331        })
332    }
333
334    /// <https://html.spec.whatwg.org/multipage/#run-a-module-script>
335    #[expect(unsafe_code)]
336    pub(crate) fn run_a_module_script(
337        &self,
338        cx: &mut JSContext,
339        module_tree: RootedTraceableBox<Rc<ModuleTree>>,
340        _rethrow_errors: bool,
341    ) {
342        // Step 1. Let settings be the settings object of script.
343        // NOTE(pylbrecht): "settings" is `self` here.
344
345        // Step 2. Check if we can run script with settings. If this returns "do not run", then
346        // return a promise resolved with undefined.
347        if !self.can_run_script() {
348            return;
349        }
350
351        // Step 3. Record module script execution start time given script.
352        // TODO
353
354        let mut realm = enter_auto_realm(cx, self);
355        let cx = &mut realm.current_realm();
356
357        // Step 4. Prepare to run script given settings.
358        run_a_script::<DomTypeHolder, _, _>(cx, self, |cx| {
359            // Step 6. If script's error to rethrow is not null, then set evaluationPromise to a
360            // promise rejected with script's error to rethrow.
361            if let Some(exception) = module_tree.rethrow_error() {
362                JS_SetPendingException(cx, exception.handle(), ExceptionStackBehavior::Capture);
363                report_pending_exception(cx);
364                return;
365            }
366
367            // Step 7.1. Otherwise: Let record be script's record.
368            let record = module_tree.record().unwrap();
369
370            // Step 7.2. Set evaluationPromise to record.Evaluate().
371            rooted!(&in(cx) let mut rval = UndefinedValue());
372            let ok = unsafe { ModuleEvaluate(cx, record.handle(), rval.handle_mut()) };
373            assert!(ok, "module evaluation failed");
374
375            rooted!(&in(cx) let mut evaluation_promise = std::ptr::null_mut::<JSObject>());
376            if rval.is_object() {
377                evaluation_promise.set(rval.to_object());
378            }
379
380            // Step 8. If preventErrorReporting is false, then upon rejection of evaluationPromise
381            // with reason, report an exception given by reason for script's settings object's
382            // global object.
383            let throw_result = unsafe {
384                ThrowOnModuleEvaluationFailure(
385                    cx,
386                    evaluation_promise.handle(),
387                    ModuleErrorBehaviour::ThrowModuleErrorsSync,
388                )
389            };
390
391            if !throw_result {
392                report_pending_exception(cx);
393            }
394        });
395    }
396
397    /// <https://html.spec.whatwg.org/multipage/#check-if-we-can-run-script>
398    pub(crate) fn can_run_script(&self) -> bool {
399        // Step 1 If the global object specified by settings is a Window object
400        // whose Document object is not fully active, then return "do not run".
401        //
402        // Step 2 If scripting is disabled for settings, then return "do not run".
403        //
404        // An user agent can also disable scripting
405        //
406        // Either settings's global object is not a Window object,
407        // or settings's global object's associated Document's active sandboxing flag set
408        // does not have its sandboxed scripts browsing context flag set.
409        if let Some(window) = self.downcast::<Window>() {
410            let doc = window.Document();
411            doc.is_fully_active() &&
412                !doc.has_active_sandboxing_flag(
413                    SandboxingFlagSet::SANDBOXED_SCRIPTS_BROWSING_CONTEXT_FLAG,
414                )
415        } else {
416            true
417        }
418    }
419}
420
421pub(crate) fn fill_compile_options(
422    cx: &mut JSContext,
423    filename: &str,
424    script_options: ScriptOptions,
425    introduction_type: Option<&'static CStr>,
426    line_number: u32,
427) -> CompileOptionsWrapper {
428    // TODO: pass filename as CString to avoid allocation
429    // See https://github.com/servo/servo/issues/42126
430    let mut options = CompileOptionsWrapper::new(cx, cformat!("{filename}"), line_number);
431    if let Some(introduction_type) = introduction_type {
432        options.set_introduction_type(introduction_type);
433    }
434
435    // https://searchfox.org/firefox-main/rev/46fa95cd7f10222996ec267947ab94c5107b1475/js/public/CompileOptions.h#284
436    options.set_muted_errors(script_options.contains(ScriptOptions::MutedErrors));
437
438    // https://searchfox.org/firefox-main/rev/46fa95cd7f10222996ec267947ab94c5107b1475/js/public/CompileOptions.h#518
439    options.set_is_run_once(true);
440    options.set_no_script_rval(!script_options.contains(ScriptOptions::ReturnsAValue));
441
442    options
443}
444
445#[expect(unsafe_code)]
446pub(crate) fn maybe_associate_with_script(
447    cx: &JSContext,
448    script: Handle<*mut JSScript>,
449    url: ServoUrl,
450    fetch_options: ScriptFetchOptions,
451) {
452    rooted!(&in(cx) let mut script_private = UndefinedValue());
453    unsafe { JS_GetScriptPrivate(script.get(), script_private.handle_mut()) };
454
455    // When `ScriptPrivate` for the compiled script is undefined,
456    // we need to set it so that it can be used in dynamic import context.
457    if script_private.is_undefined() {
458        let module_script_data = Rc::new(BaseScript::new(url, fetch_options));
459
460        unsafe {
461            SetScriptPrivate(
462                script.get(),
463                &PrivateValue(Rc::into_raw(module_script_data) as *const _),
464            );
465        }
466    }
467}