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