diff --git a/Cargo.lock b/Cargo.lock index a7dc96e32200e..b77fae9edb999 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5146,3 +5146,8 @@ dependencies = [ "cc", "libc", ] + +[[patch.unused]] +name = "serde_json" +version = "1.0.72" +source = "git+https://github.com/lucacasonato/json?branch=support_lone_surrogates_in_raw_value#51e9616deeb0ab1f42a481201aaf28043de76207" diff --git a/Cargo.toml b/Cargo.toml index 3f206b2232e3f..8403b0dcaefe5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -27,6 +27,10 @@ members = [ ] exclude = ["test_util/std/hash/_wasm"] +# use serde_json lucacasonato/json.git@5ec8141acf127d9dcbc01b2b63c6426d158ee1af +[patch.crates-io] +serde_json = { git = "https://github.com/lucacasonato/json", branch = "support_lone_surrogates_in_raw_value" } + # NB: the `bench` and `release` profiles must remain EXACTLY the same. [profile.release] codegen-units = 1 diff --git a/cli/tools/coverage/mod.rs b/cli/tools/coverage/mod.rs index 1c14859ea9e2f..7d6fed91bd593 100644 --- a/cli/tools/coverage/mod.rs +++ b/cli/tools/coverage/mod.rs @@ -43,22 +43,34 @@ impl CoverageCollector { } async fn enable_debugger(&mut self) -> Result<(), AnyError> { - self.session.post_message("Debugger.enable", None).await?; + self + .session + .post_message("Debugger.enable", None::<()>) + .await?; Ok(()) } async fn enable_profiler(&mut self) -> Result<(), AnyError> { - self.session.post_message("Profiler.enable", None).await?; + self + .session + .post_message("Profiler.enable", None::<()>) + .await?; Ok(()) } async fn disable_debugger(&mut self) -> Result<(), AnyError> { - self.session.post_message("Debugger.disable", None).await?; + self + .session + .post_message("Debugger.disable", None::<()>) + .await?; Ok(()) } async fn disable_profiler(&mut self) -> Result<(), AnyError> { - self.session.post_message("Profiler.disable", None).await?; + self + .session + .post_message("Profiler.disable", None::<()>) + .await?; Ok(()) } @@ -66,13 +78,12 @@ impl CoverageCollector { &mut self, parameters: StartPreciseCoverageParameters, ) -> Result { - let parameters_value = serde_json::to_value(parameters)?; let return_value = self .session - .post_message("Profiler.startPreciseCoverage", Some(parameters_value)) + .post_message("Profiler.startPreciseCoverage", Some(parameters)) .await?; - let return_object = serde_json::from_value(return_value)?; + let return_object = serde_json::from_str(return_value.get())?; Ok(return_object) } @@ -82,10 +93,10 @@ impl CoverageCollector { ) -> Result { let return_value = self .session - .post_message("Profiler.takePreciseCoverage", None) + .post_message("Profiler.takePreciseCoverage", None::<()>) .await?; - let return_object = serde_json::from_value(return_value)?; + let return_object = serde_json::from_str(return_value.get())?; Ok(return_object) } diff --git a/cli/tools/repl/channel.rs b/cli/tools/repl/channel.rs index 7cc8029057dc8..fcd35590104ad 100644 --- a/cli/tools/repl/channel.rs +++ b/cli/tools/repl/channel.rs @@ -2,6 +2,7 @@ use deno_core::anyhow::anyhow; use deno_core::error::AnyError; +use deno_core::serde_json::value::RawValue; use deno_core::serde_json::Value; use std::cell::RefCell; use tokio::sync::mpsc::channel; @@ -35,17 +36,17 @@ pub fn rustyline_channel( pub enum RustylineSyncMessage { PostMessage { - method: String, + method: &'static str, params: Option, }, LspCompletions { - line_text: String, + line_text: &'static str, position: usize, }, } pub enum RustylineSyncResponse { - PostMessage(Result), + PostMessage(Result, AnyError>), LspCompletions(Vec), } @@ -57,16 +58,12 @@ pub struct RustylineSyncMessageSender { impl RustylineSyncMessageSender { pub fn post_message( &self, - method: &str, + method: &'static str, params: Option, - ) -> Result { - if let Err(err) = - self - .message_tx - .blocking_send(RustylineSyncMessage::PostMessage { - method: method.to_string(), - params, - }) + ) -> Result, AnyError> { + if let Err(err) = self + .message_tx + .blocking_send(RustylineSyncMessage::PostMessage { method, params }) { Err(anyhow!("{}", err)) } else { diff --git a/cli/tools/repl/mod.rs b/cli/tools/repl/mod.rs index 0c9fdbfb0cb21..5ab537f5c1056 100644 --- a/cli/tools/repl/mod.rs +++ b/cli/tools/repl/mod.rs @@ -2,8 +2,17 @@ use crate::proc_state::ProcState; use deno_core::error::AnyError; +use deno_core::futures::FutureExt; +use deno_core::parking_lot::Mutex; +use deno_core::serde_json; +use deno_core::serde_json::json; +use deno_core::serde_json::value::RawValue; +use deno_core::LocalInspectorSession; +use deno_core::LossyString; use deno_runtime::worker::MainWorker; use rustyline::error::ReadlineError; +use serde::Deserialize; +use serde::Serialize; mod channel; mod editor; @@ -12,12 +21,733 @@ mod session; use channel::rustyline_channel; use channel::RustylineSyncMessage; use channel::RustylineSyncMessageHandler; +use channel::RustylineSyncMessageSender; use channel::RustylineSyncResponse; use editor::EditorHelper; use editor::ReplEditor; use session::EvaluationOutput; use session::ReplSession; +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct RemoteObject { + r#type: String, + value: Option>, + unserializable_value: Option>, + object_id: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct CallArgument { + #[serde(skip_serializing_if = "Option::is_none")] + value: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + unserializable_value: Option>, + #[serde(skip_serializing_if = "Option::is_none")] + object_id: Option, +} + +impl From for CallArgument { + fn from(remote_object: RemoteObject) -> Self { + CallArgument { + value: remote_object.value, + unserializable_value: remote_object.unserializable_value, + object_id: remote_object.object_id, + } + } +} + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +struct EvaluationResponse { + exception_details: Option, + result: Option, +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct CallFunctionRequest<'a> { + execution_context_id: u64, + function_declaration: &'static str, + arguments: &'a [&'a CallArgument], +} + +// Provides helpers to the editor like validation for multi-line edits, completion candidates for +// tab completion. +#[derive(Helper, Hinter)] +struct EditorHelper { + context_id: u64, + sync_sender: RustylineSyncMessageSender, +} + +impl EditorHelper { + pub fn get_global_lexical_scope_names(&self) -> Vec { + let resp = self + .sync_sender + .post_message( + "Runtime.globalLexicalScopeNames", + Some(json!({ + "executionContextId": self.context_id, + })), + ) + .unwrap(); + + #[derive(Deserialize)] + struct GlobalLexicalScopeNamesResponse { + names: Vec, + } + + let response: GlobalLexicalScopeNamesResponse = + serde_json::from_str(resp.get()).unwrap(); + + response.names.into_iter().map(|name| name.into()).collect() + } + + pub fn get_expression_property_names(&self, expr: &str) -> Vec { + // try to get the properties from the expression + if let Some(properties) = self.get_object_expr_properties(expr) { + return properties; + } + + // otherwise fall back to the prototype + let expr_type = self.get_expression_type(expr); + let object_expr = match expr_type.as_deref() { + // possibilities: https://chromedevtools.github.io/devtools-protocol/v8/Runtime/#type-RemoteObject + Some("object") => "Object.prototype", + Some("function") => "Function.prototype", + Some("string") => "String.prototype", + Some("boolean") => "Boolean.prototype", + Some("bigint") => "BigInt.prototype", + Some("number") => "Number.prototype", + _ => return Vec::new(), // undefined, symbol, and unhandled + }; + + self + .get_object_expr_properties(object_expr) + .unwrap_or_else(Vec::new) + } + + fn get_expression_type(&self, expr: &str) -> Option { + Some(self.evaluate_expression(expr)?.r#type) + } + + fn get_object_expr_properties( + &self, + object_expr: &str, + ) -> Option> { + let evaluate_result = self.evaluate_expression(object_expr)?; + let object_id = evaluate_result.object_id?; + + let resp = self + .sync_sender + .post_message( + "Runtime.getProperties", + Some(json!({ + "objectId": object_id, + })), + ) + .ok()?; + + #[derive(Deserialize)] + struct Property { + name: LossyString, + } + + #[derive(Deserialize)] + struct GetPropertiesResponse { + result: Option>, + } + + let get_properties_response: GetPropertiesResponse = + serde_json::from_str(resp.get()).unwrap(); + + Some( + get_properties_response + .result? + .into_iter() + .map(|r| r.name.into()) + .collect(), + ) + } + + fn evaluate_expression(&self, expr: &str) -> Option { + let resp = self + .sync_sender + .post_message( + "Runtime.evaluate", + Some(json!({ + "contextId": self.context_id, + "expression": expr, + "throwOnSideEffect": true, + "timeout": 200, + })), + ) + .ok()?; + + let evaluate_response: EvaluationResponse = + serde_json::from_str(resp.get()).unwrap(); + + if evaluate_response.exception_details.is_some() { + None + } else { + evaluate_response.result + } + } +} + +fn is_word_boundary(c: char) -> bool { + if c == '.' { + false + } else { + char::is_ascii_whitespace(&c) || char::is_ascii_punctuation(&c) + } +} + +fn get_expr_from_line_at_pos(line: &str, cursor_pos: usize) -> &str { + let start = line[..cursor_pos] + .rfind(is_word_boundary) + .map_or_else(|| 0, |i| i); + let end = line[cursor_pos..] + .rfind(is_word_boundary) + .map_or_else(|| cursor_pos, |i| cursor_pos + i); + + let word = &line[start..end]; + let word = word.strip_prefix(is_word_boundary).unwrap_or(word); + let word = word.strip_suffix(is_word_boundary).unwrap_or(word); + + word +} + +impl Completer for EditorHelper { + type Candidate = String; + + fn complete( + &self, + line: &str, + pos: usize, + _ctx: &Context<'_>, + ) -> Result<(usize, Vec), ReadlineError> { + let expr = get_expr_from_line_at_pos(line, pos); + + // check if the expression is in the form `obj.prop` + if let Some(index) = expr.rfind('.') { + let sub_expr = &expr[..index]; + let prop_name = &expr[index + 1..]; + let candidates = self + .get_expression_property_names(sub_expr) + .into_iter() + .filter(|n| !n.starts_with("Symbol(") && n.starts_with(prop_name)) + .collect(); + + Ok((pos - prop_name.len(), candidates)) + } else { + // combine results of declarations and globalThis properties + let mut candidates = self + .get_expression_property_names("globalThis") + .into_iter() + .chain(self.get_global_lexical_scope_names()) + .filter(|n| n.starts_with(expr)) + .collect::>(); + + // sort and remove duplicates + candidates.sort(); + candidates.dedup(); // make sure to sort first + + Ok((pos - expr.len(), candidates)) + } + } +} + +impl Validator for EditorHelper { + fn validate( + &self, + ctx: &mut ValidationContext, + ) -> Result { + let mut stack: Vec = Vec::new(); + let mut in_template = false; + + for item in deno_ast::lex(ctx.input(), deno_ast::MediaType::TypeScript) { + if let deno_ast::TokenOrComment::Token(token) = item.inner { + match token { + Token::BackQuote => in_template = !in_template, + Token::LParen + | Token::LBracket + | Token::LBrace + | Token::DollarLBrace => stack.push(token), + Token::RParen | Token::RBracket | Token::RBrace => { + match (stack.pop(), token) { + (Some(Token::LParen), Token::RParen) + | (Some(Token::LBracket), Token::RBracket) + | (Some(Token::LBrace), Token::RBrace) + | (Some(Token::DollarLBrace), Token::RBrace) => {} + (Some(left), _) => { + return Ok(ValidationResult::Invalid(Some(format!( + "Mismatched pairs: {:?} is not properly closed", + left + )))) + } + (None, _) => { + // While technically invalid when unpaired, it should be V8's task to output error instead. + // Thus marked as valid with no info. + return Ok(ValidationResult::Valid(None)); + } + } + } + Token::Error(error) => { + match error.kind() { + // If there is unterminated template, it continues to read input. + SyntaxError::UnterminatedTpl => {} + _ => { + // If it failed parsing, it should be V8's task to output error instead. + // Thus marked as valid with no info. + return Ok(ValidationResult::Valid(None)); + } + } + } + _ => {} + } + } + } + + if !stack.is_empty() || in_template { + return Ok(ValidationResult::Incomplete); + } + + Ok(ValidationResult::Valid(None)) + } +} + +impl Highlighter for EditorHelper { + fn highlight_hint<'h>(&self, hint: &'h str) -> Cow<'h, str> { + hint.into() + } + + fn highlight_candidate<'c>( + &self, + candidate: &'c str, + completion: rustyline::CompletionType, + ) -> Cow<'c, str> { + if completion == CompletionType::List { + candidate.into() + } else { + self.highlight(candidate, 0) + } + } + + fn highlight_char(&self, line: &str, _: usize) -> bool { + !line.is_empty() + } + + fn highlight<'l>(&self, line: &'l str, _: usize) -> Cow<'l, str> { + let mut out_line = String::from(line); + + for item in deno_ast::lex(line, deno_ast::MediaType::TypeScript) { + // Adding color adds more bytes to the string, + // so an offset is needed to stop spans falling out of sync. + let offset = out_line.len() - line.len(); + let span = std::ops::Range { + start: item.span.lo.0 as usize, + end: item.span.hi.0 as usize, + }; + + out_line.replace_range( + span.start + offset..span.end + offset, + &match item.inner { + deno_ast::TokenOrComment::Token(token) => match token { + Token::Str { .. } | Token::Template { .. } | Token::BackQuote => { + colors::green(&line[span]).to_string() + } + Token::Regex(_, _) => colors::red(&line[span]).to_string(), + Token::Num(_) | Token::BigInt(_) => { + colors::yellow(&line[span]).to_string() + } + Token::Word(word) => match word { + Word::True | Word::False | Word::Null => { + colors::yellow(&line[span]).to_string() + } + Word::Keyword(_) => colors::cyan(&line[span]).to_string(), + Word::Ident(ident) => { + if ident == *"undefined" { + colors::gray(&line[span]).to_string() + } else if ident == *"Infinity" || ident == *"NaN" { + colors::yellow(&line[span]).to_string() + } else if ident == *"async" || ident == *"of" { + colors::cyan(&line[span]).to_string() + } else { + line[span].to_string() + } + } + }, + _ => line[span].to_string(), + }, + deno_ast::TokenOrComment::Comment { .. } => { + colors::gray(&line[span]).to_string() + } + }, + ); + } + + out_line.into() + } +} + +#[derive(Clone)] +struct ReplEditor { + inner: Arc>>, + history_file_path: PathBuf, +} + +impl ReplEditor { + pub fn new(helper: EditorHelper, history_file_path: PathBuf) -> Self { + let editor_config = Config::builder() + .completion_type(CompletionType::List) + .build(); + + let mut editor = Editor::with_config(editor_config); + editor.set_helper(Some(helper)); + editor.load_history(&history_file_path).unwrap_or(()); + + ReplEditor { + inner: Arc::new(Mutex::new(editor)), + history_file_path, + } + } + + pub fn readline(&self) -> Result { + self.inner.lock().readline("> ") + } + + pub fn add_history_entry(&self, entry: String) { + self.inner.lock().add_history_entry(entry); + } + + pub fn save_history(&self) -> Result<(), AnyError> { + std::fs::create_dir_all(self.history_file_path.parent().unwrap())?; + + self.inner.lock().save_history(&self.history_file_path)?; + Ok(()) + } +} + +static PRELUDE: &str = r#" +Object.defineProperty(globalThis, "_", { + configurable: true, + get: () => Deno[Deno.internal].lastEvalResult, + set: (value) => { + Object.defineProperty(globalThis, "_", { + value: value, + writable: true, + enumerable: true, + configurable: true, + }); + console.log("Last evaluation result is no longer saved to _."); + }, +}); + +Object.defineProperty(globalThis, "_error", { + configurable: true, + get: () => Deno[Deno.internal].lastThrownError, + set: (value) => { + Object.defineProperty(globalThis, "_error", { + value: value, + writable: true, + enumerable: true, + configurable: true, + }); + + console.log("Last thrown error is no longer saved to _error."); + }, +}); +"#; + +enum EvaluationOutput { + Value(String), + Error(String), +} + +impl std::fmt::Display for EvaluationOutput { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + EvaluationOutput::Value(value) => f.write_str(value), + EvaluationOutput::Error(value) => f.write_str(value), + } + } +} + +struct ReplSession { + worker: MainWorker, + session: LocalInspectorSession, + pub context_id: u64, +} + +impl ReplSession { + pub async fn initialize(mut worker: MainWorker) -> Result { + let mut session = worker.create_inspector_session().await; + + worker + .with_event_loop( + session + .post_message("Runtime.enable", None::<()>) + .boxed_local(), + ) + .await?; + + // Enabling the runtime domain will always send trigger one executionContextCreated for each + // context the inspector knows about so we grab the execution context from that since + // our inspector does not support a default context (0 is an invalid context id). + let mut context_id: u64 = 0; + for notification in session.notifications() { + if notification.method == "Runtime.executionContextCreated" { + #[derive(Deserialize)] + struct Params { + context: ExecutionContextDescription, + } + #[derive(Deserialize)] + struct ExecutionContextDescription { + id: u64, + } + let params: Params = serde_json::from_str(notification.params.get())?; + context_id = params.context.id; + } + } + + let mut repl_session = ReplSession { + worker, + session, + context_id, + }; + + // inject prelude + repl_session.evaluate_expression(PRELUDE).await?; + + Ok(repl_session) + } + + pub async fn is_closing(&mut self) -> Result { + let resp = self.evaluate_expression("(this.closed)").await?; + + let closed = + serde_json::from_str(resp.result.unwrap().value.unwrap().get()).unwrap(); + + Ok(closed) + } + + pub async fn post_message_with_event_loop( + &mut self, + method: &'static str, + params: Option, + ) -> Result, AnyError> { + self + .worker + .with_event_loop(self.session.post_message(method, params).boxed_local()) + .await + } + + pub async fn run_event_loop(&mut self) -> Result<(), AnyError> { + self.worker.run_event_loop(true).await + } + + pub async fn evaluate_line_and_get_output( + &mut self, + line: &str, + ) -> Result { + match self.evaluate_line_with_object_wrapping(line).await { + Ok(evaluate_response) => { + let eval_res: CallArgument = evaluate_response.result.unwrap().into(); + let evaluate_exception_details = evaluate_response.exception_details; + + if evaluate_exception_details.is_some() { + self.set_last_thrown_error(&eval_res).await?; + } else { + self.set_last_eval_result(&eval_res).await?; + } + + let value = self.get_eval_value(&eval_res).await?; + Ok(match evaluate_exception_details { + Some(_) => EvaluationOutput::Error(format!("Uncaught {}", value)), + None => EvaluationOutput::Value(value), + }) + } + Err(err) => { + // handle a parsing diagnostic + match err.downcast_ref::() { + Some(diagnostic) => Ok(EvaluationOutput::Error(format!( + "{}: {} at {}:{}", + colors::red("parse error"), + diagnostic.message(), + diagnostic.display_position.line_number, + diagnostic.display_position.column_number, + ))), + None => Err(err), + } + } + } + } + + async fn evaluate_line_with_object_wrapping( + &mut self, + line: &str, + ) -> Result { + // It is a bit unexpected that { "foo": "bar" } is interpreted as a block + // statement rather than an object literal so we interpret it as an expression statement + // to match the behavior found in a typical prompt including browser developer tools. + let wrapped_line = if line.trim_start().starts_with('{') + && !line.trim_end().ends_with(';') + { + format!("({})", &line) + } else { + line.to_string() + }; + + let evaluate_response = self.evaluate_ts_expression(&wrapped_line).await?; + + // If that fails, we retry it without wrapping in parens letting the error bubble up to the + // user if it is still an error. + let evaluate_response = if evaluate_response.exception_details.is_some() + && wrapped_line != line + { + self.evaluate_ts_expression(line).await? + } else { + evaluate_response + }; + + Ok(evaluate_response) + } + + async fn set_last_thrown_error( + &mut self, + error: &CallArgument, + ) -> Result<(), AnyError> { + self.post_message_with_event_loop( + "Runtime.callFunctionOn", + Some(json!({ + "executionContextId": self.context_id, + "functionDeclaration": "function (object) { Deno[Deno.internal].lastThrownError = object; }", + "arguments": [ + error, + ], + })), + ).await?; + Ok(()) + } + + async fn set_last_eval_result( + &mut self, + evaluate_result: &CallArgument, + ) -> Result<(), AnyError> { + let params = CallFunctionRequest { + execution_context_id: self.context_id, + function_declaration: + "function (object) { Deno[Deno.internal].lastEvalResult = object; }", + arguments: &[evaluate_result], + }; + self + .post_message_with_event_loop("Runtime.callFunctionOn", Some(params)) + .await?; + Ok(()) + } + + pub async fn get_eval_value( + &mut self, + evaluate_result: &CallArgument, + ) -> Result { + let params = CallFunctionRequest { + execution_context_id: self.context_id, + function_declaration: r#"function (object) { + try { + return Deno[Deno.internal].inspectArgs(["%o", object], { colors: !Deno.noColor }); + } catch (err) { + return Deno[Deno.internal].inspectArgs(["%o", err]); + } + }"#, + arguments: &[evaluate_result], + }; + + // TODO(caspervonb) we should investigate using previews here but to keep things + // consistent with the previous implementation we just get the preview result from + // Deno.inspectArgs. + let inspect_response = self + .post_message_with_event_loop("Runtime.callFunctionOn", Some(params)) + .await?; + + #[derive(Deserialize)] + struct CallFunctionResult { + value: LossyString, + } + + #[derive(Deserialize)] + struct CallFunctionResponse { + result: CallFunctionResult, + } + + let resp: CallFunctionResponse = + serde_json::from_str(inspect_response.get()).unwrap(); + + Ok(resp.result.value.into()) + } + + async fn evaluate_ts_expression( + &mut self, + expression: &str, + ) -> Result { + let parsed_module = deno_ast::parse_module(deno_ast::ParseParams { + specifier: "repl.ts".to_string(), + source: deno_ast::SourceTextInfo::from_string(expression.to_string()), + media_type: deno_ast::MediaType::TypeScript, + capture_tokens: false, + maybe_syntax: None, + scope_analysis: false, + })?; + + let transpiled_src = transpile( + &parsed_module, + &crate::ast::EmitOptions { + emit_metadata: false, + source_map: false, + inline_source_map: false, + inline_sources: false, + imports_not_used_as_values: ImportsNotUsedAsValues::Preserve, + // JSX is not supported in the REPL + transform_jsx: false, + jsx_automatic: false, + jsx_development: false, + jsx_factory: "React.createElement".into(), + jsx_fragment_factory: "React.Fragment".into(), + jsx_import_source: None, + repl_imports: true, + }, + )? + .0; + + self + .evaluate_expression(&format!( + "'use strict'; void 0;\n{}", + transpiled_src + )) + .await + } + + async fn evaluate_expression( + &mut self, + expression: &str, + ) -> Result { + let resp = self + .post_message_with_event_loop( + "Runtime.evaluate", + Some(json!({ + "expression": expression, + "contextId": self.context_id, + "replMode": true, + })), + ) + .await?; + + let evaluate_resp: EvaluationResponse = serde_json::from_str(resp.get())?; + Ok(evaluate_resp) + } +} + async fn read_line_and_poll( repl_session: &mut ReplSession, message_handler: &mut RustylineSyncMessageHandler, diff --git a/core/Cargo.toml b/core/Cargo.toml index 73e1016dfdf28..d6f13de2f9665 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -22,7 +22,7 @@ once_cell = "=1.9.0" parking_lot = "0.11.1" pin-project = "1.0.7" serde = { version = "1.0.129", features = ["derive"] } -serde_json = { version = "1.0.66", features = ["preserve_order"] } +serde_json = { version = "1.0.72", features = ["preserve_order", "raw_value"] } serde_v8 = { version = "0.27.0", path = "../serde_v8" } url = { version = "2.2.2", features = ["serde"] } v8 = "0.38.1" diff --git a/core/inspector.rs b/core/inspector.rs index 97a04407d3031..24f19ab8bf0ef 100644 --- a/core/inspector.rs +++ b/core/inspector.rs @@ -19,20 +19,25 @@ use crate::futures::task; use crate::futures::task::Context; use crate::futures::task::Poll; use crate::serde_json; -use crate::serde_json::json; -use crate::serde_json::Value; use anyhow::Error; +use log::debug; use parking_lot::Mutex; +use serde::Deserialize; +use serde::Serialize; +use serde_json::value::RawValue; use std::cell::BorrowMutError; use std::cell::RefCell; use std::collections::HashMap; use std::ffi::c_void; +use std::iter::Peekable; +use std::mem::replace; use std::mem::take; use std::mem::MaybeUninit; use std::pin::Pin; use std::ptr; use std::ptr::NonNull; use std::rc::Rc; +use std::str::Chars; use std::sync::Arc; use std::thread; @@ -616,19 +621,38 @@ impl Stream for InspectorSession { } } +#[derive(Debug, Deserialize)] +struct V8InspectorMessage { + id: i32, + result: Option>, + error: Option, +} + +#[derive(Debug, Deserialize)] +struct V8Error { + code: i32, + message: LossyString, +} + +#[derive(Deserialize)] +pub struct V8InspectorNotification { + pub method: String, + pub params: Box, +} + /// A local inspector session that can be used to send and receive protocol messages directly on /// the same thread as an isolate. pub struct LocalInspectorSession { - v8_session_tx: UnboundedSender, + v8_session_tx: UnboundedSender>, v8_session_rx: UnboundedReceiver, - response_tx_map: HashMap>, + response_tx_map: HashMap>, next_message_id: i32, - notification_queue: Vec, + notification_queue: Vec, } impl LocalInspectorSession { pub fn new( - v8_session_tx: UnboundedSender, + v8_session_tx: UnboundedSender>, v8_session_rx: UnboundedReceiver, ) -> Self { let response_tx_map = HashMap::new(); @@ -645,30 +669,37 @@ impl LocalInspectorSession { } } - pub fn notifications(&mut self) -> Vec { + pub fn notifications(&mut self) -> Vec { self.notification_queue.split_off(0) } - pub async fn post_message( + pub async fn post_message( &mut self, - method: &str, - params: Option, - ) -> Result { + method: &'static str, + params: Option, + ) -> Result, Error> { let id = self.next_message_id; self.next_message_id += 1; let (response_tx, mut response_rx) = - oneshot::channel::(); + oneshot::channel::(); self.response_tx_map.insert(id, response_tx); - let message = json!({ - "id": id, - "method": method, - "params": params, - }); + #[derive(Serialize)] + struct Request { + id: i32, + method: &'static str, + params: Option, + } - let stringified_msg = serde_json::to_string(&message).unwrap(); - self.v8_session_tx.unbounded_send(stringified_msg).unwrap(); + let message = Request { id, method, params }; + + let raw_message = serde_json::to_string(&message).unwrap(); + debug!("Sending message: {}", raw_message); + self + .v8_session_tx + .unbounded_send(raw_message.as_bytes().to_vec()) + .unwrap(); loop { let receive_fut = self.receive_from_v8_session().boxed_local(); @@ -676,12 +707,16 @@ impl LocalInspectorSession { Either::Left(_) => continue, Either::Right((result, _)) => { let response = result?; - if let Some(error) = response.get("error") { - return Err(generic_error(error.to_string())); + match (response.result, response.error) { + (Some(result), None) => { + return Ok(result); + } + (None, Some(err)) => { + let message: String = err.message.into(); + return Err(generic_error(message)); + } + _ => panic!("Invalid response"), } - - let result = response.get("result").unwrap().clone(); - return Ok(result); } } } @@ -689,31 +724,16 @@ impl LocalInspectorSession { async fn receive_from_v8_session(&mut self) { let inspector_msg = self.v8_session_rx.next().await.unwrap(); + debug!("Recv message: {}", inspector_msg.content); + // If there's no call_id then it's a notification if let InspectorMsgKind::Message(msg_id) = inspector_msg.kind { - let message: serde_json::Value = + let message: V8InspectorMessage = match serde_json::from_str(&inspector_msg.content) { Ok(v) => v, - Err(error) => match error.classify() { - serde_json::error::Category::Syntax => json!({ - "id": msg_id, - "result": { - "result": { - "type": "error", - "description": "Unterminated string literal", - "value": "Unterminated string literal", - }, - "exceptionDetails": { - "exceptionId": 0, - "text": "Unterminated string literal", - "lineNumber": 0, - "columnNumber": 0 - }, - }, - }), - _ => panic!("Could not parse inspector message"), - }, + Err(e) => { + panic!("Could not parse inspector message: {}", e) + } }; - self .response_tx_map .remove(&msg_id) @@ -733,3 +753,245 @@ fn new_box_with(new_fn: impl FnOnce(*mut T) -> T) -> Box { unsafe { ptr::write(p, new_fn(p)) }; unsafe { Box::from_raw(p) } } + +/// A wrapper type for `String` that has a serde::Deserialize implementation +/// that will deserialize lossily (i.e. using replacement characters for +/// invalid UTF-8 sequences). +pub struct LossyString(String); + +static ESCAPE: [bool; 256] = { + const CT: bool = true; // control character \x00..=\x1F + const QU: bool = true; // quote \x22 + const BS: bool = true; // backslash \x5C + const __: bool = false; // allow unescaped + [ + // 1 2 3 4 5 6 7 8 9 A B C D E F + CT, CT, CT, CT, CT, CT, CT, CT, CT, CT, CT, CT, CT, CT, CT, CT, // 0 + CT, CT, CT, CT, CT, CT, CT, CT, CT, CT, CT, CT, CT, CT, CT, CT, // 1 + __, __, QU, __, __, __, __, __, __, __, __, __, __, __, __, __, // 2 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 3 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 4 + __, __, __, __, __, __, __, __, __, __, __, __, BS, __, __, __, // 5 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 6 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 7 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 8 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 9 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // A + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // B + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // C + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // D + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // E + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // F + ] +}; + +impl<'de> serde::Deserialize<'de> for LossyString { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + macro_rules! tri { + ($opt:expr) => { + match $opt { + Some(v) => v, + None => { + return Err(serde::de::Error::custom("Unexpected end of string")) + } + } + }; + } + + let val = Box::::deserialize(deserializer)?; + let mut chars = val.get().chars().peekable(); + + if tri!(chars.next()) != '"' { + return Err(serde::de::Error::custom("Expected string")); + } + + let mut str = String::new(); + + loop { + let ch = tri!(chars.next()); + if !ESCAPE[ch as usize] { + str.push(ch); + continue; + } + match ch { + '"' => { + break; + } + '\\' => match parse_escape(&mut chars, &mut str) { + Ok(_) => {} + Err(err) => return Err(serde::de::Error::custom(err)), + }, + _ => { + return Err(serde::de::Error::custom("Constrol character in string")); + } + } + } + + if chars.next() != None { + return Err(serde::de::Error::custom("Trailing characters in string")); + } + + Ok(LossyString(str)) + } +} + +static HEX: [u8; 256] = { + const __: u8 = 255; // not a hex digit + [ + // 1 2 3 4 5 6 7 8 9 A B C D E F + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 0 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 1 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 2 + 00, 01, 02, 03, 04, 05, 06, 07, 08, 09, __, __, __, __, __, __, // 3 + __, 10, 11, 12, 13, 14, 15, __, __, __, __, __, __, __, __, __, // 4 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 5 + __, 10, 11, 12, 13, 14, 15, __, __, __, __, __, __, __, __, __, // 6 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 7 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 8 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // 9 + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // A + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // B + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // C + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // D + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // E + __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, __, // F + ] +}; + +fn decode_hex_val(val: char) -> Option { + let n = HEX[val as usize] as u16; + if n == 255 { + None + } else { + Some(n) + } +} + +fn decode_hex_escape( + chars: &mut Peekable>, +) -> Result { + let mut n = 0; + for _ in 0..4 { + let ch = match chars.next() { + Some(ch) => ch, + None => return Err("Unexpected end of string"), + }; + match decode_hex_val(ch) { + None => return Err("Invalid hex escape"), + Some(val) => { + n = (n << 4) + val; + } + } + } + Ok(n) +} + +fn parse_escape( + chars: &mut Peekable>, + str: &mut String, +) -> Result<(), &'static str> { + macro_rules! tri { + ($opt:expr) => { + match $opt { + Some(v) => v, + None => return Err("Unexpected end of string"), + } + }; + } + + let ch = tri!(chars.next()); + + match ch { + '"' => str.push('"'), + '\\' => str.push('\\'), + '/' => str.push('/'), + 'b' => str.push('\x08'), + 'f' => str.push('\x0c'), + 'n' => str.push('\n'), + 'r' => str.push('\r'), + 't' => str.push('\t'), + 'u' => { + let c = match decode_hex_escape(chars)? { + 0xDC00..=0xDFFF => { + str.push('\u{FFFD}'); + return Ok(()); + } + + // Non-BMP characters are encoded as a sequence of two hex + // escapes, representing UTF-16 surrogates. If deserializing a + // utf-8 string the surrogates are required to be paired, + // whereas deserializing a byte string accepts lone surrogates. + n1 @ 0xD800..=0xDBFF => { + if *tri!(chars.peek()) == '\\' { + chars.next(); + } else { + str.push('\u{FFFD}'); + return Ok(()); + } + + if *tri!(chars.peek()) == 'u' { + chars.next(); + } else { + str.push('\u{FFFD}'); + return parse_escape(chars, str); + } + + let n2 = decode_hex_escape(chars)?; + + if n2 < 0xDC00 || n2 > 0xDFFF { + str.push('\u{FFFD}'); + return Ok(()); + } + + let n = + (((n1 - 0xD800) as u32) << 10 | (n2 - 0xDC00) as u32) + 0x1_0000; + + match char::from_u32(n) { + Some(c) => c, + None => { + str.push('\u{FFFD}'); + return Ok(()); + } + } + } + + n => match char::from_u32(n as u32) { + Some(c) => c, + None => { + str.push('\u{FFFD}'); + return Ok(()); + } + }, + }; + + str.push(c); + } + _ => { + return Err("Invalid escape sequence"); + } + } + + Ok(()) +} + +impl From for String { + fn from(lossy_string: LossyString) -> Self { + lossy_string.0 + } +} + +impl std::ops::Deref for LossyString { + type Target = String; + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl std::fmt::Debug for LossyString { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.0) + } +} diff --git a/core/lib.rs b/core/lib.rs index 2f46c1ffc9afb..9c9a61ce2ebbd 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -50,6 +50,8 @@ pub use crate::inspector::InspectorMsgKind; pub use crate::inspector::InspectorSessionProxy; pub use crate::inspector::JsRuntimeInspector; pub use crate::inspector::LocalInspectorSession; +pub use crate::inspector::LossyString; +pub use crate::inspector::V8InspectorNotification; pub use crate::module_specifier::resolve_import; pub use crate::module_specifier::resolve_path; pub use crate::module_specifier::resolve_url;