1use crate::error::{Error, Result};
8use serde::{Deserialize, Serialize};
9use sha2::{Digest as _, Sha256};
10use std::fmt;
11
12#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
14pub struct Digest {
15 pub hash: String,
17 pub size_bytes: u64,
19}
20
21impl Digest {
22 #[must_use]
24 pub fn of_bytes(bytes: &[u8]) -> Self {
25 let hash = hex::encode(Sha256::digest(bytes));
26 Self {
27 hash,
28 size_bytes: bytes.len() as u64,
29 }
30 }
31
32 #[must_use]
34 pub fn to_resource(&self) -> String {
35 format!("{}/{}", self.hash, self.size_bytes)
36 }
37}
38
39impl fmt::Display for Digest {
40 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
41 write!(f, "sha256:{}/{}", self.hash, self.size_bytes)
42 }
43}
44
45pub fn canonical_bytes<T: Serialize>(value: &T) -> Result<Vec<u8>> {
57 serde_json::to_vec(value)
58 .map_err(|e| Error::serialization(format!("canonical encode failed: {e}")))
59}
60
61pub fn digest_of<T: Serialize>(value: &T) -> Result<Digest> {
67 let bytes = canonical_bytes(value)?;
68 Ok(Digest::of_bytes(&bytes))
69}
70
71#[cfg(test)]
72mod tests {
73 use super::*;
74
75 #[test]
76 fn digest_of_empty_is_stable() {
77 let d = Digest::of_bytes(b"");
78 assert_eq!(d.size_bytes, 0);
79 assert_eq!(
80 d.hash,
81 "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
82 );
83 }
84
85 #[test]
86 fn digest_of_hello_world() {
87 let d = Digest::of_bytes(b"hello world");
88 assert_eq!(d.size_bytes, 11);
89 assert_eq!(
90 d.hash,
91 "b94d27b9934d3e08a52e52d7da7dabfac484efe37a5380ee9088f7ace2efcde9"
92 );
93 }
94
95 #[test]
96 fn display_and_resource_forms() {
97 let d = Digest::of_bytes(b"x");
98 assert!(d.to_string().starts_with("sha256:"));
99 assert!(d.to_resource().contains('/'));
100 }
101
102 #[test]
103 fn digest_of_round_trips_through_serde() {
104 let d = Digest::of_bytes(b"payload");
105 let json = serde_json::to_string(&d).unwrap();
106 let back: Digest = serde_json::from_str(&json).unwrap();
107 assert_eq!(d, back);
108 }
109}