Skip to main content

murk_cli/
integrity.rs

1use sha2::{Digest, Sha256};
2
3/// Compute a SHA-256 hash of the given bytes and return it in the
4/// `sha256:hex` format used throughout the .murk file.
5pub fn hash(data: &[u8]) -> String {
6    let digest = Sha256::digest(data);
7    format!("sha256:{}", hex::encode(digest))
8}
9
10/// Verify that `data` matches an expected hash string (e.g. "sha256:abc123...").
11/// Returns Ok(()) if valid, Err with a message if not.
12#[allow(dead_code)]
13pub fn verify(data: &[u8], expected: &str) -> Result<(), IntegrityError> {
14    let actual = hash(data);
15    if actual == expected {
16        Ok(())
17    } else {
18        Err(IntegrityError::Mismatch {
19            expected: expected.to_string(),
20            actual,
21        })
22    }
23}
24
25/// Errors that can occur during integrity verification.
26#[allow(dead_code)]
27#[derive(Debug)]
28pub enum IntegrityError {
29    Mismatch { expected: String, actual: String },
30}
31
32impl std::fmt::Display for IntegrityError {
33    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
34        match self {
35            IntegrityError::Mismatch { expected, actual } => {
36                write!(
37                    f,
38                    "integrity check failed: expected {expected}, got {actual}"
39                )
40            }
41        }
42    }
43}
44
45/// Simple hex encoding (no extra dependency needed).
46mod hex {
47    pub fn encode(bytes: impl AsRef<[u8]>) -> String {
48        use std::fmt::Write;
49        let bytes = bytes.as_ref();
50        let mut s = String::with_capacity(bytes.len() * 2);
51        for b in bytes {
52            write!(s, "{b:02x}").unwrap();
53        }
54        s
55    }
56}
57
58#[cfg(test)]
59mod tests {
60    use super::*;
61
62    #[test]
63    fn hash_format() {
64        let h = hash(b"hello world");
65        assert!(h.starts_with("sha256:"));
66        // SHA-256 produces 64 hex chars
67        assert_eq!(h.len(), "sha256:".len() + 64);
68    }
69
70    #[test]
71    fn hash_deterministic() {
72        assert_eq!(hash(b"test data"), hash(b"test data"));
73    }
74
75    #[test]
76    fn hash_known_value() {
77        // SHA-256("") = e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
78        let h = hash(b"");
79        assert_eq!(
80            h,
81            "sha256:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
82        );
83    }
84
85    #[test]
86    fn verify_matching() {
87        let data = b"some data";
88        let h = hash(data);
89        assert!(verify(data, &h).is_ok());
90    }
91
92    #[test]
93    fn verify_mismatch() {
94        let result = verify(
95            b"actual data",
96            "sha256:0000000000000000000000000000000000000000000000000000000000000000",
97        );
98        assert!(result.is_err());
99    }
100
101    #[test]
102    fn different_data_different_hash() {
103        assert_ne!(hash(b"foo"), hash(b"bar"));
104    }
105
106    #[test]
107    fn integrity_error_display() {
108        let err = IntegrityError::Mismatch {
109            expected: "sha256:aaa".into(),
110            actual: "sha256:bbb".into(),
111        };
112        let msg = err.to_string();
113        assert!(msg.contains("integrity check failed"));
114        assert!(msg.contains("sha256:aaa"));
115        assert!(msg.contains("sha256:bbb"));
116    }
117
118    #[test]
119    fn hex_encode_empty() {
120        assert_eq!(super::hex::encode(b""), "");
121    }
122
123    #[test]
124    fn hex_encode_known_values() {
125        assert_eq!(super::hex::encode(b"\x00\xff"), "00ff");
126        assert_eq!(super::hex::encode(b"\xde\xad\xbe\xef"), "deadbeef");
127    }
128}