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};
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());
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 => DOMErrorName::IndexSizeError,
116 Error::NotFound(Some(custom_message)) => {
117 return new_custom_exception(DOMErrorName::NotFoundError, custom_message);
118 },
119 Error::NotFound(None) => DOMErrorName::NotFoundError,
120 Error::HierarchyRequest => DOMErrorName::HierarchyRequestError,
121 Error::WrongDocument(Some(doc_err_custom_message)) => {
122 return new_custom_exception(DOMErrorName::WrongDocumentError, doc_err_custom_message);
123 },
124 Error::WrongDocument(None) => DOMErrorName::WrongDocumentError,
125 Error::InvalidCharacter => DOMErrorName::InvalidCharacterError,
126 Error::NotSupported => DOMErrorName::NotSupportedError,
127 Error::InUseAttribute => DOMErrorName::InUseAttributeError,
128 Error::InvalidState(Some(custom_message)) => {
129 return new_custom_exception(DOMErrorName::InvalidStateError, custom_message);
130 },
131 Error::InvalidState(None) => DOMErrorName::InvalidStateError,
132 Error::Syntax(Some(custom_message)) => {
133 return new_custom_exception(DOMErrorName::SyntaxError, custom_message);
134 },
135 Error::Syntax(None) => DOMErrorName::SyntaxError,
136 Error::Namespace => DOMErrorName::NamespaceError,
137 Error::InvalidAccess => DOMErrorName::InvalidAccessError,
138 Error::Security => DOMErrorName::SecurityError,
139 Error::Network => DOMErrorName::NetworkError,
140 Error::Abort => DOMErrorName::AbortError,
141 Error::Timeout => DOMErrorName::TimeoutError,
142 Error::InvalidNodeType => DOMErrorName::InvalidNodeTypeError,
143 Error::DataClone(Some(custom_message)) => {
144 return new_custom_exception(DOMErrorName::DataCloneError, custom_message);
145 },
146 Error::DataClone(None) => DOMErrorName::DataCloneError,
147 Error::Data => DOMErrorName::DataError,
148 Error::TransactionInactive => DOMErrorName::TransactionInactiveError,
149 Error::ReadOnly => DOMErrorName::ReadOnlyError,
150 Error::Version => DOMErrorName::VersionError,
151 Error::NoModificationAllowed => DOMErrorName::NoModificationAllowedError,
152 Error::QuotaExceeded { quota, requested } => {
153 return Ok(DomRoot::upcast(QuotaExceededError::new(
154 global,
155 DOMString::new(),
156 quota,
157 requested,
158 can_gc,
159 )));
160 },
161 Error::TypeMismatch => DOMErrorName::TypeMismatchError,
162 Error::InvalidModification => DOMErrorName::InvalidModificationError,
163 Error::NotReadable => DOMErrorName::NotReadableError,
164 Error::Operation => DOMErrorName::OperationError,
165 Error::NotAllowed => DOMErrorName::NotAllowedError,
166 Error::Encoding => DOMErrorName::EncodingError,
167 Error::Constraint => DOMErrorName::ConstraintError,
168 Error::Type(message) => return Err(JsEngineError::Type(message)),
169 Error::Range(message) => return Err(JsEngineError::Range(message)),
170 Error::JSFailed => return Err(JsEngineError::JSFailed),
171 };
172 Ok(DOMException::new(global, code, can_gc))
173}
174
175#[derive(Default)]
177pub(crate) struct ErrorInfo {
178 pub(crate) message: String,
180 pub(crate) filename: String,
182 pub(crate) lineno: c_uint,
184 pub(crate) column: c_uint,
186}
187
188impl ErrorInfo {
189 fn from_native_error(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
190 let report = unsafe { JS_ErrorFromException(*cx, object) };
191 if report.is_null() {
192 return None;
193 }
194
195 let filename = {
196 let filename = unsafe { (*report)._base.filename.data_ as *const u8 };
197 if !filename.is_null() {
198 let filename = unsafe {
199 let length = (0..).find(|idx| *filename.offset(*idx) == 0).unwrap();
200 from_raw_parts(filename, length as usize)
201 };
202 String::from_utf8_lossy(filename).into_owned()
203 } else {
204 "none".to_string()
205 }
206 };
207
208 let lineno = unsafe { (*report)._base.lineno };
209 let column = unsafe { (*report)._base.column._base };
210
211 let message = {
212 let message = unsafe { (*report)._base.message_.data_ as *const u8 };
213 let message = unsafe {
214 let length = (0..).find(|idx| *message.offset(*idx) == 0).unwrap();
215 from_raw_parts(message, length as usize)
216 };
217 String::from_utf8_lossy(message).into_owned()
218 };
219
220 Some(ErrorInfo {
221 filename,
222 message,
223 lineno,
224 column,
225 })
226 }
227
228 fn from_dom_exception(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
229 let exception = unsafe { root_from_object::<DOMException>(object.get(), *cx).ok()? };
230 Some(ErrorInfo {
231 filename: "".to_string(),
232 message: exception.stringifier().into(),
233 lineno: 0,
234 column: 0,
235 })
236 }
237
238 fn from_object(object: HandleObject, cx: SafeJSContext) -> Option<ErrorInfo> {
239 if let Some(info) = ErrorInfo::from_native_error(object, cx) {
240 return Some(info);
241 }
242 if let Some(info) = ErrorInfo::from_dom_exception(object, cx) {
243 return Some(info);
244 }
245 None
246 }
247
248 pub(crate) fn from_value(value: HandleValue, cx: SafeJSContext) -> ErrorInfo {
249 if value.is_object() {
250 rooted!(in(*cx) let object = value.to_object());
251 if let Some(info) = ErrorInfo::from_object(object.handle(), cx) {
252 return info;
253 }
254 }
255
256 match USVString::safe_from_jsval(cx, value, ()) {
257 Ok(ConversionResult::Success(USVString(string))) => ErrorInfo {
258 message: format!("uncaught exception: {}", string),
259 filename: String::new(),
260 lineno: 0,
261 column: 0,
262 },
263 _ => {
264 panic!("uncaught exception: failed to stringify primitive");
265 },
266 }
267 }
268}
269
270pub(crate) fn report_pending_exception(
275 cx: SafeJSContext,
276 dispatch_event: bool,
277 realm: InRealm,
278 can_gc: CanGc,
279) {
280 rooted!(in(*cx) let mut value = UndefinedValue());
281 if take_pending_exception(cx, value.handle_mut()) {
282 let error_info = ErrorInfo::from_value(value.handle(), cx);
283 report_error(
284 error_info,
285 value.handle(),
286 cx,
287 dispatch_event,
288 realm,
289 can_gc,
290 );
291 }
292}
293
294fn take_pending_exception(cx: SafeJSContext, value: MutableHandleValue) -> bool {
295 unsafe {
296 if !JS_IsExceptionPending(*cx) {
297 return false;
298 }
299 }
300
301 unsafe {
302 if !JS_GetPendingException(*cx, value) {
303 JS_ClearPendingException(*cx);
304 error!("Uncaught exception: JS_GetPendingException failed");
305 return false;
306 }
307
308 JS_ClearPendingException(*cx);
309 }
310 true
311}
312
313fn report_error(
314 error_info: ErrorInfo,
315 value: HandleValue,
316 cx: SafeJSContext,
317 dispatch_event: bool,
318 realm: InRealm,
319 can_gc: CanGc,
320) {
321 error!(
322 "Error at {}:{}:{} {}",
323 error_info.filename, error_info.lineno, error_info.column, error_info.message
324 );
325
326 #[cfg(feature = "js_backtrace")]
327 {
328 LAST_EXCEPTION_BACKTRACE.with(|backtrace| {
329 if let Some((js_backtrace, rust_backtrace)) = backtrace.borrow_mut().take() {
330 if let Some(stack) = js_backtrace {
331 eprintln!("JS backtrace:\n{}", stack);
332 }
333 eprintln!("Rust backtrace:\n{}", rust_backtrace);
334 }
335 });
336 }
337
338 if dispatch_event {
339 GlobalScope::from_safe_context(cx, realm).report_an_error(error_info, value, can_gc);
340 }
341}
342
343pub(crate) fn javascript_error_info_from_error_info(
344 cx: SafeJSContext,
345 error_info: &ErrorInfo,
346 value: HandleValue,
347 _: CanGc,
348) -> JavaScriptErrorInfo {
349 let stack = || {
350 if !value.is_object() {
351 return None;
352 }
353
354 rooted!(in(*cx) let object = value.to_object());
355 let stack_name = CString::new("stack").unwrap();
356 rooted!(in(*cx) let mut stack_value = UndefinedValue());
357 if unsafe {
358 !JS_GetProperty(
359 *cx,
360 object.handle().into(),
361 stack_name.as_ptr(),
362 stack_value.handle_mut().into(),
363 )
364 } {
365 return None;
366 }
367 if !stack_value.is_string() {
368 return None;
369 }
370 let stack_string = NonNull::new(stack_value.to_string())?;
371 Some(unsafe { jsstr_to_string(*cx, stack_string) })
372 };
373
374 JavaScriptErrorInfo {
375 message: error_info.message.clone(),
376 filename: error_info.filename.clone(),
377 line_number: error_info.lineno as u64,
378 column: error_info.column as u64,
379 stack: stack(),
380 }
381}
382
383pub(crate) fn take_and_report_pending_exception_for_api(
384 cx: SafeJSContext,
385 realm: InRealm,
386 can_gc: CanGc,
387) -> Option<JavaScriptErrorInfo> {
388 rooted!(in(*cx) let mut value = UndefinedValue());
389 if !take_pending_exception(cx, value.handle_mut()) {
390 return None;
391 }
392
393 let error_info = ErrorInfo::from_value(value.handle(), cx);
394 let return_value =
395 javascript_error_info_from_error_info(cx, &error_info, value.handle(), can_gc);
396 report_error(
397 error_info,
398 value.handle(),
399 cx,
400 true, realm,
402 can_gc,
403 );
404 Some(return_value)
405}
406
407pub(crate) trait ErrorToJsval {
408 fn to_jsval(
409 self,
410 cx: SafeJSContext,
411 global: &GlobalScope,
412 rval: MutableHandleValue,
413 can_gc: CanGc,
414 );
415}
416
417impl ErrorToJsval for Error {
418 #[allow(clippy::wrong_self_convention)]
420 fn to_jsval(
421 self,
422 cx: SafeJSContext,
423 global: &GlobalScope,
424 rval: MutableHandleValue,
425 can_gc: CanGc,
426 ) {
427 match self {
428 Error::JSFailed => (),
429 _ => unsafe { assert!(!JS_IsExceptionPending(*cx)) },
430 }
431 throw_dom_exception(cx, global, self, can_gc);
432 unsafe {
433 assert!(JS_IsExceptionPending(*cx));
434 assert!(JS_GetPendingException(*cx, rval));
435 JS_ClearPendingException(*cx);
436 }
437 }
438}