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) -> Result<()> {
8 for entry in WalkDir::new(dir) {
9 let entry = entry?;
10 let metadata = entry.path().symlink_metadata()?;
11 if metadata.file_type().is_symlink() {
12 fs::remove_file(entry.path())?;
13 }
14 }
15 Ok(())
16}
17
18pub fn delete_gitignored_files(repo_path: &Path, patterns: &[String]) -> Result<()> {
19 let mut builder = GitignoreBuilder::new(repo_path);
20 for pattern in patterns {
21 builder.add_line(None, pattern)?;
22 }
23 let gitignore = builder.build()?;
24
25 for entry in WalkDir::new(repo_path) {
26 let entry = entry?;
27 let path = entry.path();
28
29 let Ok(rel) = path.strip_prefix(repo_path) else {
30 continue;
31 };
32
33 let rel_str = rel.to_string_lossy();
34
35 if rel_str == ".git" || rel_str.starts_with(".git/") || rel_str.starts_with(".git\\") {
36 continue;
37 }
38
39 if rel_str.is_empty() {
40 continue;
41 }
42
43 if !entry.file_type().is_dir() && gitignore.matched(rel, false).is_ignore() {
44 let _ = fs::remove_file(path);
45 }
46 }
47
48 remove_empty_dirs(repo_path)?;
49 Ok(())
50}
51
52pub fn copy_dir_all(src: &Path, dst: &Path) -> Result<()> {
53 fs::create_dir_all(dst)?;
54 for entry in fs::read_dir(src)? {
55 let entry = entry?;
56 let ty = entry.file_type()?;
57 let dst_path = dst.join(entry.file_name());
58 if ty.is_dir() {
59 copy_dir_all(&entry.path(), &dst_path)?;
60 } else {
61 fs::copy(entry.path(), &dst_path)?;
62 copy_modified(&entry.path(), &dst_path);
63 }
64 }
65 Ok(())
66}
67
68fn copy_modified(src: &Path, dst: &Path) {
69 let Ok(modified) = fs::metadata(src).and_then(|meta| meta.modified()) else {
70 return;
71 };
72 if let Ok(file) = fs::File::open(dst) {
73 let _ = file.set_modified(modified);
74 }
75}
76
77fn remove_empty_dirs(root: &Path) -> Result<()> {
78 let mut dirs: Vec<_> = Vec::new();
79
80 for entry in WalkDir::new(root) {
81 let entry = entry?;
82 let path = entry.path();
83
84 let Ok(rel) = path.strip_prefix(root) else {
85 continue;
86 };
87
88 let rel_str = rel.to_string_lossy();
89
90 if rel_str == ".git" || rel_str.starts_with(".git/") || rel_str.starts_with(".git\\") {
91 continue;
92 }
93
94 if entry.file_type().is_dir() && path != root {
95 dirs.push(path.to_path_buf());
96 }
97 }
98
99 dirs.sort();
100 dirs.reverse();
101
102 for dir in dirs {
103 if let Ok(entries) = fs::read_dir(&dir) {
104 if entries.count() == 0 {
105 let _ = fs::remove_dir(&dir);
106 }
107 }
108 }
109
110 Ok(())
111}
112
113#[cfg(test)]
114mod tests {
115 use super::copy_dir_all;
116 use std::time::{Duration, SystemTime};
117
118 #[test]
119 fn a_copied_tree_keeps_the_mode_and_the_modification_time() {
120 let dir = tempfile::tempdir().unwrap();
121 let src = dir.path().join("src/objects");
122 std::fs::create_dir_all(&src).unwrap();
123 let object = src.join("a1b2c3");
124 std::fs::write(&object, b"blob").unwrap();
125
126 let modified = SystemTime::UNIX_EPOCH
127 .checked_add(Duration::from_secs(1_700_000_000))
128 .unwrap();
129 std::fs::File::open(&object)
130 .unwrap()
131 .set_modified(modified)
132 .unwrap();
133 #[cfg(unix)]
134 {
135 use std::os::unix::fs::PermissionsExt;
136 std::fs::set_permissions(&object, std::fs::Permissions::from_mode(0o444)).unwrap();
137 }
138
139 let dst = dir.path().join("dst");
140 copy_dir_all(&dir.path().join("src"), &dst).unwrap();
141
142 let copied = dst.join("objects/a1b2c3");
143 let meta = std::fs::metadata(&copied).unwrap();
144 assert_eq!(meta.modified().unwrap(), modified);
145 #[cfg(unix)]
146 {
147 use std::os::unix::fs::PermissionsExt;
148 assert_eq!(meta.permissions().mode() & 0o777, 0o444);
149 }
150 }
151}