use alloc::string::String;
use serde::{Serialize, Serializer};
fn is_json_display_unsafe(c: char) -> bool {
matches!(c,
'\u{007F}'
| '\u{0080}'..='\u{009F}'
| '\u{200E}' | '\u{200F}'
| '\u{202A}'..='\u{202E}'
| '\u{2066}'..='\u{2069}'
| '\u{061C}'
)
}
#[derive(Debug, Clone, Copy)]
pub struct JsonSafe<'a>(pub &'a str);
impl Serialize for JsonSafe<'_> {
fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
if !self.0.chars().any(is_json_display_unsafe) {
return serializer.serialize_str(self.0);
}
let cleaned: String = self
.0
.chars()
.map(|c| if is_json_display_unsafe(c) { '\u{FFFD}' } else { c })
.collect();
serializer.serialize_str(&cleaned)
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used)]
mod tests {
use super::*;
#[test]
fn clean_string_passes_through_unchanged() {
let j = serde_json::to_string(&JsonSafe("System")).unwrap();
assert_eq!(j, "\"System\"");
}
#[test]
fn bidi_override_is_neutralized_to_replacement_char() {
let j = serde_json::to_string(&JsonSafe("ev\u{202e}il.exe")).unwrap();
assert!(!j.contains('\u{202e}'), "bidi override must not survive: {j}");
assert!(j.contains('\u{FFFD}'), "scrubbed char marked with U+FFFD: {j}");
let v: serde_json::Value = serde_json::from_str(&j).unwrap();
assert_eq!(
v.as_str().unwrap().chars().filter(|&c| c == '\u{FFFD}').count(),
1
);
}
#[test]
fn c1_and_del_are_neutralized() {
let j = serde_json::to_string(&JsonSafe("a\u{0085}b\u{007f}c")).unwrap();
assert!(!j.contains('\u{0085}'), "C1 NEL must not survive: {j}");
assert!(!j.contains('\u{007f}'), "DEL must not survive: {j}");
}
#[test]
fn c0_control_is_left_for_serde_to_escape_losslessly() {
let j = serde_json::to_string(&JsonSafe("a\tb")).unwrap();
assert_eq!(j, "\"a\\tb\"");
}
}