script/dom/bindings/
error.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
5//! Utilities to throw exceptions from Rust bindings.
6
7use std::ffi::CString;
8use std::ptr::NonNull;
9use std::slice::from_raw_parts;
10
11#[cfg(feature = "js_backtrace")]
12use backtrace::Backtrace;
13use embedder_traits::JavaScriptErrorInfo;
14use js::conversions::jsstr_to_string;
15use js::error::{throw_range_error, throw_type_error};
16#[cfg(feature = "js_backtrace")]
17use js::jsapi::StackFormat as JSStackFormat;
18use js::jsapi::{
19    ExceptionStackBehavior, JS_ClearPendingException, JS_GetProperty, JS_IsExceptionPending,
20};
21use js::jsval::UndefinedValue;
22use js::rust::wrappers::{JS_ErrorFromException, JS_GetPendingException, JS_SetPendingException};
23use js::rust::{HandleObject, HandleValue, MutableHandleValue, describe_scripted_caller};
24use libc::c_uint;
25use script_bindings::conversions::SafeToJSValConvertible;
26pub(crate) use script_bindings::error::*;
27use script_bindings::root::DomRoot;
28use script_bindings::str::DOMString;
29
30#[cfg(feature = "js_backtrace")]
31use crate::dom::bindings::cell::DomRefCell;
32use crate::dom::bindings::conversions::{
33    ConversionResult, SafeFromJSValConvertible, root_from_object,
34};
35use crate::dom::bindings::str::USVString;
36use crate::dom::domexception::{DOMErrorName, DOMException};
37use crate::dom::globalscope::GlobalScope;
38use crate::dom::types::QuotaExceededError;
39use crate::realms::InRealm;
40use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
41
42#[cfg(feature = "js_backtrace")]
43thread_local! {
44    /// An optional stringified JS backtrace and stringified native backtrace from the
45    /// the last DOM exception that was reported.
46    static LAST_EXCEPTION_BACKTRACE: DomRefCell<Option<(Option<String>, String)>> = DomRefCell::new(None);
47}
48
49/// Error values that have no equivalent DOMException representation.
50pub(crate) enum JsEngineError {
51    /// An EMCAScript TypeError.
52    Type(String),
53    /// An ECMAScript RangeError.
54    Range(String),
55    /// The JS engine reported a thrown exception.
56    JSFailed,
57}
58
59/// Set a pending exception for the given `result` on `cx`.
60pub(crate) fn throw_dom_exception(
61    cx: SafeJSContext,
62    global: &GlobalScope,
63    result: Error,
64    can_gc: CanGc,
65) {
66    #[cfg(feature = "js_backtrace")]
67    unsafe {
68        capture_stack!(in(*cx) let stack);
69        let js_stack = stack.and_then(|s| s.as_string(None, JSStackFormat::Default));
70        let rust_stack = Backtrace::new();
71        LAST_EXCEPTION_BACKTRACE.with(|backtrace| {
72            *backtrace.borrow_mut() = Some((js_stack, format!("{:?}", rust_stack)));
73        });
74    }
75
76    match create_dom_exception(global, result, can_gc) {
77        Ok(exception) => unsafe {
78            assert!(!JS_IsExceptionPending(*cx));
79            rooted!(in(*cx) let mut thrown = UndefinedValue());
80            exception.safe_to_jsval(cx, thrown.handle_mut(), can_gc);
81            JS_SetPendingException(*cx, thrown.handle(), ExceptionStackBehavior::Capture);
82        },
83
84        Err(JsEngineError::Type(message)) => unsafe {
85            assert!(!JS_IsExceptionPending(*cx));
86            throw_type_error(*cx, &message);
87        },
88
89        Err(JsEngineError::Range(message)) => unsafe {
90            assert!(!JS_IsExceptionPending(*cx));
91            throw_range_error(*cx, &message);
92        },
93
94        Err(JsEngineError::JSFailed) => unsafe {
95            assert!(JS_IsExceptionPending(*cx));
96        },
97    }
98}
99
100/// If possible, create a new DOMException representing the provided error.
101/// If no such DOMException exists, return a subset of the original error values
102/// that may need additional handling.
103pub(crate) fn create_dom_exception(
104    global: &GlobalScope,
105    result: Error,
106    can_gc: CanGc,
107) -> Result<DomRoot<DOMException>, JsEngineError> {
108    let new_custom_exception = |error_name, message| {
109        Ok(DOMException::new_with_custom_message(
110            global, error_name, message, can_gc,
111        ))
112    };
113
114    let code = match result {
115        Error::IndexSize(Some(custom_message)) => {
116            return new_custom_exception(DOMErrorName::IndexSizeError, custom_message);
117        },
118        Error::IndexSize(None) => DOMErrorName::IndexSizeError,
119        Error::NotFound(Some(custom_message)) => {
120            return new_custom_exception(DOMErrorName::NotFoundError, custom_message);
121        },
122        Error::NotFound(None) => DOMErrorName::NotFoundError,
123        Error::HierarchyRequest(Some(custom_message)) => {
124            return new_custom_exception(DOMErrorName::HierarchyRequestError, custom_message);
125        },
126        Error::HierarchyRequest(None) => DOMErrorName::HierarchyRequestError,
127        Error::WrongDocument(Some(doc_err_custom_message)) => {
128            return new_custom_exception(DOMErrorName::WrongDocumentError, doc_err_custom_message);
129        },
130        Error::WrongDocument(None) => DOMErrorName::WrongDocumentError,
131        Error::InvalidCharacter(Some(custom_message)) => {
132            return new_custom_exception(DOMErrorName::InvalidCharacterError, custom_message);
133        },
134        Error::InvalidCharacter(None) => DOMErrorName::InvalidCharacterError,
135        Error::NotSupported(Some(custom_message)) => {
136            return new_custom_exception(DOMErrorName::NotSupportedError, custom_message);
137        },
138        Error::NotSupported(None) => DOMErrorName::NotSupportedError,
139        Error::InUseAttribute(Some(custom_message)) => {
140            return new_custom_exception(DOMErrorName::InUseAttributeError, custom_message);
141        },
142        Error::InUseAttribute(None) => DOMErrorName::InUseAttributeError,
143        Error::InvalidState(Some(custom_message)) => {
144            return new_custom_exception(DOMErrorName::InvalidStateError, custom_message);
145        },
146        Error::InvalidState(None) => DOMErrorName::InvalidStateError,
147        Error::Syntax(Some(custom_message)) => {
148            return new_custom_exception(DOMErrorName::SyntaxError, custom_message);
149        },
150        Error::Syntax(None) => DOMErrorName::SyntaxError,
151        Error::Namespace(Some(custom_message)) => {
152            return new_custom_exception(DOMErrorName::NamespaceError, custom_message);
153        },
154        Error::Namespace(None) => DOMErrorName::NamespaceError,
155        Error::InvalidAccess(Some(custom_message)) => {
156            return new_custom_exception(DOMErrorName::InvalidAccessError, custom_message);
157        },
158        Error::InvalidAccess(None) => DOMErrorName::InvalidAccessError,
159        Error::Security(Some(custom_message)) => {
160            return new_custom_exception(DOMErrorName::SecurityError, custom_message);
161        },
162        Error::Security(None) => DOMErrorName::SecurityError,
163        Error::Network(Some(custom_message)) => {
164            return new_custom_exception(DOMErrorName::NetworkError, custom_message);
165        },
166        Error::Network(None) => DOMErrorName::NetworkError,
167        Error::Abort(Some(custom_message)) => {
168            return new_custom_exception(DOMErrorName::AbortError, custom_message);
169        },
170        Error::Abort(None) => DOMErrorName::AbortError,
171        Error::Timeout(Some(custom_message)) => {
172            return new_custom_exception(DOMErrorName::TimeoutError, custom_message);
173        },
174        Error::Timeout(None) => DOMErrorName::TimeoutError,
175        Error::InvalidNodeType(Some(custom_message)) => {
176            return new_custom_exception(DOMErrorName::InvalidNodeTypeError, custom_message);
177        },
178        Error::InvalidNodeType(None) => DOMErrorName::InvalidNodeTypeError,
179        Error::DataClone(Some(custom_message)) => {
180            return new_custom_exception(DOMErrorName::DataCloneError, custom_message);
181        },
182        Error::DataClone(None) => DOMErrorName::DataCloneError,
183        Error::Data(Some(custom_message)) => {
184            return new_custom_exception(DOMErrorName::DataError, custom_message);
185        },
186        Error::Data(None) => DOMErrorName::DataError,
187        Error::TransactionInactive(Some(custom_message)) => {
188            return new_custom_exception(DOMErrorName::TransactionInactiveError, custom_message);
189        },
190        Error::TransactionInactive(None) => DOMErrorName::TransactionInactiveError,
191        Error::ReadOnly(Some(custom_message)) => {
192            return new_custom_exception(DOMErrorName::ReadOnlyError, custom_message);
193        },
194        Error::ReadOnly(None) => DOMErrorName::ReadOnlyError,
195        Error::Version(Some(custom_message)) => {
196            return new_custom_exception(DOMErrorName::VersionError, custom_message);
197        },
198        Error::Version(None) => DOMErrorName::VersionError,
199        Error::NoModificationAllowed(Some(custom_message)) => {
200            return new_custom_exception(DOMErrorName::NoModificationAllowedError, custom_message);
201        },
202        Error::NoModificationAllowed(None) => DOMErrorName::NoModificationAllowedError,
203        Error::QuotaExceeded { quota, requested } => {
204            return Ok(DomRoot::upcast(QuotaExceededError::new(
205                global,
206                DOMString::new(),
207                quota,
208                requested,
209                can_gc,
210            )));
211        },
212        Error::TypeMismatch(Some(custom_message)) => {
213            return new_custom_exception(DOMErrorName::TypeMismatchError, custom_message);
214        },
215        Error::TypeMismatch(None) => DOMErrorName::TypeMismatchError,
216        Error::InvalidModification(Some(custom_message)) => {
217            return new_custom_exception(DOMErrorName::InvalidModificationError, custom_message);
218        },
219        Error::InvalidModification(None) => DOMErrorName::InvalidModificationError,
220        Error::NotReadable(Some(custom_message)) => {
221            return new_custom_exception(DOMErrorName::NotReadableError, custom_message);
222        },
223        Error::NotReadable(None) => DOMErrorName::NotReadableError,
224        Error::Operation(Some(custom_message)) => {
225            return new_custom_exception(DOMErrorName::OperationError, custom_message);
226        },
227        Error::Operation(None) => DOMErrorName::OperationError,
228        Error::NotAllowed(Some(custom_message)) => {
229            return new_custom_exception(DOMErrorName::NotAllowedError, custom_message);
230        },
231        Error::NotAllowed(None) => DOMErrorName::NotAllowedError,
232        Error::Encoding(Some(custom_message)) => {
233            return new_custom_exception(DOMErrorName::EncodingError, custom_message);
234        },
235        Error::Encoding(None) => DOMErrorName::EncodingError,
236        Error::Constraint(Some(custom_message)) => {
237            return new_custom_exception(DOMErrorName::ConstraintError, custom_message);
238        },
239        Error::Constraint(None) => DOMErrorName::ConstraintError,
240        Error::Type(message) => return Err(JsEngineError::Type(message)),
241        Error::Range(message) => return Err(JsEngineError::Range(message)),
242        Error::JSFailed => return Err(JsEngineError::JSFailed),
243    };
244    Ok(DOMException::new(global, code, can_gc))
245}
246
247/// A struct encapsulating information about a runtime script error.
248#[derive(Default)]
249pub(crate) struct ErrorInfo {
250    /// The error message.
251    pub(crate) message: String,
252    /// The file name.
253    pub(crate) filename: String,
254    /// The line number.
255    pub(crate) lineno: c_uint,
256    /// The column number.
257    pub(crate) column: c_uint,
258}
259
260impl ErrorInfo {
261    fn from_native_error(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
262        let report = unsafe { JS_ErrorFromException(*cx, object) };
263        if report.is_null() {
264            return None;
265        }
266
267        let filename = {
268            let filename = unsafe { (*report)._base.filename.data_ as *const u8 };
269            if !filename.is_null() {
270                let filename = unsafe {
271                    let length = (0..).find(|idx| *filename.offset(*idx) == 0).unwrap();
272                    from_raw_parts(filename, length as usize)
273                };
274                String::from_utf8_lossy(filename).into_owned()
275            } else {
276                "none".to_string()
277            }
278        };
279
280        let lineno = unsafe { (*report)._base.lineno };
281        let column = unsafe { (*report)._base.column._base };
282
283        let message = {
284            let message = unsafe { (*report)._base.message_.data_ as *const u8 };
285            let message = unsafe {
286                let length = (0..).find(|idx| *message.offset(*idx) == 0).unwrap();
287                from_raw_parts(message, length as usize)
288            };
289            String::from_utf8_lossy(message).into_owned()
290        };
291
292        Some(ErrorInfo {
293            filename,
294            message,
295            lineno,
296            column,
297        })
298    }
299
300    fn from_dom_exception(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
301        let exception = unsafe { root_from_object::<DOMException>(object.get(), *cx).ok()? };
302        let scripted_caller = unsafe { describe_scripted_caller(*cx) }.unwrap_or_default();
303        Some(ErrorInfo {
304            message: exception.stringifier().into(),
305            filename: scripted_caller.filename,
306            lineno: scripted_caller.line,
307            column: scripted_caller.col + 1,
308        })
309    }
310
311    fn from_object(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
312        if let Some(info) = ErrorInfo::from_native_error(object, cx) {
313            return Some(info);
314        }
315        if let Some(info) = ErrorInfo::from_dom_exception(object, cx) {
316            return Some(info);
317        }
318        None
319    }
320
321    /// <https://html.spec.whatwg.org/multipage/#extract-error>
322    pub(crate) fn from_value(value: HandleValue, cx: SafeJSContext, can_gc: CanGc) -> ErrorInfo {
323        if value.is_object() {
324            rooted!(in(*cx) let object = value.to_object());
325            if let Some(info) = ErrorInfo::from_object(object.handle(), cx) {
326                return info;
327            }
328        }
329
330        match USVString::safe_from_jsval(cx, value, (), can_gc) {
331            Ok(ConversionResult::Success(USVString(string))) => {
332                let scripted_caller = unsafe { describe_scripted_caller(*cx) }.unwrap_or_default();
333                ErrorInfo {
334                    message: format!("uncaught exception: {}", string),
335                    filename: scripted_caller.filename,
336                    lineno: scripted_caller.line,
337                    column: scripted_caller.col + 1,
338                }
339            },
340            _ => {
341                panic!("uncaught exception: failed to stringify primitive");
342            },
343        }
344    }
345}
346
347/// Report a pending exception, thereby clearing it.
348///
349/// The `dispatch_event` argument is temporary and non-standard; passing false
350/// prevents dispatching the `error` event.
351pub(crate) fn report_pending_exception(
352    cx: SafeJSContext,
353    dispatch_event: bool,
354    realm: InRealm,
355    can_gc: CanGc,
356) {
357    rooted!(in(*cx) let mut value = UndefinedValue());
358    if take_pending_exception(cx, value.handle_mut()) {
359        let error_info = ErrorInfo::from_value(value.handle(), cx, can_gc);
360        report_error(
361            error_info,
362            value.handle(),
363            cx,
364            dispatch_event,
365            realm,
366            can_gc,
367        );
368    }
369}
370
371fn take_pending_exception(cx: SafeJSContext, value: MutableHandleValue) -> bool {
372    unsafe {
373        if !JS_IsExceptionPending(*cx) {
374            return false;
375        }
376    }
377
378    unsafe {
379        if !JS_GetPendingException(*cx, value) {
380            JS_ClearPendingException(*cx);
381            error!("Uncaught exception: JS_GetPendingException failed");
382            return false;
383        }
384
385        JS_ClearPendingException(*cx);
386    }
387    true
388}
389
390fn report_error(
391    error_info: ErrorInfo,
392    value: HandleValue,
393    cx: SafeJSContext,
394    dispatch_event: bool,
395    realm: InRealm,
396    can_gc: CanGc,
397) {
398    error!(
399        "Error at {}:{}:{} {}",
400        error_info.filename, error_info.lineno, error_info.column, error_info.message
401    );
402
403    #[cfg(feature = "js_backtrace")]
404    {
405        LAST_EXCEPTION_BACKTRACE.with(|backtrace| {
406            if let Some((js_backtrace, rust_backtrace)) = backtrace.borrow_mut().take() {
407                if let Some(stack) = js_backtrace {
408                    error!("JS backtrace:\n{}", stack);
409                }
410                error!("Rust backtrace:\n{}", rust_backtrace);
411            }
412        });
413    }
414
415    if dispatch_event {
416        GlobalScope::from_safe_context(cx, realm).report_an_error(error_info, value, can_gc);
417    }
418}
419
420pub(crate) fn javascript_error_info_from_error_info(
421    cx: SafeJSContext,
422    error_info: &ErrorInfo,
423    value: HandleValue,
424    _: CanGc,
425) -> JavaScriptErrorInfo {
426    let stack = || {
427        if !value.is_object() {
428            return None;
429        }
430
431        rooted!(in(*cx) let object = value.to_object());
432        let stack_name = CString::new("stack").unwrap();
433        rooted!(in(*cx) let mut stack_value = UndefinedValue());
434        if unsafe {
435            !JS_GetProperty(
436                *cx,
437                object.handle().into(),
438                stack_name.as_ptr(),
439                stack_value.handle_mut().into(),
440            )
441        } {
442            return None;
443        }
444        if !stack_value.is_string() {
445            return None;
446        }
447        let stack_string = NonNull::new(stack_value.to_string())?;
448        Some(unsafe { jsstr_to_string(*cx, stack_string) })
449    };
450
451    JavaScriptErrorInfo {
452        message: error_info.message.clone(),
453        filename: error_info.filename.clone(),
454        line_number: error_info.lineno as u64,
455        column: error_info.column as u64,
456        stack: stack(),
457    }
458}
459
460pub(crate) fn take_and_report_pending_exception_for_api(
461    cx: SafeJSContext,
462    realm: InRealm,
463    can_gc: CanGc,
464) -> Option<JavaScriptErrorInfo> {
465    rooted!(in(*cx) let mut value = UndefinedValue());
466    if !take_pending_exception(cx, value.handle_mut()) {
467        return None;
468    }
469
470    let error_info = ErrorInfo::from_value(value.handle(), cx, can_gc);
471    let return_value =
472        javascript_error_info_from_error_info(cx, &error_info, value.handle(), can_gc);
473    report_error(
474        error_info,
475        value.handle(),
476        cx,
477        true, /* dispatch_event */
478        realm,
479        can_gc,
480    );
481    Some(return_value)
482}
483
484pub(crate) trait ErrorToJsval {
485    fn to_jsval(
486        self,
487        cx: SafeJSContext,
488        global: &GlobalScope,
489        rval: MutableHandleValue,
490        can_gc: CanGc,
491    );
492}
493
494impl ErrorToJsval for Error {
495    /// Convert this error value to a JS value, consuming it in the process.
496    #[allow(clippy::wrong_self_convention)]
497    fn to_jsval(
498        self,
499        cx: SafeJSContext,
500        global: &GlobalScope,
501        rval: MutableHandleValue,
502        can_gc: CanGc,
503    ) {
504        match self {
505            Error::JSFailed => (),
506            _ => unsafe { assert!(!JS_IsExceptionPending(*cx)) },
507        }
508        throw_dom_exception(cx, global, self, can_gc);
509        unsafe {
510            assert!(JS_IsExceptionPending(*cx));
511            assert!(JS_GetPendingException(*cx, rval));
512            JS_ClearPendingException(*cx);
513        }
514    }
515}