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