1use 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 static LAST_EXCEPTION_BACKTRACE: DomRefCell<Option<(Option<String>, String)>> = DomRefCell::new(None);
41}
42
43pub(crate) enum JsEngineError {
45 Type(String),
47 Range(String),
49 JSFailed,
51}
52
53pub(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
94pub(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#[derive(Default)]
166pub(crate) struct ErrorInfo {
167 pub(crate) message: String,
169 pub(crate) filename: String,
171 pub(crate) lineno: c_uint,
173 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
259pub(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 #[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}