#[cfg(not(feature = "std"))]
use alloc::format;
#[cfg(not(feature = "std"))]
use alloc::string::String;
use crate::io::traits::IDestination;
pub const STR_QUOTE: &str = "\"";
pub const ESC_QUOTE: &str = "\\\"";
pub const ESC_BACKSLASH: &str = "\\\\";
pub const ESC_NEWLINE: &str = "\\n";
pub const ESC_CARRIAGE_RETURN: &str = "\\r";
pub const ESC_TAB: &str = "\\t";
pub const BYTE_QUOTE: u8 = b'"';
pub const BYTE_BACKSLASH: u8 = b'\\';
pub const BYTE_NEWLINE: u8 = b'\n';
pub const BYTE_CARRIAGE_RETURN: u8 = b'\r';
pub const BYTE_TAB: u8 = b'\t';
pub const CONTROL_CHAR_LIMIT: u8 = 32;
#[inline]
pub fn json_needs_escaping(s: &str) -> bool {
for &b in s.as_bytes() {
match b {
BYTE_QUOTE | BYTE_BACKSLASH | BYTE_NEWLINE | BYTE_CARRIAGE_RETURN | BYTE_TAB => {
return true;
}
b if b < CONTROL_CHAR_LIMIT => return true,
_ => {}
}
}
false
}
pub fn escape_for_json(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
c if (c as u32) < 0x20 => {
out.push_str(&format!("\\u{:04x}", c as u32));
}
other => out.push(other),
}
}
out
}
pub fn escape_for_xml(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'&' => out.push_str("&"),
'<' => out.push_str("<"),
'>' => out.push_str(">"),
'"' => out.push_str("""),
'\'' => out.push_str("'"),
other => out.push(other),
}
}
out
}
pub fn write_json_escaped_string(s: &str, dest: &mut dyn IDestination) {
dest.add_bytes(STR_QUOTE);
let bytes = s.as_bytes();
let mut start = 0;
let mut i = 0;
while i < bytes.len() {
let needs_escape = match bytes[i] {
BYTE_QUOTE | BYTE_BACKSLASH | BYTE_NEWLINE | BYTE_CARRIAGE_RETURN | BYTE_TAB => true,
b if b < CONTROL_CHAR_LIMIT => true,
_ => false,
};
if needs_escape {
if i > start {
dest.add_bytes(core::str::from_utf8(&bytes[start..i]).unwrap_or(""));
}
match bytes[i] {
BYTE_QUOTE => dest.add_bytes(ESC_QUOTE),
BYTE_BACKSLASH => dest.add_bytes(ESC_BACKSLASH),
BYTE_NEWLINE => dest.add_bytes(ESC_NEWLINE),
BYTE_CARRIAGE_RETURN => dest.add_bytes(ESC_CARRIAGE_RETURN),
BYTE_TAB => dest.add_bytes(ESC_TAB),
b => {
let b = b as u32;
let mut buf = [b'\\', b'u', b'0', b'0', b'0', b'0'];
for j in (2..6).rev() {
let digit = (b >> (4 * (5 - j))) & 0xF;
buf[j] = match digit {
0..=9 => b'0' + digit as u8,
_ => b'a' + (digit as u8 - 10),
};
}
if let Ok(seq) = core::str::from_utf8(&buf) {
dest.add_bytes(seq);
}
}
}
i += 1;
start = i;
} else {
i += 1;
}
}
if start < bytes.len() {
dest.add_bytes(core::str::from_utf8(&bytes[start..]).unwrap_or(""));
}
dest.add_bytes(STR_QUOTE);
}
pub fn write_xml_escaped_string(s: &str, dest: &mut dyn IDestination) {
let bytes = s.as_bytes();
let mut start = 0;
let mut i = 0;
while i < bytes.len() {
let esc = match bytes[i] {
b'&' => Some("&"),
b'<' => Some("<"),
b'>' => Some(">"),
b'"' => Some("""),
b'\'' => Some("'"),
_ => None,
};
if let Some(replacement) = esc {
if i > start {
dest.add_bytes(core::str::from_utf8(&bytes[start..i]).unwrap_or(""));
}
dest.add_bytes(replacement);
i += 1;
start = i;
} else {
i += 1;
}
}
if start < bytes.len() {
dest.add_bytes(core::str::from_utf8(&bytes[start..]).unwrap_or(""));
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::io::destinations::BufferDestination;
#[test]
fn test_escape_for_json() {
assert_eq!(escape_for_json("hello \"world\""), "hello \\\"world\\\"");
assert_eq!(escape_for_json("line1\nline2\ttab"), "line1\\nline2\\ttab");
assert_eq!(escape_for_json("control\x01char"), "control\\u0001char");
}
#[test]
fn test_escape_for_xml() {
assert_eq!(
escape_for_xml("<root key=\"val\" & 'item'>"),
"<root key="val" & 'item'>"
);
}
#[test]
fn test_write_json_escaped_string() {
let mut dest = BufferDestination::new();
write_json_escaped_string("hello\n\"world\"", &mut dest);
assert_eq!(
core::str::from_utf8(dest.as_bytes()).unwrap(),
"\"hello\\n\\\"world\\\"\""
);
}
}