use regex::Regex;
use serde_json::{Map, Value};
use sha2::{Digest, Sha256};
use std::sync::OnceLock;
pub const NORMALIZER_VERSION: &str = "v1";
const KEY_DOMAIN: &[u8] = b"oc-decision-cache-v1";
const FIELD_SEP: u8 = 0x1f;
pub fn canonical_json(value: &Value) -> String {
serde_json::to_string(&canonicalize(value)).unwrap_or_else(|e| {
tracing::error!("decision normalize: canonical serialization failed: {e}");
String::new()
})
}
pub fn decision_key(
tier_id: &str,
policy_version: &str,
normalizer_version: &str,
canonical: &str,
) -> String {
let mut hasher = Sha256::new();
hasher.update(KEY_DOMAIN);
for part in [tier_id, policy_version, normalizer_version, canonical] {
hasher.update([FIELD_SEP]);
hasher.update(part.as_bytes());
}
format!("{:x}", hasher.finalize())
}
fn canonicalize(value: &Value) -> Value {
match value {
Value::Object(map) => {
let mut entries: Vec<(String, Value)> = map
.iter()
.map(|(k, v)| (k.clone(), canonicalize(v)))
.collect();
entries.sort_by(|a, b| a.0.cmp(&b.0));
let mut out = Map::new();
for (k, v) in entries {
out.insert(k, v);
}
Value::Object(out)
}
Value::Array(items) => Value::Array(items.iter().map(canonicalize).collect()),
Value::String(s) => Value::String(mask_volatile(s)),
other => other.clone(),
}
}
fn mask_volatile(s: &str) -> String {
static DATETIME: OnceLock<Regex> = OnceLock::new();
static DATE: OnceLock<Regex> = OnceLock::new();
static UUID: OnceLock<Regex> = OnceLock::new();
static IPV4: OnceLock<Regex> = OnceLock::new();
static PATH: OnceLock<Regex> = OnceLock::new();
let datetime = DATETIME.get_or_init(|| {
Regex::new(r"\d{4}-\d{2}-\d{2}[T ]\d{2}:\d{2}(:\d{2})?(\.\d+)?(Z|[+-]\d{2}:?\d{2})?")
.expect("valid datetime pattern")
});
let date = DATE.get_or_init(|| Regex::new(r"\d{4}-\d{2}-\d{2}").expect("valid date pattern"));
let uuid = UUID.get_or_init(|| {
Regex::new(r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}")
.expect("valid uuid pattern")
});
let ipv4 = IPV4.get_or_init(|| {
Regex::new(r"\b\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}(:\d{1,5})?\b")
.expect("valid ipv4 pattern")
});
let path = PATH.get_or_init(|| {
Regex::new(r#"(?:/Users|/home|/root|/private/tmp|/tmp|/var/folders)/[^\s"']+"#)
.expect("valid path pattern")
});
let s = datetime.replace_all(s, "<ts>");
let s = date.replace_all(&s, "<date>");
let s = uuid.replace_all(&s, "<uuid>");
let s = ipv4.replace_all(&s, "<ip>");
path.replace_all(&s, "<path>").into_owned()
}