1use std::collections::HashMap;
14
15#[derive(Debug, Clone)]
17pub struct Violation {
18 pub rule: String,
20 pub file: String,
22 pub line: usize,
24 pub message: String,
26 pub suggestion: String,
28}
29
30pub struct SafetyValidator;
32
33impl SafetyValidator {
34 pub fn validate_files(files: &[(String, String)]) -> Vec<Violation> {
36 let mut violations = Vec::new();
37 for (path, content) in files {
38 if path.ends_with(".rs") {
39 violations.extend(Self::check_rust_file(path, content));
40 } else if path.ends_with(".sql") {
41 violations.extend(Self::check_sql_file(path, content));
42 }
43 }
44 violations
45 }
46
47 fn check_rust_file(path: &str, content: &str) -> Vec<Violation> {
48 let mut violations = Vec::new();
49 for (i, line) in content.lines().enumerate() {
50 let line_no = i + 1;
51 let trimmed = line.trim();
52 if trimmed.starts_with("//") || trimmed.starts_with("//!") {
53 continue;
54 }
55 if trimmed.contains("unsafe ") || trimmed == "unsafe" {
56 violations.push(Violation {
57 rule: "铁律3".to_string(),
58 file: path.to_string(),
59 line: line_no,
60 message: format!("发现 unsafe 代码: {trimmed}"),
61 suggestion: "使用安全 API 替代 unsafe".to_string(),
62 });
63 }
64 if trimmed.contains(".unwrap()") && !trimmed.contains("expect(") {
65 violations.push(Violation {
66 rule: "铁律2".to_string(),
67 file: path.to_string(),
68 line: line_no,
69 message: format!("发现裸 unwrap(): {trimmed}"),
70 suggestion: "使用 expect(\"明确原因\") 或 ? 传播错误".to_string(),
71 });
72 }
73 if trimmed.contains("std::fs::") {
74 violations.push(Violation {
75 rule: "铁律4".to_string(),
76 file: path.to_string(),
77 line: line_no,
78 message: format!("发现 std::fs 调用: {trimmed}"),
79 suggestion: "统一使用 tokio::fs".to_string(),
80 });
81 }
82 }
83 violations
84 }
85
86 fn check_sql_file(path: &str, content: &str) -> Vec<Violation> {
87 let mut violations = Vec::new();
88 for (i, line) in content.lines().enumerate() {
89 let line_no = i + 1;
90 let upper = line.to_uppercase();
91 if upper.contains("SELECT *") {
92 violations.push(Violation {
93 rule: "铁律8".to_string(),
94 file: path.to_string(),
95 line: line_no,
96 message: format!("发现 SELECT *: {line}"),
97 suggestion: "显式列投影,防止字段变更导致崩溃".to_string(),
98 });
99 }
100 }
101 violations
102 }
103
104 pub fn format_report(violations: &[Violation]) -> String {
106 if violations.is_empty() {
107 return "安全检查通过:0 个违规项".to_string();
108 }
109 let mut report = format!("安全检查失败:发现 {} 个违规项\n", violations.len());
110 report.push_str(&"─".repeat(60));
111 report.push('\n');
112 let mut by_rule: HashMap<String, Vec<&Violation>> = HashMap::new();
113 for v in violations {
114 by_rule.entry(v.rule.clone()).or_default().push(v);
115 }
116 for (rule, vs) in by_rule.iter() {
117 report.push_str(&format!("\n[{rule}] ({} 个违规)\n", vs.len()));
118 for v in vs {
119 report.push_str(&format!(
120 " {}:{} {}\n → {}\n",
121 v.file, v.line, v.message, v.suggestion
122 ));
123 }
124 }
125 report
126 }
127}
128
129#[cfg(test)]
130mod tests {
131 use super::*;
132
133 #[test]
134 fn test_no_violations() {
135 let files = vec![(
136 "src/model.rs".to_string(),
137 "pub struct Foo { pub x: i32 }\n".to_string(),
138 )];
139 let violations = SafetyValidator::validate_files(&files);
140 assert!(violations.is_empty());
141 }
142
143 #[test]
144 fn test_unsafe_violation() {
145 let files = vec![("src/foo.rs".to_string(), "unsafe { *ptr }\n".to_string())];
146 let violations = SafetyValidator::validate_files(&files);
147 assert_eq!(violations.len(), 1);
148 assert_eq!(violations[0].rule, "铁律3");
149 }
150
151 #[test]
152 fn test_unwrap_violation() {
153 let files = vec![(
154 "src/foo.rs".to_string(),
155 "let x = val.unwrap();\n".to_string(),
156 )];
157 let violations = SafetyValidator::validate_files(&files);
158 assert_eq!(violations.len(), 1);
159 assert_eq!(violations[0].rule, "铁律2");
160 }
161
162 #[test]
163 fn test_select_star_violation() {
164 let files = vec![(
165 "migrations/table.sql".to_string(),
166 "SELECT * FROM users;\n".to_string(),
167 )];
168 let violations = SafetyValidator::validate_files(&files);
169 assert_eq!(violations.len(), 1);
170 assert_eq!(violations[0].rule, "铁律8");
171 }
172
173 #[test]
174 fn test_std_fs_violation() {
175 let files = vec![(
176 "src/foo.rs".to_string(),
177 "let content = std::fs::read_to_string(path)?;\n".to_string(),
178 )];
179 let violations = SafetyValidator::validate_files(&files);
180 assert_eq!(violations.len(), 1);
181 assert_eq!(violations[0].rule, "铁律4");
182 assert!(violations[0].message.contains("std::fs"));
183 }
184
185 #[test]
186 fn test_format_report_empty() {
187 let report = SafetyValidator::format_report(&[]);
188 assert_eq!(report, "安全检查通过:0 个违规项");
189 }
190
191 #[test]
192 fn test_format_report_with_violations() {
193 let violations = vec![
194 Violation {
195 rule: "铁律3".to_string(),
196 file: "src/foo.rs".to_string(),
197 line: 10,
198 message: "发现 unsafe 代码".to_string(),
199 suggestion: "使用安全 API".to_string(),
200 },
201 Violation {
202 rule: "铁律3".to_string(),
203 file: "src/bar.rs".to_string(),
204 line: 20,
205 message: "发现 unsafe 代码".to_string(),
206 suggestion: "使用安全 API".to_string(),
207 },
208 Violation {
209 rule: "铁律8".to_string(),
210 file: "migrations/t.sql".to_string(),
211 line: 1,
212 message: "发现 SELECT *".to_string(),
213 suggestion: "显式列投影".to_string(),
214 },
215 ];
216 let report = SafetyValidator::format_report(&violations);
217 assert!(report.contains("安全检查失败:发现 3 个违规项"));
218 assert!(report.contains("[铁律3] (2 个违规)"));
219 assert!(report.contains("[铁律8] (1 个违规)"));
220 assert!(report.contains("src/foo.rs:10"));
221 assert!(report.contains("src/bar.rs:20"));
222 assert!(report.contains("migrations/t.sql:1"));
223 }
224
225 #[test]
226 fn test_validate_files_mixed_extensions() {
227 let files = vec![
229 (
230 "readme.md".to_string(),
231 "unsafe { } std::fs::read\n".to_string(),
232 ),
233 ("src/safe.rs".to_string(), "pub fn ok() {}\n".to_string()),
234 ];
235 let violations = SafetyValidator::validate_files(&files);
236 assert!(violations.is_empty());
237 }
238
239 #[test]
240 fn test_check_rust_file_skips_comments() {
241 let files = vec![(
243 "src/foo.rs".to_string(),
244 "// unsafe { } unwrap() std::fs::\n//! doc: unsafe unwrap std::fs\n".to_string(),
245 )];
246 let violations = SafetyValidator::validate_files(&files);
247 assert!(violations.is_empty());
248 }
249
250 #[test]
251 fn test_check_sql_file_no_select_star() {
252 let files = vec![(
253 "migrations/t.sql".to_string(),
254 "SELECT id, name FROM users;\n".to_string(),
255 )];
256 let violations = SafetyValidator::validate_files(&files);
257 assert!(violations.is_empty());
258 }
259}