use std::ffi::CString;
use std::ptr::NonNull;
use std::slice::from_raw_parts;
#[cfg(feature = "js_backtrace")]
use backtrace::Backtrace;
use embedder_traits::JavaScriptErrorInfo;
use js::context::JSContext;
use js::conversions::jsstr_to_string;
use js::error::{throw_range_error, throw_type_error};
use js::gc::{HandleObject, HandleValue, MutableHandleValue};
use js::jsapi::ExceptionStackBehavior;
#[cfg(feature = "js_backtrace")]
use js::jsapi::StackFormat as JSStackFormat;
use js::jsval::UndefinedValue;
use js::realm::CurrentRealm;
use js::rust::wrappers2::{
JS_ClearPendingException, JS_ErrorFromException, JS_GetPendingException, JS_GetProperty,
JS_IsExceptionPending, JS_SetPendingException,
};
use js::rust::{describe_scripted_caller, error_info_from_exception_stack};
use libc::c_uint;
#[cfg(feature = "js_backtrace")]
use script_bindings::cell::DomRefCell;
use script_bindings::conversions::SafeToJSValConvertible;
pub(crate) use script_bindings::error::*;
use script_bindings::root::DomRoot;
use script_bindings::str::DOMString;
use crate::dom::bindings::conversions::{ConversionResult, FromJSValConvertible, 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;
#[cfg(feature = "js_backtrace")]
thread_local! {
pub(crate) static LAST_EXCEPTION_BACKTRACE: DomRefCell<Option<(Option<String>, String)>> = DomRefCell::new(None);
}
pub(crate) enum JsEngineError {
Type(CString),
Range(CString),
JSFailed,
}
pub(crate) fn throw_dom_exception(cx: &mut JSContext, global: &GlobalScope, result: Error) {
#[cfg(feature = "js_backtrace")]
unsafe {
capture_stack!(&in(cx) let stack);
let js_stack = stack.and_then(|stack| stack.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(cx, global, result) {
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.raw_cx(), &message);
},
Err(JsEngineError::Range(message)) => unsafe {
assert!(!JS_IsExceptionPending(cx));
throw_range_error(cx.raw_cx(), &message);
},
Err(JsEngineError::JSFailed) => unsafe {
assert!(JS_IsExceptionPending(cx));
},
}
}
pub(crate) fn create_dom_exception(
cx: &mut JSContext,
global: &GlobalScope,
result: Error,
) -> Result<DomRoot<DOMException>, JsEngineError> {
let mut new_custom_exception = |error_name, message| {
Ok(DOMException::new_with_custom_message(
cx, global, error_name, message,
))
};
let code = match result {
Error::IndexSize(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::IndexSizeError, custom_message);
},
Error::IndexSize(None) => DOMErrorName::IndexSizeError,
Error::NotFound(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::NotFoundError, custom_message);
},
Error::NotFound(None) => DOMErrorName::NotFoundError,
Error::HierarchyRequest(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::HierarchyRequestError, custom_message);
},
Error::HierarchyRequest(None) => DOMErrorName::HierarchyRequestError,
Error::WrongDocument(Some(doc_err_custom_message)) => {
return new_custom_exception(DOMErrorName::WrongDocumentError, doc_err_custom_message);
},
Error::WrongDocument(None) => DOMErrorName::WrongDocumentError,
Error::InvalidCharacter(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::InvalidCharacterError, custom_message);
},
Error::InvalidCharacter(None) => DOMErrorName::InvalidCharacterError,
Error::NotSupported(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::NotSupportedError, custom_message);
},
Error::NotSupported(None) => DOMErrorName::NotSupportedError,
Error::InUseAttribute(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::InUseAttributeError, custom_message);
},
Error::InUseAttribute(None) => DOMErrorName::InUseAttributeError,
Error::InvalidState(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::InvalidStateError, custom_message);
},
Error::InvalidState(None) => DOMErrorName::InvalidStateError,
Error::Syntax(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::SyntaxError, custom_message);
},
Error::Syntax(None) => DOMErrorName::SyntaxError,
Error::Namespace(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::NamespaceError, custom_message);
},
Error::Namespace(None) => DOMErrorName::NamespaceError,
Error::InvalidAccess(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::InvalidAccessError, custom_message);
},
Error::InvalidAccess(None) => DOMErrorName::InvalidAccessError,
Error::Security(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::SecurityError, custom_message);
},
Error::Security(None) => DOMErrorName::SecurityError,
Error::Network(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::NetworkError, custom_message);
},
Error::Network(None) => DOMErrorName::NetworkError,
Error::Abort(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::AbortError, custom_message);
},
Error::Abort(None) => DOMErrorName::AbortError,
Error::Timeout(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::TimeoutError, custom_message);
},
Error::Timeout(None) => DOMErrorName::TimeoutError,
Error::InvalidNodeType(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::InvalidNodeTypeError, custom_message);
},
Error::InvalidNodeType(None) => DOMErrorName::InvalidNodeTypeError,
Error::DataClone(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::DataCloneError, custom_message);
},
Error::DataClone(None) => DOMErrorName::DataCloneError,
Error::Data(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::DataError, custom_message);
},
Error::Data(None) => DOMErrorName::DataError,
Error::TransactionInactive(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::TransactionInactiveError, custom_message);
},
Error::TransactionInactive(None) => DOMErrorName::TransactionInactiveError,
Error::ReadOnly(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::ReadOnlyError, custom_message);
},
Error::ReadOnly(None) => DOMErrorName::ReadOnlyError,
Error::Version(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::VersionError, custom_message);
},
Error::Version(None) => DOMErrorName::VersionError,
Error::NoModificationAllowed(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::NoModificationAllowedError, custom_message);
},
Error::NoModificationAllowed(None) => DOMErrorName::NoModificationAllowedError,
Error::QuotaExceeded { quota, requested } => {
return Ok(DomRoot::upcast(QuotaExceededError::new(
cx,
global,
DOMString::new(),
quota,
requested,
)));
},
Error::TypeMismatch(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::TypeMismatchError, custom_message);
},
Error::TypeMismatch(None) => DOMErrorName::TypeMismatchError,
Error::InvalidModification(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::InvalidModificationError, custom_message);
},
Error::InvalidModification(None) => DOMErrorName::InvalidModificationError,
Error::NotReadable(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::NotReadableError, custom_message);
},
Error::NotReadable(None) => DOMErrorName::NotReadableError,
Error::Operation(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::OperationError, custom_message);
},
Error::Operation(None) => DOMErrorName::OperationError,
Error::NotAllowed(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::NotAllowedError, custom_message);
},
Error::NotAllowed(None) => DOMErrorName::NotAllowedError,
Error::Encoding(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::EncodingError, custom_message);
},
Error::Encoding(None) => DOMErrorName::EncodingError,
Error::Constraint(Some(custom_message)) => {
return new_custom_exception(DOMErrorName::ConstraintError, custom_message);
},
Error::Constraint(None) => 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(cx, global, code))
}
#[derive(Default)]
pub(crate) struct ErrorInfo {
pub(crate) message: String,
pub(crate) filename: String,
pub(crate) lineno: c_uint,
pub(crate) column: c_uint,
}
impl ErrorInfo {
fn from_native_error(cx: &mut JSContext, object: HandleObject) -> 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(cx: &mut JSContext, object: HandleObject) -> Option<ErrorInfo> {
let exception =
unsafe { root_from_object::<DOMException>(object.get(), cx.raw_cx()).ok()? };
let scripted_caller = unsafe { describe_scripted_caller(cx.raw_cx()) }.unwrap_or_default();
Some(ErrorInfo {
message: exception.stringifier().into(),
filename: scripted_caller.filename,
lineno: scripted_caller.line,
column: scripted_caller.col + 1,
})
}
fn from_object(cx: &mut JSContext, object: HandleObject) -> Option<ErrorInfo> {
if let Some(info) = ErrorInfo::from_native_error(cx, object) {
return Some(info);
}
if let Some(info) = ErrorInfo::from_dom_exception(cx, object) {
return Some(info);
}
None
}
pub(crate) fn from_value(cx: &mut JSContext, value: HandleValue) -> ErrorInfo {
if value.is_object() {
rooted!(&in(cx) let object = value.to_object());
if let Some(info) = ErrorInfo::from_object(cx, object.handle()) {
return info;
}
}
match USVString::safe_from_jsval(cx, value, ()) {
Ok(ConversionResult::Success(USVString(string))) => {
let scripted_caller =
unsafe { describe_scripted_caller(cx.raw_cx()) }.unwrap_or_default();
ErrorInfo {
message: format!("uncaught exception: {}", string),
filename: scripted_caller.filename,
lineno: scripted_caller.line,
column: scripted_caller.col + 1,
}
},
_ => {
panic!("uncaught exception: failed to stringify primitive");
},
}
}
}
pub(crate) fn report_pending_exception(cx: &mut CurrentRealm) {
rooted!(&in(cx) let mut value = UndefinedValue());
if let Some(error_info) = error_info_from_pending_exception(cx, value.handle_mut()) {
GlobalScope::from_current_realm(cx).report_an_error(cx, error_info, value.handle());
}
}
fn error_info_from_pending_exception(
cx: &mut JSContext,
value: MutableHandleValue,
) -> Option<ErrorInfo> {
if unsafe { !JS_IsExceptionPending(cx) } {
return None;
}
let error_info = unsafe { error_info_from_exception_stack(cx.raw_cx(), value.into())? };
Some(ErrorInfo {
message: error_info.message,
filename: error_info.filename,
lineno: error_info.line,
column: error_info.col,
})
}
pub(crate) fn javascript_error_info_from_error_info(
cx: &mut JSContext,
error_info: &ErrorInfo,
value: HandleValue,
) -> JavaScriptErrorInfo {
let mut stack = || {
if !value.is_object() {
return None;
}
rooted!(&in(cx) let object = value.to_object());
rooted!(&in(cx) let mut stack_value = UndefinedValue());
if unsafe {
!JS_GetProperty(
cx,
object.handle(),
c"stack".as_ptr(),
stack_value.handle_mut(),
)
} {
return None;
}
if !stack_value.is_string() {
return None;
}
let stack_string = NonNull::new(stack_value.to_string())?;
Some(unsafe { jsstr_to_string(cx, stack_string) })
};
JavaScriptErrorInfo {
message: error_info.message.clone(),
filename: error_info.filename.clone(),
line_number: error_info.lineno as u64,
column: error_info.column as u64,
stack: stack(),
}
}
pub(crate) fn take_and_report_pending_exception_for_api(
cx: &mut CurrentRealm,
) -> Option<JavaScriptErrorInfo> {
rooted!(&in(cx) let mut value = UndefinedValue());
let error_info = error_info_from_pending_exception(cx, value.handle_mut())?;
let return_value = javascript_error_info_from_error_info(cx, &error_info, value.handle());
GlobalScope::from_current_realm(cx).report_an_error(cx, error_info, value.handle());
Some(return_value)
}
pub(crate) trait ErrorToJsval {
fn to_jsval(self, cx: &mut JSContext, global: &GlobalScope, rval: MutableHandleValue);
}
impl ErrorToJsval for Error {
fn to_jsval(self, cx: &mut JSContext, global: &GlobalScope, rval: MutableHandleValue) {
match self {
Error::JSFailed => (),
_ => unsafe { assert!(!JS_IsExceptionPending(cx)) },
}
throw_dom_exception(cx, global, self);
unsafe {
assert!(JS_IsExceptionPending(cx));
assert!(JS_GetPendingException(cx, rval));
JS_ClearPendingException(cx);
}
}
}