1use serde::{Deserialize, Serialize};
5use sha2::{Digest, Sha256};
6use std::fmt;
7
8use crate::error::{Error, Result};
9
10#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
12#[serde(transparent)]
13pub struct Digest256(String);
14
15impl Digest256 {
16 #[must_use]
18 pub fn of(bytes: &[u8]) -> Self {
19 let mut hasher = Sha256::new();
20 hasher.update(bytes);
21 let hex = hex::encode(hasher.finalize());
22 Self(format!("sha256:{hex}"))
23 }
24
25 #[must_use]
27 pub fn as_str(&self) -> &str {
28 &self.0
29 }
30
31 #[must_use]
33 pub fn hex(&self) -> &str {
34 &self.0["sha256:".len()..]
36 }
37
38 pub fn parse(s: &str) -> Result<Self> {
40 let Some(rest) = s.strip_prefix("sha256:") else {
41 return Err(Error::InvalidDigest(format!(
42 "missing sha256: prefix in {s:?}"
43 )));
44 };
45 if rest.len() != 64 || !rest.chars().all(|c| c.is_ascii_hexdigit()) {
46 return Err(Error::InvalidDigest(format!("not 64 hex chars: {s:?}")));
47 }
48 Ok(Self(s.to_owned()))
49 }
50}
51
52impl fmt::Display for Digest256 {
53 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
54 f.write_str(&self.0)
55 }
56}
57
58#[cfg(test)]
59mod tests {
60 use super::*;
61
62 #[test]
63 fn empty_input_has_known_sha256() {
64 let d = Digest256::of(b"");
65 assert_eq!(
66 d.as_str(),
67 "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
68 );
69 }
70
71 #[test]
72 fn deterministic_across_calls() {
73 let payload = b"processfork";
74 assert_eq!(Digest256::of(payload), Digest256::of(payload));
75 }
76
77 #[test]
78 fn parses_canonical_form() {
79 let d = Digest256::of(b"hi");
80 let parsed = Digest256::parse(d.as_str()).unwrap();
81 assert_eq!(d, parsed);
82 assert_eq!(parsed.hex().len(), 64);
83 }
84
85 #[test]
86 fn rejects_bad_prefix_or_length() {
87 assert!(Digest256::parse("md5:abc").is_err());
88 assert!(Digest256::parse("sha256:short").is_err());
89 assert!(Digest256::parse("sha256:zzzz").is_err());
90 }
91}