fluidattacks_core_git/
fs.rs1use anyhow::Result;
2use ignore::gitignore::GitignoreBuilder;
3use std::fs;
4use std::path::Path;
5use walkdir::WalkDir;
6
7pub fn remove_symlinks(dir: &Path) {
8 for entry in WalkDir::new(dir)
9 .min_depth(1)
10 .into_iter()
11 .filter_map(Result::ok)
12 {
13 if entry.file_type().is_symlink() {
14 let _ = fs::remove_file(entry.path());
15 }
16 }
17}
18
19pub fn delete_gitignored_files(repo_path: &Path, patterns: &[String]) -> Result<()> {
20 let mut builder = GitignoreBuilder::new(repo_path);
21 for pattern in patterns {
22 if let Err(error) = builder.add_line(None, pattern) {
23 tracing::warn!(pattern, error = %error, "skipping an exclusion git cannot compile");
24 }
25 }
26 let gitignore = builder.build()?;
27
28 for entry in WalkDir::new(repo_path) {
29 let entry = entry?;
30 let path = entry.path();
31
32 let Ok(rel) = path.strip_prefix(repo_path) else {
33 continue;
34 };
35
36 let rel_str = rel.to_string_lossy();
37
38 if rel_str == ".git" || rel_str.starts_with(".git/") || rel_str.starts_with(".git\\") {
39 continue;
40 }
41
42 if rel_str.is_empty() {
43 continue;
44 }
45
46 if !entry.file_type().is_dir()
47 && gitignore
48 .matched_path_or_any_parents(rel, false)
49 .is_ignore()
50 {
51 let _ = fs::remove_file(path);
52 }
53 }
54
55 remove_empty_dirs(repo_path)?;
56 Ok(())
57}
58
59pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
60 fs::create_dir_all(dst)?;
61 for entry in fs::read_dir(src)? {
62 let entry = entry?;
63 let ty = entry.file_type()?;
64 let dst_path = dst.join(entry.file_name());
65 if ty.is_dir() {
66 copy_dir_all(&entry.path(), &dst_path)?;
67 } else {
68 fs::copy(entry.path(), &dst_path)?;
69 copy_modified(&entry.path(), &dst_path);
70 }
71 }
72 Ok(())
73}
74
75fn copy_modified(src: &Path, dst: &Path) {
76 let Ok(modified) = fs::metadata(src).and_then(|meta| meta.modified()) else {
77 return;
78 };
79 if let Ok(file) = fs::File::open(dst) {
80 let _ = file.set_modified(modified);
81 }
82}
83
84fn remove_empty_dirs(root: &Path) -> Result<()> {
85 let mut dirs: Vec<_> = Vec::new();
86
87 for entry in WalkDir::new(root) {
88 let entry = entry?;
89 let path = entry.path();
90
91 let Ok(rel) = path.strip_prefix(root) else {
92 continue;
93 };
94
95 let rel_str = rel.to_string_lossy();
96
97 if rel_str == ".git" || rel_str.starts_with(".git/") || rel_str.starts_with(".git\\") {
98 continue;
99 }
100
101 if entry.file_type().is_dir() && path != root {
102 dirs.push(path.to_path_buf());
103 }
104 }
105
106 dirs.sort();
107 dirs.reverse();
108
109 for dir in dirs {
110 if let Ok(entries) = fs::read_dir(&dir) {
111 if entries.count() == 0 {
112 let _ = fs::remove_dir(&dir);
113 }
114 }
115 }
116
117 Ok(())
118}
119
120#[cfg(test)]
121mod tests {
122 use super::{copy_dir_all, delete_gitignored_files, remove_symlinks};
123 use std::time::{Duration, SystemTime};
124
125 #[test]
126 fn a_directory_pattern_deletes_what_is_under_it() {
127 let dir = tempfile::tempdir().unwrap();
128 let repo = dir.path();
129 std::fs::create_dir_all(repo.join("docs/sub")).unwrap();
130 std::fs::create_dir_all(repo.join(".git")).unwrap();
131 std::fs::write(repo.join("docs/a.md"), b"out").unwrap();
132 std::fs::write(repo.join("docs/sub/b.md"), b"out").unwrap();
133 std::fs::write(repo.join("keep.py"), b"in").unwrap();
134 std::fs::write(repo.join(".git/config"), b"[core]").unwrap();
135
136 delete_gitignored_files(repo, &["docs".to_owned()]).unwrap();
137
138 assert!(!repo.join("docs/a.md").exists());
139 assert!(!repo.join("docs/sub/b.md").exists());
140 assert!(repo.join("keep.py").exists());
141 assert!(repo.join(".git/config").exists());
142 }
143
144 #[test]
145 fn an_exclusion_git_cannot_compile_does_not_disable_the_others() {
146 let dir = tempfile::tempdir().unwrap();
147 let repo = dir.path();
148 std::fs::create_dir_all(repo.join("docs")).unwrap();
149 std::fs::write(repo.join("docs/a.md"), b"out").unwrap();
150 std::fs::write(repo.join("keep.py"), b"in").unwrap();
151
152 delete_gitignored_files(repo, &["docs".to_owned(), "src/{main".to_owned()]).unwrap();
153
154 assert!(!repo.join("docs/a.md").exists());
155 assert!(repo.join("keep.py").exists());
156 }
157
158 #[test]
159 fn a_copied_tree_keeps_the_mode_and_the_modification_time() {
160 let dir = tempfile::tempdir().unwrap();
161 let src = dir.path().join("src/objects");
162 std::fs::create_dir_all(&src).unwrap();
163 let object = src.join("a1b2c3");
164 std::fs::write(&object, b"blob").unwrap();
165
166 let modified = SystemTime::UNIX_EPOCH
167 .checked_add(Duration::from_secs(1_700_000_000))
168 .unwrap();
169 std::fs::File::open(&object)
170 .unwrap()
171 .set_modified(modified)
172 .unwrap();
173 #[cfg(unix)]
174 {
175 use std::os::unix::fs::PermissionsExt;
176 std::fs::set_permissions(&object, std::fs::Permissions::from_mode(0o444)).unwrap();
177 }
178
179 let dst = dir.path().join("dst");
180 copy_dir_all(&dir.path().join("src"), &dst).unwrap();
181
182 let copied = dst.join("objects/a1b2c3");
183 let meta = std::fs::metadata(&copied).unwrap();
184 assert_eq!(meta.modified().unwrap(), modified);
185 #[cfg(unix)]
186 {
187 use std::os::unix::fs::PermissionsExt;
188 assert_eq!(meta.permissions().mode() & 0o777, 0o444);
189 }
190 }
191
192 #[test]
193 #[cfg(unix)]
194 fn an_unreadable_directory_does_not_stop_the_removal() {
195 use std::os::unix::fs::PermissionsExt;
196
197 let dir = tempfile::tempdir().unwrap();
198 let target = dir.path().join("target.txt");
199 std::fs::write(&target, b"content").unwrap();
200
201 let blocked = dir.path().join("b_blocked");
202 std::fs::create_dir(&blocked).unwrap();
203 let late = dir.path().join("c_late.lnk");
204 std::os::unix::fs::symlink(&target, &late).unwrap();
205 std::fs::set_permissions(&blocked, std::fs::Permissions::from_mode(0o000)).unwrap();
206
207 remove_symlinks(dir.path());
208
209 std::fs::set_permissions(&blocked, std::fs::Permissions::from_mode(0o755)).unwrap();
210 assert!(!late.is_symlink());
211 assert!(target.exists());
212 }
213
214 #[test]
215 #[cfg(unix)]
216 fn the_directory_itself_is_never_removed() {
217 let dir = tempfile::tempdir().unwrap();
218 let real = dir.path().join("real");
219 std::fs::create_dir(&real).unwrap();
220 let alias = dir.path().join("alias");
221 std::os::unix::fs::symlink(&real, &alias).unwrap();
222
223 remove_symlinks(&alias);
224
225 assert!(alias.is_symlink());
226 }
227}