frs/repo/fix/
quarantine.rs1use std::path::Path;
4use std::path::PathBuf;
5use std::process::Command;
6use std::thread;
7
8use rootcause::report;
9
10pub struct RepoCleanup {
12 pub repo: PathBuf,
14 pub failures: Vec<rootcause::Report>,
16}
17
18pub fn clean(repos: &[PathBuf], jobs: usize) -> Vec<RepoCleanup> {
20 repos.iter().map(|repo| clean_repo(repo, repos, jobs)).collect()
21}
22
23fn clean_repo(repo: &Path, repos: &[PathBuf], jobs: usize) -> RepoCleanup {
24 let mut failures = Vec::new();
25 let mut workers = std::collections::VecDeque::new();
26 workers.push_back(thread::spawn({
27 let repo = repo.to_path_buf();
28 move || clean_path(&repo)
29 }));
30 let mut directories = match std::fs::read_dir(repo) {
31 Ok(entries) => vec![(repo.to_path_buf(), entries)],
32 Err(error) => {
33 failures.push(
34 report!("quarantine traversal failed").attach(format!("directory={} error={error}", repo.display())),
35 );
36 Vec::new()
37 }
38 };
39
40 while let Some((directory, entries)) = directories.last_mut() {
41 let directory = directory.clone();
42 let entry = entries.next();
43 let Some(entry) = entry else {
44 directories.pop();
45 continue;
46 };
47
48 let entry = match entry {
49 Ok(entry) => entry,
50 Err(error) => {
51 failures.push(
52 report!("quarantine traversal failed")
53 .attach(format!("directory={} error={error}", directory.display())),
54 );
55 continue;
56 }
57 };
58
59 let path = entry.path();
60 let file_type = match entry.file_type() {
61 Ok(file_type) => file_type,
62 Err(error) => {
63 failures
64 .push(report!("could not inspect path").attach(format!("path={} error={error}", path.display())));
65 continue;
66 }
67 };
68 if file_type.is_symlink() || repos.iter().any(|nested| nested != repo && path.starts_with(nested)) {
69 continue;
70 }
71
72 let child_entries = if file_type.is_dir() {
73 match std::fs::read_dir(&path) {
74 Ok(entries) => Some(entries),
75 Err(error) => {
76 failures.push(
77 report!("quarantine traversal failed")
78 .attach(format!("directory={} error={error}", path.display())),
79 );
80 None
81 }
82 }
83 } else {
84 None
85 };
86 if workers.len() >= jobs
87 && let Some(worker) = workers.pop_front()
88 && let Some(failure) = collect_cleanup(worker)
89 {
90 failures.push(failure);
91 }
92 if let Some(entries) = child_entries {
93 directories.push((path.clone(), entries));
94 }
95 workers.push_back(thread::spawn(move || clean_path(&path)));
96 }
97
98 for worker in workers {
99 if let Some(failure) = collect_cleanup(worker) {
100 failures.push(failure);
101 }
102 }
103
104 RepoCleanup {
105 repo: repo.to_path_buf(),
106 failures,
107 }
108}
109
110fn clean_path(path: &Path) -> rootcause::Result<()> {
111 let output = Command::new("xattr").arg(path).output().map_err(|error| {
112 report!("could not inspect quarantine metadata").attach(format!("path={} error={error}", path.display()))
113 })?;
114 if !output.status.success() {
115 return Err(report!("could not inspect quarantine metadata").attach(format!("path={}", path.display())));
116 }
117
118 let quarantine = String::from_utf8_lossy(&output.stdout)
119 .lines()
120 .any(|attribute| attribute == "com.apple.quarantine");
121 if !quarantine {
122 return Ok(());
123 }
124
125 let status = Command::new("xattr")
126 .args(["-d", "com.apple.quarantine"])
127 .arg(path)
128 .status()
129 .map_err(|error| {
130 report!("could not remove quarantine metadata").attach(format!("path={} error={error}", path.display()))
131 })?;
132
133 if status.success() {
134 return Ok(());
135 }
136 Err(report!("could not remove quarantine metadata").attach(format!("path={}", path.display())))
137}
138
139fn collect_cleanup(cleanup: thread::JoinHandle<rootcause::Result<()>>) -> Option<rootcause::Report> {
140 match cleanup.join() {
141 Ok(Ok(())) => None,
142 Ok(Err(failure)) => Some(failure),
143 Err(_) => Some(report!("quarantine cleanup worker panicked")),
144 }
145}