#[derive(Clone, Copy)]
pub(crate) struct StripPolicy {
pub strip_pua: bool,
pub keep_presentation_vs: bool,
}
#[inline]
pub(crate) fn is_tag(ch: char) -> bool {
matches!(ch, '\u{E0000}'..='\u{E007F}')
}
#[inline]
fn is_tag_letter(ch: char) -> bool {
matches!(ch, '\u{E0061}'..='\u{E007A}')
}
#[inline]
pub(crate) fn is_variation_selector(ch: char) -> bool {
matches!(ch, '\u{FE00}'..='\u{FE0F}' | '\u{E0100}'..='\u{E01EF}')
}
#[inline]
pub(crate) fn is_noncharacter(ch: char) -> bool {
let cp = ch as u32;
matches!(cp, 0xFDD0..=0xFDEF) || (cp & 0xFFFF) >= 0xFFFE
}
#[inline]
pub(crate) fn is_pua(ch: char) -> bool {
matches!(ch, '\u{E000}'..='\u{F8FF}' | '\u{F0000}'..='\u{FFFFD}' | '\u{100000}'..='\u{10FFFD}')
}
const CGJ: char = '\u{034F}';
const FLAG_BASE: char = '\u{1F3F4}';
const CANCEL_TAG: char = '\u{E007F}';
const VALID_SUBDIVISION_FLAGS: [&str; 3] = ["gbeng", "gbsct", "gbwls"];
fn consume_flag_tail(chars: &mut std::iter::Peekable<std::str::Chars<'_>>) -> Option<String> {
let mut tail = String::new();
let mut decoded = String::new();
while let Some(&c) = chars.peek() {
if is_tag_letter(c) {
tail.push(c);
if let Some(ascii) = char::from_u32(c as u32 - 0xE0000) {
decoded.push(ascii);
}
chars.next();
} else {
break;
}
}
if chars.peek() == Some(&CANCEL_TAG) && VALID_SUBDIVISION_FLAGS.contains(&decoded.as_str()) {
tail.push(CANCEL_TAG);
chars.next();
Some(tail)
} else {
None
}
}
#[inline]
fn is_presentation_base(ch: char) -> bool {
!ch.is_whitespace()
&& !ch.is_control()
&& !crate::whitespace::is_blank_render(ch)
&& !crate::whitespace::is_zero_width(ch)
}
pub(crate) fn strip_tags(text: &str) -> String {
if text.is_ascii() {
return text.to_string();
}
let mut out = String::with_capacity(text.len());
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == FLAG_BASE {
out.push(FLAG_BASE);
if let Some(tail) = consume_flag_tail(&mut chars) {
out.push_str(&tail); }
continue;
}
if !is_tag(ch) {
out.push(ch);
}
}
out
}
pub(crate) fn strip_variation_selectors(text: &str) -> String {
text.chars()
.filter(|&c| !is_variation_selector(c))
.collect()
}
pub(crate) fn strip_noncharacters(text: &str) -> String {
text.chars().filter(|&c| !is_noncharacter(c)).collect()
}
pub(crate) fn strip_pua(text: &str) -> String {
text.chars().filter(|&c| !is_pua(c)).collect()
}
#[cfg(test)]
pub(crate) fn strip_invisible_classes(text: &str, policy: StripPolicy) -> String {
let mut out = String::new();
strip_invisible_classes_into(text, policy, &mut out);
out
}
pub(crate) fn strip_invisible_classes_into(text: &str, policy: StripPolicy, out: &mut String) {
out.clear();
if text.is_ascii() {
out.push_str(text);
return;
}
out.reserve(text.len());
let mut chars = text.chars().peekable();
while let Some(ch) = chars.next() {
if ch == FLAG_BASE {
out.push(FLAG_BASE);
if let Some(tail) = consume_flag_tail(&mut chars) {
out.push_str(&tail);
}
continue;
}
if is_tag(ch) || ch == CGJ || is_noncharacter(ch) {
continue;
}
if policy.strip_pua && is_pua(ch) {
continue;
}
if is_variation_selector(ch) {
let keep = policy.keep_presentation_vs
&& matches!(ch, '\u{FE0E}' | '\u{FE0F}')
&& out.chars().next_back().is_some_and(is_presentation_base);
if keep {
out.push(ch);
}
continue;
}
out.push(ch);
}
}
#[cfg(test)]
mod tests {
use super::*;
fn comparison() -> StripPolicy {
StripPolicy {
strip_pua: true,
keep_presentation_vs: false,
}
}
fn rendering() -> StripPolicy {
StripPolicy {
strip_pua: false,
keep_presentation_vs: true,
}
}
#[test]
fn classifiers_cover_the_ranges() {
assert!(is_tag('\u{E0001}') && is_tag('\u{E0050}') && is_tag('\u{E007F}'));
assert!(!is_tag('\u{1F3F4}')); assert!(is_variation_selector('\u{FE00}') && is_variation_selector('\u{FE0F}'));
assert!(is_variation_selector('\u{E0100}') && is_variation_selector('\u{E01EF}'));
assert!(
is_noncharacter('\u{FDD0}')
&& is_noncharacter('\u{FFFE}')
&& is_noncharacter('\u{FFFF}')
);
assert!(is_noncharacter('\u{1FFFE}') && is_noncharacter('\u{10FFFF}'));
assert!(is_noncharacter('\u{FDEF}')); assert!(!is_noncharacter('\u{FDF0}')); assert!(
is_pua('\u{E000}') && is_pua('\u{F8FF}') && is_pua('\u{F0000}') && is_pua('\u{100000}')
);
assert!(!is_pua('a'));
}
#[test]
fn strips_tag_smuggling_payload() {
let payload: String = "hi"
.chars()
.chain(
"PWN"
.chars()
.map(|c| char::from_u32(0xE0000 + c as u32).unwrap()),
)
.collect();
assert_eq!(strip_tags(&payload), "hi");
assert_eq!(strip_invisible_classes(&payload, comparison()), "hi");
}
#[test]
fn fixes_deprecated_language_tag() {
assert_eq!(strip_tags("hi\u{E0001}bye"), "hibye");
}
#[test]
fn preserves_well_formed_emoji_flag() {
let scotland = "\u{1F3F4}\u{E0067}\u{E0062}\u{E0073}\u{E0063}\u{E0074}\u{E007F}";
assert_eq!(strip_tags(scotland), scotland);
assert_eq!(strip_invisible_classes(scotland, comparison()), scotland);
let bad = "\u{1F3F4}\u{E0067}\u{E0062}"; assert_eq!(strip_tags(bad), "\u{1F3F4}");
}
#[test]
fn rejects_flag_base_with_non_subdivision_tag_payload() {
let smuggled = "\u{1F3F4}\u{E0070}\u{E0077}\u{E006E}\u{E007F}";
assert_eq!(strip_tags(smuggled), "\u{1F3F4}");
assert_eq!(strip_invisible_classes(smuggled, comparison()), "\u{1F3F4}");
let wales = "\u{1F3F4}\u{E0067}\u{E0062}\u{E0077}\u{E006C}\u{E0073}\u{E007F}";
assert_eq!(strip_tags(wales), wales);
}
#[test]
fn variation_selector_policy() {
assert_eq!(
strip_invisible_classes("g\u{FE01}data", comparison()),
"gdata"
);
assert_eq!(
strip_invisible_classes("g\u{E0100}data", comparison()),
"gdata"
);
assert_eq!(
strip_invisible_classes("\u{2764}\u{FE0F}", rendering()),
"\u{2764}\u{FE0F}"
);
assert_eq!(
strip_invisible_classes("g\u{FE01}data", rendering()),
"gdata"
); assert_eq!(strip_invisible_classes("\u{FE0F}x", rendering()), "x");
}
#[test]
fn strips_cgj_noncharacters_and_pua() {
assert_eq!(
strip_invisible_classes("ad\u{034F}min", comparison()),
"admin"
);
assert_eq!(
strip_invisible_classes("a\u{FFFE}b\u{FDD0}c", comparison()),
"abc"
);
assert_eq!(strip_noncharacters("a\u{FFFF}b"), "ab");
assert_eq!(strip_pua("a\u{E000}b\u{F0000}c"), "abc");
assert_eq!(
strip_invisible_classes("a\u{E000}b", rendering()),
"a\u{E000}b"
);
}
#[test]
fn idempotent() {
let s = "hi\u{E0001}\u{FE01}ad\u{034F}min\u{FFFE}\u{E000}";
let once = strip_invisible_classes(s, comparison());
assert_eq!(once, strip_invisible_classes(&once, comparison()));
}
#[test]
fn strip_invisible_classes_into_matches_returning() {
let mut out = String::new();
for s in [
"",
"abc",
"a\u{200D}b",
"\u{1F3F4}\u{E0067}\u{E0062}\u{E0073}\u{E0063}\u{E0074}\u{E007F}",
"x\u{E0041}y",
"z\u{FE0F}",
] {
strip_invisible_classes_into(s, comparison(), &mut out);
assert_eq!(
out,
strip_invisible_classes(s, comparison()),
"comparison: {s:?}"
);
strip_invisible_classes_into(s, rendering(), &mut out);
assert_eq!(
out,
strip_invisible_classes(s, rendering()),
"rendering: {s:?}"
);
}
out.push_str("STALE");
strip_invisible_classes_into("abc", comparison(), &mut out);
assert_eq!(out, "abc");
}
}