ricecoder_files/
verifier.rs

1//! Content verification and integrity checking
2
3use crate::error::FileError;
4use sha2::{Digest, Sha256};
5use std::path::Path;
6use tokio::fs;
7
8/// Verifies file integrity through content comparison and hashing
9#[derive(Debug, Clone)]
10pub struct ContentVerifier;
11
12impl ContentVerifier {
13    /// Creates a new ContentVerifier instance
14    pub fn new() -> Self {
15        ContentVerifier
16    }
17
18    /// Computes SHA-256 hash of content
19    ///
20    /// # Arguments
21    ///
22    /// * `content` - The content to hash
23    ///
24    /// # Returns
25    ///
26    /// Hexadecimal string representation of the SHA-256 hash
27    pub fn compute_hash(content: &str) -> String {
28        let mut hasher = Sha256::new();
29        hasher.update(content.as_bytes());
30        format!("{:x}", hasher.finalize())
31    }
32
33    /// Verifies that written content matches source byte-for-byte
34    ///
35    /// # Arguments
36    ///
37    /// * `path` - Path to the written file
38    /// * `expected` - Expected content
39    ///
40    /// # Returns
41    ///
42    /// Ok(()) if verification succeeds, error otherwise
43    pub async fn verify_write(&self, path: &Path, expected: &str) -> Result<(), FileError> {
44        let written = fs::read_to_string(path).await.map_err(|e| {
45            FileError::VerificationFailed(format!("Failed to read written file: {}", e))
46        })?;
47
48        if written == expected {
49            Ok(())
50        } else {
51            Err(FileError::VerificationFailed(
52                "Written content does not match source".to_string(),
53            ))
54        }
55    }
56
57    /// Verifies backup integrity by comparing stored hash with computed hash
58    ///
59    /// # Arguments
60    ///
61    /// * `backup_path` - Path to the backup file
62    /// * `stored_hash` - Previously stored hash to compare against
63    ///
64    /// # Returns
65    ///
66    /// Ok(()) if hashes match (no corruption), error if mismatch or read fails
67    pub async fn verify_backup(
68        &self,
69        backup_path: &Path,
70        stored_hash: &str,
71    ) -> Result<(), FileError> {
72        let backup_content = fs::read_to_string(backup_path)
73            .await
74            .map_err(|e| FileError::BackupFailed(format!("Failed to read backup file: {}", e)))?;
75
76        let computed_hash = Self::compute_hash(&backup_content);
77
78        if computed_hash == stored_hash {
79            Ok(())
80        } else {
81            Err(FileError::BackupCorrupted)
82        }
83    }
84}
85
86impl Default for ContentVerifier {
87    fn default() -> Self {
88        Self::new()
89    }
90}
91
92#[cfg(test)]
93mod tests {
94    use super::*;
95
96    #[test]
97    fn test_compute_hash_deterministic() {
98        let content = "test content";
99        let hash1 = ContentVerifier::compute_hash(content);
100        let hash2 = ContentVerifier::compute_hash(content);
101        assert_eq!(hash1, hash2);
102    }
103
104    #[test]
105    fn test_compute_hash_different_content() {
106        let hash1 = ContentVerifier::compute_hash("content1");
107        let hash2 = ContentVerifier::compute_hash("content2");
108        assert_ne!(hash1, hash2);
109    }
110
111    #[test]
112    fn test_compute_hash_empty_string() {
113        let hash = ContentVerifier::compute_hash("");
114        // SHA-256 of empty string
115        assert_eq!(
116            hash,
117            "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
118        );
119    }
120
121    #[tokio::test]
122    async fn test_verify_write_matching_content() {
123        let verifier = ContentVerifier::new();
124        let temp_dir = tempfile::tempdir().unwrap();
125        let file_path = temp_dir.path().join("test.txt");
126
127        let content = "test content";
128        fs::write(&file_path, content).await.unwrap();
129
130        let result = verifier.verify_write(&file_path, content).await;
131        assert!(result.is_ok());
132    }
133
134    #[tokio::test]
135    async fn test_verify_write_mismatched_content() {
136        let verifier = ContentVerifier::new();
137        let temp_dir = tempfile::tempdir().unwrap();
138        let file_path = temp_dir.path().join("test.txt");
139
140        fs::write(&file_path, "actual content").await.unwrap();
141
142        let result = verifier.verify_write(&file_path, "expected content").await;
143        assert!(result.is_err());
144    }
145
146    #[tokio::test]
147    async fn test_verify_write_nonexistent_file() {
148        let verifier = ContentVerifier::new();
149        let temp_dir = tempfile::tempdir().unwrap();
150        let file_path = temp_dir.path().join("nonexistent.txt");
151
152        let result = verifier.verify_write(&file_path, "content").await;
153        assert!(result.is_err());
154    }
155
156    #[tokio::test]
157    async fn test_verify_backup_matching_hash() {
158        let verifier = ContentVerifier::new();
159        let temp_dir = tempfile::tempdir().unwrap();
160        let backup_path = temp_dir.path().join("backup.txt");
161
162        let content = "backup content";
163        fs::write(&backup_path, content).await.unwrap();
164        let stored_hash = ContentVerifier::compute_hash(content);
165
166        let result = verifier.verify_backup(&backup_path, &stored_hash).await;
167        assert!(result.is_ok());
168    }
169
170    #[tokio::test]
171    async fn test_verify_backup_mismatched_hash() {
172        let verifier = ContentVerifier::new();
173        let temp_dir = tempfile::tempdir().unwrap();
174        let backup_path = temp_dir.path().join("backup.txt");
175
176        fs::write(&backup_path, "actual content").await.unwrap();
177        let wrong_hash = ContentVerifier::compute_hash("different content");
178
179        let result = verifier.verify_backup(&backup_path, &wrong_hash).await;
180        assert!(result.is_err());
181        match result {
182            Err(FileError::BackupCorrupted) => (),
183            _ => panic!("Expected BackupCorrupted error"),
184        }
185    }
186
187    #[tokio::test]
188    async fn test_verify_backup_nonexistent_file() {
189        let verifier = ContentVerifier::new();
190        let temp_dir = tempfile::tempdir().unwrap();
191        let backup_path = temp_dir.path().join("nonexistent.txt");
192
193        let result = verifier.verify_backup(&backup_path, "somehash").await;
194        assert!(result.is_err());
195    }
196}