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