| /* This Source Code Form is subject to the terms of the Mozilla Public |
| * License, v. 2.0. If a copy of the MPL was not distributed with this |
| * file, You can obtain one at https://mozilla.org/MPL/2.0/. */ |
| |
| //! Utilities to throw exceptions from Rust bindings. |
| |
| use std::slice::from_raw_parts; |
| |
| #[cfg(feature = "js_backtrace")] |
| use backtrace::Backtrace; |
| use js::error::{throw_range_error, throw_type_error}; |
| #[cfg(feature = "js_backtrace")] |
| use js::jsapi::StackFormat as JSStackFormat; |
| use js::jsapi::{ExceptionStackBehavior, JS_ClearPendingException, JS_IsExceptionPending}; |
| use js::jsval::UndefinedValue; |
| use js::rust::wrappers::{JS_ErrorFromException, JS_GetPendingException, JS_SetPendingException}; |
| use js::rust::{HandleObject, HandleValue, MutableHandleValue}; |
| use libc::c_uint; |
| use script_bindings::conversions::SafeToJSValConvertible; |
| pub(crate) use script_bindings::error::*; |
| use script_bindings::root::DomRoot; |
| use script_bindings::str::DOMString; |
| |
| #[cfg(feature = "js_backtrace")] |
| use crate::dom::bindings::cell::DomRefCell; |
| use crate::dom::bindings::conversions::{ |
| ConversionResult, SafeFromJSValConvertible, root_from_object, |
| }; |
| use crate::dom::bindings::str::USVString; |
| use crate::dom::domexception::{DOMErrorName, DOMException}; |
| use crate::dom::globalscope::GlobalScope; |
| use crate::dom::types::QuotaExceededError; |
| use crate::realms::InRealm; |
| use crate::script_runtime::{CanGc, JSContext as SafeJSContext}; |
| |
| #[cfg(feature = "js_backtrace")] |
| thread_local! { |
| /// An optional stringified JS backtrace and stringified native backtrace from the |
| /// the last DOM exception that was reported. |
| static LAST_EXCEPTION_BACKTRACE: DomRefCell<Option<(Option<String>, String)>> = DomRefCell::new(None); |
| } |
| |
| /// Error values that have no equivalent DOMException representation. |
| pub(crate) enum JsEngineError { |
| /// An EMCAScript TypeError. |
| Type(String), |
| /// An ECMAScript RangeError. |
| Range(String), |
| /// The JS engine reported a thrown exception. |
| JSFailed, |
| } |
| |
| /// Set a pending exception for the given `result` on `cx`. |
| pub(crate) fn throw_dom_exception( |
| cx: SafeJSContext, |
| global: &GlobalScope, |
| result: Error, |
| can_gc: CanGc, |
| ) { |
| #[cfg(feature = "js_backtrace")] |
| unsafe { |
| capture_stack!(in(*cx) let stack); |
| let js_stack = stack.and_then(|s| s.as_string(None, JSStackFormat::Default)); |
| let rust_stack = Backtrace::new(); |
| LAST_EXCEPTION_BACKTRACE.with(|backtrace| { |
| *backtrace.borrow_mut() = Some((js_stack, format!("{:?}", rust_stack))); |
| }); |
| } |
| |
| match create_dom_exception(global, result, can_gc) { |
| Ok(exception) => unsafe { |
| assert!(!JS_IsExceptionPending(*cx)); |
| rooted!(in(*cx) let mut thrown = UndefinedValue()); |
| exception.safe_to_jsval(cx, thrown.handle_mut()); |
| JS_SetPendingException(*cx, thrown.handle(), ExceptionStackBehavior::Capture); |
| }, |
| |
| Err(JsEngineError::Type(message)) => unsafe { |
| assert!(!JS_IsExceptionPending(*cx)); |
| throw_type_error(*cx, &message); |
| }, |
| |
| Err(JsEngineError::Range(message)) => unsafe { |
| assert!(!JS_IsExceptionPending(*cx)); |
| throw_range_error(*cx, &message); |
| }, |
| |
| Err(JsEngineError::JSFailed) => unsafe { |
| assert!(JS_IsExceptionPending(*cx)); |
| }, |
| } |
| } |
| |
| /// If possible, create a new DOMException representing the provided error. |
| /// If no such DOMException exists, return a subset of the original error values |
| /// that may need additional handling. |
| pub(crate) fn create_dom_exception( |
| global: &GlobalScope, |
| result: Error, |
| can_gc: CanGc, |
| ) -> Result<DomRoot<DOMException>, JsEngineError> { |
| let code = match result { |
| Error::IndexSize => DOMErrorName::IndexSizeError, |
| Error::NotFound => DOMErrorName::NotFoundError, |
| Error::HierarchyRequest => DOMErrorName::HierarchyRequestError, |
| Error::WrongDocument => DOMErrorName::WrongDocumentError, |
| Error::InvalidCharacter => DOMErrorName::InvalidCharacterError, |
| Error::NotSupported => DOMErrorName::NotSupportedError, |
| Error::InUseAttribute => DOMErrorName::InUseAttributeError, |
| Error::InvalidState => DOMErrorName::InvalidStateError, |
| Error::Syntax(Some(custom_message)) => { |
| return Ok(DOMException::new_with_custom_message( |
| global, |
| DOMErrorName::SyntaxError, |
| custom_message, |
| can_gc, |
| )); |
| }, |
| Error::Syntax(None) => DOMErrorName::SyntaxError, |
| Error::Namespace => DOMErrorName::NamespaceError, |
| Error::InvalidAccess => DOMErrorName::InvalidAccessError, |
| Error::Security => DOMErrorName::SecurityError, |
| Error::Network => DOMErrorName::NetworkError, |
| Error::Abort => DOMErrorName::AbortError, |
| Error::Timeout => DOMErrorName::TimeoutError, |
| Error::InvalidNodeType => DOMErrorName::InvalidNodeTypeError, |
| Error::DataClone(Some(custom_message)) => { |
| return Ok(DOMException::new_with_custom_message( |
| global, |
| DOMErrorName::DataCloneError, |
| custom_message, |
| can_gc, |
| )); |
| }, |
| Error::DataClone(None) => DOMErrorName::DataCloneError, |
| Error::Data => DOMErrorName::DataError, |
| Error::TransactionInactive => DOMErrorName::TransactionInactiveError, |
| Error::ReadOnly => DOMErrorName::ReadOnlyError, |
| Error::Version => DOMErrorName::VersionError, |
| Error::NoModificationAllowed => DOMErrorName::NoModificationAllowedError, |
| Error::QuotaExceeded { quota, requested } => { |
| return Ok(DomRoot::upcast(QuotaExceededError::new( |
| global, |
| DOMString::new(), |
| quota, |
| requested, |
| can_gc, |
| ))); |
| }, |
| Error::TypeMismatch => DOMErrorName::TypeMismatchError, |
| Error::InvalidModification => DOMErrorName::InvalidModificationError, |
| Error::NotReadable => DOMErrorName::NotReadableError, |
| Error::Operation => DOMErrorName::OperationError, |
| Error::NotAllowed => DOMErrorName::NotAllowedError, |
| Error::Encoding => DOMErrorName::EncodingError, |
| Error::Constraint => DOMErrorName::ConstraintError, |
| Error::Type(message) => return Err(JsEngineError::Type(message)), |
| Error::Range(message) => return Err(JsEngineError::Range(message)), |
| Error::JSFailed => return Err(JsEngineError::JSFailed), |
| }; |
| Ok(DOMException::new(global, code, can_gc)) |
| } |
| |
| /// A struct encapsulating information about a runtime script error. |
| #[derive(Default)] |
| pub(crate) struct ErrorInfo { |
| /// The error message. |
| pub(crate) message: String, |
| /// The file name. |
| pub(crate) filename: String, |
| /// The line number. |
| pub(crate) lineno: c_uint, |
| /// The column number. |
| pub(crate) column: c_uint, |
| } |
| |
| impl ErrorInfo { |
| fn from_native_error(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> { |
| let report = unsafe { JS_ErrorFromException(*cx, object) }; |
| if report.is_null() { |
| return None; |
| } |
| |
| let filename = { |
| let filename = unsafe { (*report)._base.filename.data_ as *const u8 }; |
| if !filename.is_null() { |
| let filename = unsafe { |
| let length = (0..).find(|idx| *filename.offset(*idx) == 0).unwrap(); |
| from_raw_parts(filename, length as usize) |
| }; |
| String::from_utf8_lossy(filename).into_owned() |
| } else { |
| "none".to_string() |
| } |
| }; |
| |
| let lineno = unsafe { (*report)._base.lineno }; |
| let column = unsafe { (*report)._base.column._base }; |
| |
| let message = { |
| let message = unsafe { (*report)._base.message_.data_ as *const u8 }; |
| let message = unsafe { |
| let length = (0..).find(|idx| *message.offset(*idx) == 0).unwrap(); |
| from_raw_parts(message, length as usize) |
| }; |
| String::from_utf8_lossy(message).into_owned() |
| }; |
| |
| Some(ErrorInfo { |
| filename, |
| message, |
| lineno, |
| column, |
| }) |
| } |
| |
| fn from_dom_exception(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> { |
| let exception = unsafe { root_from_object::<DOMException>(object.get(), *cx).ok()? }; |
| Some(ErrorInfo { |
| filename: "".to_string(), |
| message: exception.stringifier().into(), |
| lineno: 0, |
| column: 0, |
| }) |
| } |
| |
| fn from_object(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> { |
| if let Some(info) = ErrorInfo::from_native_error(object, cx) { |
| return Some(info); |
| } |
| if let Some(info) = ErrorInfo::from_dom_exception(object, cx) { |
| return Some(info); |
| } |
| None |
| } |
| |
| fn from_value(value: HandleValue, cx: SafeJSContext) -> ErrorInfo { |
| if value.is_object() { |
| rooted!(in(*cx) let object = value.to_object()); |
| if let Some(info) = ErrorInfo::from_object(object.handle(), cx) { |
| return info; |
| } |
| } |
| |
| match USVString::safe_from_jsval(cx, value, ()) { |
| Ok(ConversionResult::Success(USVString(string))) => ErrorInfo { |
| message: format!("uncaught exception: {}", string), |
| filename: String::new(), |
| lineno: 0, |
| column: 0, |
| }, |
| _ => { |
| panic!("uncaught exception: failed to stringify primitive"); |
| }, |
| } |
| } |
| } |
| |
| /// Report a pending exception, thereby clearing it. |
| /// |
| /// The `dispatch_event` argument is temporary and non-standard; passing false |
| /// prevents dispatching the `error` event. |
| pub(crate) fn report_pending_exception( |
| cx: SafeJSContext, |
| dispatch_event: bool, |
| realm: InRealm, |
| can_gc: CanGc, |
| ) { |
| unsafe { |
| if !JS_IsExceptionPending(*cx) { |
| return; |
| } |
| } |
| rooted!(in(*cx) let mut value = UndefinedValue()); |
| |
| unsafe { |
| if !JS_GetPendingException(*cx, value.handle_mut()) { |
| JS_ClearPendingException(*cx); |
| error!("Uncaught exception: JS_GetPendingException failed"); |
| return; |
| } |
| |
| JS_ClearPendingException(*cx); |
| } |
| let error_info = ErrorInfo::from_value(value.handle(), cx); |
| |
| error!( |
| "Error at {}:{}:{} {}", |
| error_info.filename, error_info.lineno, error_info.column, error_info.message |
| ); |
| |
| #[cfg(feature = "js_backtrace")] |
| { |
| LAST_EXCEPTION_BACKTRACE.with(|backtrace| { |
| if let Some((js_backtrace, rust_backtrace)) = backtrace.borrow_mut().take() { |
| if let Some(stack) = js_backtrace { |
| eprintln!("JS backtrace:\n{}", stack); |
| } |
| eprintln!("Rust backtrace:\n{}", rust_backtrace); |
| } |
| }); |
| } |
| |
| if dispatch_event { |
| GlobalScope::from_safe_context(cx, realm).report_an_error( |
| error_info, |
| value.handle(), |
| can_gc, |
| ); |
| } |
| } |
| |
| pub(crate) trait ErrorToJsval { |
| fn to_jsval( |
| self, |
| cx: SafeJSContext, |
| global: &GlobalScope, |
| rval: MutableHandleValue, |
| can_gc: CanGc, |
| ); |
| } |
| |
| impl ErrorToJsval for Error { |
| /// Convert this error value to a JS value, consuming it in the process. |
| #[allow(clippy::wrong_self_convention)] |
| fn to_jsval( |
| self, |
| cx: SafeJSContext, |
| global: &GlobalScope, |
| rval: MutableHandleValue, |
| can_gc: CanGc, |
| ) { |
| match self { |
| Error::JSFailed => (), |
| _ => unsafe { assert!(!JS_IsExceptionPending(*cx)) }, |
| } |
| throw_dom_exception(cx, global, self, can_gc); |
| unsafe { |
| assert!(JS_IsExceptionPending(*cx)); |
| assert!(JS_GetPendingException(*cx, rval)); |
| JS_ClearPendingException(*cx); |
| } |
| } |
| } |