Skip to main content

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