use serde::Serialize;
pub(crate) fn embed<T: Serialize>(value: &T) -> serde_json::Result<String> {
let raw = serde_json::to_string(value)?;
Ok(raw
.replace('&', "\\u0026")
.replace('<', "\\u003c")
.replace('>', "\\u003e"))
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn plain_values_round_trip_unchanged_in_meaning() {
let embedded = embed(&serde_json::json!({"id": "api", "count": 3})).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&embedded).unwrap();
assert_eq!(parsed["id"], "api");
assert_eq!(parsed["count"], 3);
}
#[test]
fn script_breakout_attempt_is_neutralized() {
let malicious = "</script><script>alert(1)</script>";
let embedded = embed(&serde_json::json!({ "id": malicious })).unwrap();
assert!(
!embedded.contains("</script"),
"embedded payload must not contain a literal script-close sequence: {embedded}"
);
assert!(
!embedded.to_lowercase().contains("</script"),
"embedded payload must not contain a case-insensitive script-close sequence: {embedded}"
);
let parsed: serde_json::Value = serde_json::from_str(&embedded).unwrap();
assert_eq!(parsed["id"], malicious);
}
#[test]
fn ampersand_and_angle_brackets_are_escaped_everywhere() {
let embedded = embed(&serde_json::json!(["<b>&</b>"])).unwrap();
assert!(!embedded.contains('<'));
assert!(!embedded.contains('>'));
assert!(!embedded.contains('&'));
let parsed: serde_json::Value = serde_json::from_str(&embedded).unwrap();
assert_eq!(parsed[0], "<b>&</b>");
}
#[test]
fn quotes_and_backslashes_stay_valid_json() {
let embedded = embed(&serde_json::json!({"name": "a\"b\\c"})).unwrap();
let parsed: serde_json::Value = serde_json::from_str(&embedded).unwrap();
assert_eq!(parsed["name"], "a\"b\\c");
}
}