use std::fmt::{self, Write as _};
pub(crate) const MAX_NAME_CHARS: usize = 128;
pub(crate) const MAX_MESSAGE_CHARS: usize = 200;
pub(crate) fn quoted(name: &str) -> String {
let mut text = SafeText::after(String::from('"'), usize::MAX, Backslash::Double);
text.untrusted(name, MAX_NAME_CHARS);
let mut quoted = text.into_string();
quoted.push('"');
quoted
}
pub(crate) fn bounded(sentence: &dyn fmt::Display, limit: usize) -> String {
let mut text = SafeText::new(limit, Backslash::Keep);
write!(text.untrusted_writer(), "{sentence}")
.expect("invariant: the escaping writer never fails");
text.into_string()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum Backslash {
Double,
Keep,
}
pub(crate) struct SafeText {
text: String,
chars: usize,
limit: usize,
backslash: Backslash,
full: bool,
}
impl SafeText {
pub(crate) fn new(limit: usize, backslash: Backslash) -> Self {
Self::after(String::new(), limit, backslash)
}
pub(crate) fn after(prefix: String, limit: usize, backslash: Backslash) -> Self {
Self { text: prefix, chars: 0, limit, backslash, full: false }
}
pub(crate) fn fixed(&mut self, text: &str) {
self.put(&text, text.chars().count());
}
pub(crate) fn untrusted(&mut self, text: &str, cap: usize) {
let mut written = 0usize;
for character in text.chars() {
let Some(len) = self.character(character, &mut written, cap) else {
return;
};
written += len;
}
}
pub(crate) fn untrusted_writer(&mut self) -> impl fmt::Write + '_ {
Untrusted { into: self }
}
pub(crate) fn byte_len(&self) -> usize {
self.text.len()
}
pub(crate) fn into_string(self) -> String {
self.text
}
fn character(&mut self, character: char, written: &mut usize, cap: usize) -> Option<usize> {
let short;
let code;
let (shown, len): (&dyn fmt::Display, usize) = match character {
'\\' if self.backslash == Backslash::Keep => (&character, 1),
'\\' | '\n' | '\r' | '\t' => {
short = character.escape_default();
(&short, short.len())
}
_ if character.is_control() || hides_text(character) => {
code = character.escape_unicode();
(&code, code.len())
}
_ => (&character, 1),
};
if written.saturating_add(len) > cap {
self.put(&'\u{2026}', 1);
return None;
}
self.put(shown, len).then_some(len)
}
fn put(&mut self, piece: &dyn fmt::Display, len: usize) -> bool {
if self.full {
return false;
}
if self.chars + len > self.limit {
self.text.push('\u{2026}');
self.full = true;
return false;
}
write!(self.text, "{piece}").expect("invariant: writing to a String cannot fail");
self.chars += len;
true
}
}
struct Untrusted<'a> {
into: &'a mut SafeText,
}
impl fmt::Write for Untrusted<'_> {
fn write_str(&mut self, text: &str) -> fmt::Result {
let mut written = 0usize;
for character in text.chars() {
match self.into.character(character, &mut written, usize::MAX) {
Some(len) => written += len,
None => break,
}
}
Ok(())
}
}
fn hides_text(character: char) -> bool {
matches!(
character,
'\u{00ad}'
| '\u{061c}'
| '\u{180e}'
| '\u{200b}'..='\u{200f}'
| '\u{2028}'..='\u{202e}'
| '\u{2060}'..='\u{206f}'
| '\u{feff}'
| '\u{fff9}'..='\u{fffb}'
| '\u{e0000}'..='\u{e007f}'
)
}
#[cfg(test)]
#[path = "text_tests.rs"]
mod tests;