pub(crate) fn escape(text: &str) -> String {
let mut escaped = String::with_capacity(text.len());
for ch in text.chars() {
match ch {
'&' => escaped.push_str("&"),
'<' => escaped.push_str("<"),
'>' => escaped.push_str(">"),
'"' => escaped.push_str("""),
'\'' => escaped.push_str("'"),
'\n' => escaped.push_str("
"),
'\r' => {}
_ => escaped.push(ch),
}
}
escaped
}
pub(crate) fn escape_owned(text: String) -> String {
if text
.as_bytes()
.iter()
.any(|byte| matches!(byte, b'&' | b'<' | b'>' | b'"' | b'\'' | b'\n' | b'\r'))
{
escape(&text)
} else {
text
}
}
pub(crate) fn replace_escaped(text: &str) -> String {
let mut decoded = String::with_capacity(text.len());
let mut rest = text;
while let Some(index) = rest.find('&') {
decoded.push_str(&rest[..index]);
rest = &rest[index..];
let (replacement, consumed) = if rest.starts_with("<") {
(Some("<"), 4)
} else if rest.starts_with(">") {
(Some(">"), 4)
} else if rest.starts_with("&") {
(Some("&"), 5)
} else if rest.starts_with(""") {
(Some("\""), 6)
} else if rest.starts_with("'") {
(Some("'"), 5)
} else if rest.starts_with("'") {
(Some("'"), 6)
} else if rest.starts_with(" ") {
(Some(" "), 6)
} else {
(None, 1)
};
decoded.push_str(replacement.unwrap_or("&"));
rest = &rest[consumed..];
}
decoded.push_str(rest);
decoded
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn escapes_all_supported_characters_in_one_pass() {
assert_eq!(
escape("<&>\"'\n\rplain"),
"<&>"'
plain"
);
}
#[test]
fn preserves_the_allocation_for_plain_owned_text() {
let text = String::from("plain text");
let pointer = text.as_ptr();
let escaped = escape_owned(text);
assert_eq!(escaped, "plain text");
assert_eq!(escaped.as_ptr(), pointer);
}
#[test]
fn escapes_special_characters_in_owned_text() {
assert_eq!(
escape_owned("<&>\"'\n\r".to_owned()),
"<&>"'
"
);
}
#[test]
fn replaces_entities_without_recursively_decoding_them() {
assert_eq!(replace_escaped("<&' "), "<&' ");
assert_eq!(replace_escaped("&lt;"), "<");
assert_eq!(replace_escaped("plain &unknown;"), "plain &unknown;");
}
}