const LARGE_PASTE_BYTES: usize = 4096;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Assessment {
pub(crate) line_breaks: usize,
pub(crate) lines: usize,
pub(crate) characters: usize,
pub(crate) bytes: usize,
pub(crate) controls: usize,
pub(crate) unusually_large: bool,
}
impl Assessment {
pub(crate) fn requires_confirmation(self) -> bool {
self.line_breaks > 0 || self.controls > 0 || self.unusually_large
}
pub(crate) fn detail(self) -> String {
let mut reasons = Vec::new();
if self.line_breaks > 0 {
reasons.push(format!("{} line break(s)", self.line_breaks));
}
if self.controls > 0 {
reasons.push(format!("{} unsafe control character(s)", self.controls));
}
if self.unusually_large {
reasons.push(format!("{} UTF-8 bytes", self.bytes));
}
format!(
"This paste contains {}. Total: {} line(s), {} character(s), {} byte(s). The clipboard contents are intentionally hidden.",
reasons.join(", "),
self.lines,
self.characters,
self.bytes
)
}
}
pub(crate) fn classify(text: &str) -> Assessment {
let mut line_breaks = 0;
let mut lines = usize::from(!text.is_empty());
let mut controls = 0;
let mut characters = 0;
let mut previous_cr = false;
for character in text.chars() {
characters += 1;
match character {
'\r' => {
line_breaks += 1;
lines += 1;
previous_cr = true;
}
'\n' => {
if !previous_cr {
line_breaks += 1;
lines += 1;
}
previous_cr = false;
}
'\t' => previous_cr = false,
character if character.is_control() => {
controls += 1;
previous_cr = false;
}
_ => previous_cr = false,
}
}
Assessment {
line_breaks,
lines,
characters,
bytes: text.len(),
controls,
unusually_large: text.len() > LARGE_PASTE_BYTES,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn single_line_and_tab_are_safe() {
assert!(!classify("printf 'ok'\t").requires_confirmation());
}
#[test]
fn newline_control_and_large_text_are_guarded() {
assert!(classify("one\ntwo").requires_confirmation());
assert!(classify("safe\u{1b}unsafe").requires_confirmation());
assert!(classify(&"x".repeat(4097)).requires_confirmation());
}
#[test]
fn detail_never_contains_clipboard_payload() {
let secret = "secret-one\nsecret-two";
let detail = classify(secret).detail();
assert!(!detail.contains("secret-one"));
assert!(detail.contains("line break"));
}
}