spec_driven_docs/domain/
ownership.rs1use std::fmt;
10use std::str::FromStr;
11
12use camino::Utf8PathBuf;
13use serde::{Deserialize, Deserializer, Serialize, Serializer};
14
15#[derive(Debug, Clone, PartialEq, Eq, Hash)]
17pub struct Sha256(String);
18
19impl Sha256 {
20 #[must_use]
22 pub fn of(bytes: &[u8]) -> Self {
23 use sha2::Digest;
24 Self(hex::encode(sha2::Sha256::digest(bytes)))
25 }
26
27 #[must_use]
29 pub fn as_str(&self) -> &str {
30 &self.0
31 }
32}
33
34impl fmt::Display for Sha256 {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 f.write_str(&self.0)
37 }
38}
39
40#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
42#[error("'{0}' is not a sha256 digest")]
43pub struct Sha256Error(String);
44
45impl FromStr for Sha256 {
46 type Err = Sha256Error;
47
48 fn from_str(s: &str) -> Result<Self, Self::Err> {
49 if s.len() == 64
50 && s.bytes()
51 .all(|b| b.is_ascii_digit() || (b'a'..=b'f').contains(&b))
52 {
53 Ok(Self(s.to_string()))
54 } else {
55 Err(Sha256Error(s.to_string()))
56 }
57 }
58}
59
60impl Serialize for Sha256 {
61 fn serialize<S: Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
62 serializer.serialize_str(&self.0)
63 }
64}
65
66impl<'de> Deserialize<'de> for Sha256 {
67 fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
68 let s = String::deserialize(deserializer)?;
69 s.parse().map_err(serde::de::Error::custom)
70 }
71}
72
73#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
75#[serde(deny_unknown_fields)]
76pub struct ManagedEntry {
77 pub source: Utf8PathBuf,
79 pub destination: Utf8PathBuf,
81 pub sha256: Sha256,
83}
84
85#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
87#[serde(deny_unknown_fields)]
88pub struct AdoptedEntry {
89 pub source: Utf8PathBuf,
91 pub destination: Utf8PathBuf,
93 pub sha256: Sha256,
95 pub baseline_sha256: Sha256,
97}
98
99#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
101#[serde(deny_unknown_fields)]
102pub struct IntegrationBlock {
103 pub path: Utf8PathBuf,
105 pub marker_hash: Sha256,
107}
108
109#[cfg(test)]
110mod tests {
111 use super::*;
112
113 #[test]
114 fn digests_deterministically() {
115 let digest = Sha256::of(b"payload\n");
116 assert_eq!(digest.as_str().len(), 64);
117 assert_eq!(digest, Sha256::of(b"payload\n"));
118 assert_ne!(digest, Sha256::of(b"payload"));
119 }
120
121 #[test]
122 fn rejects_malformed_digests() {
123 for bad in ["", "abc", &"A".repeat(64), &"g".repeat(64)] {
124 assert!(bad.parse::<Sha256>().is_err(), "accepted {bad:?}");
125 }
126 assert!("0123456789abcdef".repeat(4).parse::<Sha256>().is_ok());
127 }
128}