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::slice::from_raw_parts;
8
9#[cfg(feature = "js_backtrace")]
10use backtrace::Backtrace;
11use js::error::{throw_range_error, throw_type_error};
12#[cfg(feature = "js_backtrace")]
13use js::jsapi::StackFormat as JSStackFormat;
14use js::jsapi::{ExceptionStackBehavior, JS_ClearPendingException, JS_IsExceptionPending};
15use js::jsval::UndefinedValue;
16use js::rust::wrappers::{JS_ErrorFromException, JS_GetPendingException, JS_SetPendingException};
17use js::rust::{HandleObject, HandleValue, MutableHandleValue};
18use libc::c_uint;
19use script_bindings::conversions::SafeToJSValConvertible;
20pub(crate) use script_bindings::error::*;
21use script_bindings::root::DomRoot;
22use script_bindings::str::DOMString;
23
24#[cfg(feature = "js_backtrace")]
25use crate::dom::bindings::cell::DomRefCell;
26use crate::dom::bindings::conversions::{
27    ConversionResult, SafeFromJSValConvertible, root_from_object,
28};
29use crate::dom::bindings::str::USVString;
30use crate::dom::domexception::{DOMErrorName, DOMException};
31use crate::dom::globalscope::GlobalScope;
32use crate::dom::types::QuotaExceededError;
33use crate::realms::InRealm;
34use crate::script_runtime::{CanGc, JSContext as SafeJSContext};
35
36#[cfg(feature = "js_backtrace")]
37thread_local! {
38    /// An optional stringified JS backtrace and stringified native backtrace from the
39    /// the last DOM exception that was reported.
40    static LAST_EXCEPTION_BACKTRACE: DomRefCell<Option<(Option<String>, String)>> = DomRefCell::new(None);
41}
42
43/// Error values that have no equivalent DOMException representation.
44pub(crate) enum JsEngineError {
45    /// An EMCAScript TypeError.
46    Type(String),
47    /// An ECMAScript RangeError.
48    Range(String),
49    /// The JS engine reported a thrown exception.
50    JSFailed,
51}
52
53/// Set a pending exception for the given `result` on `cx`.
54pub(crate) fn throw_dom_exception(
55    cx: SafeJSContext,
56    global: &GlobalScope,
57    result: Error,
58    can_gc: CanGc,
59) {
60    #[cfg(feature = "js_backtrace")]
61    unsafe {
62        capture_stack!(in(*cx) let stack);
63        let js_stack = stack.and_then(|s| s.as_string(None, JSStackFormat::Default));
64        let rust_stack = Backtrace::new();
65        LAST_EXCEPTION_BACKTRACE.with(|backtrace| {
66            *backtrace.borrow_mut() = Some((js_stack, format!("{:?}", rust_stack)));
67        });
68    }
69
70    match create_dom_exception(global, result, can_gc) {
71        Ok(exception) => unsafe {
72            assert!(!JS_IsExceptionPending(*cx));
73            rooted!(in(*cx) let mut thrown = UndefinedValue());
74            exception.safe_to_jsval(cx, thrown.handle_mut());
75            JS_SetPendingException(*cx, thrown.handle(), ExceptionStackBehavior::Capture);
76        },
77
78        Err(JsEngineError::Type(message)) => unsafe {
79            assert!(!JS_IsExceptionPending(*cx));
80            throw_type_error(*cx, &message);
81        },
82
83        Err(JsEngineError::Range(message)) => unsafe {
84            assert!(!JS_IsExceptionPending(*cx));
85            throw_range_error(*cx, &message);
86        },
87
88        Err(JsEngineError::JSFailed) => unsafe {
89            assert!(JS_IsExceptionPending(*cx));
90        },
91    }
92}
93
94/// If possible, create a new DOMException representing the provided error.
95/// If no such DOMException exists, return a subset of the original error values
96/// that may need additional handling.
97pub(crate) fn create_dom_exception(
98    global: &GlobalScope,
99    result: Error,
100    can_gc: CanGc,
101) -> Result<DomRoot<DOMException>, JsEngineError> {
102    let code = match result {
103        Error::IndexSize => DOMErrorName::IndexSizeError,
104        Error::NotFound => DOMErrorName::NotFoundError,
105        Error::HierarchyRequest => DOMErrorName::HierarchyRequestError,
106        Error::WrongDocument => DOMErrorName::WrongDocumentError,
107        Error::InvalidCharacter => DOMErrorName::InvalidCharacterError,
108        Error::NotSupported => DOMErrorName::NotSupportedError,
109        Error::InUseAttribute => DOMErrorName::InUseAttributeError,
110        Error::InvalidState => DOMErrorName::InvalidStateError,
111        Error::Syntax(Some(custom_message)) => {
112            return Ok(DOMException::new_with_custom_message(
113                global,
114                DOMErrorName::SyntaxError,
115                custom_message,
116                can_gc,
117            ));
118        },
119        Error::Syntax(None) => DOMErrorName::SyntaxError,
120        Error::Namespace => DOMErrorName::NamespaceError,
121        Error::InvalidAccess => DOMErrorName::InvalidAccessError,
122        Error::Security => DOMErrorName::SecurityError,
123        Error::Network => DOMErrorName::NetworkError,
124        Error::Abort => DOMErrorName::AbortError,
125        Error::Timeout => DOMErrorName::TimeoutError,
126        Error::InvalidNodeType => DOMErrorName::InvalidNodeTypeError,
127        Error::DataClone(Some(custom_message)) => {
128            return Ok(DOMException::new_with_custom_message(
129                global,
130                DOMErrorName::DataCloneError,
131                custom_message,
132                can_gc,
133            ));
134        },
135        Error::DataClone(None) => DOMErrorName::DataCloneError,
136        Error::Data => DOMErrorName::DataError,
137        Error::TransactionInactive => DOMErrorName::TransactionInactiveError,
138        Error::ReadOnly => DOMErrorName::ReadOnlyError,
139        Error::Version => DOMErrorName::VersionError,
140        Error::NoModificationAllowed => DOMErrorName::NoModificationAllowedError,
141        Error::QuotaExceeded { quota, requested } => {
142            return Ok(DomRoot::upcast(QuotaExceededError::new(
143                global,
144                DOMString::new(),
145                quota,
146                requested,
147                can_gc,
148            )));
149        },
150        Error::TypeMismatch => DOMErrorName::TypeMismatchError,
151        Error::InvalidModification => DOMErrorName::InvalidModificationError,
152        Error::NotReadable => DOMErrorName::NotReadableError,
153        Error::Operation => DOMErrorName::OperationError,
154        Error::NotAllowed => DOMErrorName::NotAllowedError,
155        Error::Encoding => DOMErrorName::EncodingError,
156        Error::Constraint => DOMErrorName::ConstraintError,
157        Error::Type(message) => return Err(JsEngineError::Type(message)),
158        Error::Range(message) => return Err(JsEngineError::Range(message)),
159        Error::JSFailed => return Err(JsEngineError::JSFailed),
160    };
161    Ok(DOMException::new(global, code, can_gc))
162}
163
164/// A struct encapsulating information about a runtime script error.
165#[derive(Default)]
166pub(crate) struct ErrorInfo {
167    /// The error message.
168    pub(crate) message: String,
169    /// The file name.
170    pub(crate) filename: String,
171    /// The line number.
172    pub(crate) lineno: c_uint,
173    /// The column number.
174    pub(crate) column: c_uint,
175}
176
177impl ErrorInfo {
178    fn from_native_error(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
179        let report = unsafe { JS_ErrorFromException(*cx, object) };
180        if report.is_null() {
181            return None;
182        }
183
184        let filename = {
185            let filename = unsafe { (*report)._base.filename.data_ as *const u8 };
186            if !filename.is_null() {
187                let filename = unsafe {
188                    let length = (0..).find(|idx| *filename.offset(*idx) == 0).unwrap();
189                    from_raw_parts(filename, length as usize)
190                };
191                String::from_utf8_lossy(filename).into_owned()
192            } else {
193                "none".to_string()
194            }
195        };
196
197        let lineno = unsafe { (*report)._base.lineno };
198        let column = unsafe { (*report)._base.column._base };
199
200        let message = {
201            let message = unsafe { (*report)._base.message_.data_ as *const u8 };
202            let message = unsafe {
203                let length = (0..).find(|idx| *message.offset(*idx) == 0).unwrap();
204                from_raw_parts(message, length as usize)
205            };
206            String::from_utf8_lossy(message).into_owned()
207        };
208
209        Some(ErrorInfo {
210            filename,
211            message,
212            lineno,
213            column,
214        })
215    }
216
217    fn from_dom_exception(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
218        let exception = unsafe { root_from_object::<DOMException>(object.get(), *cx).ok()? };
219        Some(ErrorInfo {
220            filename: "".to_string(),
221            message: exception.stringifier().into(),
222            lineno: 0,
223            column: 0,
224        })
225    }
226
227    fn from_object(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
228        if let Some(info) = ErrorInfo::from_native_error(object, cx) {
229            return Some(info);
230        }
231        if let Some(info) = ErrorInfo::from_dom_exception(object, cx) {
232            return Some(info);
233        }
234        None
235    }
236
237    fn from_value(value: HandleValue, cx: SafeJSContext) -> ErrorInfo {
238        if value.is_object() {
239            rooted!(in(*cx) let object = value.to_object());
240            if let Some(info) = ErrorInfo::from_object(object.handle(), cx) {
241                return info;
242            }
243        }
244
245        match USVString::safe_from_jsval(cx, value, ()) {
246            Ok(ConversionResult::Success(USVString(string))) => ErrorInfo {
247                message: format!("uncaught exception: {}", string),
248                filename: String::new(),
249                lineno: 0,
250                column: 0,
251            },
252            _ => {
253                panic!("uncaught exception: failed to stringify primitive");
254            },
255        }
256    }
257}
258
259/// Report a pending exception, thereby clearing it.
260///
261/// The `dispatch_event` argument is temporary and non-standard; passing false
262/// prevents dispatching the `error` event.
263pub(crate) fn report_pending_exception(
264    cx: SafeJSContext,
265    dispatch_event: bool,
266    realm: InRealm,
267    can_gc: CanGc,
268) {
269    unsafe {
270        if !JS_IsExceptionPending(*cx) {
271            return;
272        }
273    }
274    rooted!(in(*cx) let mut value = UndefinedValue());
275
276    unsafe {
277        if !JS_GetPendingException(*cx, value.handle_mut()) {
278            JS_ClearPendingException(*cx);
279            error!("Uncaught exception: JS_GetPendingException failed");
280            return;
281        }
282
283        JS_ClearPendingException(*cx);
284    }
285    let error_info = ErrorInfo::from_value(value.handle(), cx);
286
287    error!(
288        "Error at {}:{}:{} {}",
289        error_info.filename, error_info.lineno, error_info.column, error_info.message
290    );
291
292    #[cfg(feature = "js_backtrace")]
293    {
294        LAST_EXCEPTION_BACKTRACE.with(|backtrace| {
295            if let Some((js_backtrace, rust_backtrace)) = backtrace.borrow_mut().take() {
296                if let Some(stack) = js_backtrace {
297                    eprintln!("JS backtrace:\n{}", stack);
298                }
299                eprintln!("Rust backtrace:\n{}", rust_backtrace);
300            }
301        });
302    }
303
304    if dispatch_event {
305        GlobalScope::from_safe_context(cx, realm).report_an_error(
306            error_info,
307            value.handle(),
308            can_gc,
309        );
310    }
311}
312
313pub(crate) trait ErrorToJsval {
314    fn to_jsval(
315        self,
316        cx: SafeJSContext,
317        global: &GlobalScope,
318        rval: MutableHandleValue,
319        can_gc: CanGc,
320    );
321}
322
323impl ErrorToJsval for Error {
324    /// Convert this error value to a JS value, consuming it in the process.
325    #[allow(clippy::wrong_self_convention)]
326    fn to_jsval(
327        self,
328        cx: SafeJSContext,
329        global: &GlobalScope,
330        rval: MutableHandleValue,
331        can_gc: CanGc,
332    ) {
333        match self {
334            Error::JSFailed => (),
335            _ => unsafe { assert!(!JS_IsExceptionPending(*cx)) },
336        }
337        throw_dom_exception(cx, global, self, can_gc);
338        unsafe {
339            assert!(JS_IsExceptionPending(*cx));
340            assert!(JS_GetPendingException(*cx, rval));
341            JS_ClearPendingException(*cx);
342        }
343    }
344}