Files
servo/components/script/dom/global_scope_script_execution.rs
Gae24 e0d7542d37 script: Extract ErrorInfo from pending exception stack (#43632)
When reporting an exception attempt to extract `ErrorInfo` from the
stack of the exception.

Testing: Covered by existing tests, expectations updated.

Signed-off-by: Gae24 <96017547+Gae24@users.noreply.github.com>
2026-04-25 09:06:46 +00:00

384 lines
15 KiB
Rust

/* 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, Heap, JSScript, SetScriptPrivate};
use js::jsval::{PrivateValue, UndefinedValue};
use js::panic::maybe_resume_unwind;
use js::rust::wrappers2::{
Compile1, JS_ClearPendingException, JS_ExecuteScript, JS_GetScriptPrivate,
JS_IsExceptionPending, 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 script_bindings::trace::RootedTraceableBox;
use servo_url::ServoUrl;
use crate::DomTypeHolder;
use crate::dom::bindings::codegen::Bindings::WindowBinding::WindowMethods;
use crate::dom::bindings::error::{Error, ErrorResult, report_pending_exception};
use crate::dom::bindings::inheritance::Castable;
use crate::dom::bindings::str::DOMString;
use crate::dom::globalscope::GlobalScope;
use crate::dom::window::Window;
use crate::realms::{InRealm, enter_auto_realm};
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>
#[ignore_malloc_size_of = "mozjs"]
pub record: Result<RootedTraceableBox<Heap<*mut 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))
} else {
Ok(RootedTraceableBox::from_box(Heap::boxed(
compiled_script.get(),
)))
};
// 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.
let mut realm = enter_auto_realm(cx, self);
let cx = &mut realm.current_realm();
// 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.
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());
let script_ptr = NonNull::new(compiled_script.get())
.expect("Compiled script must not be null");
result = evaluate_script(
cx,
script_ptr,
script.url,
script.fetch_options,
rval.handle_mut(),
);
},
}
// Step 8. If evaluationStatus is an abrupt completion, then:
if unsafe { JS_IsExceptionPending(cx) } {
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:
_ => {
let in_realm_proof = cx.into();
let in_realm = InRealm::Already(&in_realm_proof);
// Report an exception given by evaluationStatus.[[Value]] for script's settings object's global object.
report_pending_exception(cx.into(), in_realm, 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(cx, self);
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(cx, self, record, rval.handle_mut());
// 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(cx, self);
}
}
});
}
/// <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) }
}