#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PayloadScheme {
TagAscii,
VariationBytes,
ZeroWidthBinary,
PercentEscape,
}
impl PayloadScheme {
#[must_use]
pub fn as_str(self) -> &'static str {
match self {
Self::TagAscii => "tag_ascii",
Self::VariationBytes => "variation_bytes",
Self::ZeroWidthBinary => "zero_width_binary",
Self::PercentEscape => "percent_escape",
}
}
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Payload {
pub scheme: PayloadScheme,
pub start: usize,
pub end: usize,
pub units: usize,
pub bytes: Vec<u8>,
pub text: Option<String>,
}
const MIN_VARIATION_RUN: usize = 2;
const MIN_PERCENT_RUN: usize = 2;
pub fn decode_smuggled(text: &str) -> Vec<Payload> {
decode(text, true)
}
pub(crate) fn decode_carriers(text: &str) -> Vec<Payload> {
decode(text, false)
}
fn decode(text: &str, with_percent: bool) -> Vec<Payload> {
let chars: Vec<(usize, char)> = text.char_indices().collect();
let just_chars: Vec<char> = chars.iter().map(|&(_, c)| c).collect();
let mut out = Vec::new();
let mut i = 0;
while i < chars.len() {
let (offset, ch) = chars[i];
if let Some(len) = crate::invisibles::subdivision_flag_len(&just_chars[i..]) {
i += len;
continue;
}
if let Some(p) = scan_tag_ascii(&chars, i, offset) {
i += p.units;
out.push(p);
continue;
}
if let Some(p) = scan_variation(&chars, i, offset) {
i += p.units;
out.push(p);
continue;
}
if with_percent {
if let Some(p) = scan_percent(&chars, i, offset) {
i += p.units;
out.push(p);
continue;
}
}
if let Some((p, consumed)) = scan_zero_width(&chars, i, offset) {
i += consumed;
if let Some(p) = p {
out.push(p);
}
continue;
}
let _ = ch;
i += 1;
}
out
}
fn finish(
scheme: PayloadScheme,
start: usize,
end: usize,
units: usize,
bytes: Vec<u8>,
) -> Payload {
let text = printable(&bytes);
Payload {
scheme,
start,
end,
units,
bytes,
text,
}
}
fn printable(bytes: &[u8]) -> Option<String> {
if bytes.is_empty() {
return None;
}
let s = std::str::from_utf8(bytes).ok()?;
s.chars().all(is_visible).then(|| s.to_owned())
}
fn is_visible(c: char) -> bool {
!(c.is_control()
|| crate::invisibles::is_zero_width(c)
|| crate::invisibles::is_variation_selector(c)
|| crate::invisibles::is_default_ignorable_format(c)
|| crate::invisibles::is_tag(c)
|| crate::invisibles::is_noncharacter(c)
|| crate::invisibles::is_invisible_filler(c)
|| crate::scripts::is_bidi_control(c))
}
const CANCEL_TAG: char = '\u{E007F}';
fn is_tag_byte(ch: char) -> bool {
matches!(ch, '\u{E0020}'..='\u{E007E}')
}
fn scan_tag_ascii(chars: &[(usize, char)], i: usize, offset: usize) -> Option<Payload> {
if !is_tag_byte(chars[i].1) {
return None;
}
let mut bytes = Vec::new();
let mut j = i;
while j < chars.len() && is_tag_byte(chars[j].1) {
bytes.push((chars[j].1 as u32 - 0xE0000) as u8);
j += 1;
}
if chars.get(j).is_some_and(|&(_, c)| c == CANCEL_TAG) {
j += 1;
}
let end = chars
.get(j)
.map_or_else(|| offset + tail_len(chars, i, j), |&(o, _)| o);
Some(finish(PayloadScheme::TagAscii, offset, end, j - i, bytes))
}
fn tail_len(chars: &[(usize, char)], i: usize, j: usize) -> usize {
chars[i..j].iter().map(|&(_, c)| c.len_utf8()).sum()
}
fn variation_byte(ch: char) -> Option<u8> {
let cp = ch as u32;
match cp {
0xFE00..=0xFE0F => u8::try_from(cp - 0xFE00).ok(),
0xE0100..=0xE01EF => u8::try_from(cp - 0xE0100 + 16).ok(),
_ => None,
}
}
fn scan_variation(chars: &[(usize, char)], i: usize, offset: usize) -> Option<Payload> {
variation_byte(chars[i].1)?;
let mut bytes = Vec::new();
let mut j = i;
while j < chars.len() {
match variation_byte(chars[j].1) {
Some(b) => bytes.push(b),
None => break,
}
j += 1;
}
if j - i < MIN_VARIATION_RUN {
return None;
}
let end = chars
.get(j)
.map_or_else(|| offset + tail_len(chars, i, j), |&(o, _)| o);
Some(finish(
PayloadScheme::VariationBytes,
offset,
end,
j - i,
bytes,
))
}
fn percent_byte(chars: &[(usize, char)], i: usize) -> Option<u8> {
if chars.get(i)?.1 != '%' {
return None;
}
let hex = |c: char| c.is_ascii_hexdigit().then(|| c.to_digit(16)).flatten();
let hi = hex(chars.get(i + 1)?.1)?;
let lo = hex(chars.get(i + 2)?.1)?;
u8::try_from(hi * 16 + lo).ok()
}
fn scan_percent(chars: &[(usize, char)], i: usize, offset: usize) -> Option<Payload> {
percent_byte(chars, i)?;
let mut bytes = Vec::new();
let mut j = i;
while let Some(b) = percent_byte(chars, j) {
bytes.push(b);
j += 3;
}
if bytes.len() < MIN_PERCENT_RUN {
return None;
}
let end = chars
.get(j)
.map_or_else(|| offset + tail_len(chars, i, j), |&(o, _)| o);
Some(finish(
PayloadScheme::PercentEscape,
offset,
end,
j - i,
bytes,
))
}
fn is_zw_separator(ch: char) -> bool {
matches!(ch, '\u{200D}' | '\u{2060}' | '\u{FEFF}')
}
fn scan_zero_width(
chars: &[(usize, char)],
i: usize,
offset: usize,
) -> Option<(Option<Payload>, usize)> {
if !matches!(chars[i].1, '\u{200B}' | '\u{200C}') {
return None;
}
let mut bits: Vec<u8> = Vec::new();
let mut j = i;
while j < chars.len() {
match chars[j].1 {
'\u{200B}' => bits.push(0),
'\u{200C}' => bits.push(1),
c if is_zw_separator(c) => {}
_ => break,
}
j += 1;
}
let units = j - i;
let bytes: Vec<u8> = bits
.as_chunks::<8>()
.0
.iter()
.map(|c| c.iter().fold(0u8, |acc, &b| (acc << 1) | b))
.collect();
if bytes.is_empty() {
return Some((None, units));
}
let end = chars
.get(j)
.map_or_else(|| offset + tail_len(chars, i, j), |&(o, _)| o);
Some((
Some(finish(
PayloadScheme::ZeroWidthBinary,
offset,
end,
units,
bytes,
)),
units,
))
}
#[cfg(test)]
mod tests {
use super::*;
fn tags(s: &str) -> String {
s.chars()
.map(|c| char::from_u32(c as u32 + 0xE0000).unwrap())
.collect()
}
fn zw(s: &str) -> String {
s.bytes()
.flat_map(|b| {
(0..8).map(move |i| {
if (b >> (7 - i)) & 1 == 1 {
'\u{200C}'
} else {
'\u{200B}'
}
})
})
.collect()
}
#[test]
fn a_tag_run_decodes_to_what_it_spells() {
let found = decode_smuggled(&format!("hello{}", tags("tracked-by:acct-99213")));
assert_eq!(found.len(), 1);
assert_eq!(found[0].scheme, PayloadScheme::TagAscii);
assert_eq!(found[0].text.as_deref(), Some("tracked-by:acct-99213"));
assert_eq!(found[0].start, 5, "byte offset of the first carrier");
assert_eq!(found[0].units, 21);
}
#[test]
fn a_zero_width_run_decodes_to_what_it_spells() {
let found = decode_smuggled(&format!("hi{}", zw("hi")));
assert_eq!(found.len(), 1);
assert_eq!(found[0].scheme, PayloadScheme::ZeroWidthBinary);
assert_eq!(found[0].text.as_deref(), Some("hi"));
assert_eq!(found[0].units, 16, "two bytes, eight carriers each");
}
#[test]
fn a_variation_run_decodes_to_what_it_spells() {
let carriers: String = "hi"
.bytes()
.map(|b| char::from_u32(0xE0100 + u32::from(b) - 16).unwrap())
.collect();
let found = decode_smuggled(&carriers);
assert_eq!(found.len(), 1);
assert_eq!(found[0].scheme, PayloadScheme::VariationBytes);
assert_eq!(found[0].text.as_deref(), Some("hi"));
}
#[test]
fn undecodable_bytes_are_reported_without_a_string() {
let found = decode_smuggled("\u{FE00}\u{FE01}");
assert_eq!(found.len(), 1);
assert_eq!(found[0].bytes, vec![0, 1]);
assert_eq!(found[0].text, None, "a garbage decode must not be reported");
}
#[test]
fn a_subdivision_flag_is_not_a_payload() {
for flag in ["gbeng", "gbsct", "gbwls"] {
let s = format!("\u{1F3F4}{}\u{E007F}", tags(flag));
assert!(
decode_smuggled(&s).is_empty(),
"{flag} reported as a payload"
);
}
}
#[test]
fn a_flag_base_with_another_tail_is_still_decoded() {
let s = format!("\u{1F3F4}{}\u{E007F}", tags("ushi"));
let found = decode_smuggled(&s);
assert_eq!(found.len(), 1);
assert_eq!(found[0].text.as_deref(), Some("ushi"));
}
#[test]
fn a_lone_variation_selector_is_not_a_payload() {
assert!(decode_smuggled("\u{2602}\u{FE0F}").is_empty());
assert!(decode_smuggled("text\u{FE0E}").is_empty());
}
#[test]
fn a_bit_short_zero_width_run_yields_nothing() {
assert!(decode_smuggled("a\u{200B}\u{200C}\u{200B}b").is_empty());
}
#[test]
fn trailing_bits_are_dropped_not_padded() {
let found = decode_smuggled(&format!("{}\u{200C}\u{200C}", zw("A")));
assert_eq!(found.len(), 1);
assert_eq!(found[0].bytes, b"A".to_vec());
assert_eq!(found[0].units, 10, "the stray bits are still consumed");
}
#[test]
fn an_invisible_payload_is_not_recovered_text() {
for payload in [
"\u{202E}\u{200B}", "\u{200B}\u{200C}", "\u{FE00}", "\u{3164}", "\u{FFFE}", ] {
let carriers: String = payload
.as_bytes()
.iter()
.flat_map(|b| {
(0..8).map(move |i| {
if (b >> (7 - i)) & 1 == 1 {
'\u{200C}'
} else {
'\u{200B}'
}
})
})
.collect();
let found = decode_smuggled(&carriers);
assert_eq!(found.len(), 1, "{payload:?}");
assert_eq!(
found[0].text, None,
"{payload:?} was reported as readable text"
);
}
assert_eq!(
decode_smuggled(&zw("hi there"))
.first()
.and_then(|p| p.text.as_deref()),
Some("hi there")
);
}
#[test]
fn a_terminated_tag_run_is_one_span() {
let s = format!("x{}{CANCEL_TAG}y", tags("hi"));
let found = decode_smuggled(&s);
assert_eq!(found.len(), 1, "{found:?}");
assert_eq!(found[0].text.as_deref(), Some("hi"));
assert_eq!(found[0].units, 3, "two letters and the terminator");
assert_eq!(found[0].bytes.len(), 2, "the terminator carries no byte");
assert_eq!(&s[found[0].end..], "y");
}
fn pct(s: &str) -> String {
use std::fmt::Write as _;
s.bytes().fold(String::new(), |mut out, b| {
let _ = write!(out, "%{b:02X}");
out
})
}
#[test]
fn a_percent_run_decodes_to_what_it_spells() {
let found = decode_smuggled(&format!("q={}", pct("tracked-by:acct-99213")));
assert_eq!(found.len(), 1);
assert_eq!(found[0].scheme, PayloadScheme::PercentEscape);
assert_eq!(found[0].text.as_deref(), Some("tracked-by:acct-99213"));
assert_eq!(found[0].units, 21 * 3, "three characters per byte");
assert_eq!(found[0].start, 2);
}
#[test]
fn the_error_contract() {
let found = decode_smuggled("%FF%FE");
assert_eq!(found.len(), 1);
assert_eq!(found[0].bytes, vec![0xFF, 0xFE]);
assert_eq!(found[0].text, None);
let found = decode_smuggled("%48%69%4");
assert_eq!(found.len(), 1);
assert_eq!(found[0].text.as_deref(), Some("Hi"));
assert_eq!(found[0].units, 6, "the malformed `%4` is not consumed");
assert!(decode_smuggled("%4x%zz").is_empty());
let found = decode_smuggled("%25%32%45");
assert_eq!(found[0].text.as_deref(), Some("%2E"));
}
#[test]
fn the_detector_path_equals_the_public_one_minus_percent() {
let url = format!(
"https://x.test/?q={}&t={}&z={}%48%69",
tags("hi"),
"\u{FE00}\u{FE01}",
zw("ok")
);
for s in [url.as_str(), "plain", "%41%42", "a\u{200B}\u{200C}b"] {
let expect: Vec<Payload> = decode_smuggled(s)
.into_iter()
.filter(|p| p.scheme != PayloadScheme::PercentEscape)
.collect();
assert_eq!(decode_carriers(s), expect, "{s:?}");
}
assert!(decode_carriers("%48%69%20%41").is_empty());
assert_eq!(decode_smuggled("%48%69%20%41").len(), 1);
}
#[test]
fn hex_digits_are_ascii_only() {
assert_eq!(
'\u{661}'.to_digit(16),
None,
"stdlib to_digit is ASCII-only"
);
assert!(decode_smuggled("%\u{661}\u{662}%\u{663}\u{664}").is_empty());
assert!(decode_smuggled("%\u{FF21}\u{FF21}%\u{B2}\u{B2}").is_empty());
assert_eq!(decode_smuggled("%41%42")[0].text.as_deref(), Some("AB"));
}
#[test]
fn a_single_escape_is_not_a_payload() {
assert!(decode_smuggled("a%20b").is_empty());
assert!(decode_smuggled("/path%2Fseg").is_empty());
}
#[test]
fn hex_digits_are_case_insensitive_and_offsets_are_bytes() {
let s = "caf\u{e9}=%68%69";
let found = decode_smuggled(s);
assert_eq!(found[0].text.as_deref(), Some("hi"));
assert_eq!(&s[found[0].start..found[0].end], "%68%69");
assert_eq!(decode_smuggled("%6a%6B")[0].text.as_deref(), Some("jk"));
}
#[test]
fn ordinary_text_decodes_to_nothing() {
for s in [
"hello world",
"",
"café",
"\u{1F600}",
"Москва",
"100%",
"50% off",
] {
assert!(decode_smuggled(s).is_empty(), "{s:?}");
}
}
#[test]
fn several_runs_are_reported_in_order() {
let s = format!("a{}b{}c", tags("one"), zw("hi"));
let found = decode_smuggled(&s);
assert_eq!(found.len(), 2);
assert_eq!(found[0].text.as_deref(), Some("one"));
assert_eq!(found[1].text.as_deref(), Some("hi"));
assert!(found[0].start < found[1].start);
}
#[test]
fn the_span_indexes_the_input() {
let s = format!("hello{}world", tags("hi"));
let found = decode_smuggled(&s);
assert_eq!(found.len(), 1);
let (start, end) = (found[0].start, found[0].end);
assert_eq!(&s[..start], "hello");
assert_eq!(&s[end..], "world");
assert_eq!(s[start..end].chars().count(), 2);
}
}