use deno_core::parking_lot::Mutex;
use deno_terminal::colors;
use once_cell::sync::Lazy;
use std::fmt::Write;
use std::io::BufRead;
use std::io::IsTerminal;
use std::io::StderrLock;
use std::io::StdinLock;
use std::io::Write as IoWrite;
use crate::is_standalone;
fn escape_control_characters(s: &str) -> std::borrow::Cow<str> {
if !s.contains(|c: char| c.is_ascii_control() || c.is_control()) {
return std::borrow::Cow::Borrowed(s);
}
let mut output = String::with_capacity(s.len() * 2);
for c in s.chars() {
match c {
c if c.is_ascii_control() => output.push_str(
&colors::white_bold_on_red(c.escape_debug().to_string()).to_string(),
),
c if c.is_control() => output.push_str(
&colors::white_bold_on_red(c.escape_debug().to_string()).to_string(),
),
c => output.push(c),
}
}
output.into()
}
pub const PERMISSION_EMOJI: &str = "⚠️";
const MAX_PERMISSION_PROMPT_LENGTH: usize = 10 * 1024;
#[derive(Debug, Eq, PartialEq)]
pub enum PromptResponse {
Allow,
Deny,
AllowAll,
}
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>,
is_unary: bool,
) -> 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, is_unary);
if let Some(after_callback) = MAYBE_AFTER_PROMPT_CALLBACK.lock().as_mut() {
after_callback();
}
r
}
pub fn set_prompt_callbacks(
before_callback: PromptCallback,
after_callback: PromptCallback,
) {
*MAYBE_BEFORE_PROMPT_CALLBACK.lock() = Some(before_callback);
*MAYBE_AFTER_PROMPT_CALLBACK.lock() = Some(after_callback);
}
pub fn set_prompter(prompter: Box<dyn PermissionPrompter>) {
*PERMISSION_PROMPTER.lock() = prompter;
}
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>,
is_unary: bool,
) -> PromptResponse;
}
pub struct TtyPrompter;
#[cfg(unix)]
fn clear_stdin(
_stdin_lock: &mut StdinLock,
_stderr_lock: &mut StderrLock,
) -> Result<(), std::io::Error> {
use std::mem::MaybeUninit;
const STDIN_FD: i32 = 0;
unsafe {
let mut raw_fd_set = MaybeUninit::<libc::fd_set>::uninit();
libc::FD_ZERO(raw_fd_set.as_mut_ptr());
libc::FD_SET(STDIN_FD, raw_fd_set.as_mut_ptr());
loop {
let r = libc::tcflush(STDIN_FD, libc::TCIFLUSH);
if r != 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"clear_stdin failed (tcflush)",
));
}
let mut timeout = libc::timeval {
tv_sec: 0,
tv_usec: 100_000,
};
let r = libc::select(
STDIN_FD + 1, raw_fd_set.as_mut_ptr(),
std::ptr::null_mut(),
std::ptr::null_mut(),
&mut timeout,
);
if r < 0 {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
"clear_stdin failed (select)",
));
}
if r == 0 {
break; }
}
}
Ok(())
}
#[cfg(not(unix))]
fn clear_stdin(
stdin_lock: &mut StdinLock,
stderr_lock: &mut StderrLock,
) -> Result<(), std::io::Error> {
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;
unsafe {
let stdin = GetStdHandle(STD_INPUT_HANDLE);
emulate_enter_key_press(stdin)?;
read_stdin_line(stdin_lock)?;
if is_input_buffer_empty(stdin)? {
move_cursor_up(stderr_lock)?;
} else {
flush_input_buffer(stdin)?;
}
}
return Ok(());
unsafe fn flush_input_buffer(stdin: HANDLE) -> Result<(), std::io::Error> {
let success = FlushConsoleInputBuffer(stdin);
if success != TRUE {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Could not flush the console input buffer: {}",
std::io::Error::last_os_error()
),
));
}
Ok(())
}
unsafe fn emulate_enter_key_press(
stdin: HANDLE,
) -> Result<(), std::io::Error> {
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 {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Could not emulate enter key press: {}",
std::io::Error::last_os_error()
),
));
}
Ok(())
}
unsafe fn is_input_buffer_empty(
stdin: HANDLE,
) -> Result<bool, std::io::Error> {
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 {
return Err(std::io::Error::new(
std::io::ErrorKind::Other,
format!(
"Could not peek the console input buffer: {}",
std::io::Error::last_os_error()
),
));
}
Ok(events_read == 0)
}
fn move_cursor_up(
stderr_lock: &mut StderrLock,
) -> Result<(), std::io::Error> {
write!(stderr_lock, "\x1B[1A")
}
fn read_stdin_line(stdin_lock: &mut StdinLock) -> Result<(), std::io::Error> {
let mut input = String::new();
stdin_lock.read_line(&mut input)?;
Ok(())
}
}
fn clear_n_lines(stderr_lock: &mut StderrLock, n: usize) {
write!(stderr_lock, "\x1B[{n}A\x1B[0J").unwrap();
}
#[cfg(unix)]
fn get_stdin_metadata() -> std::io::Result<std::fs::Metadata> {
use std::os::fd::FromRawFd;
use std::os::fd::IntoRawFd;
unsafe {
let stdin = std::fs::File::from_raw_fd(0);
let metadata = stdin.metadata().unwrap();
let _ = stdin.into_raw_fd();
Ok(metadata)
}
}
impl PermissionPrompter for TtyPrompter {
fn prompt(
&mut self,
message: &str,
name: &str,
api_name: Option<&str>,
is_unary: bool,
) -> PromptResponse {
if !std::io::stdin().is_terminal() || !std::io::stderr().is_terminal() {
return PromptResponse::Deny;
};
#[allow(clippy::print_stderr)]
if message.len() > MAX_PERMISSION_PROMPT_LENGTH {
eprintln!("❌ Permission prompt length ({} bytes) was larger than the configured maximum length ({} bytes): denying request.", message.len(), MAX_PERMISSION_PROMPT_LENGTH);
eprintln!("❌ WARNING: This may indicate that code is trying to bypass or hide permission check requests.");
eprintln!("❌ Run again with --allow-{name} to bypass this check if this is really what you want to do.");
return PromptResponse::Deny;
}
#[cfg(unix)]
let metadata_before = get_stdin_metadata().unwrap();
let stdout_lock = std::io::stdout().lock();
let mut stderr_lock = std::io::stderr().lock();
let mut stdin_lock = std::io::stdin().lock();
#[allow(clippy::print_stderr)]
if let Err(err) = clear_stdin(&mut stdin_lock, &mut stderr_lock) {
eprintln!("Error clearing stdin for permission prompt. {err:#}");
return PromptResponse::Deny; }
let message = escape_control_characters(message);
let name = escape_control_characters(name);
let api_name = api_name.map(escape_control_characters);
let opts: String = if is_unary {
format!("[y/n/A] (y = yes, allow; n = no, deny; A = allow all {name} permissions)")
} else {
"[y/n] (y = yes, allow; n = no, deny)".to_string()
};
{
let mut output = String::new();
write!(&mut output, "┏ {PERMISSION_EMOJI} ").unwrap();
write!(&mut output, "{}", colors::bold("Deno requests ")).unwrap();
write!(&mut output, "{}", colors::bold(message.clone())).unwrap();
writeln!(&mut output, "{}", colors::bold(".")).unwrap();
if let Some(api_name) = api_name.clone() {
writeln!(
&mut output,
"┠─ Requested by `{}` API.",
colors::bold(api_name)
)
.unwrap();
}
let msg = format!(
"Learn more at: {}",
colors::cyan_with_underline(&format!(
"https://docs.deno.com/go/--allow-{}",
name
))
);
writeln!(&mut output, "┠─ {}", colors::italic(&msg)).unwrap();
let msg = if is_standalone() {
format!("Specify the required permissions during compile time using `deno compile --allow-{name}`.")
} else {
format!("Run again with --allow-{name} to bypass this prompt.")
};
writeln!(&mut output, "┠─ {}", colors::italic(&msg)).unwrap();
write!(&mut output, "┗ {}", colors::bold("Allow?")).unwrap();
write!(&mut output, " {opts} > ").unwrap();
stderr_lock.write_all(output.as_bytes()).unwrap();
}
let value = loop {
#[allow(clippy::print_stderr)]
#[cfg(unix)]
if let Err(err) = clear_stdin(&mut stdin_lock, &mut stderr_lock) {
eprintln!("Error clearing stdin for permission prompt. {err:#}");
return PromptResponse::Deny; }
let mut input = String::new();
let result = stdin_lock.read_line(&mut input);
let input = input.trim_end_matches(['\r', '\n']);
if result.is_err() || input.len() != 1 {
break PromptResponse::Deny;
};
match input.as_bytes()[0] as char {
'y' | 'Y' => {
clear_n_lines(
&mut stderr_lock,
if api_name.is_some() { 5 } else { 4 },
);
let msg = format!("Granted {message}.");
writeln!(stderr_lock, "✅ {}", colors::bold(&msg)).unwrap();
break PromptResponse::Allow;
}
'n' | 'N' | '\x1b' => {
clear_n_lines(
&mut stderr_lock,
if api_name.is_some() { 5 } else { 4 },
);
let msg = format!("Denied {message}.");
writeln!(stderr_lock, "❌ {}", colors::bold(&msg)).unwrap();
break PromptResponse::Deny;
}
'A' if is_unary => {
clear_n_lines(
&mut stderr_lock,
if api_name.is_some() { 5 } else { 4 },
);
let msg = format!("Granted all {name} access.");
writeln!(stderr_lock, "✅ {}", colors::bold(&msg)).unwrap();
break PromptResponse::AllowAll;
}
_ => {
clear_n_lines(&mut stderr_lock, 1);
write!(
stderr_lock,
"┗ {} {opts} > ",
colors::bold("Unrecognized option. Allow?")
)
.unwrap();
}
};
};
drop(stdout_lock);
drop(stderr_lock);
drop(stdin_lock);
#[cfg(unix)]
{
use std::os::unix::fs::MetadataExt;
let metadata_after = get_stdin_metadata().unwrap();
assert_eq!(metadata_before.dev(), metadata_after.dev());
assert_eq!(metadata_before.ino(), metadata_after.ino());
assert_eq!(metadata_before.rdev(), metadata_after.rdev());
assert_eq!(metadata_before.uid(), metadata_after.uid());
assert_eq!(metadata_before.gid(), metadata_after.gid());
assert_eq!(metadata_before.mode(), metadata_after.mode());
}
assert!(std::io::stdin().is_terminal() && std::io::stderr().is_terminal());
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>,
_is_unary: bool,
) -> 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);
}
}
}