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};
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 => DOMErrorName::NotSupportedError,
136        Error::InUseAttribute => DOMErrorName::InUseAttributeError,
137        Error::InvalidState(Some(custom_message)) => {
138            return new_custom_exception(DOMErrorName::InvalidStateError, custom_message);
139        },
140        Error::InvalidState(None) => DOMErrorName::InvalidStateError,
141        Error::Syntax(Some(custom_message)) => {
142            return new_custom_exception(DOMErrorName::SyntaxError, custom_message);
143        },
144        Error::Syntax(None) => DOMErrorName::SyntaxError,
145        Error::Namespace => DOMErrorName::NamespaceError,
146        Error::InvalidAccess => DOMErrorName::InvalidAccessError,
147        Error::Security => DOMErrorName::SecurityError,
148        Error::Network => DOMErrorName::NetworkError,
149        Error::Abort => DOMErrorName::AbortError,
150        Error::Timeout => DOMErrorName::TimeoutError,
151        Error::InvalidNodeType => DOMErrorName::InvalidNodeTypeError,
152        Error::DataClone(Some(custom_message)) => {
153            return new_custom_exception(DOMErrorName::DataCloneError, custom_message);
154        },
155        Error::DataClone(None) => DOMErrorName::DataCloneError,
156        Error::Data => DOMErrorName::DataError,
157        Error::TransactionInactive => DOMErrorName::TransactionInactiveError,
158        Error::ReadOnly => DOMErrorName::ReadOnlyError,
159        Error::Version => DOMErrorName::VersionError,
160        Error::NoModificationAllowed => DOMErrorName::NoModificationAllowedError,
161        Error::QuotaExceeded { quota, requested } => {
162            return Ok(DomRoot::upcast(QuotaExceededError::new(
163                global,
164                DOMString::new(),
165                quota,
166                requested,
167                can_gc,
168            )));
169        },
170        Error::TypeMismatch => DOMErrorName::TypeMismatchError,
171        Error::InvalidModification => DOMErrorName::InvalidModificationError,
172        Error::NotReadable => DOMErrorName::NotReadableError,
173        Error::Operation => DOMErrorName::OperationError,
174        Error::NotAllowed => DOMErrorName::NotAllowedError,
175        Error::Encoding => DOMErrorName::EncodingError,
176        Error::Constraint => DOMErrorName::ConstraintError,
177        Error::Type(message) => return Err(JsEngineError::Type(message)),
178        Error::Range(message) => return Err(JsEngineError::Range(message)),
179        Error::JSFailed => return Err(JsEngineError::JSFailed),
180    };
181    Ok(DOMException::new(global, code, can_gc))
182}
183
184/// A struct encapsulating information about a runtime script error.
185#[derive(Default)]
186pub(crate) struct ErrorInfo {
187    /// The error message.
188    pub(crate) message: String,
189    /// The file name.
190    pub(crate) filename: String,
191    /// The line number.
192    pub(crate) lineno: c_uint,
193    /// The column number.
194    pub(crate) column: c_uint,
195}
196
197impl ErrorInfo {
198    fn from_native_error(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
199        let report = unsafe { JS_ErrorFromException(*cx, object) };
200        if report.is_null() {
201            return None;
202        }
203
204        let filename = {
205            let filename = unsafe { (*report)._base.filename.data_ as *const u8 };
206            if !filename.is_null() {
207                let filename = unsafe {
208                    let length = (0..).find(|idx| *filename.offset(*idx) == 0).unwrap();
209                    from_raw_parts(filename, length as usize)
210                };
211                String::from_utf8_lossy(filename).into_owned()
212            } else {
213                "none".to_string()
214            }
215        };
216
217        let lineno = unsafe { (*report)._base.lineno };
218        let column = unsafe { (*report)._base.column._base };
219
220        let message = {
221            let message = unsafe { (*report)._base.message_.data_ as *const u8 };
222            let message = unsafe {
223                let length = (0..).find(|idx| *message.offset(*idx) == 0).unwrap();
224                from_raw_parts(message, length as usize)
225            };
226            String::from_utf8_lossy(message).into_owned()
227        };
228
229        Some(ErrorInfo {
230            filename,
231            message,
232            lineno,
233            column,
234        })
235    }
236
237    fn from_dom_exception(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
238        let exception = unsafe { root_from_object::<DOMException>(object.get(), *cx).ok()? };
239        Some(ErrorInfo {
240            filename: "".to_string(),
241            message: exception.stringifier().into(),
242            lineno: 0,
243            column: 0,
244        })
245    }
246
247    fn from_object(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
248        if let Some(info) = ErrorInfo::from_native_error(object, cx) {
249            return Some(info);
250        }
251        if let Some(info) = ErrorInfo::from_dom_exception(object, cx) {
252            return Some(info);
253        }
254        None
255    }
256
257    pub(crate) fn from_value(value: HandleValue, cx: SafeJSContext, can_gc: CanGc) -> ErrorInfo {
258        if value.is_object() {
259            rooted!(in(*cx) let object = value.to_object());
260            if let Some(info) = ErrorInfo::from_object(object.handle(), cx) {
261                return info;
262            }
263        }
264
265        match USVString::safe_from_jsval(cx, value, (), can_gc) {
266            Ok(ConversionResult::Success(USVString(string))) => ErrorInfo {
267                message: format!("uncaught exception: {}", string),
268                filename: String::new(),
269                lineno: 0,
270                column: 0,
271            },
272            _ => {
273                panic!("uncaught exception: failed to stringify primitive");
274            },
275        }
276    }
277}
278
279/// Report a pending exception, thereby clearing it.
280///
281/// The `dispatch_event` argument is temporary and non-standard; passing false
282/// prevents dispatching the `error` event.
283pub(crate) fn report_pending_exception(
284    cx: SafeJSContext,
285    dispatch_event: bool,
286    realm: InRealm,
287    can_gc: CanGc,
288) {
289    rooted!(in(*cx) let mut value = UndefinedValue());
290    if take_pending_exception(cx, value.handle_mut()) {
291        let error_info = ErrorInfo::from_value(value.handle(), cx, can_gc);
292        report_error(
293            error_info,
294            value.handle(),
295            cx,
296            dispatch_event,
297            realm,
298            can_gc,
299        );
300    }
301}
302
303fn take_pending_exception(cx: SafeJSContext, value: MutableHandleValue) -> bool {
304    unsafe {
305        if !JS_IsExceptionPending(*cx) {
306            return false;
307        }
308    }
309
310    unsafe {
311        if !JS_GetPendingException(*cx, value) {
312            JS_ClearPendingException(*cx);
313            error!("Uncaught exception: JS_GetPendingException failed");
314            return false;
315        }
316
317        JS_ClearPendingException(*cx);
318    }
319    true
320}
321
322fn report_error(
323    error_info: ErrorInfo,
324    value: HandleValue,
325    cx: SafeJSContext,
326    dispatch_event: bool,
327    realm: InRealm,
328    can_gc: CanGc,
329) {
330    error!(
331        "Error at {}:{}:{} {}",
332        error_info.filename, error_info.lineno, error_info.column, error_info.message
333    );
334
335    #[cfg(feature = "js_backtrace")]
336    {
337        LAST_EXCEPTION_BACKTRACE.with(|backtrace| {
338            if let Some((js_backtrace, rust_backtrace)) = backtrace.borrow_mut().take() {
339                if let Some(stack) = js_backtrace {
340                    eprintln!("JS backtrace:\n{}", stack);
341                }
342                eprintln!("Rust backtrace:\n{}", rust_backtrace);
343            }
344        });
345    }
346
347    if dispatch_event {
348        GlobalScope::from_safe_context(cx, realm).report_an_error(error_info, value, can_gc);
349    }
350}
351
352pub(crate) fn javascript_error_info_from_error_info(
353    cx: SafeJSContext,
354    error_info: &ErrorInfo,
355    value: HandleValue,
356    _: CanGc,
357) -> JavaScriptErrorInfo {
358    let stack = || {
359        if !value.is_object() {
360            return None;
361        }
362
363        rooted!(in(*cx) let object = value.to_object());
364        let stack_name = CString::new("stack").unwrap();
365        rooted!(in(*cx) let mut stack_value = UndefinedValue());
366        if unsafe {
367            !JS_GetProperty(
368                *cx,
369                object.handle().into(),
370                stack_name.as_ptr(),
371                stack_value.handle_mut().into(),
372            )
373        } {
374            return None;
375        }
376        if !stack_value.is_string() {
377            return None;
378        }
379        let stack_string = NonNull::new(stack_value.to_string())?;
380        Some(unsafe { jsstr_to_string(*cx, stack_string) })
381    };
382
383    JavaScriptErrorInfo {
384        message: error_info.message.clone(),
385        filename: error_info.filename.clone(),
386        line_number: error_info.lineno as u64,
387        column: error_info.column as u64,
388        stack: stack(),
389    }
390}
391
392pub(crate) fn take_and_report_pending_exception_for_api(
393    cx: SafeJSContext,
394    realm: InRealm,
395    can_gc: CanGc,
396) -> Option<JavaScriptErrorInfo> {
397    rooted!(in(*cx) let mut value = UndefinedValue());
398    if !take_pending_exception(cx, value.handle_mut()) {
399        return None;
400    }
401
402    let error_info = ErrorInfo::from_value(value.handle(), cx, can_gc);
403    let return_value =
404        javascript_error_info_from_error_info(cx, &error_info, value.handle(), can_gc);
405    report_error(
406        error_info,
407        value.handle(),
408        cx,
409        true, /* dispatch_event */
410        realm,
411        can_gc,
412    );
413    Some(return_value)
414}
415
416pub(crate) trait ErrorToJsval {
417    fn to_jsval(
418        self,
419        cx: SafeJSContext,
420        global: &GlobalScope,
421        rval: MutableHandleValue,
422        can_gc: CanGc,
423    );
424}
425
426impl ErrorToJsval for Error {
427    /// Convert this error value to a JS value, consuming it in the process.
428    #[allow(clippy::wrong_self_convention)]
429    fn to_jsval(
430        self,
431        cx: SafeJSContext,
432        global: &GlobalScope,
433        rval: MutableHandleValue,
434        can_gc: CanGc,
435    ) {
436        match self {
437            Error::JSFailed => (),
438            _ => unsafe { assert!(!JS_IsExceptionPending(*cx)) },
439        }
440        throw_dom_exception(cx, global, self, can_gc);
441        unsafe {
442            assert!(JS_IsExceptionPending(*cx));
443            assert!(JS_GetPendingException(*cx, rval));
444            JS_ClearPendingException(*cx);
445        }
446    }
447}