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