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