servo-script 0.1.0

A component of the servo web-engine.
Documentation
/* This Source Code Form is subject to the terms of the Mozilla Public
 * License, v. 2.0. If a copy of the MPL was not distributed with this
 * file, You can obtain one at https://mozilla.org/MPL/2.0/. */

use std::borrow::Cow;
use std::ffi::CStr;
use std::ptr::NonNull;
use std::rc::Rc;

use content_security_policy::sandboxing_directive::SandboxingFlagSet;
use js::context::JSContext;
use js::jsapi::{ExceptionStackBehavior, JSScript, SetScriptPrivate};
use js::jsval::{PrivateValue, UndefinedValue};
use js::panic::maybe_resume_unwind;
use js::rust::wrappers2::{
    Compile1, JS_ClearPendingException, JS_ExecuteScript, JS_GetPendingException,
    JS_GetScriptPrivate, JS_SetPendingException,
};
use js::rust::{CompileOptionsWrapper, MutableHandleValue, transform_str_to_source_text};
use script_bindings::cformat;
use script_bindings::settings_stack::run_a_script;
use servo_url::ServoUrl;

use crate::DomTypeHolder;
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use crate::dom::bindings::error::{Error, ErrorResult};
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::str::DOMString;
use crate::dom::globalscope::GlobalScope;
use crate::dom::window::Window;
use crate::script_module::{
    ModuleScript, ModuleSource, ModuleTree, RethrowError, ScriptFetchOptions,
};
use crate::script_runtime::CanGc;
use crate::unminify::unminify_js;

/// <https://html.spec.whatwg.org/multipage/#classic-script>
#[derive(JSTraceable, MallocSizeOf)]
pub(crate) struct ClassicScript {
    /// On script parsing success this will be <https://html.spec.whatwg.org/multipage/#concept-script-record>
    /// On failure <https://html.spec.whatwg.org/multipage/#concept-script-error-to-rethrow>
    #[no_trace]
    #[ignore_malloc_size_of = "mozjs"]
    pub record: Result<NonNull<JSScript>, RethrowError>,
    /// <https://html.spec.whatwg.org/multipage/#concept-script-script-fetch-options>
    fetch_options: ScriptFetchOptions,
    /// <https://html.spec.whatwg.org/multipage/#concept-script-base-url>
    #[no_trace]
    url: ServoUrl,
    /// <https://html.spec.whatwg.org/multipage/#muted-errors>
    muted_errors: ErrorReporting,
}

#[derive(Clone, Copy, JSTraceable, MallocSizeOf)]
pub(crate) enum ErrorReporting {
    Muted,
    Unmuted,
}

impl From<bool> for ErrorReporting {
    fn from(boolean: bool) -> Self {
        if boolean {
            ErrorReporting::Muted
        } else {
            ErrorReporting::Unmuted
        }
    }
}

pub(crate) enum RethrowErrors {
    Yes,
    No,
}

impl GlobalScope {
    /// <https://html.spec.whatwg.org/multipage/#creating-a-classic-script>
    #[expect(clippy::too_many_arguments)]
    pub(crate) fn create_a_classic_script(
        &self,
        cx: &mut JSContext,
        source: Cow<'_, str>,
        url: ServoUrl,
        fetch_options: ScriptFetchOptions,
        muted_errors: ErrorReporting,
        introduction_type: Option<&'static CStr>,
        line_number: u32,
        external: bool,
    ) -> ClassicScript {
        let mut script_source = ModuleSource {
            source: Rc::new(DOMString::from(source)),
            unminified_dir: self.unminified_js_dir(),
            external,
            url: url.clone(),
        };
        unminify_js(&mut script_source);

        rooted!(&in(cx) let mut compiled_script = std::ptr::null_mut::<JSScript>());

        // TODO Step 1. If mutedErrors is true, then set baseURL to about:blank.

        // TODO Step 2. If scripting is disabled for settings, then set source to the empty string.

        // TODO Step 4. Set script's settings object to settings.

        // TODO Step 9. Record classic script creation time given script and sourceURLForWindowScripts.

        // Step 10. Let result be ParseScript(source, settings's realm, script).
        compiled_script.set(compile_script(
            cx,
            &script_source.source.str(),
            url.as_str(),
            line_number,
            introduction_type,
            muted_errors,
            true,
        ));

        // Step 11. If result is a list of errors, then:
        let record = if compiled_script.get().is_null() {
            // Step 11.1. Set script's parse error and its error to rethrow to result[0].
            // Step 11.2. Return script.
            Err(RethrowError::from_pending_exception(cx.into()))
        } else {
            Ok(NonNull::new(*compiled_script).expect("Can't be null"))
        };

        // Step 3. Let script be a new classic script that this algorithm will subsequently initialize.
        // Step 5. Set script's base URL to baseURL.
        // Step 6. Set script's fetch options to options.
        // Step 7. Set script's muted errors to mutedErrors.
        // Step 12. Set script's record to result.
        // Step 13. Return script.
        ClassicScript {
            record,
            url,
            fetch_options,
            muted_errors,
        }
    }

    /// <https://html.spec.whatwg.org/multipage/#run-a-classic-script>
    #[expect(unsafe_code)]
    pub(crate) fn run_a_classic_script(
        &self,
        cx: &mut JSContext,
        script: ClassicScript,
        rethrow_errors: RethrowErrors,
    ) -> ErrorResult {
        // TODO Step 1. Let settings be the settings object of script.

        // Step 2. Check if we can run script with settings. If this returns "do not run", then return NormalCompletion(empty).
        if !self.can_run_script() {
            return Ok(());
        }

        // TODO Step 3. Record classic script execution start time given script.

        // Step 4. Prepare to run script given settings.
        // Once dropped this will run "Step 9. Clean up after running script" steps
        run_a_script::<DomTypeHolder, _>(self, || {
            // Step 5. Let evaluationStatus be null.
            rooted!(&in(cx) let mut evaluation_status = UndefinedValue());
            let mut result = false;

            match script.record {
                // Step 6. If script's error to rethrow is not null, then set evaluationStatus to ThrowCompletion(script's error to rethrow).
                Err(error_to_rethrow) => unsafe {
                    JS_SetPendingException(
                        cx,
                        error_to_rethrow.handle(),
                        ExceptionStackBehavior::Capture,
                    )
                },
                // Step 7. Otherwise, set evaluationStatus to ScriptEvaluation(script's record).
                Ok(compiled_script) => {
                    rooted!(&in(cx) let mut rval = UndefinedValue());
                    result = evaluate_script(
                        cx,
                        compiled_script,
                        script.url,
                        script.fetch_options,
                        rval.handle_mut(),
                    );
                },
            }

            unsafe { JS_GetPendingException(cx, evaluation_status.handle_mut()) };

            // Step 8. If evaluationStatus is an abrupt completion, then:
            if !evaluation_status.is_undefined() {
                warn!("Error evaluating script");

                match (rethrow_errors, script.muted_errors) {
                    // Step 8.1. If rethrow errors is true and script's muted errors is false, then:
                    (RethrowErrors::Yes, ErrorReporting::Unmuted) => {
                        // Rethrow evaluationStatus.[[Value]].
                        return Err(Error::JSFailed);
                    },
                    // Step 8.2. If rethrow errors is true and script's muted errors is true, then:
                    (RethrowErrors::Yes, ErrorReporting::Muted) => {
                        unsafe { JS_ClearPendingException(cx) };
                        // Throw a "NetworkError" DOMException.
                        return Err(Error::Network(None));
                    },
                    // Step 8.3. Otherwise, rethrow errors is false. Perform the following steps:
                    _ => {
                        unsafe { JS_ClearPendingException(cx) };
                        // Report an exception given by evaluationStatus.[[Value]] for script's settings object's global object.
                        self.report_an_exception(
                            cx.into(),
                            evaluation_status.handle(),
                            CanGc::from_cx(cx),
                        );

                        // Return evaluationStatus.
                        return Err(Error::JSFailed);
                    },
                }
            }

            maybe_resume_unwind();

            // Step 10. If evaluationStatus is a normal completion, then return evaluationStatus.
            if result {
                return Ok(());
            }

            // Step 11. If we've reached this point, evaluationStatus was left as null because the script
            // was aborted prematurely during evaluation. Return ThrowCompletion(a new QuotaExceededError).
            Err(Error::QuotaExceeded {
                quota: None,
                requested: None,
            })
        })
    }

    /// <https://html.spec.whatwg.org/multipage/#run-a-module-script>
    pub(crate) fn run_a_module_script(
        &self,
        cx: &mut JSContext,
        module_tree: Rc<ModuleTree>,
        _rethrow_errors: bool,
    ) {
        // Step 1. Let settings be the settings object of script.
        // NOTE(pylbrecht): "settings" is `self` here.

        // Step 2. Check if we can run script with settings. If this returns "do not run", then
        // return a promise resolved with undefined.
        if !self.can_run_script() {
            return;
        }

        // Step 3. Record module script execution start time given script.
        // TODO

        // Step 4. Prepare to run script given settings.
        run_a_script::<DomTypeHolder, _>(self, || {
            // Step 6. If script's error to rethrow is not null, then set evaluationPromise to a
            // promise rejected with script's error to rethrow.
            {
                let module_error = module_tree.get_rethrow_error().borrow();
                if module_error.is_some() {
                    module_tree.report_error(self, CanGc::from_cx(cx));
                    return;
                }
            }

            // Step 7.1. Otherwise: Let record be script's record.
            let record = module_tree.get_record().map(|record| record.handle());

            if let Some(record) = record {
                // Step 7.2. Set evaluationPromise to record.Evaluate().
                rooted!(&in(cx) let mut rval = UndefinedValue());
                let evaluated =
                    module_tree.execute_module(self, record, rval.handle_mut(), CanGc::from_cx(cx));

                // Step 8. If preventErrorReporting is false, then upon rejection of evaluationPromise
                // with reason, report an exception given by reason for script's settings object's
                // global object.
                if let Err(exception) = evaluated {
                    module_tree.set_rethrow_error(exception);
                    module_tree.report_error(self, CanGc::from_cx(cx));
                }
            }
        });
    }

    /// <https://html.spec.whatwg.org/multipage/#check-if-we-can-run-script>
    fn can_run_script(&self) -> bool {
        // Step 1 If the global object specified by settings is a Window object
        // whose Document object is not fully active, then return "do not run".
        //
        // Step 2 If scripting is disabled for settings, then return "do not run".
        //
        // An user agent can also disable scripting
        //
        // Either settings's global object is not a Window object,
        // or settings's global object's associated Document's active sandboxing flag set
        // does not have its sandboxed scripts browsing context flag set.
        if let Some(window) = self.downcast::<Window>() {
            let doc = window.Document();
            doc.is_fully_active() ||
                !doc.has_active_sandboxing_flag(
                    SandboxingFlagSet::SANDBOXED_SCRIPTS_BROWSING_CONTEXT_FLAG,
                )
        } else {
            true
        }
    }
}

#[expect(unsafe_code)]
pub(crate) fn compile_script(
    cx: &mut JSContext,
    text: &str,
    filename: &str,
    line_number: u32,
    introduction_type: Option<&'static CStr>,
    muted_errors: ErrorReporting,
    no_script_rval: bool,
) -> *mut JSScript {
    let muted_errors = match muted_errors {
        ErrorReporting::Muted => true,
        ErrorReporting::Unmuted => false,
    };

    // TODO: pass filename as CString to avoid allocation
    // See https://github.com/servo/servo/issues/42126
    let filename = cformat!("{filename}");
    let mut options = CompileOptionsWrapper::new(cx, filename, line_number);
    if let Some(introduction_type) = introduction_type {
        options.set_introduction_type(introduction_type);
    }

    // https://searchfox.org/firefox-main/rev/46fa95cd7f10222996ec267947ab94c5107b1475/js/public/CompileOptions.h#284
    options.set_muted_errors(muted_errors);

    // https://searchfox.org/firefox-main/rev/46fa95cd7f10222996ec267947ab94c5107b1475/js/public/CompileOptions.h#518
    options.set_is_run_once(true);
    options.set_no_script_rval(no_script_rval);

    debug!("Compiling script");
    unsafe { Compile1(cx, options.ptr, &mut transform_str_to_source_text(text)) }
}

/// <https://tc39.es/ecma262/#sec-runtime-semantics-scriptevaluation>
#[expect(unsafe_code)]
pub(crate) fn evaluate_script(
    cx: &mut JSContext,
    compiled_script: NonNull<JSScript>,
    url: ServoUrl,
    fetch_options: ScriptFetchOptions,
    rval: MutableHandleValue,
) -> bool {
    rooted!(&in(cx) let record = compiled_script.as_ptr());
    rooted!(&in(cx) let mut script_private = UndefinedValue());

    unsafe { JS_GetScriptPrivate(*record, script_private.handle_mut()) };

    // When `ScriptPrivate` for the compiled script is undefined,
    // we need to set it so that it can be used in dynamic import context.
    if script_private.is_undefined() {
        debug!("Set script private for {}", url);
        let module_script_data = Rc::new(ModuleScript::new(
            url,
            fetch_options,
            // We can't initialize an module owner here because
            // the executing context of script might be different
            // from the dynamic import script's executing context.
            None,
        ));

        unsafe {
            SetScriptPrivate(
                *record,
                &PrivateValue(Rc::into_raw(module_script_data) as *const _),
            );
        }
    }

    unsafe { JS_ExecuteScript(cx, record.handle(), rval) }
}