1use std::fs;
7use std::panic::{catch_unwind, AssertUnwindSafe};
8
9use crate::output::types::{Verifier, VerifyResult};
10
11pub struct SyntaxVerifier;
17
18impl Verifier for SyntaxVerifier {
19 fn verify(&self, path: &str, _module_id: &str) -> VerifyResult {
20 if path.is_empty() {
21 return VerifyResult::ok();
22 }
23 let content = match fs::read_to_string(path) {
24 Ok(c) => c,
25 Err(e) => return VerifyResult::fail(format!("Cannot read file: {e}")),
26 };
27
28 if content.trim().is_empty() {
29 return VerifyResult::fail("File is empty".into());
30 }
31
32 match syn::parse_file(&content) {
33 Ok(_) => VerifyResult::ok(),
34 Err(e) => VerifyResult::fail(format!("Invalid Rust syntax: {e}")),
35 }
36 }
37}
38
39pub struct YAMLVerifier;
41
42impl Verifier for YAMLVerifier {
43 fn verify(&self, path: &str, _module_id: &str) -> VerifyResult {
44 if path.is_empty() {
45 return VerifyResult::ok();
46 }
47 let content = match fs::read_to_string(path) {
48 Ok(c) => c,
49 Err(e) => return VerifyResult::fail(format!("Cannot read file: {e}")),
50 };
51
52 let parsed: serde_yaml_ng::Value = match serde_yaml_ng::from_str(&content) {
53 Ok(v) => v,
54 Err(e) => return VerifyResult::fail(format!("Invalid YAML: {e}")),
55 };
56
57 let bindings = match parsed.get("bindings") {
58 Some(b) => b,
59 None => return VerifyResult::fail("Missing or empty 'bindings' list".into()),
60 };
61
62 let bindings_seq = match bindings.as_sequence() {
63 Some(s) if !s.is_empty() => s,
64 _ => return VerifyResult::fail("Missing or empty 'bindings' list".into()),
65 };
66
67 for (i, entry) in bindings_seq.iter().enumerate() {
68 for field in &["module_id", "target"] {
69 match entry.get(*field) {
70 Some(v) if !v.is_null() => {}
71 _ => {
72 return VerifyResult::fail(format!(
73 "Missing required field '{field}' in binding at index {i}"
74 ))
75 }
76 }
77 }
78 }
79
80 VerifyResult::ok()
81 }
82}
83
84pub struct JSONVerifier {
86 }
89
90impl JSONVerifier {
91 pub fn new() -> Self {
93 Self {}
94 }
95}
96
97impl Default for JSONVerifier {
98 fn default() -> Self {
99 Self::new()
100 }
101}
102
103impl Verifier for JSONVerifier {
104 fn verify(&self, path: &str, _module_id: &str) -> VerifyResult {
105 if path.is_empty() {
106 return VerifyResult::ok();
107 }
108 let content = match fs::read_to_string(path) {
109 Ok(c) => c,
110 Err(e) => return VerifyResult::fail(format!("Cannot read file: {e}")),
111 };
112
113 match serde_json::from_str::<serde_json::Value>(&content) {
114 Ok(_) => VerifyResult::ok(),
115 Err(e) => VerifyResult::fail(format!("Invalid JSON: {e}")),
116 }
117 }
118}
119
120pub struct MagicBytesVerifier {
122 expected: Vec<u8>,
123}
124
125impl MagicBytesVerifier {
126 pub fn new(expected: Vec<u8>) -> Self {
128 Self { expected }
129 }
130}
131
132impl Verifier for MagicBytesVerifier {
133 fn verify(&self, path: &str, _module_id: &str) -> VerifyResult {
134 if path.is_empty() {
135 return VerifyResult::ok();
136 }
137 let content = match fs::read(path) {
138 Ok(c) => c,
139 Err(e) => return VerifyResult::fail(format!("Cannot read file: {e}")),
140 };
141
142 if content.len() < self.expected.len() {
143 return VerifyResult::fail(format!(
144 "File too short: expected at least {} bytes, got {}",
145 self.expected.len(),
146 content.len()
147 ));
148 }
149
150 let header = &content[..self.expected.len()];
151 if header != self.expected.as_slice() {
152 return VerifyResult::fail(format!(
153 "Magic bytes mismatch: expected {:?}, got {:?}",
154 self.expected, header
155 ));
156 }
157
158 VerifyResult::ok()
159 }
160}
161
162pub struct RegistryVerifier<'a> {
164 registry: &'a apcore::Registry,
165}
166
167impl<'a> RegistryVerifier<'a> {
168 pub fn new(registry: &'a apcore::Registry) -> Self {
170 Self { registry }
171 }
172}
173
174impl Verifier for RegistryVerifier<'_> {
175 fn verify(&self, _path: &str, module_id: &str) -> VerifyResult {
176 if self.registry.has(module_id) {
177 VerifyResult::ok()
178 } else {
179 VerifyResult::fail(format!(
180 "Module '{module_id}' not found in registry after registration"
181 ))
182 }
183 }
184}
185
186pub fn run_verifier_chain(
192 verifiers: &[&dyn Verifier],
193 path: &str,
194 module_id: &str,
195) -> VerifyResult {
196 for verifier in verifiers {
197 let verifier = AssertUnwindSafe(verifier);
198 let path = path.to_string();
199 let module_id = module_id.to_string();
200 let outcome = catch_unwind(move || verifier.verify(&path, &module_id));
201 match outcome {
202 Ok(result) if !result.ok => return result,
203 Ok(_) => {} Err(panic_info) => {
205 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
206 (*s).to_string()
207 } else if let Some(s) = panic_info.downcast_ref::<String>() {
208 s.clone()
209 } else {
210 "unknown panic".to_string()
211 };
212 return VerifyResult::fail(format!("Verifier crashed: {msg}"));
213 }
214 }
215 }
216 VerifyResult::ok()
217}
218
219#[cfg(test)]
220mod tests {
221 use super::*;
222 use std::io::Write;
223 use tempfile::NamedTempFile;
224
225 #[test]
228 fn test_syntax_verifier_empty_path_passes() {
229 assert!(SyntaxVerifier.verify("", "mod").ok);
230 }
231
232 #[test]
233 fn test_yaml_verifier_empty_path_passes() {
234 assert!(YAMLVerifier.verify("", "mod").ok);
235 }
236
237 #[test]
238 fn test_json_verifier_empty_path_passes() {
239 assert!(JSONVerifier::new().verify("", "mod").ok);
240 }
241
242 #[test]
243 fn test_magic_bytes_verifier_empty_path_passes() {
244 let v = MagicBytesVerifier::new(b"PNG".to_vec());
245 assert!(v.verify("", "mod").ok);
246 }
247
248 #[test]
249 fn test_yaml_verifier_valid() {
250 let mut f = NamedTempFile::new().unwrap();
251 writeln!(f, "bindings:\n - module_id: test\n target: app:func").unwrap();
252 let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
253 assert!(result.ok);
254 }
255
256 #[test]
257 fn test_yaml_verifier_invalid_yaml() {
258 let mut f = NamedTempFile::new().unwrap();
259 writeln!(f, "{{invalid: yaml: [}}").unwrap();
260 let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
261 assert!(!result.ok);
262 assert!(result.error.unwrap().contains("Invalid YAML"));
263 }
264
265 #[test]
266 fn test_yaml_verifier_missing_bindings() {
267 let mut f = NamedTempFile::new().unwrap();
268 writeln!(f, "other_key: value").unwrap();
269 let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
270 assert!(!result.ok);
271 }
272
273 #[test]
274 fn test_yaml_verifier_multi_binding_second_entry_missing_target() {
275 let mut f = NamedTempFile::new().unwrap();
276 writeln!(
277 f,
278 "bindings:\n - module_id: first\n target: app:fn1\n - module_id: second"
279 )
280 .unwrap();
281 let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
282 assert!(!result.ok, "should fail when second entry lacks 'target'");
283 assert!(
284 result.error.unwrap().contains("target"),
285 "error should mention missing field"
286 );
287 }
288
289 #[test]
290 fn test_yaml_verifier_multi_binding_all_valid() {
291 let mut f = NamedTempFile::new().unwrap();
292 writeln!(
293 f,
294 "bindings:\n - module_id: first\n target: app:fn1\n - module_id: second\n target: app:fn2"
295 )
296 .unwrap();
297 let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
298 assert!(result.ok, "should pass when all entries are valid");
299 }
300
301 #[test]
302 fn test_yaml_verifier_missing_required_field() {
303 let mut f = NamedTempFile::new().unwrap();
304 writeln!(f, "bindings:\n - module_id: test").unwrap();
305 let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
306 assert!(!result.ok);
307 assert!(result.error.unwrap().contains("target"));
308 }
309
310 #[test]
311 fn test_json_verifier_valid() {
312 let mut f = NamedTempFile::new().unwrap();
313 writeln!(f, r#"{{"key": "value"}}"#).unwrap();
314 let result = JSONVerifier::new().verify(f.path().to_str().unwrap(), "test");
315 assert!(result.ok);
316 }
317
318 #[test]
319 fn test_json_verifier_invalid() {
320 let mut f = NamedTempFile::new().unwrap();
321 writeln!(f, "not json").unwrap();
322 let result = JSONVerifier::new().verify(f.path().to_str().unwrap(), "test");
323 assert!(!result.ok);
324 }
325
326 #[test]
327 fn test_magic_bytes_verifier_match() {
328 let mut f = NamedTempFile::new().unwrap();
329 f.write_all(b"\x89PNG\r\n\x1a\nrest of file").unwrap();
330 let verifier = MagicBytesVerifier::new(b"\x89PNG\r\n\x1a\n".to_vec());
331 let result = verifier.verify(f.path().to_str().unwrap(), "test");
332 assert!(result.ok);
333 }
334
335 #[test]
336 fn test_magic_bytes_verifier_mismatch() {
337 let mut f = NamedTempFile::new().unwrap();
338 f.write_all(b"NOT PNG").unwrap();
339 let verifier = MagicBytesVerifier::new(b"\x89PNG".to_vec());
340 let result = verifier.verify(f.path().to_str().unwrap(), "test");
341 assert!(!result.ok);
342 assert!(result.error.unwrap().contains("mismatch"));
343 }
344
345 #[test]
346 fn test_run_verifier_chain_all_pass() {
347 let v1 = JSONVerifier::new();
348 let mut f = NamedTempFile::new().unwrap();
349 writeln!(f, r#"{{"ok": true}}"#).unwrap();
350 let verifiers: Vec<&dyn Verifier> = vec![&v1];
351 let result = run_verifier_chain(&verifiers, f.path().to_str().unwrap(), "test");
352 assert!(result.ok);
353 }
354
355 #[test]
356 fn test_run_verifier_chain_stops_on_failure() {
357 let v1 = JSONVerifier::new();
358 let mut f = NamedTempFile::new().unwrap();
359 writeln!(f, "not json").unwrap();
360 let verifiers: Vec<&dyn Verifier> = vec![&v1];
361 let result = run_verifier_chain(&verifiers, f.path().to_str().unwrap(), "test");
362 assert!(!result.ok);
363 }
364
365 #[test]
366 fn test_run_verifier_chain_empty() {
367 let verifiers: Vec<&dyn Verifier> = vec![];
368 let result = run_verifier_chain(&verifiers, "", "test");
369 assert!(result.ok);
370 }
371
372 #[test]
373 fn test_yaml_verifier_nonexistent_file() {
374 let result = YAMLVerifier.verify("/tmp/nonexistent_file_abc123.yaml", "test");
375 assert!(!result.ok);
376 assert!(result.error.unwrap().contains("Cannot read file"));
377 }
378
379 #[test]
380 fn test_json_verifier_nonexistent_file() {
381 let result = JSONVerifier::new().verify("/tmp/nonexistent_file_abc123.json", "test");
382 assert!(!result.ok);
383 assert!(result.error.unwrap().contains("Cannot read file"));
384 }
385
386 #[test]
387 fn test_magic_bytes_verifier_file_too_short() {
388 let mut f = NamedTempFile::new().unwrap();
389 f.write_all(b"AB").unwrap();
390 let verifier = MagicBytesVerifier::new(b"ABCDEF".to_vec());
391 let result = verifier.verify(f.path().to_str().unwrap(), "test");
392 assert!(!result.ok);
393 let err = result.error.unwrap();
394 assert!(err.contains("File too short"), "got: {err}");
395 assert!(err.contains("6"), "should mention expected length");
396 assert!(err.contains("2"), "should mention actual length");
397 }
398
399 struct PanickingVerifier;
401
402 impl Verifier for PanickingVerifier {
403 fn verify(&self, _path: &str, _module_id: &str) -> VerifyResult {
404 panic!("verifier exploded");
405 }
406 }
407
408 #[test]
409 fn test_run_verifier_chain_panic_caught() {
410 let bad = PanickingVerifier;
411 let verifiers: Vec<&dyn Verifier> = vec![&bad];
412 let result = run_verifier_chain(&verifiers, "/fake", "test");
413 assert!(!result.ok);
414 let err = result.error.unwrap();
415 assert!(
416 err.contains("Verifier crashed"),
417 "expected crash message, got: {err}"
418 );
419 assert!(
420 err.contains("verifier exploded"),
421 "expected panic message, got: {err}"
422 );
423 }
424
425 struct AlwaysFailVerifier {
427 message: String,
428 }
429
430 impl AlwaysFailVerifier {
431 fn new(message: &str) -> Self {
432 Self {
433 message: message.to_string(),
434 }
435 }
436 }
437
438 impl Verifier for AlwaysFailVerifier {
439 fn verify(&self, _path: &str, _module_id: &str) -> VerifyResult {
440 VerifyResult::fail(self.message.clone())
441 }
442 }
443
444 struct AlwaysPassVerifier;
446
447 impl Verifier for AlwaysPassVerifier {
448 fn verify(&self, _path: &str, _module_id: &str) -> VerifyResult {
449 VerifyResult::ok()
450 }
451 }
452
453 #[test]
454 fn test_run_verifier_chain_crash_caught() {
455 let bad = AlwaysFailVerifier::new("simulated crash");
456 let verifiers: Vec<&dyn Verifier> = vec![&bad];
457 let result = run_verifier_chain(&verifiers, "/fake", "test");
458 assert!(!result.ok);
459 assert_eq!(result.error.as_deref(), Some("simulated crash"));
460 }
461
462 #[test]
463 fn test_run_verifier_chain_first_failure_stops() {
464 let fail_v = AlwaysFailVerifier::new("first failed");
466 let pass_v = AlwaysPassVerifier;
467 let verifiers: Vec<&dyn Verifier> = vec![&fail_v, &pass_v];
468 let result = run_verifier_chain(&verifiers, "/fake", "test");
469 assert!(!result.ok);
470 assert_eq!(result.error.as_deref(), Some("first failed"));
471 }
472
473 #[test]
474 fn test_syntax_verifier_valid_rust() {
475 let mut f = NamedTempFile::new().unwrap();
476 writeln!(f, "fn main() {{\n println!(\"hello\");\n}}").unwrap();
477 let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
478 assert!(result.ok, "expected ok, got: {:?}", result.error);
479 }
480
481 #[test]
482 fn test_syntax_verifier_invalid_rust() {
483 let mut f = NamedTempFile::new().unwrap();
484 writeln!(f, "fn main() {{{{{{").unwrap();
485 let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
486 assert!(!result.ok);
487 assert!(result.error.unwrap().contains("Invalid Rust syntax"));
488 }
489
490 #[test]
491 fn test_syntax_verifier_empty_file() {
492 let mut f = NamedTempFile::new().unwrap();
493 write!(f, "").unwrap();
494 let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
495 assert!(!result.ok);
496 assert!(result.error.unwrap().contains("File is empty"));
497 }
498
499 #[test]
500 fn test_syntax_verifier_nonexistent_file() {
501 let result = SyntaxVerifier.verify("/tmp/nonexistent_rs_file_abc123.rs", "test");
502 assert!(!result.ok);
503 assert!(result.error.unwrap().contains("Cannot read file"));
504 }
505
506 #[test]
507 fn test_syntax_verifier_whitespace_only() {
508 let mut f = NamedTempFile::new().unwrap();
509 write!(f, " \n\n \t ").unwrap();
510 let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
511 assert!(!result.ok);
512 assert!(result.error.unwrap().contains("File is empty"));
513 }
514
515 #[test]
516 fn test_syntax_verifier_complex_valid() {
517 let mut f = NamedTempFile::new().unwrap();
518 writeln!(
519 f,
520 "use std::collections::HashMap;\n\
521 \n\
522 pub struct Foo {{\n\
523 pub name: String,\n\
524 pub values: HashMap<String, i32>,\n\
525 }}\n\
526 \n\
527 impl Foo {{\n\
528 pub fn new(name: String) -> Self {{\n\
529 Self {{ name, values: HashMap::new() }}\n\
530 }}\n\
531 }}"
532 )
533 .unwrap();
534 let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
535 assert!(result.ok, "expected ok, got: {:?}", result.error);
536 }
537}