ricecoder_files/
verifier.rs1use crate::error::FileError;
4use sha2::{Digest, Sha256};
5use std::path::Path;
6use tokio::fs;
7
8#[derive(Debug, Clone)]
10pub struct ContentVerifier;
11
12impl ContentVerifier {
13 pub fn new() -> Self {
15 ContentVerifier
16 }
17
18 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 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 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 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}