use std::borrow::Cow;
enum Value {
Char(char),
Str(&'static str),
}
fn escape_char(c: char) -> Value {
match c {
'<' => Value::Str("<"),
'>' => Value::Str(">"),
'"' => Value::Str("""),
'\'' => Value::Str("'"),
'&' => Value::Str("&"),
'\n' => Value::Str("
"),
'\r' => Value::Str("
"),
_ => Value::Char(c),
}
}
fn needs_xlsx_escape(c: char) -> bool {
let cp = c as u32;
matches!(cp, 0x00..=0x08 | 0x0B | 0x0C | 0x0E..=0x1F)
}
fn starts_xlsx_escape_pattern(bytes: &[u8]) -> bool {
bytes.len() >= 7
&& bytes[0] == b'_'
&& bytes[1] == b'x'
&& bytes[6] == b'_'
&& bytes[2].is_ascii_hexdigit()
&& bytes[3].is_ascii_hexdigit()
&& bytes[4].is_ascii_hexdigit()
&& bytes[5].is_ascii_hexdigit()
}
pub fn escape_xml(s: &'_ str) -> Cow<'_, str> {
let needs_escape = s.char_indices().any(|(i, c)| {
matches!(c, '<' | '>' | '"' | '\'' | '&' | '\n' | '\r')
|| needs_xlsx_escape(c)
|| (c == '_' && starts_xlsx_escape_pattern(&s.as_bytes()[i..]))
});
if !needs_escape {
return Cow::Borrowed(s);
}
let mut result = String::with_capacity(s.len() + 8);
let bytes = s.as_bytes();
let mut i = 0;
while i < s.len() {
let c = s[i..].chars().next().unwrap();
if needs_xlsx_escape(c) {
result.push_str(&format!("_x{:04X}_", c as u32));
} else if c == '_' && starts_xlsx_escape_pattern(&bytes[i..]) {
result.push_str("_x005F_");
} else {
match escape_char(c) {
Value::Str(esc) => result.push_str(esc),
Value::Char(ch) => result.push(ch),
}
}
i += c.len_utf8();
}
Cow::Owned(result)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::import::shared_strings::decode_xlsx_escapes;
fn roundtrip(s: &str) -> String {
decode_xlsx_escapes(&escape_xml(s))
}
#[test]
fn test_control_chars_encoded() {
assert_eq!(escape_xml("\x01").as_ref(), "_x0001_");
assert_eq!(escape_xml("\x0B").as_ref(), "_x000B_");
assert_eq!(escape_xml("\x1F").as_ref(), "_x001F_");
assert_eq!(escape_xml("\x00").as_ref(), "_x0000_");
}
#[test]
fn test_literal_escape_sequence_encoded() {
assert_eq!(escape_xml("_x0001_").as_ref(), "_x005F_x0001_");
assert_eq!(escape_xml("_x005F_").as_ref(), "_x005F_x005F_");
}
#[test]
fn test_plain_underscore_not_encoded() {
assert_eq!(escape_xml("_hello").as_ref(), "_hello");
assert_eq!(escape_xml("a_b").as_ref(), "a_b");
assert_eq!(escape_xml("_x").as_ref(), "_x");
}
#[test]
fn test_control_chars_roundtrip() {
assert_eq!(roundtrip("\x00"), "\x00");
assert_eq!(roundtrip("\x01"), "\x01");
assert_eq!(roundtrip("\x1F"), "\x1F");
assert_eq!(roundtrip("\x0B"), "\x0B");
}
#[test]
fn test_literal_escape_sequence_roundtrip() {
assert_eq!(roundtrip("_x0001_"), "_x0001_");
assert_eq!(roundtrip("_x005F_"), "_x005F_");
assert_eq!(roundtrip("hello _x0001_ world"), "hello _x0001_ world");
}
#[test]
fn test_plain_underscore_roundtrip() {
assert_eq!(roundtrip("_hello"), "_hello");
assert_eq!(roundtrip("a_b"), "a_b");
assert_eq!(roundtrip("_x"), "_x");
}
}