Skip to main content

systemprompt_models/artifacts/
digest.rs

1//! Content identity of an artifact body.
2//!
3//! The digest is over the canonical (compact, key-ordered) JSON of the body,
4//! so two results with the same content hash the same wherever they were
5//! seen. It is the key of the content-addressed payload store and the
6//! "already scanned" cache.
7//!
8//! Copyright (c) systemprompt.io — Business Source License 1.1.
9//! See <https://systemprompt.io> for licensing details.
10
11use serde_json::Value as JsonValue;
12use sha2::{Digest, Sha256};
13
14#[derive(Debug, Clone, PartialEq, Eq)]
15pub struct PayloadDigest {
16    pub sha256: String,
17    pub byte_len: usize,
18}
19
20// JSON: any typed artifact body; canonicalised by sorted-key serialisation.
21#[must_use]
22pub fn payload_digest(body: &JsonValue) -> PayloadDigest {
23    let canonical = canonical_json(body);
24    PayloadDigest {
25        sha256: hex::encode(Sha256::digest(canonical.as_bytes())),
26        byte_len: canonical.len(),
27    }
28}
29
30// JSON: recursive key-ordering of an open-shaped value.
31fn canonical_json(value: &JsonValue) -> String {
32    fn sort(value: &JsonValue) -> JsonValue {
33        match value {
34            JsonValue::Object(map) => {
35                let mut entries: Vec<(&String, &JsonValue)> = map.iter().collect();
36                entries.sort_by(|a, b| a.0.cmp(b.0));
37                let mut out = serde_json::Map::with_capacity(entries.len());
38                for (key, item) in entries {
39                    out.insert(key.clone(), sort(item));
40                }
41                JsonValue::Object(out)
42            },
43            JsonValue::Array(items) => JsonValue::Array(items.iter().map(sort).collect()),
44            other => other.clone(),
45        }
46    }
47    sort(value).to_string()
48}