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