summaryrefslogtreecommitdiff
path: root/runtime
diff options
context:
space:
mode:
authorBartek Iwańczuk <biwanczuk@gmail.com>2022-12-18 01:12:28 +0100
committerGitHub <noreply@github.com>2022-12-18 01:12:28 +0100
commit7b212bc5741ea0cfb6bd87c280be0c864cb32813 (patch)
tree0943c299d10f26d0b2e797ff02eb011336c074e3 /runtime
parent59dedf217f0d43dd75da0f615846526d088dbf0b (diff)
refactor(permissions): factor out PermissionPrompter trait, add callbacks (#16975)
This commit refactors several things in "runtime/permissions" module: - splits it into "mod.rs" and "prompter.rs" - adds "PermissionPrompter" trait with two implementations: * "TtyPrompter" * "TestPrompter" - adds "before" and "after" prompt callback which can be used to hide progress bar in the CLI (to be done in a follow up) - "permissions_prompt" API returns "PromptResponse" enum, instead of a boolean; this allows to add "allow all"/"deny all" functionality for the prompt
Diffstat (limited to 'runtime')
-rw-r--r--runtime/permissions/mod.rs (renamed from runtime/permissions.rs)428
-rw-r--r--runtime/permissions/prompter.rs288
2 files changed, 419 insertions, 297 deletions
diff --git a/runtime/permissions.rs b/runtime/permissions/mod.rs
index 23fb7df5d..23c9917cd 100644
--- a/runtime/permissions.rs
+++ b/runtime/permissions/mod.rs
@@ -6,8 +6,6 @@ use deno_core::error::custom_error;
use deno_core::error::type_error;
use deno_core::error::uri_error;
use deno_core::error::AnyError;
-#[cfg(test)]
-use deno_core::parking_lot::Mutex;
use deno_core::serde::de;
use deno_core::serde::Deserialize;
use deno_core::serde::Deserializer;
@@ -24,12 +22,14 @@ use std::hash::Hash;
use std::path::{Path, PathBuf};
use std::str::FromStr;
use std::string::ToString;
-#[cfg(test)]
-use std::sync::atomic::AtomicBool;
-#[cfg(test)]
-use std::sync::atomic::Ordering;
-const PERMISSION_EMOJI: &str = "⚠️";
+mod prompter;
+use prompter::permission_prompt;
+use prompter::PromptResponse;
+use prompter::PERMISSION_EMOJI;
+
+pub use prompter::set_prompt_callbacks;
+pub use prompter::PromptCallback;
static DEBUG_LOG_ENABLED: Lazy<bool> =
Lazy::new(|| log::log_enabled!(log::Level::Debug));
@@ -110,7 +110,7 @@ impl PermissionState {
name,
info().map_or(String::new(), |info| { format!(" to {}", info) }),
);
- if permission_prompt(&msg, name, api_name) {
+ if PromptResponse::Allow == permission_prompt(&msg, name, api_name) {
Self::log_perm_access(name, info);
(Ok(()), true)
} else {
@@ -153,11 +153,13 @@ impl UnitPermission {
pub fn request(&mut self) -> PermissionState {
if self.state == PermissionState::Prompt {
- if permission_prompt(
- &format!("access to {}", self.description),
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ &format!("access to {}", self.description),
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self.state = PermissionState::Granted;
} else {
self.state = PermissionState::Denied;
@@ -352,11 +354,13 @@ impl UnaryPermission<ReadDescriptor> {
let (resolved_path, display_path) = resolved_and_display_path(path);
let state = self.query(Some(&resolved_path));
if state == PermissionState::Prompt {
- if permission_prompt(
- &format!("read access to \"{}\"", display_path.display()),
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ &format!("read access to \"{}\"", display_path.display()),
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self.granted_list.insert(ReadDescriptor(resolved_path));
PermissionState::Granted
} else {
@@ -373,11 +377,13 @@ impl UnaryPermission<ReadDescriptor> {
} else {
let state = self.query(None);
if state == PermissionState::Prompt {
- if permission_prompt(
- "read access",
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ "read access",
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self.granted_list.clear();
self.global_state = PermissionState::Granted;
PermissionState::Granted
@@ -521,11 +527,13 @@ impl UnaryPermission<WriteDescriptor> {
let (resolved_path, display_path) = resolved_and_display_path(path);
let state = self.query(Some(&resolved_path));
if state == PermissionState::Prompt {
- if permission_prompt(
- &format!("write access to \"{}\"", display_path.display()),
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ &format!("write access to \"{}\"", display_path.display()),
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self.granted_list.insert(WriteDescriptor(resolved_path));
PermissionState::Granted
} else {
@@ -542,11 +550,13 @@ impl UnaryPermission<WriteDescriptor> {
} else {
let state = self.query(None);
if state == PermissionState::Prompt {
- if permission_prompt(
- "write access",
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ "write access",
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self.granted_list.clear();
self.global_state = PermissionState::Granted;
PermissionState::Granted
@@ -672,11 +682,13 @@ impl UnaryPermission<NetDescriptor> {
let state = self.query(Some(host));
let host = NetDescriptor::new(&host);
if state == PermissionState::Prompt {
- if permission_prompt(
- &format!("network access to \"{}\"", host),
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ &format!("network access to \"{}\"", host),
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self.granted_list.insert(host);
PermissionState::Granted
} else {
@@ -693,11 +705,13 @@ impl UnaryPermission<NetDescriptor> {
} else {
let state = self.query::<&str>(None);
if state == PermissionState::Prompt {
- if permission_prompt(
- "network access",
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ "network access",
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self.granted_list.clear();
self.global_state = PermissionState::Granted;
PermissionState::Granted
@@ -842,11 +856,13 @@ impl UnaryPermission<EnvDescriptor> {
if let Some(env) = env {
let state = self.query(Some(env));
if state == PermissionState::Prompt {
- if permission_prompt(
- &format!("env access to \"{}\"", env),
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ &format!("env access to \"{}\"", env),
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self.granted_list.insert(EnvDescriptor::new(env));
PermissionState::Granted
} else {
@@ -863,11 +879,13 @@ impl UnaryPermission<EnvDescriptor> {
} else {
let state = self.query(None);
if state == PermissionState::Prompt {
- if permission_prompt(
- "env access",
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ "env access",
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self.granted_list.clear();
self.global_state = PermissionState::Granted;
PermissionState::Granted
@@ -972,11 +990,13 @@ impl UnaryPermission<SysDescriptor> {
}
if let Some(kind) = kind {
let desc = SysDescriptor(kind.to_string());
- if permission_prompt(
- &format!("sys access to \"{}\"", kind),
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ &format!("sys access to \"{}\"", kind),
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self.granted_list.insert(desc);
PermissionState::Granted
} else {
@@ -985,11 +1005,13 @@ impl UnaryPermission<SysDescriptor> {
PermissionState::Denied
}
} else {
- if permission_prompt(
- "sys access",
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ "sys access",
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self.global_state = PermissionState::Granted;
} else {
self.granted_list.clear();
@@ -1091,11 +1113,13 @@ impl UnaryPermission<RunDescriptor> {
if let Some(cmd) = cmd {
let state = self.query(Some(cmd));
if state == PermissionState::Prompt {
- if permission_prompt(
- &format!("run access to \"{}\"", cmd),
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ &format!("run access to \"{}\"", cmd),
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self
.granted_list
.insert(RunDescriptor::from_str(cmd).unwrap());
@@ -1118,11 +1142,13 @@ impl UnaryPermission<RunDescriptor> {
} else {
let state = self.query(None);
if state == PermissionState::Prompt {
- if permission_prompt(
- "run access",
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ "run access",
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self.granted_list.clear();
self.global_state = PermissionState::Granted;
PermissionState::Granted
@@ -1238,11 +1264,13 @@ impl UnaryPermission<FfiDescriptor> {
let (resolved_path, display_path) = resolved_and_display_path(path);
let state = self.query(Some(&resolved_path));
if state == PermissionState::Prompt {
- if permission_prompt(
- &format!("ffi access to \"{}\"", display_path.display()),
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ &format!("ffi access to \"{}\"", display_path.display()),
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self.granted_list.insert(FfiDescriptor(resolved_path));
PermissionState::Granted
} else {
@@ -1259,11 +1287,13 @@ impl UnaryPermission<FfiDescriptor> {
} else {
let state = self.query(None);
if state == PermissionState::Prompt {
- if permission_prompt(
- "ffi access",
- self.name,
- Some("Deno.permissions.query()"),
- ) {
+ if PromptResponse::Allow
+ == permission_prompt(
+ "ffi access",
+ self.name,
+ Some("Deno.permissions.query()"),
+ )
+ {
self.granted_list.clear();
self.global_state = PermissionState::Granted;
PermissionState::Granted
@@ -2262,225 +2292,12 @@ pub fn create_child_permissions(
Ok(worker_perms)
}
-/// Shows the permission prompt and returns the answer according to the user input.
-/// This loops until the user gives the proper input.
-#[cfg(not(test))]
-fn permission_prompt(
- message: &str,
- name: &str,
- api_name: Option<&str>,
-) -> bool {
- if !atty::is(atty::Stream::Stdin) || !atty::is(atty::Stream::Stderr) {
- return false;
- };
-
- #[cfg(unix)]
- fn clear_stdin() -> Result<(), AnyError> {
- // TODO(bartlomieju):
- #[allow(clippy::undocumented_unsafe_blocks)]
- let r = unsafe { libc::tcflush(0, libc::TCIFLUSH) };
- assert_eq!(r, 0);
- Ok(())
- }
-
- #[cfg(not(unix))]
- fn clear_stdin() -> Result<(), AnyError> {
- use deno_core::anyhow::bail;
- use winapi::shared::minwindef::TRUE;
- use winapi::shared::minwindef::UINT;
- use winapi::shared::minwindef::WORD;
- use winapi::shared::ntdef::WCHAR;
- use winapi::um::processenv::GetStdHandle;
- use winapi::um::winbase::STD_INPUT_HANDLE;
- use winapi::um::wincon::FlushConsoleInputBuffer;
- use winapi::um::wincon::PeekConsoleInputW;
- use winapi::um::wincon::WriteConsoleInputW;
- use winapi::um::wincontypes::INPUT_RECORD;
- use winapi::um::wincontypes::KEY_EVENT;
- use winapi::um::winnt::HANDLE;
- use winapi::um::winuser::MapVirtualKeyW;
- use winapi::um::winuser::MAPVK_VK_TO_VSC;
- use winapi::um::winuser::VK_RETURN;
-
- // SAFETY: winapi calls
- unsafe {
- let stdin = GetStdHandle(STD_INPUT_HANDLE);
- // emulate an enter key press to clear any line buffered console characters
- emulate_enter_key_press(stdin)?;
- // read the buffered line or enter key press
- read_stdin_line()?;
- // check if our emulated key press was executed
- if is_input_buffer_empty(stdin)? {
- // if so, move the cursor up to prevent a blank line
- move_cursor_up()?;
- } else {
- // the emulated key press is still pending, so a buffered line was read
- // and we can flush the emulated key press
- flush_input_buffer(stdin)?;
- }
- }
-
- return Ok(());
-
- unsafe fn flush_input_buffer(stdin: HANDLE) -> Result<(), AnyError> {
- let success = FlushConsoleInputBuffer(stdin);
- if success != TRUE {
- bail!(
- "Could not flush the console input buffer: {}",
- std::io::Error::last_os_error()
- )
- }
- Ok(())
- }
-
- unsafe fn emulate_enter_key_press(stdin: HANDLE) -> Result<(), AnyError> {
- // https://github.com/libuv/libuv/blob/a39009a5a9252a566ca0704d02df8dabc4ce328f/src/win/tty.c#L1121-L1131
- let mut input_record: INPUT_RECORD = std::mem::zeroed();
- input_record.EventType = KEY_EVENT;
- input_record.Event.KeyEvent_mut().bKeyDown = TRUE;
- input_record.Event.KeyEvent_mut().wRepeatCount = 1;
- input_record.Event.KeyEvent_mut().wVirtualKeyCode = VK_RETURN as WORD;
- input_record.Event.KeyEvent_mut().wVirtualScanCode =
- MapVirtualKeyW(VK_RETURN as UINT, MAPVK_VK_TO_VSC) as WORD;
- *input_record.Event.KeyEvent_mut().uChar.UnicodeChar_mut() =
- '\r' as WCHAR;
-
- let mut record_written = 0;
- let success =
- WriteConsoleInputW(stdin, &input_record, 1, &mut record_written);
- if success != TRUE {
- bail!(
- "Could not emulate enter key press: {}",
- std::io::Error::last_os_error()
- );
- }
- Ok(())
- }
-
- unsafe fn is_input_buffer_empty(stdin: HANDLE) -> Result<bool, AnyError> {
- let mut buffer = Vec::with_capacity(1);
- let mut events_read = 0;
- let success =
- PeekConsoleInputW(stdin, buffer.as_mut_ptr(), 1, &mut events_read);
- if success != TRUE {
- bail!(
- "Could not peek the console input buffer: {}",
- std::io::Error::last_os_error()
- )
- }
- Ok(events_read == 0)
- }
-
- fn move_cursor_up() -> Result<(), AnyError> {
- use std::io::Write;
- write!(std::io::stderr(), "\x1B[1A")?;
- Ok(())
- }
-
- fn read_stdin_line() -> Result<(), AnyError> {
- let mut input = String::new();
- let stdin = std::io::stdin();
- stdin.read_line(&mut input)?;
- Ok(())
- }
- }
-
- // Clear n-lines in terminal and move cursor to the beginning of the line.
- fn clear_n_lines(n: usize) {
- eprint!("\x1B[{}A\x1B[0J", n);
- }
-
- // For security reasons we must consume everything in stdin so that previously
- // buffered data cannot effect the prompt.
- if let Err(err) = clear_stdin() {
- eprintln!("Error clearing stdin for permission prompt. {:#}", err);
- return false; // don't grant permission if this fails
- }
-
- // print to stderr so that if stdout is piped this is still displayed.
- const OPTS: &str = "[y/n] (y = yes, allow; n = no, deny)";
- eprint!("{} ┌ ", PERMISSION_EMOJI);
- eprint!("{}", colors::bold("Deno requests "));
- eprint!("{}", colors::bold(message));
- eprintln!("{}", colors::bold("."));
- if let Some(api_name) = api_name {
- eprintln!(" ├ Requested by `{}` API", api_name);
- }
- let msg = format!(
- " ├ Run again with --allow-{} to bypass this prompt.",
- name
- );
- eprintln!("{}", colors::italic(&msg));
- eprint!(" └ {}", colors::bold("Allow?"));
- eprint!(" {} > ", OPTS);
- loop {
- let mut input = String::new();
- let stdin = std::io::stdin();
- let result = stdin.read_line(&mut input);
- if result.is_err() {
- return false;
- };
- let ch = match input.chars().next() {
- None => return false,
- Some(v) => v,
- };
- match ch.to_ascii_lowercase() {
- 'y' => {
- clear_n_lines(if api_name.is_some() { 4 } else { 3 });
- let msg = format!("Granted {}.", message);
- eprintln!("✅ {}", colors::bold(&msg));
- return true;
- }
- 'n' => {
- clear_n_lines(if api_name.is_some() { 4 } else { 3 });
- let msg = format!("Denied {}.", message);
- eprintln!("❌ {}", colors::bold(&msg));
- return false;
- }
- _ => {
- // If we don't get a recognized option try again.
- clear_n_lines(1);
- eprint!(" └ {}", colors::bold("Unrecognized option. Allow?"));
- eprint!(" {} > ", OPTS);
- }
- };
- }
-}
-
-// When testing, permission prompt returns the value of STUB_PROMPT_VALUE
-// which we set from the test functions.
-#[cfg(test)]
-fn permission_prompt(
- _message: &str,
- _flag: &str,
- _api_name: Option<&str>,
-) -> bool {
- STUB_PROMPT_VALUE.load(Ordering::SeqCst)
-}
-
-#[cfg(test)]
-static STUB_PROMPT_VALUE: AtomicBool = AtomicBool::new(true);
-
-#[cfg(test)]
-static PERMISSION_PROMPT_STUB_VALUE_SETTER: Lazy<
- Mutex<PermissionPromptStubValueSetter>,
-> = Lazy::new(|| Mutex::new(PermissionPromptStubValueSetter));
-
-#[cfg(test)]
-struct PermissionPromptStubValueSetter;
-
-#[cfg(test)]
-impl PermissionPromptStubValueSetter {
- pub fn set(&self, value: bool) {
- STUB_PROMPT_VALUE.store(value, Ordering::SeqCst);
- }
-}
-
#[cfg(test)]
mod tests {
use super::*;
use deno_core::resolve_url_or_path;
use deno_core::serde_json::json;
+ use prompter::tests::*;
// Creates vector of strings, Vec<String>
macro_rules! svec {
@@ -2489,6 +2306,7 @@ mod tests {
#[test]
fn check_paths() {
+ set_prompter(Box::new(TestPrompter));
let allowlist = vec![
PathBuf::from("/a/specific/dir/name"),
PathBuf::from("/a/specific"),
@@ -2590,6 +2408,7 @@ mod tests {
#[test]
fn test_check_net_with_values() {
+ set_prompter(Box::new(TestPrompter));
let mut perms = Permissions::from_options(&PermissionsOptions {
allow_net: Some(svec![
"localhost",
@@ -2633,6 +2452,7 @@ mod tests {
#[test]
fn test_check_net_only_flag() {
+ set_prompter(Box::new(TestPrompter));
let mut perms = Permissions::from_options(&PermissionsOptions {
allow_net: Some(svec![]), // this means `--allow-net` is present without values following `=` sign
..Default::default()
@@ -2668,6 +2488,7 @@ mod tests {
#[test]
fn test_check_net_no_flag() {
+ set_prompter(Box::new(TestPrompter));
let mut perms = Permissions::from_options(&PermissionsOptions {
allow_net: None,
..Default::default()
@@ -2763,6 +2584,7 @@ mod tests {
#[test]
fn check_specifiers() {
+ set_prompter(Box::new(TestPrompter));
let read_allowlist = if cfg!(target_os = "windows") {
vec![PathBuf::from("C:\\a")]
} else {
@@ -2807,6 +2629,7 @@ mod tests {
#[test]
fn check_invalid_specifiers() {
+ set_prompter(Box::new(TestPrompter));
let mut perms = Permissions::allow_all();
let mut test_cases = vec![];
@@ -2827,6 +2650,7 @@ mod tests {
#[test]
fn test_query() {
+ set_prompter(Box::new(TestPrompter));
let perms1 = Permissions::allow_all();
let perms2 = Permissions {
read: UnaryPermission {
@@ -2906,6 +2730,7 @@ mod tests {
#[test]
fn test_request() {
+ set_prompter(Box::new(TestPrompter));
let mut perms: Permissions = Default::default();
#[rustfmt::skip]
{
@@ -2953,6 +2778,7 @@ mod tests {
#[test]
fn test_revoke() {
+ set_prompter(Box::new(TestPrompter));
let mut perms = Permissions {
read: UnaryPermission {
global_state: PermissionState::Prompt,
@@ -3026,6 +2852,7 @@ mod tests {
#[test]
fn test_check() {
+ set_prompter(Box::new(TestPrompter));
let mut perms = Permissions {
read: Permissions::new_read(&None, true).unwrap(),
write: Permissions::new_write(&None, true).unwrap(),
@@ -3089,6 +2916,7 @@ mod tests {
#[test]
fn test_check_fail() {
+ set_prompter(Box::new(TestPrompter));
let mut perms = Permissions {
read: Permissions::new_read(&None, true).unwrap(),
write: Permissions::new_write(&None, true).unwrap(),
@@ -3169,6 +2997,7 @@ mod tests {
#[test]
#[cfg(windows)]
fn test_env_windows() {
+ set_prompter(Box::new(TestPrompter));
let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock();
let mut perms = Permissions::allow_all();
perms.env = UnaryPermission {
@@ -3187,6 +3016,7 @@ mod tests {
#[test]
fn test_deserialize_child_permissions_arg() {
+ set_prompter(Box::new(TestPrompter));
assert_eq!(
ChildPermissionsArg::inherit(),
ChildPermissionsArg {
@@ -3341,6 +3171,7 @@ mod tests {
#[test]
fn test_create_child_permissions() {
+ set_prompter(Box::new(TestPrompter));
let mut main_perms = Permissions {
env: Permissions::new_env(&Some(vec![]), false).unwrap(),
hrtime: Permissions::new_hrtime(true),
@@ -3393,6 +3224,7 @@ mod tests {
#[test]
fn test_create_child_permissions_with_prompt() {
+ set_prompter(Box::new(TestPrompter));
let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock();
let mut main_perms = Permissions::from_options(&PermissionsOptions {
prompt: true,
@@ -3414,6 +3246,7 @@ mod tests {
#[test]
fn test_create_child_permissions_with_inherited_denied_list() {
+ set_prompter(Box::new(TestPrompter));
let prompt_value = PERMISSION_PROMPT_STUB_VALUE_SETTER.lock();
let mut main_perms = Permissions::from_options(&PermissionsOptions {
prompt: true,
@@ -3432,6 +3265,7 @@ mod tests {
#[test]
fn test_handle_empty_value() {
+ set_prompter(Box::new(TestPrompter));
assert!(Permissions::new_read(&Some(vec![PathBuf::new()]), false).is_err());
assert!(Permissions::new_env(&Some(vec![String::new()]), false).is_err());
assert!(Permissions::new_sys(&Some(vec![String::new()]), false).is_err());
diff --git a/runtime/permissions/prompter.rs b/runtime/permissions/prompter.rs
new file mode 100644
index 000000000..d231f7344
--- /dev/null
+++ b/runtime/permissions/prompter.rs
@@ -0,0 +1,288 @@
+// Copyright 2018-2022 the Deno authors. All rights reserved. MIT license.
+
+use crate::colors;
+use deno_core::error::AnyError;
+use deno_core::parking_lot::Mutex;
+use once_cell::sync::Lazy;
+
+pub const PERMISSION_EMOJI: &str = "⚠️";
+
+#[derive(Debug, Eq, PartialEq)]
+pub enum PromptResponse {
+ Allow,
+ Deny,
+}
+
+static PERMISSION_PROMPTER: Lazy<Mutex<Box<dyn PermissionPrompter>>> =
+ Lazy::new(|| Mutex::new(Box::new(TtyPrompter)));
+
+static MAYBE_BEFORE_PROMPT_CALLBACK: Lazy<Mutex<Option<PromptCallback>>> =
+ Lazy::new(|| Mutex::new(None));
+
+static MAYBE_AFTER_PROMPT_CALLBACK: Lazy<Mutex<Option<PromptCallback>>> =
+ Lazy::new(|| Mutex::new(None));
+
+pub fn permission_prompt(
+ message: &str,
+ flag: &str,
+ api_name: Option<&str>,
+) -> PromptResponse {
+ if let Some(before_callback) = MAYBE_BEFORE_PROMPT_CALLBACK.lock().as_mut() {
+ before_callback();
+ }
+ let r = PERMISSION_PROMPTER.lock().prompt(message, flag, api_name);
+ if let Some(after_callback) = MAYBE_AFTER_PROMPT_CALLBACK.lock().as_mut() {
+ after_callback();
+ }
+ r
+}
+
+pub fn set_prompt_callbacks(
+ before_callback: Option<PromptCallback>,
+ after_callback: Option<PromptCallback>,
+) {
+ *MAYBE_BEFORE_PROMPT_CALLBACK.lock() = before_callback;
+ *MAYBE_AFTER_PROMPT_CALLBACK.lock() = after_callback;
+}
+
+pub type PromptCallback = Box<dyn FnMut() + Send + Sync>;
+
+pub trait PermissionPrompter: Send + Sync {
+ fn prompt(
+ &mut self,
+ message: &str,
+ name: &str,
+ api_name: Option<&str>,
+ ) -> PromptResponse;
+}
+
+pub struct TtyPrompter;
+
+impl PermissionPrompter for TtyPrompter {
+ fn prompt(
+ &mut self,
+ message: &str,
+ name: &str,
+ api_name: Option<&str>,
+ ) -> PromptResponse {
+ if !atty::is(atty::Stream::Stdin) || !atty::is(atty::Stream::Stderr) {
+ return PromptResponse::Deny;
+ };
+
+ #[cfg(unix)]
+ fn clear_stdin() -> Result<(), AnyError> {
+ // TODO(bartlomieju):
+ #[allow(clippy::undocumented_unsafe_blocks)]
+ let r = unsafe { libc::tcflush(0, libc::TCIFLUSH) };
+ assert_eq!(r, 0);
+ Ok(())
+ }
+
+ #[cfg(not(unix))]
+ fn clear_stdin() -> Result<(), AnyError> {
+ use deno_core::anyhow::bail;
+ use winapi::shared::minwindef::TRUE;
+ use winapi::shared::minwindef::UINT;
+ use winapi::shared::minwindef::WORD;
+ use winapi::shared::ntdef::WCHAR;
+ use winapi::um::processenv::GetStdHandle;
+ use winapi::um::winbase::STD_INPUT_HANDLE;
+ use winapi::um::wincon::FlushConsoleInputBuffer;
+ use winapi::um::wincon::PeekConsoleInputW;
+ use winapi::um::wincon::WriteConsoleInputW;
+ use winapi::um::wincontypes::INPUT_RECORD;
+ use winapi::um::wincontypes::KEY_EVENT;
+ use winapi::um::winnt::HANDLE;
+ use winapi::um::winuser::MapVirtualKeyW;
+ use winapi::um::winuser::MAPVK_VK_TO_VSC;
+ use winapi::um::winuser::VK_RETURN;
+
+ // SAFETY: winapi calls
+ unsafe {
+ let stdin = GetStdHandle(STD_INPUT_HANDLE);
+ // emulate an enter key press to clear any line buffered console characters
+ emulate_enter_key_press(stdin)?;
+ // read the buffered line or enter key press
+ read_stdin_line()?;
+ // check if our emulated key press was executed
+ if is_input_buffer_empty(stdin)? {
+ // if so, move the cursor up to prevent a blank line
+ move_cursor_up()?;
+ } else {
+ // the emulated key press is still pending, so a buffered line was read
+ // and we can flush the emulated key press
+ flush_input_buffer(stdin)?;
+ }
+ }
+
+ return Ok(());
+
+ unsafe fn flush_input_buffer(stdin: HANDLE) -> Result<(), AnyError> {
+ let success = FlushConsoleInputBuffer(stdin);
+ if success != TRUE {
+ bail!(
+ "Could not flush the console input buffer: {}",
+ std::io::Error::last_os_error()
+ )
+ }
+ Ok(())
+ }
+
+ unsafe fn emulate_enter_key_press(stdin: HANDLE) -> Result<(), AnyError> {
+ // https://github.com/libuv/libuv/blob/a39009a5a9252a566ca0704d02df8dabc4ce328f/src/win/tty.c#L1121-L1131
+ let mut input_record: INPUT_RECORD = std::mem::zeroed();
+ input_record.EventType = KEY_EVENT;
+ input_record.Event.KeyEvent_mut().bKeyDown = TRUE;
+ input_record.Event.KeyEvent_mut().wRepeatCount = 1;
+ input_record.Event.KeyEvent_mut().wVirtualKeyCode = VK_RETURN as WORD;
+ input_record.Event.KeyEvent_mut().wVirtualScanCode =
+ MapVirtualKeyW(VK_RETURN as UINT, MAPVK_VK_TO_VSC) as WORD;
+ *input_record.Event.KeyEvent_mut().uChar.UnicodeChar_mut() =
+ '\r' as WCHAR;
+
+ let mut record_written = 0;
+ let success =
+ WriteConsoleInputW(stdin, &input_record, 1, &mut record_written);
+ if success != TRUE {
+ bail!(
+ "Could not emulate enter key press: {}",
+ std::io::Error::last_os_error()
+ );
+ }
+ Ok(())
+ }
+
+ unsafe fn is_input_buffer_empty(stdin: HANDLE) -> Result<bool, AnyError> {
+ let mut buffer = Vec::with_capacity(1);
+ let mut events_read = 0;
+ let success =
+ PeekConsoleInputW(stdin, buffer.as_mut_ptr(), 1, &mut events_read);
+ if success != TRUE {
+ bail!(
+ "Could not peek the console input buffer: {}",
+ std::io::Error::last_os_error()
+ )
+ }
+ Ok(events_read == 0)
+ }
+
+ fn move_cursor_up() -> Result<(), AnyError> {
+ use std::io::Write;
+ write!(std::io::stderr(), "\x1B[1A")?;
+ Ok(())
+ }
+
+ fn read_stdin_line() -> Result<(), AnyError> {
+ let mut input = String::new();
+ let stdin = std::io::stdin();
+ stdin.read_line(&mut input)?;
+ Ok(())
+ }
+ }
+
+ // Clear n-lines in terminal and move cursor to the beginning of the line.
+ fn clear_n_lines(n: usize) {
+ eprint!("\x1B[{}A\x1B[0J", n);
+ }
+
+ // For security reasons we must consume everything in stdin so that previously
+ // buffered data cannot effect the prompt.
+ if let Err(err) = clear_stdin() {
+ eprintln!("Error clearing stdin for permission prompt. {:#}", err);
+ return PromptResponse::Deny; // don't grant permission if this fails
+ }
+
+ // print to stderr so that if stdout is piped this is still displayed.
+ const OPTS: &str = "[y/n] (y = yes, allow; n = no, deny)";
+ eprint!("{} ┌ ", PERMISSION_EMOJI);
+ eprint!("{}", colors::bold("Deno requests "));
+ eprint!("{}", colors::bold(message));
+ eprintln!("{}", colors::bold("."));
+ if let Some(api_name) = api_name {
+ eprintln!(" ├ Requested by `{}` API", api_name);
+ }
+ let msg = format!(
+ " ├ Run again with --allow-{} to bypass this prompt.",
+ name
+ );
+ eprintln!("{}", colors::italic(&msg));
+ eprint!(" └ {}", colors::bold("Allow?"));
+ eprint!(" {} > ", OPTS);
+ let value = loop {
+ let mut input = String::new();
+ let stdin = std::io::stdin();
+ let result = stdin.read_line(&mut input);
+ if result.is_err() {
+ break PromptResponse::Deny;
+ };
+ let ch = match input.chars().next() {
+ None => break PromptResponse::Deny,
+ Some(v) => v,
+ };
+ match ch.to_ascii_lowercase() {
+ 'y' => {
+ clear_n_lines(if api_name.is_some() { 4 } else { 3 });
+ let msg = format!("Granted {}.", message);
+ eprintln!("✅ {}", colors::bold(&msg));
+ break PromptResponse::Allow;
+ }
+ 'n' => {
+ clear_n_lines(if api_name.is_some() { 4 } else { 3 });
+ let msg = format!("Denied {}.", message);
+ eprintln!("❌ {}", colors::bold(&msg));
+ break PromptResponse::Deny;
+ }
+ _ => {
+ // If we don't get a recognized option try again.
+ clear_n_lines(1);
+ eprint!(" └ {}", colors::bold("Unrecognized option. Allow?"));
+ eprint!(" {} > ", OPTS);
+ }
+ };
+ };
+
+ value
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use std::sync::atomic::AtomicBool;
+ use std::sync::atomic::Ordering;
+
+ pub struct TestPrompter;
+
+ impl PermissionPrompter for TestPrompter {
+ fn prompt(
+ &mut self,
+ _message: &str,
+ _name: &str,
+ _api_name: Option<&str>,
+ ) -> PromptResponse {
+ if STUB_PROMPT_VALUE.load(Ordering::SeqCst) {
+ PromptResponse::Allow
+ } else {
+ PromptResponse::Deny
+ }
+ }
+ }
+
+ static STUB_PROMPT_VALUE: AtomicBool = AtomicBool::new(true);
+
+ pub static PERMISSION_PROMPT_STUB_VALUE_SETTER: Lazy<
+ Mutex<PermissionPromptStubValueSetter>,
+ > = Lazy::new(|| Mutex::new(PermissionPromptStubValueSetter));
+
+ pub struct PermissionPromptStubValueSetter;
+
+ impl PermissionPromptStubValueSetter {
+ pub fn set(&self, value: bool) {
+ STUB_PROMPT_VALUE.store(value, Ordering::SeqCst);
+ }
+ }
+
+ pub fn set_prompter(prompter: Box<dyn PermissionPrompter>) {
+ *PERMISSION_PROMPTER.lock() = prompter;
+ }
+}