pub(crate) fn encode_hex_utf8(text: &str) -> String {
const HEX: &[u8; 16] = b"0123456789abcdef";
let mut out = String::with_capacity(text.len() * 2);
for byte in text.as_bytes() {
out.push(char::from(HEX[usize::from(byte >> 4)]));
out.push(char::from(HEX[usize::from(byte & 0x0f)]));
}
out
}
pub(crate) fn decode_hex_utf8(hex: &str) -> Option<String> {
if !hex.len().is_multiple_of(2) {
return None;
}
let mut bytes = Vec::with_capacity(hex.len() / 2);
let mut digits = hex.bytes();
while let (Some(hi), Some(lo)) = (digits.next(), digits.next()) {
bytes.push((hex_digit(hi)? << 4) | hex_digit(lo)?);
}
let text = String::from_utf8(bytes).ok()?;
text.chars().all(is_xml_char).then_some(text)
}
fn hex_digit(byte: u8) -> Option<u8> {
match byte {
b'0'..=b'9' => Some(byte - b'0'),
b'a'..=b'f' => Some(byte - b'a' + 10),
b'A'..=b'F' => Some(byte - b'A' + 10),
_ => None,
}
}
fn is_xml_char(ch: char) -> bool {
matches!(
ch,
'\u{9}' | '\u{A}' | '\u{D}' | '\u{20}'..='\u{D7FF}' | '\u{E000}'..='\u{FFFD}' | '\u{10000}'..='\u{10FFFF}'
)
}
#[cfg(test)]
#[path = "hex_tests.rs"]
mod tests;