nautilus_test_kit/
files.rs1use std::{
17 fs::{File, OpenOptions},
18 io::{BufReader, BufWriter, Read, copy},
19 path::Path,
20};
21
22use reqwest::blocking::Client;
23use ring::digest;
24use serde_json::Value;
25
26pub fn ensure_file_exists_or_download_http(
36 filepath: &Path,
37 url: &str,
38 checksums: Option<&Path>,
39) -> anyhow::Result<()> {
40 if filepath.exists() {
41 println!("File already exists: {}", filepath.display());
42
43 if let Some(checksums_file) = checksums {
44 if verify_sha256_checksum(filepath, checksums_file)? {
45 println!("File is valid");
46 return Ok(());
47 } else {
48 let new_checksum = calculate_sha256(filepath)?;
49 println!("Adding checksum for existing file: {new_checksum}");
50 update_sha256_checksums(filepath, checksums_file, &new_checksum)?;
51 return Ok(());
52 }
53 }
54 return Ok(());
55 }
56
57 download_file(filepath, url)?;
58
59 if let Some(checksums_file) = checksums {
60 let new_checksum = calculate_sha256(filepath)?;
61 update_sha256_checksums(filepath, checksums_file, &new_checksum)?;
62 }
63
64 Ok(())
65}
66
67fn download_file(filepath: &Path, url: &str) -> anyhow::Result<()> {
68 println!("Downloading file from {url} to {}", filepath.display());
69
70 if let Some(parent) = filepath.parent() {
71 std::fs::create_dir_all(parent)?;
72 }
73
74 let mut response = Client::new().get(url).send()?;
75 if !response.status().is_success() {
76 anyhow::bail!("Failed to download file: HTTP {}", response.status());
77 }
78
79 let mut out = File::create(filepath)?;
80 copy(&mut response, &mut out)?;
81
82 println!("File downloaded to {}", filepath.display());
83 Ok(())
84}
85
86fn calculate_sha256(filepath: &Path) -> anyhow::Result<String> {
87 let mut file = File::open(filepath)?;
88 let mut context = digest::Context::new(&digest::SHA256);
89 let mut buffer = [0; 4096];
90
91 loop {
92 let count = file.read(&mut buffer)?;
93 if count == 0 {
94 break;
95 }
96 context.update(&buffer[..count]);
97 }
98
99 let digest = context.finish();
100 Ok(hex::encode(digest.as_ref()))
101}
102
103fn verify_sha256_checksum(filepath: &Path, checksums: &Path) -> anyhow::Result<bool> {
104 let file = File::open(checksums)?;
105 let reader = BufReader::new(file);
106 let checksums: Value = serde_json::from_reader(reader)?;
107
108 let filename = filepath.file_name().unwrap().to_str().unwrap();
109 if let Some(expected_checksum) = checksums.get(filename) {
110 let expected_checksum_str = expected_checksum.as_str().unwrap();
111 let expected_hash = expected_checksum_str
112 .strip_prefix("sha256:")
113 .unwrap_or(expected_checksum_str);
114 let calculated_checksum = calculate_sha256(filepath)?;
115 if expected_hash == calculated_checksum {
116 return Ok(true);
117 }
118 }
119
120 Ok(false)
121}
122
123fn update_sha256_checksums(
124 filepath: &Path,
125 checksums_file: &Path,
126 new_checksum: &str,
127) -> anyhow::Result<()> {
128 let checksums: Value = if checksums_file.exists() {
129 let file = File::open(checksums_file)?;
130 let reader = BufReader::new(file);
131 serde_json::from_reader(reader)?
132 } else {
133 serde_json::json!({})
134 };
135
136 let mut checksums_map = checksums.as_object().unwrap().clone();
137
138 let filename = filepath.file_name().unwrap().to_str().unwrap().to_string();
140 let prefixed_checksum = format!("sha256:{new_checksum}");
141 checksums_map.insert(filename, Value::String(prefixed_checksum));
142
143 let file = OpenOptions::new()
144 .write(true)
145 .create(true)
146 .truncate(true)
147 .open(checksums_file)?;
148 let writer = BufWriter::new(file);
149 serde_json::to_writer_pretty(writer, &serde_json::Value::Object(checksums_map))?;
150
151 Ok(())
152}
153
154#[cfg(test)]
158mod tests {
159 use std::{
160 fs,
161 io::{BufWriter, Write},
162 net::SocketAddr,
163 sync::Arc,
164 };
165
166 use axum::{Router, http::StatusCode, routing::get, serve};
167 use rstest::*;
168 use serde_json::{json, to_writer};
169 use tempfile::TempDir;
170 use tokio::{
171 net::TcpListener,
172 task,
173 time::{Duration, sleep},
174 };
175
176 use super::*;
177
178 async fn setup_test_server(
179 server_content: Option<String>,
180 status_code: StatusCode,
181 ) -> SocketAddr {
182 let server_content = Arc::new(server_content);
183 let server_content_clone = server_content.clone();
184 let app = Router::new().route(
185 "/testfile.txt",
186 get(move || {
187 let server_content = server_content_clone.clone();
188 async move {
189 let response_body = match &*server_content {
190 Some(content) => content.clone(),
191 None => "File not found".to_string(),
192 };
193 (status_code, response_body)
194 }
195 }),
196 );
197
198 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
199 let addr = listener.local_addr().unwrap();
200 let server = serve(listener, app);
201
202 task::spawn(async move {
203 if let Err(e) = server.await {
204 eprintln!("server error: {e}");
205 }
206 });
207
208 sleep(Duration::from_millis(100)).await;
209
210 addr
211 }
212
213 #[tokio::test]
214 async fn test_file_already_exists() {
215 let temp_dir = TempDir::new().unwrap();
216 let file_path = temp_dir.path().join("testfile.txt");
217 fs::write(&file_path, "Existing file content").unwrap();
218
219 let url = "http://example.com/testfile.txt".to_string();
220 let result = ensure_file_exists_or_download_http(&file_path, &url, None);
221
222 assert!(result.is_ok());
223 let content = fs::read_to_string(&file_path).unwrap();
224 assert_eq!(content, "Existing file content");
225 }
226
227 #[tokio::test]
228 async fn test_download_file_success() {
229 let temp_dir = TempDir::new().unwrap();
230 let filepath = temp_dir.path().join("testfile.txt");
231 let filepath_clone = filepath.clone();
232
233 let server_content = Some("Server file content".to_string());
234 let status_code = StatusCode::OK;
235 let addr = setup_test_server(server_content.clone(), status_code).await;
236 let url = format!("http://{addr}/testfile.txt");
237
238 let result = tokio::task::spawn_blocking(move || {
239 ensure_file_exists_or_download_http(&filepath_clone, &url, None)
240 })
241 .await
242 .unwrap();
243
244 assert!(result.is_ok());
245 let content = fs::read_to_string(&filepath).unwrap();
246 assert_eq!(content, server_content.unwrap());
247 }
248
249 #[tokio::test]
250 async fn test_download_file_not_found() {
251 let temp_dir = TempDir::new().unwrap();
252 let file_path = temp_dir.path().join("testfile.txt");
253
254 let server_content = None;
255 let status_code = StatusCode::NOT_FOUND;
256 let addr = setup_test_server(server_content, status_code).await;
257 let url = format!("http://{addr}/testfile.txt");
258
259 let result = tokio::task::spawn_blocking(move || {
260 ensure_file_exists_or_download_http(&file_path, &url, None)
261 })
262 .await
263 .unwrap();
264
265 assert!(result.is_err());
266 let err_msg = format!("{}", result.unwrap_err());
267 assert!(
268 err_msg.contains("Failed to download file"),
269 "Unexpected error message: {err_msg}"
270 );
271 }
272
273 #[tokio::test]
274 async fn test_network_error() {
275 let temp_dir = TempDir::new().unwrap();
276 let file_path = temp_dir.path().join("testfile.txt");
277
278 let url = "http://127.0.0.1:0/testfile.txt".to_string();
280
281 let result = tokio::task::spawn_blocking(move || {
282 ensure_file_exists_or_download_http(&file_path, &url, None)
283 })
284 .await
285 .unwrap();
286
287 assert!(result.is_err());
288 let err_msg = format!("{}", result.unwrap_err());
289 assert!(
290 err_msg.contains("error"),
291 "Unexpected error message: {err_msg}"
292 );
293 }
294
295 #[rstest]
296 fn test_calculate_sha256() -> anyhow::Result<()> {
297 let temp_dir = TempDir::new()?;
298 let test_file_path = temp_dir.path().join("test_file.txt");
299 let mut test_file = File::create(&test_file_path)?;
300 let content = b"Hello, world!";
301 test_file.write_all(content)?;
302
303 let expected_hash = "315f5bdb76d078c43b8ac0064e4a0164612b1fce77c869345bfc94c75894edd3";
304 let calculated_hash = calculate_sha256(&test_file_path)?;
305
306 assert_eq!(calculated_hash, expected_hash);
307 Ok(())
308 }
309
310 #[rstest]
311 fn test_verify_sha256_checksum() -> anyhow::Result<()> {
312 let temp_dir = TempDir::new()?;
313 let test_file_path = temp_dir.path().join("test_file.txt");
314 let mut test_file = File::create(&test_file_path)?;
315 let content = b"Hello, world!";
316 test_file.write_all(content)?;
317
318 let calculated_checksum = calculate_sha256(&test_file_path)?;
319
320 let checksums_path = temp_dir.path().join("checksums.json");
322 let checksums_data = json!({
323 "test_file.txt": format!("sha256:{}", calculated_checksum)
324 });
325 let checksums_file = File::create(&checksums_path)?;
326 let writer = BufWriter::new(checksums_file);
327 to_writer(writer, &checksums_data)?;
328
329 let is_valid = verify_sha256_checksum(&test_file_path, &checksums_path)?;
330 assert!(is_valid, "The checksum should be valid");
331 Ok(())
332 }
333}