Skip to main content

fluidattacks_core_git/
fs.rs

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