use crate::Separators;
use std::borrow::Cow;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Escape<'a> {
Text(&'a str),
Field,
Component,
Subcomponent,
Repetition,
EscapeCharacter,
Hex(&'a str),
Highlight,
Normal,
Formatting(&'a str),
Local(&'a str),
SingleByteCharacterSet(&'a str),
MultiByteCharacterSet(&'a str),
Unknown(&'a str),
Unterminated(&'a str),
}
impl Escape<'_> {
pub fn write_er7(&self, out: &mut String, separators: &Separators) {
let escape = separators.escape;
let mut sequence = |selector: &str, body: &str| {
out.push(escape);
out.push_str(selector);
out.push_str(body);
out.push(escape);
};
match *self {
Escape::Text(text) | Escape::Unterminated(text) => out.push_str(text),
Escape::Field => sequence("F", ""),
Escape::Component => sequence("S", ""),
Escape::Subcomponent => sequence("T", ""),
Escape::Repetition => sequence("R", ""),
Escape::EscapeCharacter => sequence("E", ""),
Escape::Highlight => sequence("H", ""),
Escape::Normal => sequence("N", ""),
Escape::Hex(body) => sequence("X", body),
Escape::Local(body) => sequence("Z", body),
Escape::SingleByteCharacterSet(body) => sequence("C", body),
Escape::MultiByteCharacterSet(body) => sequence("M", body),
Escape::Formatting(body) => sequence(".", body),
Escape::Unknown(body) => sequence("", body),
}
}
}
#[must_use]
pub fn escapes<'a>(text: &'a str, separators: &Separators) -> Escapes<'a> {
Escapes {
rest: text,
escape: separators.escape,
}
}
#[derive(Debug, Clone)]
pub struct Escapes<'a> {
rest: &'a str,
escape: char,
}
impl<'a> Iterator for Escapes<'a> {
type Item = Escape<'a>;
fn next(&mut self) -> Option<Escape<'a>> {
if self.rest.is_empty() {
return None;
}
let width = self.escape.len_utf8();
match self.rest.find(self.escape) {
None => Some(Escape::Text(self.take_all())),
Some(0) => {
let after = &self.rest[width..];
match after.find(self.escape) {
None => Some(Escape::Unterminated(self.take_all())),
Some(end) => {
let body = &after[..end];
self.rest = &after[end + width..];
Some(classify(body))
}
}
}
Some(start) => {
let text = &self.rest[..start];
self.rest = &self.rest[start..];
Some(Escape::Text(text))
}
}
}
}
impl<'a> Escapes<'a> {
fn take_all(&mut self) -> &'a str {
std::mem::take(&mut self.rest)
}
}
fn classify(body: &str) -> Escape<'_> {
match body {
"F" => return Escape::Field,
"S" => return Escape::Component,
"T" => return Escape::Subcomponent,
"R" => return Escape::Repetition,
"E" => return Escape::EscapeCharacter,
"H" => return Escape::Highlight,
"N" => return Escape::Normal,
_ => {}
}
let mut chars = body.chars();
match (chars.next(), chars.as_str()) {
(Some('X'), rest) => Escape::Hex(rest),
(Some('Z'), rest) => Escape::Local(rest),
(Some('C'), rest) => Escape::SingleByteCharacterSet(rest),
(Some('M'), rest) => Escape::MultiByteCharacterSet(rest),
(Some('.'), rest) => Escape::Formatting(rest),
_ => Escape::Unknown(body),
}
}
#[must_use]
pub fn decode_hex(body: &str) -> Option<String> {
if body.is_empty() || !body.len().is_multiple_of(2) || !body.is_ascii() {
return None;
}
let mut bytes = Vec::with_capacity(body.len() / 2);
let mut digits = body.chars();
while let (Some(high), Some(low)) = (digits.next(), digits.next()) {
let byte = high.to_digit(16)? * 16 + low.to_digit(16)?;
bytes.push(u8::try_from(byte).ok()?);
}
Some(String::from_utf8_lossy(&bytes).into_owned())
}
#[must_use]
pub fn unescape<'a>(text: &'a str, separators: &Separators) -> Cow<'a, str> {
if !text.contains(separators.escape) {
return Cow::Borrowed(text);
}
let mut out = String::with_capacity(text.len());
for token in escapes(text, separators) {
match token {
Escape::Text(run) | Escape::Unterminated(run) => out.push_str(run),
Escape::Field => out.push(separators.field),
Escape::Component => out.push(separators.component),
Escape::Subcomponent => out.push(separators.subcomponent),
Escape::Repetition => out.push(separators.repetition),
Escape::EscapeCharacter => out.push(separators.escape),
Escape::Hex(body) => match decode_hex(body) {
Some(decoded) => out.push_str(&decoded),
None => token.write_er7(&mut out, separators),
},
_ => token.write_er7(&mut out, separators),
}
}
Cow::Owned(out)
}
#[must_use]
pub fn escape<'a>(text: &'a str, separators: &Separators) -> Cow<'a, str> {
let needs_escaping = |c: char| {
c == separators.field
|| c == separators.component
|| c == separators.repetition
|| c == separators.escape
|| c == separators.subcomponent
|| c == '\r'
|| c == '\n'
};
if !text.contains(needs_escaping) {
return Cow::Borrowed(text);
}
let mut out = String::with_capacity(text.len() + 8);
for c in text.chars() {
let token = if c == separators.escape {
Escape::EscapeCharacter
} else if c == separators.field {
Escape::Field
} else if c == separators.component {
Escape::Component
} else if c == separators.repetition {
Escape::Repetition
} else if c == separators.subcomponent {
Escape::Subcomponent
} else if c == '\r' {
Escape::Hex("0D")
} else if c == '\n' {
Escape::Hex("0A")
} else {
out.push(c);
continue;
};
token.write_er7(&mut out, separators);
}
Cow::Owned(out)
}
#[cfg(test)]
mod tests {
use super::*;
fn seps() -> Separators {
Separators::default()
}
#[test]
fn tokenizes_losslessly() {
let source = r"a\F\b\.sp 2\c\Q\d\E";
let mut rebuilt = String::new();
for token in escapes(source, &seps()) {
token.write_er7(&mut rebuilt, &seps());
}
assert_eq!(rebuilt, source);
}
#[test]
fn classifies_every_sequence() {
let tokens: Vec<_> = escapes(
r"\F\\S\\T\\R\\E\\H\\N\\X0D\\Z99\\C2842\\M0F2842\\.br\\??\",
&seps(),
)
.collect();
assert_eq!(
tokens,
vec![
Escape::Field,
Escape::Component,
Escape::Subcomponent,
Escape::Repetition,
Escape::EscapeCharacter,
Escape::Highlight,
Escape::Normal,
Escape::Hex("0D"),
Escape::Local("99"),
Escape::SingleByteCharacterSet("2842"),
Escape::MultiByteCharacterSet("0F2842"),
Escape::Formatting("br"),
Escape::Unknown("??"),
]
);
}
#[test]
fn unescapes_delimiters_and_hex() {
let seps = seps();
assert_eq!(unescape(r"a\F\b", &seps), "a|b");
assert_eq!(unescape(r"a\S\b", &seps), "a^b");
assert_eq!(unescape(r"a\T\b", &seps), "a&b");
assert_eq!(unescape(r"a\R\b", &seps), "a~b");
assert_eq!(unescape(r"a\E\b", &seps), r"a\b");
assert_eq!(unescape(r"\X0D\", &seps), "\r");
assert_eq!(unescape(r"\X4142\", &seps), "AB");
}
#[test]
fn keeps_undecodable_sequences_literal() {
let seps = seps();
assert_eq!(unescape(r"line\.br\next", &seps), r"line\.br\next");
assert_eq!(unescape(r"\H\loud\N\", &seps), r"\H\loud\N\");
assert_eq!(unescape(r"\Z0102\", &seps), r"\Z0102\");
assert_eq!(unescape(r"\XZZ\", &seps), r"\XZZ\");
assert_eq!(unescape(r"\X123\", &seps), r"\X123\");
assert_eq!(unescape(r"a\Fb", &seps), r"a\Fb");
}
#[test]
fn borrows_when_there_is_nothing_to_do() {
assert!(matches!(unescape("plain text", &seps()), Cow::Borrowed(_)));
assert!(matches!(escape("plain text", &seps()), Cow::Borrowed(_)));
}
#[test]
fn escapes_delimiters_and_segment_terminators() {
let seps = seps();
assert_eq!(escape(r"a\b", &seps), r"a\E\b");
assert_eq!(escape("a|b^c~d&e", &seps), r"a\F\b\S\c\R\d\T\e");
assert_eq!(escape("line\r\nnext", &seps), r"line\X0D\\X0A\next");
}
#[test]
fn escape_and_unescape_round_trip() {
let seps = seps();
for value in ["plain", r"a|b^c~d&e\f", "with\rcr", "Smith & Jones"] {
assert_eq!(unescape(&escape(value, &seps), &seps), value);
}
}
#[test]
fn honors_custom_delimiters() {
let seps = Separators {
field: '#',
component: '*',
repetition: '!',
escape: '?',
subcomponent: '@',
truncation: None,
};
assert_eq!(unescape("a?F?b", &seps), "a#b");
assert_eq!(escape("a#b", &seps), "a?F?b");
}
#[test]
fn decodes_hex_bodies() {
assert_eq!(decode_hex("0D0A"), Some("\r\n".to_string()));
assert_eq!(decode_hex(""), None);
assert_eq!(decode_hex("A"), None);
assert_eq!(decode_hex("GG"), None);
}
}