// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use rusty_v8 as v8; use std::any::Any; use std::any::TypeId; use std::borrow::Cow; use std::convert::TryFrom; use std::convert::TryInto; use std::error::Error; use std::fmt; use std::io; // The Send and Sync traits are required because deno is multithreaded and we // need to be able to handle errors across threads. pub trait AnyError: Any + Error + Send + Sync + 'static {} impl AnyError for T where T: Any + Error + Send + Sync + Sized + 'static {} #[derive(Debug)] pub enum ErrBox { Simple { class: &'static str, message: Cow<'static, str>, }, Boxed(Box), } impl ErrBox { pub fn new( class: &'static str, message: impl Into>, ) -> Self { Self::Simple { class, message: message.into(), } } pub fn bad_resource(message: impl Into>) -> Self { Self::new("BadResource", message) } pub fn bad_resource_id() -> Self { Self::new("BadResource", "Bad resource ID") } pub fn error(message: impl Into>) -> Self { Self::new("Error", message) } pub fn not_supported() -> Self { Self::new("NotSupported", "The operation is supported") } pub fn resource_unavailable() -> Self { Self::new( "Busy", "Resource is unavailable because it is in use by a promise", ) } pub fn type_error(message: impl Into>) -> Self { Self::new("TypeError", message) } pub fn last_os_error() -> Self { Self::from(io::Error::last_os_error()) } pub fn downcast(self) -> Result { match self { Self::Boxed(error) if Any::type_id(&*error) == TypeId::of::() => { let error = Box::into_raw(error) as *mut T; let error = unsafe { Box::from_raw(error) }; Ok(*error) } other => Err(other), } } pub fn downcast_ref(&self) -> Option<&T> { match self { Self::Boxed(error) if Any::type_id(&**error) == TypeId::of::() => { let error = &**error as *const dyn AnyError as *const T; let error = unsafe { &*error }; Some(error) } _ => None, } } } impl fmt::Display for ErrBox { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { match self { Self::Simple { message, .. } => f.write_str(message), Self::Boxed(error) => error.fmt(f), } } } impl From for ErrBox { fn from(error: T) -> Self { Self::Boxed(Box::new(error)) } } impl From> for ErrBox { fn from(boxed: Box) -> Self { Self::Boxed(boxed) } } /// A `JSError` represents an exception coming from V8, with stack frames and /// line numbers. The deno_cli crate defines another `JSError` type, which wraps /// the one defined here, that adds source map support and colorful formatting. #[derive(Debug, PartialEq, Clone)] pub struct JSError { pub message: String, pub source_line: Option, pub script_resource_name: Option, pub line_number: Option, pub start_column: Option, // 0-based pub end_column: Option, // 0-based pub frames: Vec, pub formatted_frames: Vec, } #[derive(Debug, PartialEq, Clone)] pub struct JSStackFrame { pub type_name: Option, pub function_name: Option, pub method_name: Option, pub file_name: Option, pub line_number: Option, pub column_number: Option, pub eval_origin: Option, pub is_top_level: Option, pub is_eval: bool, pub is_native: bool, pub is_constructor: bool, pub is_async: bool, pub is_promise_all: bool, pub promise_index: Option, } fn get_property<'a>( scope: &mut v8::HandleScope<'a>, object: v8::Local, key: &str, ) -> Option> { let key = v8::String::new(scope, key).unwrap(); object.get(scope, key.into()) } impl JSError { pub(crate) fn create(js_error: Self) -> ErrBox { js_error.into() } pub fn from_v8_exception( scope: &mut v8::HandleScope, exception: v8::Local, ) -> Self { // Create a new HandleScope because we're creating a lot of new local // handles below. let scope = &mut v8::HandleScope::new(scope); let msg = v8::Exception::create_message(scope, exception); let (message, frames, formatted_frames) = if exception.is_native_error() { // The exception is a JS Error object. let exception: v8::Local = exception.clone().try_into().unwrap(); // Get the message by formatting error.name and error.message. let name = get_property(scope, exception, "name") .and_then(|m| m.to_string(scope)) .map(|s| s.to_rust_string_lossy(scope)) .unwrap_or_else(|| "undefined".to_string()); let message_prop = get_property(scope, exception, "message") .and_then(|m| m.to_string(scope)) .map(|s| s.to_rust_string_lossy(scope)) .unwrap_or_else(|| "undefined".to_string()); let message = format!("Uncaught {}: {}", name, message_prop); // Access error.stack to ensure that prepareStackTrace() has been called. // This should populate error.__callSiteEvals and error.__formattedFrames. let _ = get_property(scope, exception, "stack"); // Read an array of structured frames from error.__callSiteEvals. let frames_v8 = get_property(scope, exception, "__callSiteEvals"); let frames_v8: Option> = frames_v8.and_then(|a| a.try_into().ok()); // Read an array of pre-formatted frames from error.__formattedFrames. let formatted_frames_v8 = get_property(scope, exception, "__formattedFrames"); let formatted_frames_v8: Option> = formatted_frames_v8.and_then(|a| a.try_into().ok()); // Convert them into Vec and Vec respectively. let mut frames: Vec = vec![]; let mut formatted_frames: Vec = vec![]; if let (Some(frames_v8), Some(formatted_frames_v8)) = (frames_v8, formatted_frames_v8) { for i in 0..frames_v8.length() { let call_site: v8::Local = frames_v8.get_index(scope, i).unwrap().try_into().unwrap(); let type_name: Option> = get_property(scope, call_site, "typeName") .unwrap() .try_into() .ok(); let type_name = type_name.map(|s| s.to_rust_string_lossy(scope)); let function_name: Option> = get_property(scope, call_site, "functionName") .unwrap() .try_into() .ok(); let function_name = function_name.map(|s| s.to_rust_string_lossy(scope)); let method_name: Option> = get_property(scope, call_site, "methodName") .unwrap() .try_into() .ok(); let method_name = method_name.map(|s| s.to_rust_string_lossy(scope)); let file_name: Option> = get_property(scope, call_site, "fileName") .unwrap() .try_into() .ok(); let file_name = file_name.map(|s| s.to_rust_string_lossy(scope)); let line_number: Option> = get_property(scope, call_site, "lineNumber") .unwrap() .try_into() .ok(); let line_number = line_number.map(|n| n.value()); let column_number: Option> = get_property(scope, call_site, "columnNumber") .unwrap() .try_into() .ok(); let column_number = column_number.map(|n| n.value()); let eval_origin: Option> = get_property(scope, call_site, "evalOrigin") .unwrap() .try_into() .ok(); let eval_origin = eval_origin.map(|s| s.to_rust_string_lossy(scope)); let is_top_level: Option> = get_property(scope, call_site, "isTopLevel") .unwrap() .try_into() .ok(); let is_top_level = is_top_level.map(|b| b.is_true()); let is_eval: v8::Local = get_property(scope, call_site, "isEval") .unwrap() .try_into() .unwrap(); let is_eval = is_eval.is_true(); let is_native: v8::Local = get_property(scope, call_site, "isNative") .unwrap() .try_into() .unwrap(); let is_native = is_native.is_true(); let is_constructor: v8::Local = get_property(scope, call_site, "isConstructor") .unwrap() .try_into() .unwrap(); let is_constructor = is_constructor.is_true(); let is_async: v8::Local = get_property(scope, call_site, "isAsync") .unwrap() .try_into() .unwrap(); let is_async = is_async.is_true(); let is_promise_all: v8::Local = get_property(scope, call_site, "isPromiseAll") .unwrap() .try_into() .unwrap(); let is_promise_all = is_promise_all.is_true(); let promise_index: Option> = get_property(scope, call_site, "columnNumber") .unwrap() .try_into() .ok(); let promise_index = promise_index.map(|n| n.value()); frames.push(JSStackFrame { type_name, function_name, method_name, file_name, line_number, column_number, eval_origin, is_top_level, is_eval, is_native, is_constructor, is_async, is_promise_all, promise_index, }); let formatted_frame: v8::Local = formatted_frames_v8 .get_index(scope, i) .unwrap() .try_into() .unwrap(); let formatted_frame = formatted_frame.to_rust_string_lossy(scope); formatted_frames.push(formatted_frame) } } (message, frames, formatted_frames) } else { // The exception is not a JS Error object. // Get the message given by V8::Exception::create_message(), and provide // empty frames. (msg.get(scope).to_rust_string_lossy(scope), vec![], vec![]) }; Self { message, script_resource_name: msg .get_script_resource_name(scope) .and_then(|v| v8::Local::::try_from(v).ok()) .map(|v| v.to_rust_string_lossy(scope)), source_line: msg .get_source_line(scope) .map(|v| v.to_rust_string_lossy(scope)), line_number: msg.get_line_number(scope).and_then(|v| v.try_into().ok()), start_column: msg.get_start_column().try_into().ok(), end_column: msg.get_end_column().try_into().ok(), frames, formatted_frames, } } } impl Error for JSError {} fn format_source_loc( file_name: &str, line_number: i64, column_number: i64, ) -> String { let line_number = line_number; let column_number = column_number; format!("{}:{}:{}", file_name, line_number, column_number) } impl fmt::Display for JSError { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { if let Some(script_resource_name) = &self.script_resource_name { if self.line_number.is_some() && self.start_column.is_some() { assert!(self.line_number.is_some()); assert!(self.start_column.is_some()); let source_loc = format_source_loc( script_resource_name, self.line_number.unwrap(), self.start_column.unwrap(), ); write!(f, "{}", source_loc)?; } if self.source_line.is_some() { let source_line = self.source_line.as_ref().unwrap(); write!(f, "\n{}\n", source_line)?; let mut s = String::new(); for i in 0..self.end_column.unwrap() { if i >= self.start_column.unwrap() { s.push('^'); } else if source_line.chars().nth(i as usize).unwrap() == '\t' { s.push('\t'); } else { s.push(' '); } } writeln!(f, "{}", s)?; } } write!(f, "{}", self.message)?; for formatted_frame in &self.formatted_frames { // TODO: Strip ANSI color from formatted_frame. write!(f, "\n at {}", formatted_frame)?; } Ok(()) } } pub(crate) fn attach_handle_to_error( scope: &mut v8::Isolate, err: ErrBox, handle: v8::Local, ) -> ErrBox { // TODO(bartomieju): this is a special case... ErrWithV8Handle::new(scope, err, handle).into() } // TODO(piscisaureus): rusty_v8 should implement the Error trait on // values of type v8::Global. pub struct ErrWithV8Handle { err: ErrBox, handle: v8::Global, } impl ErrWithV8Handle { pub fn new( scope: &mut v8::Isolate, err: ErrBox, handle: v8::Local, ) -> Self { let handle = v8::Global::new(scope, handle); Self { err, handle } } pub fn get_handle<'s>( &self, scope: &mut v8::HandleScope<'s>, ) -> v8::Local<'s, v8::Value> { v8::Local::new(scope, &self.handle) } } unsafe impl Send for ErrWithV8Handle {} unsafe impl Sync for ErrWithV8Handle {} impl Error for ErrWithV8Handle {} impl fmt::Display for ErrWithV8Handle { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.err.fmt(f) } } impl fmt::Debug for ErrWithV8Handle { fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { self.err.fmt(f) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_bad_resource() { let err = ErrBox::bad_resource("Resource has been closed"); assert!(matches!(err, ErrBox::Simple { class: "BadResource", .. })); assert_eq!(err.to_string(), "Resource has been closed"); } #[test] fn test_bad_resource_id() { let err = ErrBox::bad_resource_id(); assert!(matches!(err, ErrBox::Simple { class: "BadResource", .. })); assert_eq!(err.to_string(), "Bad resource ID"); } }