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 std::fs::write(&self.file_path, &self.modified_content)
34 }
35
36 pub fn render_diff(&self) -> String {
38 generate_unified_diff(
39 &self.file_path,
40 &self.original_content,
41 &self.modified_content,
42 )
43 }
44}
45
46#[derive(Default)]
48pub struct FixEngine;
49
50impl FixEngine {
51 pub fn new() -> Self {
52 Self
53 }
54
55 pub fn generate_patches(
57 &self,
58 findings: &[Finding],
59 project_root: &Path,
60 filter_rules: Option<&[String]>,
61 ) -> Result<Vec<FilePatch>> {
62 let mut file_map: std::collections::HashMap<PathBuf, Vec<&Finding>> =
63 std::collections::HashMap::new();
64
65 for finding in findings {
66 if let Some(rules) = filter_rules {
67 if !rules.is_empty()
68 && !rules
69 .iter()
70 .any(|r| r.eq_ignore_ascii_case(&finding.rule_id))
71 {
72 continue;
73 }
74 }
75
76 if let Some(ref loc) = finding.location {
77 let resolved_path = if loc.file.exists() {
78 loc.file.clone()
79 } else if project_root.join(&loc.file).exists() {
80 project_root.join(&loc.file)
81 } else if let Some(file_name) = loc.file.file_name() {
82 if project_root.join(file_name).exists() {
83 project_root.join(file_name)
84 } else {
85 continue;
86 }
87 } else {
88 continue;
89 };
90
91 let abs_path = resolved_path.canonicalize().unwrap_or(resolved_path);
92 file_map.entry(abs_path).or_default().push(finding);
93 }
94 }
95
96 let mut patches = Vec::new();
97
98 for (file_path, file_findings) in file_map {
99 if !file_path.exists() || !file_path.is_file() {
100 continue;
101 }
102
103 let content = match std::fs::read_to_string(&file_path) {
104 Ok(c) => c,
105 Err(_) => continue,
106 };
107
108 let mut current_content = content.clone();
109 let mut applied_fixes = Vec::new();
110
111 if file_findings.iter().any(|f| f.rule_id == "SHIELD-016") {
113 if let Some(patched) =
114 deserializer::fix_unsafe_deserializers(¤t_content, &file_path)
115 {
116 current_content = patched.content;
117 applied_fixes.extend(patched.fixes);
118 }
119 }
120
121 if file_findings.iter().any(|f| f.rule_id == "SHIELD-009") {
123 if let Some(patched) =
124 dependencies::fix_unpinned_dependencies(¤t_content, &file_path)
125 {
126 current_content = patched.content;
127 applied_fixes.extend(patched.fixes);
128 }
129 }
130
131 if current_content != content {
132 patches.push(FilePatch {
133 file_path,
134 original_content: content,
135 modified_content: current_content,
136 applied_fixes,
137 });
138 }
139 }
140
141 Ok(patches)
142 }
143}
144
145#[derive(Debug, Clone)]
147pub struct FixOutput {
148 pub content: String,
149 pub fixes: Vec<AppliedFix>,
150}
151
152pub fn generate_unified_diff(file_path: &Path, original: &str, modified: &str) -> String {
154 let orig_lines: Vec<&str> = original.lines().collect();
155 let mod_lines: Vec<&str> = modified.lines().collect();
156
157 let mut diff = String::new();
158 diff.push_str(&format!("--- a/{}\n", file_path.display()));
159 diff.push_str(&format!("+++ b/{}\n", file_path.display()));
160
161 let mut i = 0;
162 let mut j = 0;
163
164 while i < orig_lines.len() || j < mod_lines.len() {
165 if i < orig_lines.len() && j < mod_lines.len() && orig_lines[i] == mod_lines[j] {
166 i += 1;
167 j += 1;
168 continue;
169 }
170
171 let start_i = i;
172 let start_j = j;
173
174 let mut orig_chunk = Vec::new();
176 let mut mod_chunk = Vec::new();
177
178 while i < orig_lines.len() && (j >= mod_lines.len() || orig_lines[i] != mod_lines[j]) {
179 orig_chunk.push(orig_lines[i]);
180 i += 1;
181 if i >= orig_lines.len() || j >= mod_lines.len() || orig_lines[i] == mod_lines[j] {
182 break;
183 }
184 }
185
186 while j < mod_lines.len() && (i >= orig_lines.len() || orig_lines[i] != mod_lines[j]) {
187 mod_chunk.push(mod_lines[j]);
188 j += 1;
189 if i < orig_lines.len() && j < mod_lines.len() && orig_lines[i] == mod_lines[j] {
190 break;
191 }
192 }
193
194 diff.push_str(&format!(
195 "@@ -{},{} +{},{} @@\n",
196 start_i + 1,
197 orig_chunk.len(),
198 start_j + 1,
199 mod_chunk.len()
200 ));
201
202 for line in orig_chunk {
203 diff.push_str(&format!("-{line}\n"));
204 }
205 for line in mod_chunk {
206 diff.push_str(&format!("+{line}\n"));
207 }
208 }
209
210 diff
211}
212
213#[cfg(test)]
214mod tests {
215 use super::*;
216
217 #[test]
218 fn test_generate_unified_diff() {
219 let orig = "line 1\nline 2\nline 3\n";
220 let modified = "line 1\nline 2 modified\nline 3\n";
221 let diff = generate_unified_diff(Path::new("test.txt"), orig, modified);
222 assert!(diff.contains("--- a/test.txt"));
223 assert!(diff.contains("+++ b/test.txt"));
224 assert!(diff.contains("-line 2"));
225 assert!(diff.contains("+line 2 modified"));
226 }
227}