Skip to content

Commit

Permalink
feat(core): highlight unprintable chars in permission prompts (#22468)
Browse files Browse the repository at this point in the history
If we strip out unprintable chars, we don't see the full filename being
requested by permission prompts. Instead, we highlight and escape them
to make them visible.
  • Loading branch information
mmastrac committed Feb 19, 2024
1 parent 9a43a2b commit 7e6b942
Show file tree
Hide file tree
Showing 2 changed files with 29 additions and 21 deletions.
30 changes: 21 additions & 9 deletions runtime/permissions/prompter.rs
Expand Up @@ -11,12 +11,24 @@ use std::io::StderrLock;
use std::io::StdinLock;
use std::io::Write as IoWrite;

/// Helper function to strip ansi codes and ASCII control characters.
fn strip_ansi_codes_and_ascii_control(s: &str) -> std::borrow::Cow<str> {
console_static_text::ansi::strip_ansi_codes(s)
.chars()
.filter(|c| !c.is_ascii_control())
.collect()
/// Helper function to make control characters visible so users can see the underlying filename.
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 = "⚠️";
Expand Down Expand Up @@ -249,9 +261,9 @@ impl PermissionPrompter for TtyPrompter {
return PromptResponse::Deny; // don't grant permission if this fails
}

let message = strip_ansi_codes_and_ascii_control(message);
let name = strip_ansi_codes_and_ascii_control(name);
let api_name = api_name.map(strip_ansi_codes_and_ascii_control);
let message = escape_control_characters(message);
let name = escape_control_characters(name);
let api_name = api_name.map(escape_control_characters);

// print to stderr so that if stdout is piped this is still displayed.
let opts: String = if is_unary {
Expand Down
20 changes: 8 additions & 12 deletions tests/integration/run_tests.rs
Expand Up @@ -4725,19 +4725,19 @@ fn stdio_streams_are_locked_in_permission_prompt() {
}

#[test]
fn permission_prompt_strips_ansi_codes_and_control_chars() {
fn permission_prompt_escapes_ansi_codes_and_control_chars() {
util::with_pty(&["repl"], |mut console| {
console.write_line(
r#"Deno.permissions.request({ name: "env", variable: "\rDo you like ice cream? y/n" });"#
);
// will be uppercase on windows
let env_name = if cfg!(windows) {
"DO YOU LIKE ICE CREAM? Y/N"
"\\rDO YOU LIKE ICE CREAM? Y/N"
} else {
"Do you like ice cream? y/n"
"\\rDo you like ice cream? y/n"
};
console.expect(format!(
"┌ ⚠️ Deno requests env access to \"{}\".",
"\u{250c} \u{26a0}\u{fe0f} Deno requests env access to \"{}\".",
env_name
))
});
Expand All @@ -4747,14 +4747,10 @@ fn permission_prompt_strips_ansi_codes_and_control_chars() {
console.expect("undefined");
console.write_line_raw(r#"const unboldANSI = "\u001b[22m";"#);
console.expect("undefined");
console.write_line_raw(r#"const prompt = `┌ ⚠️ ${boldANSI}Deno requests run access to "echo"${unboldANSI}\n ├ Requested by \`Deno.Command().output()`"#);
console.expect("undefined");
console.write_line_raw(r#"const moveANSIUp = "\u001b[1A";"#);
console.expect("undefined");
console.write_line_raw(r#"const clearANSI = "\u001b[2K";"#);
console.expect("undefined");
console.write_line_raw(r#"const moveANSIStart = "\u001b[1000D";"#);
console.expect("undefined");
console.write_line_raw(
r#"new Deno.Command(`${boldANSI}cat${unboldANSI}`).spawn();"#,
);
console.expect("\u{250c} \u{26a0}\u{fe0f} Deno requests run access to \"\\u{1b}[1mcat\\u{1b}[22m\".");
});
}

Expand Down

0 comments on commit 7e6b942

Please sign in to comment.