use unicode_normalization::char::is_combining_mark;
fn occupies_cell(ch: char) -> bool {
!(is_combining_mark(ch)
|| crate::invisibles::is_zero_width(ch)
|| crate::invisibles::is_variation_selector(ch)
|| crate::invisibles::is_default_ignorable_format(ch)
|| crate::invisibles::is_tag(ch)
|| crate::scripts::is_bidi_control(ch))
}
const BS: char = '\u{8}';
const DEL: char = '\u{7F}';
const CR: char = '\r';
const LF: char = '\n';
pub(crate) fn resolve_deletions_into(text: &str, cr: bool, out: &mut String) -> bool {
if !text.chars().any(|c| c == BS || c == DEL || (cr && c == CR)) {
return false;
}
out.clear();
out.reserve(text.len());
let mut line: Vec<String> = Vec::new();
let mut col = 0usize;
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == BS || ch == DEL {
col = col.saturating_sub(1);
line.truncate(col);
} else if ch == LF || (ch == CR && (!cr || chars.peek().is_none_or(|&n| n == LF))) {
for cell in line.drain(..) {
out.push_str(&cell);
}
out.push(ch);
col = 0;
} else if ch == CR {
col = 0;
} else if !occupies_cell(ch) && col > 0 {
line[col - 1].push(ch);
} else if col < line.len() {
line[col] = ch.to_string();
col += 1;
} else {
line.push(ch.to_string());
col += 1;
}
}
for cell in line {
out.push_str(&cell);
}
out != text
}
#[cfg(test)]
mod tests {
use super::*;
fn resolve(s: &str, cr: bool) -> String {
let mut out = String::new();
if resolve_deletions_into(s, cr, &mut out) {
out
} else {
s.to_owned()
}
}
#[test]
fn the_attack_construction_resolves_to_the_clean_word() {
let attack: String = "paypal".chars().flat_map(|c| [c, 'X', BS]).collect();
assert_eq!(resolve(&attack, false), "paypal");
let with_del: String = "paypal".chars().flat_map(|c| [c, 'X', DEL]).collect();
assert_eq!(resolve(&with_del, false), "paypal");
}
#[test]
fn a_format_character_and_a_mark_occupy_no_cell() {
assert_eq!(
resolve("X\u{200B}\u{8}", false),
"",
"ZWSP joins the X's cell"
);
assert_eq!(
resolve("e\u{301}\u{8}", false),
"",
"the mark joins the e's cell"
);
assert_eq!(resolve("ab\u{200B}\u{8}", false), "a");
}
#[test]
fn a_rendering_format_character_still_occupies_a_cell() {
assert_eq!(
resolve("a\u{605}\u{8}", false),
"a",
"the mark's own cell is erased"
);
}
#[test]
fn overstrike_bold_resolves_to_the_letter_every_renderer_shows() {
assert_eq!(resolve("c\u{8}c", false), "c");
assert_eq!(resolve("b\u{8}bo\u{8}old", false), "bold");
}
#[test]
fn line_endings_pass_through_whatever_the_cr_flag_says() {
for cr in [false, true] {
assert_eq!(
resolve("line1\r\nline2", cr),
"line1\r\nline2",
"CRLF, cr={cr}"
);
assert_eq!(
resolve("trailing\r", cr),
"trailing\r",
"CR at EOF, cr={cr}"
);
assert_eq!(resolve("a\nb\nc", cr), "a\nb\nc", "LF, cr={cr}");
}
}
#[test]
fn a_lone_cr_overwrites_only_under_its_flag() {
assert_eq!(resolve("abc\rxy", false), "abc\rxy", "off by default");
assert_eq!(resolve("abc\rxy", true), "xyc");
assert_eq!(resolve("ZZZZZZ\rpaypal", true), "paypal");
assert_eq!(resolve("line1\rline2", true), "line2");
}
#[test]
fn erasing_past_the_start_of_a_line_stops_there() {
assert_eq!(resolve("\u{8}\u{8}abc", false), "abc");
assert_eq!(resolve("a\u{8}\u{8}\u{8}b", false), "b");
}
#[test]
fn a_cr_that_passes_through_is_not_a_change() {
let mut out = String::new();
for s in [
"line1\r\nline2",
"trailing\r",
"a\r\nb\r\nc",
"no cr at all",
] {
assert!(
!resolve_deletions_into(s, true, &mut out),
"{s:?} under cr=true"
);
assert!(
!resolve_deletions_into(s, false, &mut out),
"{s:?} under cr=false"
);
}
assert!(resolve_deletions_into("abc\rxy", true, &mut out));
}
#[test]
fn text_with_no_erasing_control_is_left_alone() {
let mut out = String::new();
for s in ["paypal", "", "caf\u{e9}", "line1\r\nline2", "\u{1F3F4}"] {
assert!(!resolve_deletions_into(s, false, &mut out), "{s:?}");
}
assert!(!resolve_deletions_into("a\rb", false, &mut out));
assert!(resolve_deletions_into("a\rb", true, &mut out));
}
#[test]
fn it_is_idempotent() {
let probes = [
"paypal",
"p\u{8}X",
"X\u{200B}\u{8}",
"abc\rxy",
"line1\r\nline2",
"e\u{301}\u{8}",
"c\u{8}c",
];
for cr in [false, true] {
for p in probes {
let once = resolve(p, cr);
assert_eq!(resolve(&once, cr), once, "{p:?} cr={cr}");
}
}
}
}