1use std::path::{Path, PathBuf};
2
3use crate::error::Result;
4use crate::rules::Finding;
5
6pub mod dependencies;
7pub mod deserializer;
8
9#[derive(Debug, Clone, PartialEq, Eq)]
11pub struct AppliedFix {
12 pub rule_id: String,
13 pub description: String,
14 pub line_number: usize,
15}
16
17#[derive(Debug, Clone)]
19pub struct FilePatch {
20 pub file_path: PathBuf,
21 pub original_content: String,
22 pub modified_content: String,
23 pub applied_fixes: Vec<AppliedFix>,
24}
25
26impl FilePatch {
27 pub fn has_changes(&self) -> bool {
28 self.original_content != self.modified_content
29 }
30
31 pub fn write_to_disk(&self) -> std::io::Result<()> {
33 let parent = self.file_path.parent().unwrap_or_else(|| Path::new("."));
34 let mut temp_file = tempfile::NamedTempFile::new_in(parent)?;
35
36 use std::io::Write;
37 temp_file.write_all(self.modified_content.as_bytes())?;
38 temp_file.flush()?;
39
40 if let Ok(metadata) = std::fs::metadata(&self.file_path) {
41 let _ = temp_file.as_file().set_permissions(metadata.permissions());
42 }
43
44 temp_file.persist(&self.file_path).map_err(|e| e.error)?;
45 Ok(())
46 }
47
48 pub fn render_diff(&self) -> String {
50 generate_unified_diff(
51 &self.file_path,
52 &self.original_content,
53 &self.modified_content,
54 )
55 }
56}
57
58#[derive(Default)]
60pub struct FixEngine;
61
62impl FixEngine {
63 pub fn new() -> Self {
64 Self
65 }
66
67 pub fn generate_patches(
69 &self,
70 findings: &[Finding],
71 project_root: &Path,
72 filter_rules: Option<&[String]>,
73 ) -> Result<Vec<FilePatch>> {
74 let mut file_map: std::collections::HashMap<PathBuf, Vec<&Finding>> =
75 std::collections::HashMap::new();
76
77 for finding in findings {
78 if let Some(rules) = filter_rules {
79 if !rules.is_empty()
80 && !rules
81 .iter()
82 .any(|r| r.eq_ignore_ascii_case(&finding.rule_id))
83 {
84 continue;
85 }
86 }
87
88 if let Some(ref loc) = finding.location {
89 let resolved_path = if loc.file.exists() {
90 loc.file.clone()
91 } else if project_root.join(&loc.file).exists() {
92 project_root.join(&loc.file)
93 } else if let Some(file_name) = loc.file.file_name() {
94 if project_root.join(file_name).exists() {
95 project_root.join(file_name)
96 } else {
97 continue;
98 }
99 } else {
100 continue;
101 };
102
103 let abs_path = resolved_path.canonicalize().unwrap_or(resolved_path);
104 file_map.entry(abs_path).or_default().push(finding);
105 }
106 }
107
108 let mut patches = Vec::new();
109
110 for (file_path, file_findings) in file_map {
111 if !file_path.exists() || !file_path.is_file() {
112 continue;
113 }
114
115 let content = match std::fs::read_to_string(&file_path) {
116 Ok(c) => c,
117 Err(_) => continue,
118 };
119
120 let mut current_content = content.clone();
121 let mut applied_fixes = Vec::new();
122
123 if file_findings.iter().any(|f| f.rule_id == "SHIELD-016") {
125 if let Some(patched) =
126 deserializer::fix_unsafe_deserializers(¤t_content, &file_path)
127 {
128 current_content = patched.content;
129 applied_fixes.extend(patched.fixes);
130 }
131 }
132
133 if file_findings.iter().any(|f| f.rule_id == "SHIELD-009") {
135 if let Some(patched) =
136 dependencies::fix_unpinned_dependencies(¤t_content, &file_path)
137 {
138 current_content = patched.content;
139 applied_fixes.extend(patched.fixes);
140 }
141 }
142
143 if current_content != content {
144 patches.push(FilePatch {
145 file_path,
146 original_content: content,
147 modified_content: current_content,
148 applied_fixes,
149 });
150 }
151 }
152
153 Ok(patches)
154 }
155}
156
157#[derive(Debug, Clone)]
159pub struct FixOutput {
160 pub content: String,
161 pub fixes: Vec<AppliedFix>,
162}
163
164pub fn generate_unified_diff(file_path: &Path, original: &str, modified: &str) -> String {
166 let orig_lines: Vec<&str> = original.lines().collect();
167 let mod_lines: Vec<&str> = modified.lines().collect();
168
169 let mut diff = String::new();
170 diff.push_str(&format!("--- a/{}\n", file_path.display()));
171 diff.push_str(&format!("+++ b/{}\n", file_path.display()));
172
173 let mut i = 0;
174 let mut j = 0;
175
176 while i < orig_lines.len() || j < mod_lines.len() {
177 if i < orig_lines.len() && j < mod_lines.len() && orig_lines[i] == mod_lines[j] {
178 i += 1;
179 j += 1;
180 continue;
181 }
182
183 let start_i = i;
184 let start_j = j;
185
186 let mut orig_chunk = Vec::new();
188 let mut mod_chunk = Vec::new();
189
190 while i < orig_lines.len() && (j >= mod_lines.len() || orig_lines[i] != mod_lines[j]) {
191 orig_chunk.push(orig_lines[i]);
192 i += 1;
193 if i >= orig_lines.len() || j >= mod_lines.len() || orig_lines[i] == mod_lines[j] {
194 break;
195 }
196 }
197
198 while j < mod_lines.len() && (i >= orig_lines.len() || orig_lines[i] != mod_lines[j]) {
199 mod_chunk.push(mod_lines[j]);
200 j += 1;
201 if i < orig_lines.len() && j < mod_lines.len() && orig_lines[i] == mod_lines[j] {
202 break;
203 }
204 }
205
206 diff.push_str(&format!(
207 "@@ -{},{} +{},{} @@\n",
208 start_i + 1,
209 orig_chunk.len(),
210 start_j + 1,
211 mod_chunk.len()
212 ));
213
214 for line in orig_chunk {
215 diff.push_str(&format!("-{line}\n"));
216 }
217 for line in mod_chunk {
218 diff.push_str(&format!("+{line}\n"));
219 }
220 }
221
222 diff
223}
224
225#[cfg(test)]
226mod tests {
227 use super::*;
228 use std::io::Read;
229
230 #[test]
231 fn test_write_to_disk_atomic() {
232 let temp_dir = tempfile::tempdir().unwrap();
233 let file_path = temp_dir.path().join("test_atomic.txt");
234
235 std::fs::write(&file_path, "original").unwrap();
236
237 let patch = FilePatch {
238 file_path: file_path.clone(),
239 original_content: "original".into(),
240 modified_content: "modified".into(),
241 applied_fixes: vec![],
242 };
243
244 patch.write_to_disk().unwrap();
245
246 let mut content = String::new();
247 std::fs::File::open(&file_path)
248 .unwrap()
249 .read_to_string(&mut content)
250 .unwrap();
251 assert_eq!(content, "modified");
252 }
253
254 #[cfg(unix)]
255 #[test]
256 fn test_write_to_disk_preserves_permissions() {
257 use std::os::unix::fs::PermissionsExt;
258
259 let temp_dir = tempfile::tempdir().unwrap();
260 let file_path = temp_dir.path().join("test_perms.txt");
261
262 std::fs::write(&file_path, "original").unwrap();
263 let mut perms = std::fs::metadata(&file_path).unwrap().permissions();
264 perms.set_mode(0o755);
265 std::fs::set_permissions(&file_path, perms).unwrap();
266
267 let patch = FilePatch {
268 file_path: file_path.clone(),
269 original_content: "original".into(),
270 modified_content: "modified".into(),
271 applied_fixes: vec![],
272 };
273
274 patch.write_to_disk().unwrap();
275
276 let new_perms = std::fs::metadata(&file_path).unwrap().permissions();
277 assert_eq!(new_perms.mode() & 0o777, 0o755);
278 }
279
280 #[test]
281 fn test_generate_unified_diff() {
282 let orig = "line 1\nline 2\nline 3\n";
283 let modified = "line 1\nline 2 modified\nline 3\n";
284 let diff = generate_unified_diff(Path::new("test.txt"), orig, modified);
285 assert!(diff.contains("--- a/test.txt"));
286 assert!(diff.contains("+++ b/test.txt"));
287 assert!(diff.contains("-line 2"));
288 assert!(diff.contains("+line 2 modified"));
289 }
290}