use serde_json::Value;
use sha2::{Digest, Sha256};
pub fn fingerprint(merged: &Value, name: Option<&str>) -> String {
let mut canon = String::new();
write_canonical(merged, &mut canon);
let mut hasher = Sha256::new();
hasher.update(name.unwrap_or("").as_bytes());
hasher.update([0u8]); hasher.update(canon.as_bytes());
format!("{:x}", hasher.finalize())
}
pub fn request_fingerprint(
config_fingerprint: &str,
clock: Option<&str>,
timeout_secs: Option<u64>,
labels: &std::collections::BTreeMap<String, String>,
) -> String {
let mut hasher = Sha256::new();
hasher.update(config_fingerprint.as_bytes());
hasher.update([0u8]);
hasher.update(clock.unwrap_or("").as_bytes());
hasher.update([0u8]);
hasher.update(
timeout_secs
.map(|t| t.to_string())
.unwrap_or_default()
.as_bytes(),
);
hasher.update([0u8]);
for (k, v) in labels {
hasher.update(k.as_bytes());
hasher.update([0u8]);
hasher.update(v.as_bytes());
hasher.update([0u8]);
}
format!("{:x}", hasher.finalize())
}
fn write_canonical(v: &Value, out: &mut String) {
match v {
Value::Object(map) => {
let mut keys: Vec<&String> = map.keys().collect();
keys.sort_unstable();
out.push('{');
for (i, k) in keys.iter().enumerate() {
if i > 0 {
out.push(',');
}
out.push_str(&serde_json::to_string(k).expect("string key serializes"));
out.push(':');
write_canonical(&map[*k], out);
}
out.push('}');
}
Value::Array(items) => {
out.push('[');
for (i, item) in items.iter().enumerate() {
if i > 0 {
out.push(',');
}
write_canonical(item, out);
}
out.push(']');
}
other => out.push_str(&serde_json::to_string(other).expect("scalar serializes")),
}
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn stable_and_order_independent() {
let a = json!({ "b": 1, "a": [1, 2], "c": { "y": true, "x": null } });
let b = json!({ "c": { "x": null, "y": true }, "a": [1, 2], "b": 1 });
assert_eq!(fingerprint(&a, Some("p")), fingerprint(&b, Some("p")));
}
#[test]
fn differs_on_value_change() {
let a = json!({ "a": 1 });
let b = json!({ "a": 2 });
assert_ne!(fingerprint(&a, Some("p")), fingerprint(&b, Some("p")));
}
#[test]
fn differs_on_name() {
let v = json!({ "a": 1 });
assert_ne!(fingerprint(&v, Some("p1")), fingerprint(&v, Some("p2")));
}
#[test]
fn array_order_is_significant() {
assert_ne!(
fingerprint(&json!([1, 2]), None),
fingerprint(&json!([2, 1]), None)
);
}
#[test]
fn request_fingerprint_includes_run_affecting_fields() {
use std::collections::BTreeMap;
let cfg_fp = fingerprint(&json!({ "a": 1 }), Some("p"));
let empty = BTreeMap::new();
let base = request_fingerprint(&cfg_fp, None, None, &empty);
assert_eq!(base, request_fingerprint(&cfg_fp, None, None, &empty));
assert_ne!(
base,
request_fingerprint(&cfg_fp, Some("2026-01-01T00:00:00Z"), None, &empty)
);
assert_ne!(base, request_fingerprint(&cfg_fp, None, Some(30), &empty));
let mut labels = BTreeMap::new();
labels.insert("env".to_string(), "prod".to_string());
assert_ne!(base, request_fingerprint(&cfg_fp, None, None, &labels));
let other_cfg = fingerprint(&json!({ "a": 2 }), Some("p"));
assert_ne!(base, request_fingerprint(&other_cfg, None, None, &empty));
}
#[test]
fn request_fingerprint_label_value_is_significant() {
use std::collections::BTreeMap;
let cfg_fp = fingerprint(&json!({}), None);
let mut a = BTreeMap::new();
a.insert("k".to_string(), "v1".to_string());
let mut b = BTreeMap::new();
b.insert("k".to_string(), "v2".to_string());
assert_ne!(
request_fingerprint(&cfg_fp, None, None, &a),
request_fingerprint(&cfg_fp, None, None, &b)
);
}
}