Skip to main content

headless_lms_utils/
stable_digest.rs

1/// A hex BLAKE3 digest of `parts` that is the same in every process and every release.
2///
3/// Each part is length-prefixed, so no two different lists of parts can ever hash to the same
4/// value: pass the parts as they are and do not add separators of your own. Returns all 64 hex
5/// characters; truncate at the call site if a shorter identifier is wanted.
6///
7/// Unkeyed, so the digest of a guessable input is guessable. Anything that has to resist that must
8/// include a secret part of its own, the way [`crate::page_visit_hasher`] mixes in the day's key.
9///
10/// New callers use this; [`crate::error_identifier`]'s own scheme stays only because its stored
11/// identifiers may not change.
12pub fn stable_digest(parts: &[&[u8]]) -> String {
13    let mut hasher = blake3::Hasher::new();
14    for part in parts {
15        hasher.update(&(part.len() as u64).to_le_bytes());
16        hasher.update(part);
17    }
18    hasher.finalize().to_hex().to_string()
19}
20
21#[cfg(test)]
22mod tests {
23    use super::*;
24
25    /// The digest identifies whatever the caller split into parts, so two different splits sharing
26    /// their concatenation have to disagree. Without the length prefix they would not, and callers
27    /// would silently share cache entries or error groups.
28    #[test]
29    fn parts_that_only_differ_in_where_they_are_split_digest_differently() {
30        assert_ne!(stable_digest(&[b"ab", b"c"]), stable_digest(&[b"a", b"bc"]));
31        assert_ne!(stable_digest(&[b"a", b""]), stable_digest(&[b"a"]));
32    }
33
34    /// Callers store digests and compare them across releases and across replicas, so the value
35    /// has to be pinned rather than merely deterministic within one run.
36    #[test]
37    fn a_given_input_digests_to_the_same_value_on_every_host_and_release() {
38        assert_eq!(
39            stable_digest(&[b"one", b"two"]),
40            "895cdc7d0d102a0763bd1cf348febb7c06c5e74b1b3889773f138c723acb7b5c"
41        );
42    }
43}