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