extern crate alloc;
use alloc::string::String;
use alloc::vec::Vec;
use unicode_normalization::UnicodeNormalization;
use uor_foundation::enforcement::ShapeViolation;
use uor_foundation::ViolationKind;
const INVALID_JSON_VIOLATION: ShapeViolation = ShapeViolation {
shape_iri: "https://uor.foundation/addr/JsonInput",
constraint_iri: "https://uor.foundation/addr/JsonInput/validUtf8Json",
property_iri: "https://uor.foundation/addr/inputBytes",
expected_range: "https://uor.foundation/addr/ValidUtf8Json",
min_count: 0,
max_count: 1,
kind: ViolationKind::ValueCheck,
};
pub fn jcs_nfc(input_bytes: &[u8]) -> Result<Vec<u8>, ShapeViolation> {
let parsed: serde_json::Value =
serde_json::from_slice(input_bytes).map_err(|_| INVALID_JSON_VIOLATION)?;
let nfc = nfc_recursive(&parsed);
Ok(serde_json::to_vec(&nfc).expect(
"nfc-normalised serde_json::Value re-serialises (only failure mode is non-finite \
numbers, which serde_json rejects at parse time)",
))
}
fn nfc_recursive(value: &serde_json::Value) -> serde_json::Value {
match value {
serde_json::Value::String(s) => serde_json::Value::String(s.nfc().collect::<String>()),
serde_json::Value::Array(arr) => {
serde_json::Value::Array(arr.iter().map(nfc_recursive).collect())
}
serde_json::Value::Object(obj) => {
let mut new_obj = serde_json::Map::new();
for (k, v) in obj {
let nfc_k: String = k.nfc().collect();
new_obj.insert(nfc_k, nfc_recursive(v));
}
serde_json::Value::Object(new_obj)
}
other => other.clone(),
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canonicalize_sorts_keys() {
let canon = jcs_nfc(br#"{"b": 1, "a": 2}"#).expect("valid JSON");
assert_eq!(canon, br#"{"a":2,"b":1}"#);
}
#[test]
fn canonicalize_strips_whitespace() {
let canon = jcs_nfc(b"{ \"foo\" : \"bar\" }").expect("valid JSON");
assert_eq!(canon, br#"{"foo":"bar"}"#);
}
#[test]
fn canonicalize_nfc_normalises_decomposed_strings() {
let composed = jcs_nfc(b"{\"name\":\"caf\xc3\xa9\"}").expect("valid JSON");
let decomposed = jcs_nfc(b"{\"name\":\"cafe\xcc\x81\"}").expect("valid JSON");
assert_eq!(composed, decomposed);
}
#[test]
fn canonicalize_rejects_invalid_json() {
let err = jcs_nfc(b"not json").expect_err("invalid JSON must fail");
assert_eq!(err.shape_iri, INVALID_JSON_VIOLATION.shape_iri);
}
}