1use std::collections::HashMap;
2use std::path::Path;
3
4use crate::manifest::schema::PatchFileInfo;
5use crate::patch::file_hash::compute_file_git_sha256;
6
7#[derive(Debug, Clone, PartialEq, Eq)]
9pub enum VerifyRollbackStatus {
10 Ready,
12 AlreadyOriginal,
14 HashMismatch,
16 NotFound,
18 MissingBlob,
20}
21
22#[derive(Debug, Clone)]
24pub struct VerifyRollbackResult {
25 pub file: String,
26 pub status: VerifyRollbackStatus,
27 pub message: Option<String>,
28 pub current_hash: Option<String>,
29 pub expected_hash: Option<String>,
30 pub target_hash: Option<String>,
31}
32
33#[derive(Debug, Clone)]
35pub struct RollbackResult {
36 pub package_key: String,
37 pub package_path: String,
38 pub success: bool,
39 pub files_verified: Vec<VerifyRollbackResult>,
40 pub files_rolled_back: Vec<String>,
41 pub error: Option<String>,
42}
43
44fn normalize_file_path(file_name: &str) -> &str {
46 const PACKAGE_PREFIX: &str = "package/";
47 if let Some(stripped) = file_name.strip_prefix(PACKAGE_PREFIX) {
48 stripped
49 } else {
50 file_name
51 }
52}
53
54pub async fn verify_file_rollback(
66 pkg_path: &Path,
67 file_name: &str,
68 file_info: &PatchFileInfo,
69 blobs_path: &Path,
70) -> VerifyRollbackResult {
71 let normalized = normalize_file_path(file_name);
72 let filepath = pkg_path.join(normalized);
73
74 let is_new_file = file_info.before_hash.is_empty();
75
76 if is_new_file {
78 if tokio::fs::metadata(&filepath).await.is_err() {
79 return VerifyRollbackResult {
81 file: file_name.to_string(),
82 status: VerifyRollbackStatus::AlreadyOriginal,
83 message: None,
84 current_hash: None,
85 expected_hash: None,
86 target_hash: None,
87 };
88 }
89 let current_hash = compute_file_git_sha256(&filepath).await.unwrap_or_default();
90 if current_hash == file_info.after_hash {
91 return VerifyRollbackResult {
92 file: file_name.to_string(),
93 status: VerifyRollbackStatus::Ready,
94 message: None,
95 current_hash: Some(current_hash),
96 expected_hash: None,
97 target_hash: None,
98 };
99 }
100 return VerifyRollbackResult {
101 file: file_name.to_string(),
102 status: VerifyRollbackStatus::HashMismatch,
103 message: Some(
104 "File has been modified after patching. Cannot safely rollback.".to_string(),
105 ),
106 current_hash: Some(current_hash),
107 expected_hash: Some(file_info.after_hash.clone()),
108 target_hash: None,
109 };
110 }
111
112 if tokio::fs::metadata(&filepath).await.is_err() {
114 return VerifyRollbackResult {
115 file: file_name.to_string(),
116 status: VerifyRollbackStatus::NotFound,
117 message: Some("File not found".to_string()),
118 current_hash: None,
119 expected_hash: None,
120 target_hash: None,
121 };
122 }
123
124 let current_hash = match compute_file_git_sha256(&filepath).await {
126 Ok(h) => h,
127 Err(e) => {
128 return VerifyRollbackResult {
129 file: file_name.to_string(),
130 status: VerifyRollbackStatus::NotFound,
131 message: Some(format!("Failed to hash file: {}", e)),
132 current_hash: None,
133 expected_hash: None,
134 target_hash: None,
135 };
136 }
137 };
138
139 if current_hash == file_info.before_hash {
145 return VerifyRollbackResult {
146 file: file_name.to_string(),
147 status: VerifyRollbackStatus::AlreadyOriginal,
148 message: None,
149 current_hash: Some(current_hash),
150 expected_hash: None,
151 target_hash: None,
152 };
153 }
154
155 let before_blob_path = blobs_path.join(&file_info.before_hash);
157 if tokio::fs::metadata(&before_blob_path).await.is_err() {
158 return VerifyRollbackResult {
159 file: file_name.to_string(),
160 status: VerifyRollbackStatus::MissingBlob,
161 message: Some(format!(
162 "Before blob not found: {}. Re-download the patch to enable rollback.",
163 file_info.before_hash
164 )),
165 current_hash: Some(current_hash),
166 expected_hash: None,
167 target_hash: Some(file_info.before_hash.clone()),
168 };
169 }
170
171 if current_hash != file_info.after_hash {
173 return VerifyRollbackResult {
174 file: file_name.to_string(),
175 status: VerifyRollbackStatus::HashMismatch,
176 message: Some(
177 "File has been modified after patching. Cannot safely rollback.".to_string(),
178 ),
179 current_hash: Some(current_hash),
180 expected_hash: Some(file_info.after_hash.clone()),
181 target_hash: Some(file_info.before_hash.clone()),
182 };
183 }
184
185 VerifyRollbackResult {
186 file: file_name.to_string(),
187 status: VerifyRollbackStatus::Ready,
188 message: None,
189 current_hash: Some(current_hash),
190 expected_hash: None,
191 target_hash: Some(file_info.before_hash.clone()),
192 }
193}
194
195pub async fn rollback_file_patch(
226 pkg_path: &Path,
227 file_name: &str,
228 original_content: &[u8],
229 expected_hash: &str,
230) -> Result<(), std::io::Error> {
231 crate::patch::apply::apply_file_patch(pkg_path, file_name, original_content, expected_hash)
232 .await
233}
234
235pub async fn rollback_package_patch(
242 package_key: &str,
243 pkg_path: &Path,
244 files: &HashMap<String, PatchFileInfo>,
245 blobs_path: &Path,
246 dry_run: bool,
247) -> RollbackResult {
248 let mut result = RollbackResult {
249 package_key: package_key.to_string(),
250 package_path: pkg_path.display().to_string(),
251 success: false,
252 files_verified: Vec::new(),
253 files_rolled_back: Vec::new(),
254 error: None,
255 };
256
257 for (file_name, file_info) in files {
259 let verify_result = verify_file_rollback(pkg_path, file_name, file_info, blobs_path).await;
260
261 if verify_result.status != VerifyRollbackStatus::Ready
263 && verify_result.status != VerifyRollbackStatus::AlreadyOriginal
264 {
265 let msg = verify_result
266 .message
267 .clone()
268 .unwrap_or_else(|| format!("{:?}", verify_result.status));
269 result.error = Some(format!("Cannot rollback: {} - {}", verify_result.file, msg));
270 result.files_verified.push(verify_result);
271 return result;
272 }
273
274 result.files_verified.push(verify_result);
275 }
276
277 let all_original = result
279 .files_verified
280 .iter()
281 .all(|v| v.status == VerifyRollbackStatus::AlreadyOriginal);
282 if all_original {
283 result.success = true;
284 return result;
285 }
286
287 if dry_run {
289 result.success = true;
290 return result;
291 }
292
293 for (file_name, file_info) in files {
295 let verify_result = result.files_verified.iter().find(|v| v.file == *file_name);
296 if let Some(vr) = verify_result {
297 if vr.status == VerifyRollbackStatus::AlreadyOriginal {
298 continue;
299 }
300 }
301
302 if file_info.before_hash.is_empty() {
304 let normalized = normalize_file_path(file_name);
305 let filepath = pkg_path.join(normalized);
306 let dir_guard = crate::patch::apply::DirWriteGuard::acquire(filepath.parent()).await;
313 let remove_result = tokio::fs::remove_file(&filepath).await;
314 dir_guard.restore().await;
315 if let Err(e) = remove_result {
316 result.error = Some(format!("Failed to delete {}: {}", file_name, e));
317 return result;
318 }
319 result.files_rolled_back.push(file_name.clone());
320 continue;
321 }
322
323 let blob_path = blobs_path.join(&file_info.before_hash);
325 let original_content = match tokio::fs::read(&blob_path).await {
326 Ok(content) => content,
327 Err(e) => {
328 result.error = Some(format!(
329 "Failed to read blob {}: {}",
330 file_info.before_hash, e
331 ));
332 return result;
333 }
334 };
335
336 if let Err(e) = rollback_file_patch(
338 pkg_path,
339 file_name,
340 &original_content,
341 &file_info.before_hash,
342 )
343 .await
344 {
345 result.error = Some(e.to_string());
346 return result;
347 }
348
349 result.files_rolled_back.push(file_name.clone());
350 }
351
352 result.success = true;
353 result
354}
355
356#[cfg(test)]
357mod tests {
358 use super::*;
359 use crate::hash::git_sha256::compute_git_sha256_from_bytes;
360
361 #[tokio::test]
362 async fn test_verify_file_rollback_not_found() {
363 let pkg_dir = tempfile::tempdir().unwrap();
364 let blobs_dir = tempfile::tempdir().unwrap();
365
366 let file_info = PatchFileInfo {
367 before_hash: "aaa".to_string(),
368 after_hash: "bbb".to_string(),
369 };
370
371 let result = verify_file_rollback(
372 pkg_dir.path(),
373 "nonexistent.js",
374 &file_info,
375 blobs_dir.path(),
376 )
377 .await;
378 assert_eq!(result.status, VerifyRollbackStatus::NotFound);
379 }
380
381 #[tokio::test]
382 async fn test_verify_file_rollback_missing_blob() {
383 let pkg_dir = tempfile::tempdir().unwrap();
384 let blobs_dir = tempfile::tempdir().unwrap();
385
386 let content = b"patched content";
387 tokio::fs::write(pkg_dir.path().join("index.js"), content)
388 .await
389 .unwrap();
390
391 let file_info = PatchFileInfo {
392 before_hash: "missing_blob_hash".to_string(),
393 after_hash: compute_git_sha256_from_bytes(content),
394 };
395
396 let result =
397 verify_file_rollback(pkg_dir.path(), "index.js", &file_info, blobs_dir.path()).await;
398 assert_eq!(result.status, VerifyRollbackStatus::MissingBlob);
399 assert!(result.message.unwrap().contains("Before blob not found"));
400 }
401
402 #[tokio::test]
403 async fn test_verify_file_rollback_ready() {
404 let pkg_dir = tempfile::tempdir().unwrap();
405 let blobs_dir = tempfile::tempdir().unwrap();
406
407 let original = b"original content";
408 let patched = b"patched content";
409 let before_hash = compute_git_sha256_from_bytes(original);
410 let after_hash = compute_git_sha256_from_bytes(patched);
411
412 tokio::fs::write(pkg_dir.path().join("index.js"), patched)
414 .await
415 .unwrap();
416
417 tokio::fs::write(blobs_dir.path().join(&before_hash), original)
419 .await
420 .unwrap();
421
422 let file_info = PatchFileInfo {
423 before_hash: before_hash.clone(),
424 after_hash: after_hash.clone(),
425 };
426
427 let result =
428 verify_file_rollback(pkg_dir.path(), "index.js", &file_info, blobs_dir.path()).await;
429 assert_eq!(result.status, VerifyRollbackStatus::Ready);
430 assert_eq!(result.current_hash.unwrap(), after_hash);
431 }
432
433 #[tokio::test]
434 async fn test_verify_file_rollback_already_original() {
435 let pkg_dir = tempfile::tempdir().unwrap();
436 let blobs_dir = tempfile::tempdir().unwrap();
437
438 let original = b"original content";
439 let before_hash = compute_git_sha256_from_bytes(original);
440
441 tokio::fs::write(pkg_dir.path().join("index.js"), original)
443 .await
444 .unwrap();
445
446 tokio::fs::write(blobs_dir.path().join(&before_hash), original)
448 .await
449 .unwrap();
450
451 let file_info = PatchFileInfo {
452 before_hash: before_hash.clone(),
453 after_hash: "bbbb".to_string(),
454 };
455
456 let result =
457 verify_file_rollback(pkg_dir.path(), "index.js", &file_info, blobs_dir.path()).await;
458 assert_eq!(result.status, VerifyRollbackStatus::AlreadyOriginal);
459 }
460
461 #[tokio::test]
462 async fn test_verify_file_rollback_hash_mismatch() {
463 let pkg_dir = tempfile::tempdir().unwrap();
464 let blobs_dir = tempfile::tempdir().unwrap();
465
466 let original = b"original content";
467 let before_hash = compute_git_sha256_from_bytes(original);
468
469 tokio::fs::write(pkg_dir.path().join("index.js"), b"something unexpected")
471 .await
472 .unwrap();
473
474 tokio::fs::write(blobs_dir.path().join(&before_hash), original)
476 .await
477 .unwrap();
478
479 let file_info = PatchFileInfo {
480 before_hash,
481 after_hash: "expected_after_hash".to_string(),
482 };
483
484 let result =
485 verify_file_rollback(pkg_dir.path(), "index.js", &file_info, blobs_dir.path()).await;
486 assert_eq!(result.status, VerifyRollbackStatus::HashMismatch);
487 assert!(result.message.unwrap().contains("modified after patching"));
488 }
489
490 #[tokio::test]
491 async fn test_rollback_file_patch_success() {
492 let dir = tempfile::tempdir().unwrap();
493 let original = b"original content";
494 let original_hash = compute_git_sha256_from_bytes(original);
495
496 tokio::fs::write(dir.path().join("index.js"), b"patched")
498 .await
499 .unwrap();
500
501 rollback_file_patch(dir.path(), "index.js", original, &original_hash)
502 .await
503 .unwrap();
504
505 let written = tokio::fs::read(dir.path().join("index.js")).await.unwrap();
506 assert_eq!(written, original);
507 }
508
509 #[tokio::test]
510 async fn test_rollback_file_patch_hash_mismatch() {
511 let dir = tempfile::tempdir().unwrap();
512 tokio::fs::write(dir.path().join("index.js"), b"patched")
513 .await
514 .unwrap();
515
516 let result =
517 rollback_file_patch(dir.path(), "index.js", b"original content", "wrong_hash").await;
518 assert!(result.is_err());
519 assert!(result
520 .unwrap_err()
521 .to_string()
522 .contains("Hash verification failed"));
523 }
524
525 #[tokio::test]
532 async fn test_rollback_file_patch_hash_mismatch_leaves_file_intact() {
533 let dir = tempfile::tempdir().unwrap();
534 let path = dir.path().join("index.js");
535 tokio::fs::write(&path, b"patched bytes on disk")
536 .await
537 .unwrap();
538
539 let result =
540 rollback_file_patch(dir.path(), "index.js", b"original content", "wrong_hash").await;
541 assert!(result.is_err());
542
543 assert_eq!(
545 tokio::fs::read(&path).await.unwrap(),
546 b"patched bytes on disk"
547 );
548
549 let mut entries = tokio::fs::read_dir(dir.path()).await.unwrap();
551 while let Some(entry) = entries.next_entry().await.unwrap() {
552 let name = entry.file_name().to_string_lossy().to_string();
553 assert!(
554 !name.starts_with(".socket-stage-") && !name.starts_with(".socket-cow-"),
555 "stage/cow litter leaked: {name}"
556 );
557 }
558 }
559
560 #[cfg(unix)]
566 #[tokio::test]
567 async fn test_rollback_file_patch_does_not_propagate_to_hardlinked_sibling() {
568 let dir = tempfile::tempdir().unwrap();
569 let project = dir.path().join("project").join("foo.js");
570 let sibling = dir.path().join("sibling.js");
571 tokio::fs::create_dir_all(project.parent().unwrap())
572 .await
573 .unwrap();
574
575 tokio::fs::write(&sibling, b"patched bytes").await.unwrap();
577 tokio::fs::hard_link(&sibling, &project).await.unwrap();
578
579 let original = b"original bytes";
580 let original_hash = compute_git_sha256_from_bytes(original);
581 rollback_file_patch(
582 project.parent().unwrap(),
583 "foo.js",
584 original,
585 &original_hash,
586 )
587 .await
588 .unwrap();
589
590 assert_eq!(tokio::fs::read(&project).await.unwrap(), original);
592 assert_eq!(tokio::fs::read(&sibling).await.unwrap(), b"patched bytes");
594 }
595
596 #[cfg(unix)]
602 #[tokio::test]
603 async fn test_rollback_file_patch_preserves_readonly_mode() {
604 use std::os::unix::fs::PermissionsExt;
605
606 let dir = tempfile::tempdir().unwrap();
607 let path = dir.path().join("index.js");
608 let original = b"original content";
609 let original_hash = compute_git_sha256_from_bytes(original);
610
611 tokio::fs::write(&path, b"patched content").await.unwrap();
612 tokio::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o444))
614 .await
615 .unwrap();
616
617 rollback_file_patch(dir.path(), "index.js", original, &original_hash)
618 .await
619 .unwrap();
620
621 assert_eq!(tokio::fs::read(&path).await.unwrap(), original);
622 let mode = tokio::fs::metadata(&path)
623 .await
624 .unwrap()
625 .permissions()
626 .mode()
627 & 0o7777;
628 assert_eq!(
629 mode, 0o444,
630 "rollback must restore the read-only mode, not leave the file writable"
631 );
632 }
633
634 #[cfg(unix)]
640 #[tokio::test]
641 async fn test_rollback_package_patch_in_readonly_dir() {
642 use std::os::unix::fs::PermissionsExt;
643
644 let pkg_dir = tempfile::tempdir().unwrap();
645 let blobs_dir = tempfile::tempdir().unwrap();
646
647 let original = b"original content";
648 let patched = b"patched content";
649 let before_hash = compute_git_sha256_from_bytes(original);
650 let after_hash = compute_git_sha256_from_bytes(patched);
651
652 tokio::fs::write(pkg_dir.path().join("index.js"), patched)
653 .await
654 .unwrap();
655 tokio::fs::write(blobs_dir.path().join(&before_hash), original)
656 .await
657 .unwrap();
658 tokio::fs::set_permissions(
660 pkg_dir.path().join("index.js"),
661 std::fs::Permissions::from_mode(0o444),
662 )
663 .await
664 .unwrap();
665 tokio::fs::set_permissions(pkg_dir.path(), std::fs::Permissions::from_mode(0o555))
666 .await
667 .unwrap();
668
669 let mut files = HashMap::new();
670 files.insert(
671 "index.js".to_string(),
672 PatchFileInfo {
673 before_hash,
674 after_hash,
675 },
676 );
677
678 let result = rollback_package_patch(
679 "pkg:golang/example.com/x@1.0.0",
680 pkg_dir.path(),
681 &files,
682 blobs_dir.path(),
683 false,
684 )
685 .await;
686
687 assert!(result.success, "expected success: {:?}", result.error);
688 assert_eq!(result.files_rolled_back.len(), 1);
689 assert_eq!(
690 tokio::fs::read(pkg_dir.path().join("index.js"))
691 .await
692 .unwrap(),
693 original
694 );
695 assert_eq!(
697 tokio::fs::metadata(pkg_dir.path())
698 .await
699 .unwrap()
700 .permissions()
701 .mode()
702 & 0o7777,
703 0o555,
704 );
705
706 tokio::fs::set_permissions(pkg_dir.path(), std::fs::Permissions::from_mode(0o755))
708 .await
709 .unwrap();
710 }
711
712 #[tokio::test]
713 async fn test_rollback_package_patch_success() {
714 let pkg_dir = tempfile::tempdir().unwrap();
715 let blobs_dir = tempfile::tempdir().unwrap();
716
717 let original = b"original content";
718 let patched = b"patched content";
719 let before_hash = compute_git_sha256_from_bytes(original);
720 let after_hash = compute_git_sha256_from_bytes(patched);
721
722 tokio::fs::write(pkg_dir.path().join("index.js"), patched)
724 .await
725 .unwrap();
726
727 tokio::fs::write(blobs_dir.path().join(&before_hash), original)
729 .await
730 .unwrap();
731
732 let mut files = HashMap::new();
733 files.insert(
734 "index.js".to_string(),
735 PatchFileInfo {
736 before_hash: before_hash.clone(),
737 after_hash,
738 },
739 );
740
741 let result = rollback_package_patch(
742 "pkg:npm/test@1.0.0",
743 pkg_dir.path(),
744 &files,
745 blobs_dir.path(),
746 false,
747 )
748 .await;
749
750 assert!(result.success);
751 assert_eq!(result.files_rolled_back.len(), 1);
752 assert!(result.error.is_none());
753
754 let content = tokio::fs::read(pkg_dir.path().join("index.js"))
756 .await
757 .unwrap();
758 assert_eq!(content, original);
759 }
760
761 #[tokio::test]
762 async fn test_rollback_package_patch_dry_run() {
763 let pkg_dir = tempfile::tempdir().unwrap();
764 let blobs_dir = tempfile::tempdir().unwrap();
765
766 let original = b"original content";
767 let patched = b"patched content";
768 let before_hash = compute_git_sha256_from_bytes(original);
769 let after_hash = compute_git_sha256_from_bytes(patched);
770
771 tokio::fs::write(pkg_dir.path().join("index.js"), patched)
772 .await
773 .unwrap();
774 tokio::fs::write(blobs_dir.path().join(&before_hash), original)
775 .await
776 .unwrap();
777
778 let mut files = HashMap::new();
779 files.insert(
780 "index.js".to_string(),
781 PatchFileInfo {
782 before_hash,
783 after_hash,
784 },
785 );
786
787 let result = rollback_package_patch(
788 "pkg:npm/test@1.0.0",
789 pkg_dir.path(),
790 &files,
791 blobs_dir.path(),
792 true, )
794 .await;
795
796 assert!(result.success);
797 assert_eq!(result.files_rolled_back.len(), 0); let content = tokio::fs::read(pkg_dir.path().join("index.js"))
801 .await
802 .unwrap();
803 assert_eq!(content, patched);
804 }
805
806 #[tokio::test]
807 async fn test_rollback_package_patch_all_original() {
808 let pkg_dir = tempfile::tempdir().unwrap();
809 let blobs_dir = tempfile::tempdir().unwrap();
810
811 let original = b"original content";
812 let before_hash = compute_git_sha256_from_bytes(original);
813
814 tokio::fs::write(pkg_dir.path().join("index.js"), original)
816 .await
817 .unwrap();
818 tokio::fs::write(blobs_dir.path().join(&before_hash), original)
819 .await
820 .unwrap();
821
822 let mut files = HashMap::new();
823 files.insert(
824 "index.js".to_string(),
825 PatchFileInfo {
826 before_hash,
827 after_hash: "bbbb".to_string(),
828 },
829 );
830
831 let result = rollback_package_patch(
832 "pkg:npm/test@1.0.0",
833 pkg_dir.path(),
834 &files,
835 blobs_dir.path(),
836 false,
837 )
838 .await;
839
840 assert!(result.success);
841 assert_eq!(result.files_rolled_back.len(), 0);
842 }
843
844 #[tokio::test]
845 async fn test_rollback_package_patch_missing_blob_blocks() {
846 let pkg_dir = tempfile::tempdir().unwrap();
847 let blobs_dir = tempfile::tempdir().unwrap();
848
849 tokio::fs::write(pkg_dir.path().join("index.js"), b"patched content")
850 .await
851 .unwrap();
852
853 let mut files = HashMap::new();
854 files.insert(
855 "index.js".to_string(),
856 PatchFileInfo {
857 before_hash: "missing_hash".to_string(),
858 after_hash: "bbbb".to_string(),
859 },
860 );
861
862 let result = rollback_package_patch(
863 "pkg:npm/test@1.0.0",
864 pkg_dir.path(),
865 &files,
866 blobs_dir.path(),
867 false,
868 )
869 .await;
870
871 assert!(!result.success);
872 assert!(result.error.is_some());
873 }
874
875 #[tokio::test]
883 async fn test_verify_file_rollback_already_original_without_blob() {
884 let pkg_dir = tempfile::tempdir().unwrap();
885 let blobs_dir = tempfile::tempdir().unwrap();
886
887 let original = b"original content";
888 let before_hash = compute_git_sha256_from_bytes(original);
889
890 tokio::fs::write(pkg_dir.path().join("index.js"), original)
892 .await
893 .unwrap();
894
895 let file_info = PatchFileInfo {
896 before_hash,
897 after_hash: "some_after_hash".to_string(),
898 };
899
900 let result =
901 verify_file_rollback(pkg_dir.path(), "index.js", &file_info, blobs_dir.path()).await;
902 assert_eq!(result.status, VerifyRollbackStatus::AlreadyOriginal);
903 }
904
905 #[tokio::test]
911 async fn test_rollback_package_patch_already_original_missing_blob_does_not_block() {
912 let pkg_dir = tempfile::tempdir().unwrap();
913 let blobs_dir = tempfile::tempdir().unwrap();
914
915 let a_original = b"a original";
917 let a_before = compute_git_sha256_from_bytes(a_original);
918 tokio::fs::write(pkg_dir.path().join("a.js"), a_original)
919 .await
920 .unwrap();
921
922 let b_original = b"b original";
924 let b_patched = b"b patched";
925 let b_before = compute_git_sha256_from_bytes(b_original);
926 let b_after = compute_git_sha256_from_bytes(b_patched);
927 tokio::fs::write(pkg_dir.path().join("b.js"), b_patched)
928 .await
929 .unwrap();
930 tokio::fs::write(blobs_dir.path().join(&b_before), b_original)
931 .await
932 .unwrap();
933
934 let mut files = HashMap::new();
935 files.insert(
936 "a.js".to_string(),
937 PatchFileInfo {
938 before_hash: a_before,
939 after_hash: "a_after".to_string(),
940 },
941 );
942 files.insert(
943 "b.js".to_string(),
944 PatchFileInfo {
945 before_hash: b_before,
946 after_hash: b_after,
947 },
948 );
949
950 let result = rollback_package_patch(
951 "pkg:npm/test@1.0.0",
952 pkg_dir.path(),
953 &files,
954 blobs_dir.path(),
955 false,
956 )
957 .await;
958
959 assert!(result.success, "expected success: {:?}", result.error);
960 assert_eq!(result.files_rolled_back, vec!["b.js".to_string()]);
961 assert_eq!(
962 tokio::fs::read(pkg_dir.path().join("b.js")).await.unwrap(),
963 b_original
964 );
965 assert_eq!(
967 tokio::fs::read(pkg_dir.path().join("a.js")).await.unwrap(),
968 a_original
969 );
970 }
971
972 #[tokio::test]
975 async fn test_rollback_package_patch_new_file_deleted() {
976 let pkg_dir = tempfile::tempdir().unwrap();
977 let blobs_dir = tempfile::tempdir().unwrap();
978
979 let added = b"file added by the patch\n";
980 let after_hash = compute_git_sha256_from_bytes(added);
981 let path = pkg_dir.path().join("added.js");
982 tokio::fs::write(&path, added).await.unwrap();
983
984 let mut files = HashMap::new();
985 files.insert(
986 "added.js".to_string(),
987 PatchFileInfo {
988 before_hash: String::new(),
989 after_hash,
990 },
991 );
992
993 let result = rollback_package_patch(
994 "pkg:npm/test@1.0.0",
995 pkg_dir.path(),
996 &files,
997 blobs_dir.path(),
998 false,
999 )
1000 .await;
1001
1002 assert!(result.success, "expected success: {:?}", result.error);
1003 assert_eq!(result.files_rolled_back, vec!["added.js".to_string()]);
1004 assert!(
1005 tokio::fs::metadata(&path).await.is_err(),
1006 "the patch-added file must be deleted on rollback"
1007 );
1008 }
1009
1010 #[tokio::test]
1013 async fn test_rollback_package_patch_new_file_already_gone() {
1014 let pkg_dir = tempfile::tempdir().unwrap();
1015 let blobs_dir = tempfile::tempdir().unwrap();
1016
1017 let mut files = HashMap::new();
1018 files.insert(
1019 "added.js".to_string(),
1020 PatchFileInfo {
1021 before_hash: String::new(),
1022 after_hash: compute_git_sha256_from_bytes(b"whatever"),
1023 },
1024 );
1025
1026 let result = rollback_package_patch(
1027 "pkg:npm/test@1.0.0",
1028 pkg_dir.path(),
1029 &files,
1030 blobs_dir.path(),
1031 false,
1032 )
1033 .await;
1034
1035 assert!(result.success, "expected success: {:?}", result.error);
1036 assert_eq!(result.files_rolled_back.len(), 0);
1037 }
1038
1039 #[cfg(unix)]
1045 #[tokio::test]
1046 async fn test_rollback_package_patch_new_file_delete_in_readonly_dir() {
1047 use std::os::unix::fs::PermissionsExt;
1048
1049 let pkg_dir = tempfile::tempdir().unwrap();
1050 let blobs_dir = tempfile::tempdir().unwrap();
1051
1052 let added = b"added by patch\n";
1053 let after_hash = compute_git_sha256_from_bytes(added);
1054 let path = pkg_dir.path().join("added.js");
1055 tokio::fs::write(&path, added).await.unwrap();
1056 tokio::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o444))
1058 .await
1059 .unwrap();
1060 tokio::fs::set_permissions(pkg_dir.path(), std::fs::Permissions::from_mode(0o555))
1061 .await
1062 .unwrap();
1063
1064 let mut files = HashMap::new();
1065 files.insert(
1066 "added.js".to_string(),
1067 PatchFileInfo {
1068 before_hash: String::new(),
1069 after_hash,
1070 },
1071 );
1072
1073 let result = rollback_package_patch(
1074 "pkg:golang/example.com/x@1.0.0",
1075 pkg_dir.path(),
1076 &files,
1077 blobs_dir.path(),
1078 false,
1079 )
1080 .await;
1081
1082 assert!(result.success, "expected success: {:?}", result.error);
1083 assert_eq!(result.files_rolled_back, vec!["added.js".to_string()]);
1084 assert!(tokio::fs::metadata(&path).await.is_err());
1085 assert_eq!(
1087 tokio::fs::metadata(pkg_dir.path())
1088 .await
1089 .unwrap()
1090 .permissions()
1091 .mode()
1092 & 0o7777,
1093 0o555,
1094 );
1095
1096 tokio::fs::set_permissions(pkg_dir.path(), std::fs::Permissions::from_mode(0o755))
1098 .await
1099 .unwrap();
1100 }
1101}