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(serde_yaml_ng::Value::String(s)) if !s.trim().is_empty() => {}
71 _ => {
72 return VerifyResult::fail(format!(
73 "Entry {i}: missing or invalid '{field}' field"
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(
203 verifiers: &[&dyn Verifier],
204 path: &str,
205 module_id: &str,
206) -> VerifyResult {
207 for verifier in verifiers {
208 let verifier = AssertUnwindSafe(verifier);
209 let path = path.to_string();
210 let module_id = module_id.to_string();
211 let outcome = catch_unwind(move || verifier.verify(&path, &module_id));
212 match outcome {
213 Ok(result) if !result.ok => return result,
214 Ok(_) => {} Err(panic_info) => {
216 let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
217 (*s).to_string()
218 } else if let Some(s) = panic_info.downcast_ref::<String>() {
219 s.clone()
220 } else {
221 "unknown panic".to_string()
222 };
223 return VerifyResult::fail(format!("Verifier crashed: {msg}"));
224 }
225 }
226 }
227 VerifyResult::ok()
228}
229
230#[cfg(test)]
231mod tests {
232 use super::*;
233 use std::io::Write;
234 use tempfile::NamedTempFile;
235
236 #[test]
239 fn test_syntax_verifier_empty_path_passes() {
240 assert!(SyntaxVerifier.verify("", "mod").ok);
241 }
242
243 #[test]
244 fn test_yaml_verifier_empty_path_passes() {
245 assert!(YAMLVerifier.verify("", "mod").ok);
246 }
247
248 #[test]
249 fn test_json_verifier_empty_path_passes() {
250 assert!(JSONVerifier::new().verify("", "mod").ok);
251 }
252
253 #[test]
254 fn test_magic_bytes_verifier_empty_path_passes() {
255 let v = MagicBytesVerifier::new(b"PNG".to_vec());
256 assert!(v.verify("", "mod").ok);
257 }
258
259 #[test]
260 fn test_yaml_verifier_valid() {
261 let mut f = NamedTempFile::new().unwrap();
262 writeln!(f, "bindings:\n - module_id: test\n target: app:func").unwrap();
263 let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
264 assert!(result.ok);
265 }
266
267 #[test]
268 fn test_yaml_verifier_invalid_yaml() {
269 let mut f = NamedTempFile::new().unwrap();
270 writeln!(f, "{{invalid: yaml: [}}").unwrap();
271 let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
272 assert!(!result.ok);
273 assert!(result.error.unwrap().contains("Invalid YAML"));
274 }
275
276 #[test]
277 fn test_yaml_verifier_missing_bindings() {
278 let mut f = NamedTempFile::new().unwrap();
279 writeln!(f, "other_key: value").unwrap();
280 let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
281 assert!(!result.ok);
282 }
283
284 #[test]
285 fn test_yaml_verifier_multi_binding_second_entry_missing_target() {
286 let mut f = NamedTempFile::new().unwrap();
287 writeln!(
288 f,
289 "bindings:\n - module_id: first\n target: app:fn1\n - module_id: second"
290 )
291 .unwrap();
292 let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
293 assert!(!result.ok, "should fail when second entry lacks 'target'");
294 assert!(
295 result.error.unwrap().contains("target"),
296 "error should mention missing field"
297 );
298 }
299
300 #[test]
301 fn test_yaml_verifier_multi_binding_all_valid() {
302 let mut f = NamedTempFile::new().unwrap();
303 writeln!(
304 f,
305 "bindings:\n - module_id: first\n target: app:fn1\n - module_id: second\n target: app:fn2"
306 )
307 .unwrap();
308 let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
309 assert!(result.ok, "should pass when all entries are valid");
310 }
311
312 #[test]
313 fn test_yaml_verifier_missing_required_field() {
314 let mut f = NamedTempFile::new().unwrap();
315 writeln!(f, "bindings:\n - module_id: test").unwrap();
316 let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
317 assert!(!result.ok);
318 assert!(result.error.unwrap().contains("target"));
319 }
320
321 #[test]
322 fn test_yaml_verifier_whitespace_only_module_id() {
323 let mut f = NamedTempFile::new().unwrap();
325 writeln!(f, "bindings:\n - module_id: \" \"\n target: app:func").unwrap();
326 let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
327 assert!(!result.ok, "whitespace-only module_id should be rejected");
328 assert!(
329 result.error.unwrap().contains("module_id"),
330 "error should mention the invalid field"
331 );
332 }
333
334 #[test]
335 fn test_yaml_verifier_integer_module_id() {
336 let mut f = NamedTempFile::new().unwrap();
338 writeln!(f, "bindings:\n - module_id: 42\n target: app:func").unwrap();
339 let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
340 assert!(!result.ok, "integer module_id should be rejected");
341 assert!(
342 result.error.unwrap().contains("module_id"),
343 "error should mention the invalid field"
344 );
345 }
346
347 #[test]
348 fn test_json_verifier_valid() {
349 let mut f = NamedTempFile::new().unwrap();
350 writeln!(f, r#"{{"key": "value"}}"#).unwrap();
351 let result = JSONVerifier::new().verify(f.path().to_str().unwrap(), "test");
352 assert!(result.ok);
353 }
354
355 #[test]
356 fn test_json_verifier_invalid() {
357 let mut f = NamedTempFile::new().unwrap();
358 writeln!(f, "not json").unwrap();
359 let result = JSONVerifier::new().verify(f.path().to_str().unwrap(), "test");
360 assert!(!result.ok);
361 }
362
363 #[test]
364 fn test_magic_bytes_verifier_match() {
365 let mut f = NamedTempFile::new().unwrap();
366 f.write_all(b"\x89PNG\r\n\x1a\nrest of file").unwrap();
367 let verifier = MagicBytesVerifier::new(b"\x89PNG\r\n\x1a\n".to_vec());
368 let result = verifier.verify(f.path().to_str().unwrap(), "test");
369 assert!(result.ok);
370 }
371
372 #[test]
373 fn test_magic_bytes_verifier_mismatch() {
374 let mut f = NamedTempFile::new().unwrap();
375 f.write_all(b"NOT PNG").unwrap();
376 let verifier = MagicBytesVerifier::new(b"\x89PNG".to_vec());
377 let result = verifier.verify(f.path().to_str().unwrap(), "test");
378 assert!(!result.ok);
379 assert!(result.error.unwrap().contains("mismatch"));
380 }
381
382 #[test]
383 fn test_run_verifier_chain_all_pass() {
384 let v1 = JSONVerifier::new();
385 let mut f = NamedTempFile::new().unwrap();
386 writeln!(f, r#"{{"ok": true}}"#).unwrap();
387 let verifiers: Vec<&dyn Verifier> = vec![&v1];
388 let result = run_verifier_chain(&verifiers, f.path().to_str().unwrap(), "test");
389 assert!(result.ok);
390 }
391
392 #[test]
393 fn test_run_verifier_chain_stops_on_failure() {
394 let v1 = JSONVerifier::new();
395 let mut f = NamedTempFile::new().unwrap();
396 writeln!(f, "not json").unwrap();
397 let verifiers: Vec<&dyn Verifier> = vec![&v1];
398 let result = run_verifier_chain(&verifiers, f.path().to_str().unwrap(), "test");
399 assert!(!result.ok);
400 }
401
402 #[test]
403 fn test_run_verifier_chain_empty() {
404 let verifiers: Vec<&dyn Verifier> = vec![];
405 let result = run_verifier_chain(&verifiers, "", "test");
406 assert!(result.ok);
407 }
408
409 #[test]
410 fn test_yaml_verifier_nonexistent_file() {
411 let result = YAMLVerifier.verify("/tmp/nonexistent_file_abc123.yaml", "test");
412 assert!(!result.ok);
413 assert!(result.error.unwrap().contains("Cannot read file"));
414 }
415
416 #[test]
417 fn test_json_verifier_nonexistent_file() {
418 let result = JSONVerifier::new().verify("/tmp/nonexistent_file_abc123.json", "test");
419 assert!(!result.ok);
420 assert!(result.error.unwrap().contains("Cannot read file"));
421 }
422
423 #[test]
424 fn test_magic_bytes_verifier_file_too_short() {
425 let mut f = NamedTempFile::new().unwrap();
426 f.write_all(b"AB").unwrap();
427 let verifier = MagicBytesVerifier::new(b"ABCDEF".to_vec());
428 let result = verifier.verify(f.path().to_str().unwrap(), "test");
429 assert!(!result.ok);
430 let err = result.error.unwrap();
431 assert!(err.contains("File too short"), "got: {err}");
432 assert!(err.contains("6"), "should mention expected length");
433 assert!(err.contains("2"), "should mention actual length");
434 }
435
436 struct PanickingVerifier;
438
439 impl Verifier for PanickingVerifier {
440 fn verify(&self, _path: &str, _module_id: &str) -> VerifyResult {
441 panic!("verifier exploded");
442 }
443 }
444
445 #[test]
446 fn test_run_verifier_chain_panic_caught() {
447 let bad = PanickingVerifier;
448 let verifiers: Vec<&dyn Verifier> = vec![&bad];
449 let result = run_verifier_chain(&verifiers, "/fake", "test");
450 assert!(!result.ok);
451 let err = result.error.unwrap();
452 assert!(
453 err.contains("Verifier crashed"),
454 "expected crash message, got: {err}"
455 );
456 assert!(
457 err.contains("verifier exploded"),
458 "expected panic message, got: {err}"
459 );
460 }
461
462 struct AlwaysFailVerifier {
464 message: String,
465 }
466
467 impl AlwaysFailVerifier {
468 fn new(message: &str) -> Self {
469 Self {
470 message: message.to_string(),
471 }
472 }
473 }
474
475 impl Verifier for AlwaysFailVerifier {
476 fn verify(&self, _path: &str, _module_id: &str) -> VerifyResult {
477 VerifyResult::fail(self.message.clone())
478 }
479 }
480
481 struct AlwaysPassVerifier;
483
484 impl Verifier for AlwaysPassVerifier {
485 fn verify(&self, _path: &str, _module_id: &str) -> VerifyResult {
486 VerifyResult::ok()
487 }
488 }
489
490 #[test]
491 fn test_run_verifier_chain_crash_caught() {
492 let bad = AlwaysFailVerifier::new("simulated crash");
493 let verifiers: Vec<&dyn Verifier> = vec![&bad];
494 let result = run_verifier_chain(&verifiers, "/fake", "test");
495 assert!(!result.ok);
496 assert_eq!(result.error.as_deref(), Some("simulated crash"));
497 }
498
499 #[test]
500 fn test_run_verifier_chain_first_failure_stops() {
501 let fail_v = AlwaysFailVerifier::new("first failed");
503 let pass_v = AlwaysPassVerifier;
504 let verifiers: Vec<&dyn Verifier> = vec![&fail_v, &pass_v];
505 let result = run_verifier_chain(&verifiers, "/fake", "test");
506 assert!(!result.ok);
507 assert_eq!(result.error.as_deref(), Some("first failed"));
508 }
509
510 #[test]
511 fn test_syntax_verifier_valid_rust() {
512 let mut f = NamedTempFile::new().unwrap();
513 writeln!(f, "fn main() {{\n println!(\"hello\");\n}}").unwrap();
514 let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
515 assert!(result.ok, "expected ok, got: {:?}", result.error);
516 }
517
518 #[test]
519 fn test_syntax_verifier_invalid_rust() {
520 let mut f = NamedTempFile::new().unwrap();
521 writeln!(f, "fn main() {{{{{{").unwrap();
522 let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
523 assert!(!result.ok);
524 assert!(result.error.unwrap().contains("Invalid Rust syntax"));
525 }
526
527 #[test]
528 fn test_syntax_verifier_empty_file() {
529 let mut f = NamedTempFile::new().unwrap();
530 write!(f, "").unwrap();
531 let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
532 assert!(!result.ok);
533 assert!(result.error.unwrap().contains("File is empty"));
534 }
535
536 #[test]
537 fn test_syntax_verifier_nonexistent_file() {
538 let result = SyntaxVerifier.verify("/tmp/nonexistent_rs_file_abc123.rs", "test");
539 assert!(!result.ok);
540 assert!(result.error.unwrap().contains("Cannot read file"));
541 }
542
543 #[test]
544 fn test_syntax_verifier_whitespace_only() {
545 let mut f = NamedTempFile::new().unwrap();
546 write!(f, " \n\n \t ").unwrap();
547 let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
548 assert!(!result.ok);
549 assert!(result.error.unwrap().contains("File is empty"));
550 }
551
552 #[test]
553 fn test_syntax_verifier_complex_valid() {
554 let mut f = NamedTempFile::new().unwrap();
555 writeln!(
556 f,
557 "use std::collections::HashMap;\n\
558 \n\
559 pub struct Foo {{\n\
560 pub name: String,\n\
561 pub values: HashMap<String, i32>,\n\
562 }}\n\
563 \n\
564 impl Foo {{\n\
565 pub fn new(name: String) -> Self {{\n\
566 Self {{ name, values: HashMap::new() }}\n\
567 }}\n\
568 }}"
569 )
570 .unwrap();
571 let result = SyntaxVerifier.verify(f.path().to_str().unwrap(), "test");
572 assert!(result.ok, "expected ok, got: {:?}", result.error);
573 }
574}