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_safe, throw_type_error_safe};
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_safe(cx, &message);
83        },
84
85        Err(JsEngineError::Range(message)) => unsafe {
86            assert!(!JS_IsExceptionPending(cx));
87            throw_range_error_safe(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: &JSContext, object: HandleObject) -> Option<ErrorInfo> {
258        js::rust::borrowed_error_report(cx, |cx, report| {
259            let success = unsafe { JS_ErrorFromException(cx, object, report) };
260            if !success {
261                return None;
262            }
263            let report = report.report_;
264            let filename = {
265                let filename = unsafe { (*report)._base.filename.data_ as *const u8 };
266                if !filename.is_null() {
267                    let filename = unsafe {
268                        let length = (0..).find(|idx| *filename.offset(*idx) == 0).unwrap();
269                        from_raw_parts(filename, length as usize)
270                    };
271                    String::from_utf8_lossy(filename).into_owned()
272                } else {
273                    "none".to_string()
274                }
275            };
276
277            let lineno = unsafe { (*report)._base.lineno };
278            let column = unsafe { (*report)._base.column._base };
279
280            let message = {
281                let message = unsafe { (*report)._base.message_.data_ as *const u8 };
282                let message = unsafe {
283                    let length = (0..).find(|idx| *message.offset(*idx) == 0).unwrap();
284                    from_raw_parts(message, length as usize)
285                };
286                String::from_utf8_lossy(message).into_owned()
287            };
288
289            Some(ErrorInfo {
290                filename,
291                message,
292                lineno,
293                column,
294            })
295        })
296    }
297
298    fn from_dom_exception(cx: &mut JSContext, object: HandleObject) -> Option<ErrorInfo> {
299        let exception = root_from_handleobject::<DOMException>(cx, object).ok()?;
300        let scripted_caller = describe_scripted_caller_safe(cx).unwrap_or_default();
301        Some(ErrorInfo {
302            message: exception.stringifier().into(),
303            filename: scripted_caller.filename,
304            lineno: scripted_caller.line,
305            column: scripted_caller.col + 1,
306        })
307    }
308
309    fn from_object(cx: &mut JSContext, object: HandleObject) -> Option<ErrorInfo> {
310        if let Some(info) = ErrorInfo::from_native_error(cx, object) {
311            return Some(info);
312        }
313        if let Some(info) = ErrorInfo::from_dom_exception(cx, object) {
314            return Some(info);
315        }
316        None
317    }
318
319    /// <https://html.spec.whatwg.org/multipage/#extract-error>
320    pub(crate) fn from_value(cx: &mut JSContext, value: HandleValue) -> ErrorInfo {
321        if value.is_object() {
322            rooted!(&in(cx) let object = value.to_object());
323            if let Some(info) = ErrorInfo::from_object(cx, object.handle()) {
324                return info;
325            }
326        }
327
328        match USVString::safe_from_jsval(cx, value, ()) {
329            Ok(ConversionResult::Success(USVString(string))) => {
330                let scripted_caller = describe_scripted_caller_safe(cx).unwrap_or_default();
331                ErrorInfo {
332                    message: format!("uncaught exception: {}", string),
333                    filename: scripted_caller.filename,
334                    lineno: scripted_caller.line,
335                    column: scripted_caller.col + 1,
336                }
337            },
338            _ => {
339                panic!("uncaught exception: failed to stringify primitive");
340            },
341        }
342    }
343}
344
345/// Report a pending exception, thereby clearing it.
346pub(crate) fn report_pending_exception(cx: &mut CurrentRealm) {
347    rooted!(&in(cx) let mut value = UndefinedValue());
348    if let Some(error_info) = error_info_from_pending_exception(cx, value.handle_mut()) {
349        GlobalScope::from_current_realm(cx).report_an_error(cx, error_info, value.handle());
350    }
351}
352
353fn error_info_from_pending_exception(
354    cx: &mut JSContext,
355    value: MutableHandleValue,
356) -> Option<ErrorInfo> {
357    if unsafe { !JS_IsExceptionPending(cx) } {
358        return None;
359    }
360
361    let error_info = error_info_from_exception_stack_safe(cx, value)?;
362
363    Some(ErrorInfo {
364        message: error_info.message,
365        filename: error_info.filename,
366        lineno: error_info.line,
367        column: error_info.col,
368    })
369}
370
371pub(crate) fn javascript_error_info_from_error_info(
372    cx: &mut JSContext,
373    error_info: &ErrorInfo,
374    value: HandleValue,
375) -> JavaScriptErrorInfo {
376    let mut stack = || {
377        if !value.is_object() {
378            return None;
379        }
380
381        rooted!(&in(cx) let object = value.to_object());
382        rooted!(&in(cx) let mut stack_value = UndefinedValue());
383        if unsafe {
384            !JS_GetProperty(
385                cx,
386                object.handle(),
387                c"stack".as_ptr(),
388                stack_value.handle_mut(),
389            )
390        } {
391            return None;
392        }
393        if !stack_value.is_string() {
394            return None;
395        }
396        let stack_string = NonNull::new(stack_value.to_string())?;
397        Some(unsafe { jsstr_to_string(cx, stack_string) })
398    };
399
400    JavaScriptErrorInfo {
401        message: error_info.message.clone(),
402        filename: error_info.filename.clone(),
403        line_number: error_info.lineno as u64,
404        column: error_info.column as u64,
405        stack: stack(),
406    }
407}
408
409pub(crate) fn take_and_report_pending_exception_for_api(
410    cx: &mut CurrentRealm,
411) -> Option<JavaScriptErrorInfo> {
412    rooted!(&in(cx) let mut value = UndefinedValue());
413    let error_info = error_info_from_pending_exception(cx, value.handle_mut())?;
414
415    let return_value = javascript_error_info_from_error_info(cx, &error_info, value.handle());
416    GlobalScope::from_current_realm(cx).report_an_error(cx, error_info, value.handle());
417
418    Some(return_value)
419}
420
421pub(crate) trait ErrorToJsval {
422    fn to_jsval(self, cx: &mut JSContext, global: &GlobalScope, rval: MutableHandleValue);
423}
424
425impl ErrorToJsval for Error {
426    /// Convert this error value to a JS value, consuming it in the process.
427    fn to_jsval(self, cx: &mut JSContext, global: &GlobalScope, rval: MutableHandleValue) {
428        match self {
429            Error::JSFailed => (),
430            _ => unsafe { assert!(!JS_IsExceptionPending(cx)) },
431        }
432        throw_dom_exception(cx, global, self);
433        unsafe {
434            assert!(JS_IsExceptionPending(cx));
435            assert!(JS_GetPendingException(cx, rval));
436            JS_ClearPendingException(cx);
437        }
438    }
439}