1pub mod fs;
2
3use std::fmt;
4use std::str::FromStr;
5
6use serde::{Deserialize, Serialize};
7use sha2::{Digest as Sha2Digest, Sha256};
8use thiserror::Error;
9
10#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct Digest([u8; 32]);
13
14impl Digest {
15 pub fn compute(data: &[u8]) -> Self {
17 let hash = Sha256::digest(data);
18 let mut bytes = [0u8; 32];
19 bytes.copy_from_slice(&hash);
20 Self(bytes)
21 }
22
23 pub fn as_bytes(&self) -> &[u8; 32] {
25 &self.0
26 }
27
28 pub fn to_hex(&self) -> String {
30 hex::encode(self.0)
31 }
32}
33
34impl fmt::Display for Digest {
35 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
36 f.write_str(&self.to_hex())
37 }
38}
39
40impl fmt::Debug for Digest {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 write!(f, "Digest({})", &self.to_hex()[..12])
43 }
44}
45
46impl FromStr for Digest {
47 type Err = CasError;
48
49 fn from_str(s: &str) -> std::result::Result<Self, Self::Err> {
50 let bytes = hex::decode(s).map_err(|_| CasError::InvalidDigest(s.to_string()))?;
51 if bytes.len() != 32 {
52 return Err(CasError::InvalidDigest(s.to_string()));
53 }
54 let mut arr = [0u8; 32];
55 arr.copy_from_slice(&bytes);
56 Ok(Self(arr))
57 }
58}
59
60#[derive(Debug, Error)]
62pub enum CasError {
63 #[error("blob not found: {0}")]
64 NotFound(Digest),
65
66 #[error("invalid digest hex: {0}")]
67 InvalidDigest(String),
68
69 #[error("io error: {0}")]
70 Io(#[from] std::io::Error),
71}
72
73pub type Result<T> = std::result::Result<T, CasError>;
74
75pub trait CasStore: Send + Sync {
77 fn put(&self, data: &[u8]) -> Result<Digest>;
79
80 fn get(&self, digest: &Digest) -> Result<Vec<u8>>;
82
83 fn exists(&self, digest: &Digest) -> Result<bool>;
85}
86
87#[cfg(test)]
88mod tests {
89 use super::*;
90
91 #[test]
92 fn digest_display_fromstr_roundtrip() {
93 let d = Digest::compute(b"hello world");
94 let hex = d.to_string();
95 assert_eq!(hex.len(), 64);
96 let parsed: Digest = hex.parse().unwrap();
97 assert_eq!(d, parsed);
98 }
99
100 #[test]
101 fn digest_fromstr_invalid_hex() {
102 assert!("not-valid-hex".parse::<Digest>().is_err());
103 }
104
105 #[test]
106 fn digest_fromstr_wrong_length() {
107 assert!("abcd".parse::<Digest>().is_err());
108 }
109
110 #[test]
111 fn digest_deterministic() {
112 let a = Digest::compute(b"test data");
113 let b = Digest::compute(b"test data");
114 assert_eq!(a, b);
115 }
116
117 #[test]
118 fn digest_different_data_different_hash() {
119 let a = Digest::compute(b"data a");
120 let b = Digest::compute(b"data b");
121 assert_ne!(a, b);
122 }
123}