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