// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. use rusty_v8 as v8; use std::borrow::Cow; use std::convert::TryFrom; use std::convert::TryInto; use std::error::Error; use std::fmt; use std::fmt::Debug; use std::fmt::Display; use std::fmt::Formatter; use std::io; /// A generic wrapper that can encapsulate any concrete error type. pub type AnyError = anyhow::Error; /// Creates a new error with a caller-specified error class name and message. pub fn custom_error( class: &'static str, message: impl Into>, ) -> AnyError { CustomError { class, message: message.into(), } .into() } pub fn generic_error(message: impl Into>) -> AnyError { custom_error("Error", message) } pub fn type_error(message: impl Into>) -> AnyError { custom_error("TypeError", message) } pub fn uri_error(message: impl Into>) -> AnyError { custom_error("URIError", message) } pub fn last_os_error() -> AnyError { io::Error::last_os_error().into() } pub fn bad_resource(message: impl Into>) -> AnyError { custom_error("BadResource", message) } pub fn bad_resource_id() -> AnyError { custom_error("BadResource", "Bad resource ID") } pub fn not_supported() -> AnyError { custom_error("NotSupported", "The operation is supported") } pub fn resource_unavailable() -> AnyError { custom_error( "Busy", "Resource is unavailable because it is in use by a promise", ) } /// A simple error type that lets the creator specify both the error message and /// the error class name. This type is private; externally it only ever appears /// wrapped in an `AnyError`. To retrieve the error class name from a wrapped /// `CustomError`, use the function `get_custom_error_class()`. #[derive(Debug)] struct CustomError { class: &'static str, message: Cow<'static, str>, } impl Display for CustomError { fn fmt(&self, f: &mut Formatter) -> fmt::Result { f.write_str(&self.message) } } impl Error for CustomError {} /// If this error was crated with `custom_error()`, return the specified error /// class name. In all other cases this function returns `None`. pub fn get_custom_error_class(error: &AnyError) -> Option<&'static str> { error.downcast_ref::().map(|e| e.class) } /// 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) -> AnyError { 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 Display for JsError { fn fmt(&self, f: &mut 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: AnyError, handle: v8::Local, ) -> AnyError { // 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(crate) struct ErrWithV8Handle { err: AnyError, handle: v8::Global, } impl ErrWithV8Handle { pub fn new( scope: &mut v8::Isolate, err: AnyError, 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 Display for ErrWithV8Handle { fn fmt(&self, f: &mut Formatter) -> fmt::Result { ::fmt(&self.err, f) } } impl Debug for ErrWithV8Handle { fn fmt(&self, f: &mut Formatter) -> fmt::Result { ::fmt(self, f) } } #[cfg(test)] mod tests { use super::*; #[test] fn test_bad_resource() { let err = bad_resource("Resource has been closed"); assert_eq!(err.to_string(), "Resource has been closed"); } #[test] fn test_bad_resource_id() { let err = bad_resource_id(); assert_eq!(err.to_string(), "Bad resource ID"); } }