Skip to main content

dotloom_document/
canonical.rs

1//! Canonical form and content hash.
2//!
3//! Two documents are semantically equal when their canonical JSON is equal:
4//! object keys sorted, `-0.0` normalized to `0.0`, entities in draw order (draw
5//! order is semantic), every other collection sorted by ID. Map iteration order
6//! never influences the result.
7
8use std::collections::BTreeMap;
9
10use serde_json::Value;
11
12use crate::{DocError, Document};
13
14fn normalize(v: Value) -> Value {
15    match v {
16        Value::Object(m) => {
17            let sorted: BTreeMap<String, Value> = m.into_iter().map(|(k, v)| (k, normalize(v))).collect();
18            Value::Object(sorted.into_iter().collect())
19        }
20        Value::Array(a) => Value::Array(a.into_iter().map(normalize).collect()),
21        Value::Number(n) => match n.as_f64() {
22            Some(f) if f == 0.0 && n.is_f64() => serde_json::json!(0.0),
23            _ => Value::Number(n),
24        },
25        other => other,
26    }
27}
28
29/// FNV-1a 64-bit hash.
30#[must_use]
31pub fn fnv1a64(bytes: &[u8]) -> u64 {
32    let mut h: u64 = 0xcbf2_9ce4_8422_2325;
33    for b in bytes {
34        h ^= u64::from(*b);
35        h = h.wrapping_mul(0x0100_0000_01b3);
36    }
37    h
38}
39
40impl Document {
41    /// Canonical JSON value.
42    pub fn canonical_value(&self) -> Result<Value, DocError> {
43        let v = serde_json::to_value(self).map_err(|e| DocError::Malformed(e.to_string()))?;
44        Ok(normalize(v))
45    }
46
47    /// Canonical JSON string (compact).
48    pub fn canonical_json(&self) -> Result<String, DocError> {
49        let v = self.canonical_value()?;
50        serde_json::to_string(&v).map_err(|e| DocError::Malformed(e.to_string()))
51    }
52
53    /// Content hash of the canonical form, as 16 hex digits.
54    pub fn content_hash(&self) -> Result<String, DocError> {
55        Ok(format!("{:016x}", fnv1a64(self.canonical_json()?.as_bytes())))
56    }
57
58    /// Semantic equality (canonical forms equal).
59    #[must_use]
60    pub fn semantic_eq(&self, other: &Self) -> bool {
61        match (self.canonical_json(), other.canonical_json()) {
62            (Ok(a), Ok(b)) => a == b,
63            _ => false,
64        }
65    }
66}