Skip to main content

aivcs_core/cas/
mod.rs

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/// SHA-256 digest used as a content address.
11#[derive(Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
12pub struct Digest([u8; 32]);
13
14impl Digest {
15    /// Compute the SHA-256 digest of `data`.
16    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    /// Return the raw bytes.
24    pub fn as_bytes(&self) -> &[u8; 32] {
25        &self.0
26    }
27
28    /// Hex-encoded string.
29    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/// Errors from CAS operations.
61#[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
75/// Content-addressed store interface.
76pub trait CasStore: Send + Sync {
77    /// Store `data` and return its digest. Deduplicates automatically.
78    fn put(&self, data: &[u8]) -> Result<Digest>;
79
80    /// Retrieve the blob for `digest`.
81    fn get(&self, digest: &Digest) -> Result<Vec<u8>>;
82
83    /// Check whether `digest` exists without reading the blob.
84    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}