1use blake3::Hasher;
7use serde::{Deserialize, Serialize};
8use std::collections::HashMap;
9use std::fs::File;
10use std::io::{BufReader, Read};
11use std::path::Path;
12use tracing::{debug, info, warn};
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
16pub struct FileHash {
17 pub hash: String,
19 pub size: u64,
21}
22
23#[derive(Debug, Clone, Default, Serialize, Deserialize)]
25pub struct ArtifactManifest {
26 pub files: HashMap<String, FileHash>,
28 pub created_at: u64,
30 #[serde(skip_serializing_if = "Option::is_none")]
32 pub worker_id: Option<String>,
33}
34
35#[derive(Debug, Clone, Serialize, Deserialize)]
37pub struct VerificationResult {
38 pub passed: Vec<String>,
40 pub failed: Vec<VerificationFailure>,
42 pub skipped: Vec<String>,
44}
45
46#[derive(Debug, Clone, Serialize, Deserialize)]
48pub struct VerificationFailure {
49 pub path: String,
51 pub expected_hash: String,
53 pub actual_hash: String,
55 pub expected_size: u64,
57 pub actual_size: u64,
59}
60
61impl VerificationResult {
62 pub fn all_passed(&self) -> bool {
64 self.failed.is_empty()
65 }
66
67 pub fn summary(&self) -> String {
69 format!(
70 "{} passed, {} failed, {} skipped",
71 self.passed.len(),
72 self.failed.len(),
73 self.skipped.len()
74 )
75 }
76
77 pub fn format_failures(&self) -> String {
79 if self.failed.is_empty() {
80 return String::new();
81 }
82
83 let mut msg = String::new();
84 msg.push_str("Artifact integrity verification failed:\n\n");
85
86 for failure in &self.failed {
87 msg.push_str(&format!(
90 " {} - HASH MISMATCH\n",
91 crate::redaction::redact_path(&failure.path)
92 ));
93 msg.push_str(&format!(
94 " Expected: {} ({} bytes)\n",
95 short_hash(&failure.expected_hash),
96 failure.expected_size
97 ));
98 msg.push_str(&format!(
99 " Actual: {} ({} bytes)\n",
100 short_hash(&failure.actual_hash),
101 failure.actual_size
102 ));
103 }
104
105 msg.push_str("\nThis may indicate:\n");
106 msg.push_str(" - Transfer corruption (retry may help)\n");
107 msg.push_str(" - Incomplete transfer\n");
108 msg.push_str(" - Worker build cache inconsistency\n");
109 msg.push_str("\nSuggested actions:\n");
110 msg.push_str(" 1. Run `rch diagnose` for detailed analysis\n");
111 msg.push_str(" 2. Re-run the build to verify consistency\n");
112 msg.push_str(" 3. Check worker health: `rch workers probe`\n");
113
114 msg
115 }
116}
117
118impl VerificationFailure {
119 pub fn new(
121 path: impl Into<String>,
122 expected_hash: impl Into<String>,
123 actual_hash: impl Into<String>,
124 expected_size: u64,
125 actual_size: u64,
126 ) -> Self {
127 Self {
128 path: path.into(),
129 expected_hash: expected_hash.into(),
130 actual_hash: actual_hash.into(),
131 expected_size,
132 actual_size,
133 }
134 }
135}
136
137pub fn compute_file_hash(path: &Path) -> std::io::Result<FileHash> {
145 let file = File::open(path)?;
146 let metadata = file.metadata()?;
147 let size = metadata.len();
148
149 let mut reader = BufReader::new(file);
150 let mut hasher = Hasher::new();
151 let mut buffer = [0u8; 65536]; loop {
154 let bytes_read = reader.read(&mut buffer)?;
155 if bytes_read == 0 {
156 break;
157 }
158 hasher.update(&buffer[..bytes_read]);
159 }
160
161 let hash = hasher.finalize().to_hex().to_string();
162
163 Ok(FileHash { hash, size })
164}
165
166fn short_hash(hash: &str) -> String {
174 hash.chars().take(16).collect()
175}
176
177fn is_safe_path(path_str: &str) -> bool {
179 let path = Path::new(path_str);
180 if path.is_absolute() {
181 return false;
182 }
183 for component in path.components() {
184 match component {
185 std::path::Component::ParentDir => return false,
186 std::path::Component::RootDir | std::path::Component::Prefix(_) => return false,
187 _ => {}
188 }
189 }
190 true
191}
192
193pub fn verify_artifacts(
203 base_dir: &Path,
204 manifest: &ArtifactManifest,
205 max_size: u64,
206) -> VerificationResult {
207 let mut result = VerificationResult {
208 passed: Vec::new(),
209 failed: Vec::new(),
210 skipped: Vec::new(),
211 };
212
213 for (rel_path, expected) in &manifest.files {
214 if !is_safe_path(rel_path) {
216 warn!("Skipping unsafe path in manifest: {}", rel_path);
217 result.skipped.push(rel_path.clone());
218 continue;
219 }
220
221 let full_path = base_dir.join(rel_path);
222
223 if !full_path.exists() {
225 debug!("Skipping verification of missing file: {}", rel_path);
226 result.skipped.push(rel_path.clone());
227 continue;
228 }
229
230 let actual_size = match std::fs::metadata(&full_path) {
232 Ok(meta) => meta.len(),
233 Err(e) => {
234 warn!("Skipping verification of {}: {}", rel_path, e);
235 result.skipped.push(rel_path.clone());
236 continue;
237 }
238 };
239 if actual_size > max_size {
240 debug!(
241 "Skipping verification of large file: {} ({} bytes > {} max)",
242 rel_path, actual_size, max_size
243 );
244 result.skipped.push(rel_path.clone());
245 continue;
246 }
247
248 match compute_file_hash(&full_path) {
250 Ok(actual) => {
251 if actual.hash == expected.hash && actual.size == expected.size {
252 debug!("Verification passed: {}", rel_path);
253 result.passed.push(rel_path.clone());
254 } else {
255 warn!(
256 "Verification failed for {}: expected {} ({} bytes), got {} ({} bytes)",
257 rel_path,
258 short_hash(&expected.hash),
259 expected.size,
260 short_hash(&actual.hash),
261 actual.size
262 );
263 result.failed.push(VerificationFailure::new(
264 rel_path,
265 &expected.hash,
266 actual.hash,
267 expected.size,
268 actual.size,
269 ));
270 }
271 }
272 Err(e) => {
273 warn!("Failed to hash {}: {}", rel_path, e);
274 result.skipped.push(rel_path.clone());
275 }
276 }
277 }
278
279 info!("Artifact verification complete: {}", result.summary());
280
281 result
282}
283
284pub fn create_manifest(
294 base_dir: &Path,
295 rel_paths: &[String],
296 worker_id: Option<String>,
297) -> ArtifactManifest {
298 let mut manifest = ArtifactManifest {
299 files: HashMap::new(),
300 created_at: std::time::SystemTime::now()
301 .duration_since(std::time::UNIX_EPOCH)
302 .unwrap_or_default()
303 .as_secs(),
304 worker_id,
305 };
306
307 for rel_path in rel_paths {
308 let full_path = base_dir.join(rel_path);
309 match compute_file_hash(&full_path) {
310 Ok(hash) => {
311 manifest.files.insert(rel_path.clone(), hash);
312 }
313 Err(e) => {
314 debug!("Skipping {} in manifest: {}", rel_path, e);
315 }
316 }
317 }
318
319 manifest
320}
321
322#[cfg(test)]
323mod tests {
324 use super::*;
325
326 use tempfile::TempDir;
327 use tracing::info;
328
329 fn init_test_logging() {
330 let _ = tracing_subscriber::fmt()
331 .with_test_writer()
332 .with_max_level(tracing::Level::DEBUG)
333 .try_init();
334 }
335
336 #[test]
337 fn test_short_hash_tolerates_short_and_non_ascii() {
338 assert_eq!(short_hash(""), "");
343 assert_eq!(short_hash("ab"), "ab");
344 assert_eq!(
345 short_hash("0123456789abcdef0123456789abcdef"),
346 "0123456789abcdef"
347 );
348 let multi = "é".repeat(20);
351 assert_eq!(short_hash(&multi).chars().count(), 16);
352 }
353
354 #[test]
355 fn test_format_failures_tolerates_short_hashes() {
356 let result = VerificationResult {
357 passed: vec![],
358 failed: vec![VerificationFailure::new("foo", "ab", "cd", 1, 1)],
359 skipped: vec![],
360 };
361 let msg = result.format_failures();
363 assert!(msg.contains("foo"));
364 assert!(msg.contains("ab"));
365 assert!(msg.contains("cd"));
366 }
367
368 #[test]
369 fn test_compute_file_hash_basic() {
370 init_test_logging();
371 info!("TEST START: test_compute_file_hash_basic");
372
373 let temp_dir = TempDir::new().unwrap();
374 let test_file = temp_dir.path().join("test.txt");
375
376 let content = b"Hello, World!";
378 std::fs::write(&test_file, content).unwrap();
379
380 let hash = compute_file_hash(&test_file).unwrap();
381
382 assert_eq!(hash.size, 13);
383 assert_eq!(hash.hash.len(), 64); info!("Hash: {}", hash.hash);
385
386 let hash2 = compute_file_hash(&test_file).unwrap();
388 assert_eq!(hash.hash, hash2.hash);
389
390 info!("TEST PASS: test_compute_file_hash_basic");
391 }
392
393 #[test]
394 fn test_compute_file_hash_empty() {
395 init_test_logging();
396 info!("TEST START: test_compute_file_hash_empty");
397
398 let temp_dir = TempDir::new().unwrap();
399 let test_file = temp_dir.path().join("empty.txt");
400 std::fs::write(&test_file, b"").unwrap();
401
402 let hash = compute_file_hash(&test_file).unwrap();
403
404 assert_eq!(hash.size, 0);
405 assert_eq!(hash.hash.len(), 64);
406 info!("Empty file hash: {}", hash.hash);
407
408 info!("TEST PASS: test_compute_file_hash_empty");
409 }
410
411 #[test]
412 fn test_compute_file_hash_nonexistent() {
413 init_test_logging();
414 info!("TEST START: test_compute_file_hash_nonexistent");
415
416 let result = compute_file_hash(Path::new("/nonexistent/file"));
417 assert!(result.is_err());
418
419 info!("TEST PASS: test_compute_file_hash_nonexistent");
420 }
421
422 #[test]
423 fn test_verify_artifacts_all_pass() {
424 init_test_logging();
425 info!("TEST START: test_verify_artifacts_all_pass");
426
427 let temp_dir = TempDir::new().unwrap();
428
429 std::fs::write(temp_dir.path().join("a.txt"), b"content a").unwrap();
431 std::fs::write(temp_dir.path().join("b.txt"), b"content b").unwrap();
432
433 let manifest = create_manifest(
435 temp_dir.path(),
436 &["a.txt".to_string(), "b.txt".to_string()],
437 Some("worker1".to_string()),
438 );
439
440 let result = verify_artifacts(temp_dir.path(), &manifest, 1024 * 1024);
442
443 assert!(result.all_passed());
444 assert_eq!(result.passed.len(), 2);
445 assert!(result.failed.is_empty());
446
447 info!("TEST PASS: test_verify_artifacts_all_pass");
448 }
449
450 #[test]
451 fn test_verify_artifacts_with_mismatch() {
452 init_test_logging();
453 info!("TEST START: test_verify_artifacts_with_mismatch");
454
455 let temp_dir = TempDir::new().unwrap();
456
457 std::fs::write(temp_dir.path().join("test.txt"), b"original").unwrap();
459
460 let manifest = create_manifest(temp_dir.path(), &["test.txt".to_string()], None);
462
463 std::fs::write(temp_dir.path().join("test.txt"), b"modified").unwrap();
465
466 let result = verify_artifacts(temp_dir.path(), &manifest, 1024 * 1024);
468
469 assert!(!result.all_passed());
470 assert_eq!(result.failed.len(), 1);
471 assert_eq!(result.failed[0].path, "test.txt");
472
473 info!("Failure details:\n{}", result.format_failures());
474
475 info!("TEST PASS: test_verify_artifacts_with_mismatch");
476 }
477
478 #[test]
479 fn test_verify_artifacts_skip_large() {
480 init_test_logging();
481 info!("TEST START: test_verify_artifacts_skip_large");
482
483 let temp_dir = TempDir::new().unwrap();
484
485 std::fs::write(temp_dir.path().join("large.txt"), b"some content here").unwrap();
487
488 let manifest = create_manifest(temp_dir.path(), &["large.txt".to_string()], None);
489
490 let result = verify_artifacts(temp_dir.path(), &manifest, 5);
492
493 assert!(result.all_passed()); assert_eq!(result.skipped.len(), 1);
495
496 info!("TEST PASS: test_verify_artifacts_skip_large");
497 }
498
499 #[test]
500 fn test_verify_artifacts_missing_file() {
501 init_test_logging();
502 info!("TEST START: test_verify_artifacts_missing_file");
503
504 let temp_dir = TempDir::new().unwrap();
505
506 let mut manifest = ArtifactManifest::default();
508 manifest.files.insert(
509 "missing.txt".to_string(),
510 FileHash {
511 hash: "abcd1234".repeat(8),
512 size: 100,
513 },
514 );
515
516 let result = verify_artifacts(temp_dir.path(), &manifest, 1024 * 1024);
517
518 assert!(result.all_passed()); assert_eq!(result.skipped.len(), 1);
520
521 info!("TEST PASS: test_verify_artifacts_missing_file");
522 }
523
524 #[test]
525 fn test_verification_result_summary() {
526 init_test_logging();
527 info!("TEST START: test_verification_result_summary");
528
529 let result = VerificationResult {
530 passed: vec!["a.txt".to_string(), "b.txt".to_string()],
531 failed: vec![VerificationFailure::new("c.txt", "abc", "def", 100, 200)],
532 skipped: vec!["d.txt".to_string()],
533 };
534
535 let summary = result.summary();
536 assert!(summary.contains("2 passed"));
537 assert!(summary.contains("1 failed"));
538 assert!(summary.contains("1 skipped"));
539
540 info!("Summary: {}", summary);
541 info!("TEST PASS: test_verification_result_summary");
542 }
543
544 #[test]
545 fn test_create_manifest() {
546 init_test_logging();
547 info!("TEST START: test_create_manifest");
548
549 let temp_dir = TempDir::new().unwrap();
550
551 std::fs::write(temp_dir.path().join("file1.txt"), b"content1").unwrap();
552 std::fs::write(temp_dir.path().join("file2.txt"), b"content2").unwrap();
553
554 let manifest = create_manifest(
555 temp_dir.path(),
556 &[
557 "file1.txt".to_string(),
558 "file2.txt".to_string(),
559 "missing.txt".to_string(),
560 ],
561 Some("test-worker".to_string()),
562 );
563
564 assert_eq!(manifest.files.len(), 2);
566 assert!(manifest.files.contains_key("file1.txt"));
567 assert!(manifest.files.contains_key("file2.txt"));
568 assert!(!manifest.files.contains_key("missing.txt"));
569 assert_eq!(manifest.worker_id, Some("test-worker".to_string()));
570 assert!(manifest.created_at > 0);
571
572 info!("TEST PASS: test_create_manifest");
573 }
574
575 #[test]
576 fn test_file_hash_equality() {
577 init_test_logging();
578 info!("TEST START: test_file_hash_equality");
579
580 let hash1 = FileHash {
581 hash: "abc123".to_string(),
582 size: 100,
583 };
584 let hash2 = FileHash {
585 hash: "abc123".to_string(),
586 size: 100,
587 };
588 let hash3 = FileHash {
589 hash: "def456".to_string(),
590 size: 100,
591 };
592
593 assert_eq!(hash1, hash2);
594 assert_ne!(hash1, hash3);
595
596 info!("TEST PASS: test_file_hash_equality");
597 }
598
599 #[test]
600 fn test_file_hash_serialization() {
601 init_test_logging();
602 info!("TEST START: test_file_hash_serialization");
603
604 let hash = FileHash {
605 hash: "0".repeat(64),
606 size: 1024,
607 };
608
609 let json = serde_json::to_string(&hash).unwrap();
610 let deserialized: FileHash = serde_json::from_str(&json).unwrap();
611
612 assert_eq!(hash, deserialized);
613 assert!(json.contains("\"hash\""));
614 assert!(json.contains("\"size\""));
615
616 info!("TEST PASS: test_file_hash_serialization");
617 }
618
619 #[test]
620 fn test_file_hash_clone() {
621 init_test_logging();
622 info!("TEST START: test_file_hash_clone");
623
624 let original = FileHash {
625 hash: "test_hash".to_string(),
626 size: 500,
627 };
628 let cloned = original.clone();
629
630 assert_eq!(original.hash, cloned.hash);
631 assert_eq!(original.size, cloned.size);
632
633 info!("TEST PASS: test_file_hash_clone");
634 }
635
636 #[test]
637 fn test_artifact_manifest_default() {
638 init_test_logging();
639 info!("TEST START: test_artifact_manifest_default");
640
641 let manifest = ArtifactManifest::default();
642
643 assert!(manifest.files.is_empty());
644 assert_eq!(manifest.created_at, 0);
645 assert!(manifest.worker_id.is_none());
646
647 info!("TEST PASS: test_artifact_manifest_default");
648 }
649
650 #[test]
651 fn test_artifact_manifest_serialization() {
652 init_test_logging();
653 info!("TEST START: test_artifact_manifest_serialization");
654
655 let mut manifest = ArtifactManifest {
656 created_at: 1706380800,
657 worker_id: Some("worker-1".to_string()),
658 ..ArtifactManifest::default()
659 };
660 manifest.files.insert(
661 "test.bin".to_string(),
662 FileHash {
663 hash: "a".repeat(64),
664 size: 256,
665 },
666 );
667
668 let json = serde_json::to_string(&manifest).unwrap();
669 let deserialized: ArtifactManifest = serde_json::from_str(&json).unwrap();
670
671 assert_eq!(manifest.created_at, deserialized.created_at);
672 assert_eq!(manifest.worker_id, deserialized.worker_id);
673 assert_eq!(manifest.files.len(), deserialized.files.len());
674
675 info!("TEST PASS: test_artifact_manifest_serialization");
676 }
677
678 #[test]
679 fn test_artifact_manifest_without_worker_id() {
680 init_test_logging();
681 info!("TEST START: test_artifact_manifest_without_worker_id");
682
683 let manifest = ArtifactManifest {
684 files: HashMap::new(),
685 created_at: 0,
686 worker_id: None,
687 };
688
689 let json = serde_json::to_string(&manifest).unwrap();
690 assert!(!json.contains("worker_id"));
692
693 info!("TEST PASS: test_artifact_manifest_without_worker_id");
694 }
695
696 #[test]
697 fn test_verification_result_all_passed_empty() {
698 init_test_logging();
699 info!("TEST START: test_verification_result_all_passed_empty");
700
701 let result = VerificationResult {
702 passed: vec![],
703 failed: vec![],
704 skipped: vec![],
705 };
706
707 assert!(result.all_passed());
708 assert_eq!(result.summary(), "0 passed, 0 failed, 0 skipped");
709
710 info!("TEST PASS: test_verification_result_all_passed_empty");
711 }
712
713 #[test]
714 fn test_verification_result_format_failures_empty() {
715 init_test_logging();
716 info!("TEST START: test_verification_result_format_failures_empty");
717
718 let result = VerificationResult {
719 passed: vec!["a.txt".to_string()],
720 failed: vec![],
721 skipped: vec![],
722 };
723
724 let failures = result.format_failures();
725 assert!(failures.is_empty());
726
727 info!("TEST PASS: test_verification_result_format_failures_empty");
728 }
729
730 #[test]
731 fn test_verification_result_format_failures_content() {
732 init_test_logging();
733 info!("TEST START: test_verification_result_format_failures_content");
734
735 let result = VerificationResult {
736 passed: vec![],
737 failed: vec![VerificationFailure::new(
738 "binary.exe",
739 "a".repeat(64),
740 "b".repeat(64),
741 1000,
742 1001,
743 )],
744 skipped: vec![],
745 };
746
747 let failures = result.format_failures();
748 assert!(failures.contains("binary.exe"));
749 assert!(failures.contains("HASH MISMATCH"));
750 assert!(failures.contains("Expected:"));
751 assert!(failures.contains("Actual:"));
752 assert!(failures.contains("1000 bytes"));
753 assert!(failures.contains("1001 bytes"));
754 assert!(failures.contains("Suggested actions"));
755 assert!(failures.contains("rch diagnose"));
756
757 info!("TEST PASS: test_verification_result_format_failures_content");
758 }
759
760 #[test]
761 fn test_verification_failure_new() {
762 init_test_logging();
763 info!("TEST START: test_verification_failure_new");
764
765 let failure = VerificationFailure::new(
766 "path/to/file.bin",
767 "expected_hash_value",
768 "actual_hash_value",
769 500,
770 600,
771 );
772
773 assert_eq!(failure.path, "path/to/file.bin");
774 assert_eq!(failure.expected_hash, "expected_hash_value");
775 assert_eq!(failure.actual_hash, "actual_hash_value");
776 assert_eq!(failure.expected_size, 500);
777 assert_eq!(failure.actual_size, 600);
778
779 info!("TEST PASS: test_verification_failure_new");
780 }
781
782 #[test]
783 fn test_verification_failure_from_string() {
784 init_test_logging();
785 info!("TEST START: test_verification_failure_from_string");
786
787 let failure = VerificationFailure::new(
789 String::from("owned_path"),
790 String::from("owned_expected"),
791 String::from("owned_actual"),
792 100,
793 200,
794 );
795
796 assert_eq!(failure.path, "owned_path");
797 assert_eq!(failure.expected_hash, "owned_expected");
798 assert_eq!(failure.actual_hash, "owned_actual");
799
800 info!("TEST PASS: test_verification_failure_from_string");
801 }
802
803 #[test]
804 fn test_verification_failure_serialization() {
805 init_test_logging();
806 info!("TEST START: test_verification_failure_serialization");
807
808 let failure = VerificationFailure::new("test.bin", "hash1", "hash2", 100, 200);
809
810 let json = serde_json::to_string(&failure).unwrap();
811 let deserialized: VerificationFailure = serde_json::from_str(&json).unwrap();
812
813 assert_eq!(failure.path, deserialized.path);
814 assert_eq!(failure.expected_hash, deserialized.expected_hash);
815 assert_eq!(failure.actual_hash, deserialized.actual_hash);
816 assert_eq!(failure.expected_size, deserialized.expected_size);
817 assert_eq!(failure.actual_size, deserialized.actual_size);
818
819 info!("TEST PASS: test_verification_failure_serialization");
820 }
821
822 #[test]
823 fn test_verification_result_serialization() {
824 init_test_logging();
825 info!("TEST START: test_verification_result_serialization");
826
827 let result = VerificationResult {
828 passed: vec!["a.txt".to_string()],
829 failed: vec![VerificationFailure::new("b.txt", "h1", "h2", 10, 20)],
830 skipped: vec!["c.txt".to_string()],
831 };
832
833 let json = serde_json::to_string(&result).unwrap();
834 let deserialized: VerificationResult = serde_json::from_str(&json).unwrap();
835
836 assert_eq!(result.passed, deserialized.passed);
837 assert_eq!(result.failed.len(), deserialized.failed.len());
838 assert_eq!(result.skipped, deserialized.skipped);
839
840 info!("TEST PASS: test_verification_result_serialization");
841 }
842
843 #[test]
844 fn test_compute_file_hash_deterministic() {
845 init_test_logging();
846 info!("TEST START: test_compute_file_hash_deterministic");
847
848 let temp_dir = TempDir::new().unwrap();
849 let file1 = temp_dir.path().join("file1.txt");
850 let file2 = temp_dir.path().join("file2.txt");
851
852 let content = b"Identical content for hashing test";
854 std::fs::write(&file1, content).unwrap();
855 std::fs::write(&file2, content).unwrap();
856
857 let hash1 = compute_file_hash(&file1).unwrap();
858 let hash2 = compute_file_hash(&file2).unwrap();
859
860 assert_eq!(hash1.hash, hash2.hash);
861 assert_eq!(hash1.size, hash2.size);
862
863 info!("TEST PASS: test_compute_file_hash_deterministic");
864 }
865
866 #[test]
867 fn test_compute_file_hash_different_content() {
868 init_test_logging();
869 info!("TEST START: test_compute_file_hash_different_content");
870
871 let temp_dir = TempDir::new().unwrap();
872 let file1 = temp_dir.path().join("file1.txt");
873 let file2 = temp_dir.path().join("file2.txt");
874
875 std::fs::write(&file1, b"content one").unwrap();
876 std::fs::write(&file2, b"content two").unwrap();
877
878 let hash1 = compute_file_hash(&file1).unwrap();
879 let hash2 = compute_file_hash(&file2).unwrap();
880
881 assert_ne!(hash1.hash, hash2.hash);
882
883 info!("TEST PASS: test_compute_file_hash_different_content");
884 }
885
886 #[test]
887 fn test_verification_with_size_mismatch() {
888 init_test_logging();
889 info!("TEST START: test_verification_with_size_mismatch");
890
891 let temp_dir = TempDir::new().unwrap();
892 std::fs::write(temp_dir.path().join("test.txt"), b"content").unwrap();
893
894 let hash = compute_file_hash(&temp_dir.path().join("test.txt")).unwrap();
896 let mut manifest = ArtifactManifest::default();
897 manifest.files.insert(
898 "test.txt".to_string(),
899 FileHash {
900 hash: hash.hash, size: 9999, },
903 );
904
905 let result = verify_artifacts(temp_dir.path(), &manifest, 1024 * 1024);
906
907 assert!(!result.all_passed());
909 assert_eq!(result.failed.len(), 1);
910
911 info!("TEST PASS: test_verification_with_size_mismatch");
912 }
913
914 #[test]
915 fn test_create_manifest_empty_list() {
916 init_test_logging();
917 info!("TEST START: test_create_manifest_empty_list");
918
919 let temp_dir = TempDir::new().unwrap();
920 let manifest = create_manifest(temp_dir.path(), &[], None);
921
922 assert!(manifest.files.is_empty());
923 assert!(manifest.worker_id.is_none());
924
925 info!("TEST PASS: test_create_manifest_empty_list");
926 }
927
928 #[test]
929 fn test_verification_clone_traits() {
930 init_test_logging();
931 info!("TEST START: test_verification_clone_traits");
932
933 let result = VerificationResult {
934 passed: vec!["a.txt".to_string()],
935 failed: vec![],
936 skipped: vec![],
937 };
938
939 let cloned = result.clone();
940 assert_eq!(result.passed, cloned.passed);
941 assert_eq!(result.failed.len(), cloned.failed.len());
942 assert_eq!(result.skipped, cloned.skipped);
943
944 info!("TEST PASS: test_verification_clone_traits");
945 }
946
947 #[test]
948 fn test_verification_failure_clone() {
949 init_test_logging();
950 info!("TEST START: test_verification_failure_clone");
951
952 let failure = VerificationFailure::new("path", "h1", "h2", 10, 20);
953 let cloned = failure.clone();
954
955 assert_eq!(failure.path, cloned.path);
956 assert_eq!(failure.expected_hash, cloned.expected_hash);
957 assert_eq!(failure.actual_hash, cloned.actual_hash);
958
959 info!("TEST PASS: test_verification_failure_clone");
960 }
961
962 #[test]
963 fn test_artifact_manifest_clone() {
964 init_test_logging();
965 info!("TEST START: test_artifact_manifest_clone");
966
967 let mut manifest = ArtifactManifest {
968 created_at: 12345,
969 worker_id: Some("worker".to_string()),
970 ..ArtifactManifest::default()
971 };
972 manifest.files.insert(
973 "f.txt".to_string(),
974 FileHash {
975 hash: "h".to_string(),
976 size: 1,
977 },
978 );
979
980 let cloned = manifest.clone();
981 assert_eq!(manifest.created_at, cloned.created_at);
982 assert_eq!(manifest.worker_id, cloned.worker_id);
983 assert_eq!(manifest.files.len(), cloned.files.len());
984
985 info!("TEST PASS: test_artifact_manifest_clone");
986 }
987
988 #[test]
989 fn test_file_hash_debug() {
990 init_test_logging();
991 info!("TEST START: test_file_hash_debug");
992
993 let hash = FileHash {
994 hash: "abc".to_string(),
995 size: 100,
996 };
997
998 let debug = format!("{:?}", hash);
999 assert!(debug.contains("FileHash"));
1000 assert!(debug.contains("abc"));
1001 assert!(debug.contains("100"));
1002
1003 info!("TEST PASS: test_file_hash_debug");
1004 }
1005
1006 #[test]
1007 fn test_verification_result_debug() {
1008 init_test_logging();
1009 info!("TEST START: test_verification_result_debug");
1010
1011 let result = VerificationResult {
1012 passed: vec!["a.txt".to_string()],
1013 failed: vec![],
1014 skipped: vec![],
1015 };
1016
1017 let debug = format!("{:?}", result);
1018 assert!(debug.contains("VerificationResult"));
1019 assert!(debug.contains("a.txt"));
1020
1021 info!("TEST PASS: test_verification_result_debug");
1022 }
1023
1024 #[test]
1025 fn test_verify_artifacts_rejects_unsafe_paths() {
1026 init_test_logging();
1027 info!("TEST START: test_verify_artifacts_rejects_unsafe_paths");
1028
1029 let temp_dir = TempDir::new().unwrap();
1030 let mut manifest = create_manifest(temp_dir.path(), &[], None);
1032
1033 manifest.files.insert(
1035 "../outside.txt".to_string(),
1036 FileHash {
1037 hash: "abc".to_string(),
1038 size: 100,
1039 },
1040 );
1041 manifest.files.insert(
1042 "/etc/passwd".to_string(),
1043 FileHash {
1044 hash: "abc".to_string(),
1045 size: 100,
1046 },
1047 );
1048 manifest.files.insert(
1050 "safe.txt".to_string(),
1051 FileHash {
1052 hash: "abc".to_string(),
1053 size: 100,
1054 },
1055 );
1056 let result = verify_artifacts(temp_dir.path(), &manifest, 1024);
1061
1062 assert!(result.skipped.contains(&"../outside.txt".to_string()));
1064 assert!(result.skipped.contains(&"/etc/passwd".to_string()));
1065
1066 info!("TEST PASS: test_verify_artifacts_rejects_unsafe_paths");
1078 }
1079}