Skip to main content

apcore_toolkit/output/
verifiers.rs

1// Built-in verifiers for output writers.
2//
3// Each verifier implements the Verifier trait and checks a specific
4// aspect of a written artifact.
5
6use std::fs;
7use std::panic::{catch_unwind, AssertUnwindSafe};
8
9use crate::output::types::{Verifier, VerifyResult};
10
11/// Verify that a YAML binding file is parseable and contains required fields.
12pub struct YAMLVerifier;
13
14impl Verifier for YAMLVerifier {
15    fn verify(&self, path: &str, _module_id: &str) -> VerifyResult {
16        let content = match fs::read_to_string(path) {
17            Ok(c) => c,
18            Err(e) => return VerifyResult::fail(format!("Cannot read file: {e}")),
19        };
20
21        let parsed: serde_yaml::Value = match serde_yaml::from_str(&content) {
22            Ok(v) => v,
23            Err(e) => return VerifyResult::fail(format!("Invalid YAML: {e}")),
24        };
25
26        let bindings = match parsed.get("bindings") {
27            Some(b) => b,
28            None => return VerifyResult::fail("Missing or empty 'bindings' list".into()),
29        };
30
31        let bindings_seq = match bindings.as_sequence() {
32            Some(s) if !s.is_empty() => s,
33            _ => return VerifyResult::fail("Missing or empty 'bindings' list".into()),
34        };
35
36        let first = &bindings_seq[0];
37        for field in &["module_id", "target"] {
38            match first.get(*field) {
39                Some(v) if !v.is_null() => {}
40                _ => {
41                    return VerifyResult::fail(format!(
42                        "Missing required field '{field}' in binding"
43                    ))
44                }
45            }
46        }
47
48        VerifyResult::ok()
49    }
50}
51
52/// Verify that a file contains valid JSON, with optional schema validation.
53pub struct JSONVerifier {
54    // Schema validation is omitted (would require jsonschema crate).
55    // Only checks that the file is valid JSON.
56}
57
58impl JSONVerifier {
59    /// Create a new JSONVerifier.
60    pub fn new() -> Self {
61        Self {}
62    }
63}
64
65impl Default for JSONVerifier {
66    fn default() -> Self {
67        Self::new()
68    }
69}
70
71impl Verifier for JSONVerifier {
72    fn verify(&self, path: &str, _module_id: &str) -> VerifyResult {
73        let content = match fs::read_to_string(path) {
74            Ok(c) => c,
75            Err(e) => return VerifyResult::fail(format!("Cannot read file: {e}")),
76        };
77
78        match serde_json::from_str::<serde_json::Value>(&content) {
79            Ok(_) => VerifyResult::ok(),
80            Err(e) => VerifyResult::fail(format!("Invalid JSON: {e}")),
81        }
82    }
83}
84
85/// Verify that a file starts with expected magic bytes.
86pub struct MagicBytesVerifier {
87    expected: Vec<u8>,
88}
89
90impl MagicBytesVerifier {
91    /// Create a new MagicBytesVerifier with the expected byte sequence.
92    pub fn new(expected: Vec<u8>) -> Self {
93        Self { expected }
94    }
95}
96
97impl Verifier for MagicBytesVerifier {
98    fn verify(&self, path: &str, _module_id: &str) -> VerifyResult {
99        let content = match fs::read(path) {
100            Ok(c) => c,
101            Err(e) => return VerifyResult::fail(format!("Cannot read file: {e}")),
102        };
103
104        if content.len() < self.expected.len() {
105            return VerifyResult::fail(format!(
106                "File too short: expected at least {} bytes, got {}",
107                self.expected.len(),
108                content.len()
109            ));
110        }
111
112        let header = &content[..self.expected.len()];
113        if header != self.expected.as_slice() {
114            return VerifyResult::fail(format!(
115                "Magic bytes mismatch: expected {:?}, got {:?}",
116                self.expected, header
117            ));
118        }
119
120        VerifyResult::ok()
121    }
122}
123
124/// Verify that a module is registered and retrievable from a registry.
125pub struct RegistryVerifier<'a> {
126    registry: &'a apcore::Registry,
127}
128
129impl<'a> RegistryVerifier<'a> {
130    /// Create a new RegistryVerifier checking against the given registry.
131    pub fn new(registry: &'a apcore::Registry) -> Self {
132        Self { registry }
133    }
134}
135
136impl Verifier for RegistryVerifier<'_> {
137    fn verify(&self, _path: &str, module_id: &str) -> VerifyResult {
138        if self.registry.has(module_id) {
139            VerifyResult::ok()
140        } else {
141            VerifyResult::fail(format!(
142                "Module '{module_id}' not found in registry after registration"
143            ))
144        }
145    }
146}
147
148/// Run verifiers in order; stop on first failure.
149///
150/// Each verifier call is wrapped in `catch_unwind` so that a panicking
151/// verifier does not crash the caller. A panic is reported as a
152/// `VerifyResult::fail` with a descriptive message.
153pub fn run_verifier_chain(
154    verifiers: &[&dyn Verifier],
155    path: &str,
156    module_id: &str,
157) -> VerifyResult {
158    for verifier in verifiers {
159        let verifier = AssertUnwindSafe(verifier);
160        let path = path.to_string();
161        let module_id = module_id.to_string();
162        let outcome = catch_unwind(move || verifier.verify(&path, &module_id));
163        match outcome {
164            Ok(result) if !result.ok => return result,
165            Ok(_) => {} // passed, continue
166            Err(panic_info) => {
167                let msg = if let Some(s) = panic_info.downcast_ref::<&str>() {
168                    (*s).to_string()
169                } else if let Some(s) = panic_info.downcast_ref::<String>() {
170                    s.clone()
171                } else {
172                    "unknown panic".to_string()
173                };
174                return VerifyResult::fail(format!("Verifier crashed: {msg}"));
175            }
176        }
177    }
178    VerifyResult::ok()
179}
180
181#[cfg(test)]
182mod tests {
183    use super::*;
184    use std::io::Write;
185    use tempfile::NamedTempFile;
186
187    #[test]
188    fn test_yaml_verifier_valid() {
189        let mut f = NamedTempFile::new().unwrap();
190        writeln!(f, "bindings:\n  - module_id: test\n    target: app:func").unwrap();
191        let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
192        assert!(result.ok);
193    }
194
195    #[test]
196    fn test_yaml_verifier_invalid_yaml() {
197        let mut f = NamedTempFile::new().unwrap();
198        writeln!(f, "{{invalid: yaml: [}}").unwrap();
199        let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
200        assert!(!result.ok);
201        assert!(result.error.unwrap().contains("Invalid YAML"));
202    }
203
204    #[test]
205    fn test_yaml_verifier_missing_bindings() {
206        let mut f = NamedTempFile::new().unwrap();
207        writeln!(f, "other_key: value").unwrap();
208        let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
209        assert!(!result.ok);
210    }
211
212    #[test]
213    fn test_yaml_verifier_missing_required_field() {
214        let mut f = NamedTempFile::new().unwrap();
215        writeln!(f, "bindings:\n  - module_id: test").unwrap();
216        let result = YAMLVerifier.verify(f.path().to_str().unwrap(), "test");
217        assert!(!result.ok);
218        assert!(result.error.unwrap().contains("target"));
219    }
220
221    #[test]
222    fn test_json_verifier_valid() {
223        let mut f = NamedTempFile::new().unwrap();
224        writeln!(f, r#"{{"key": "value"}}"#).unwrap();
225        let result = JSONVerifier::new().verify(f.path().to_str().unwrap(), "test");
226        assert!(result.ok);
227    }
228
229    #[test]
230    fn test_json_verifier_invalid() {
231        let mut f = NamedTempFile::new().unwrap();
232        writeln!(f, "not json").unwrap();
233        let result = JSONVerifier::new().verify(f.path().to_str().unwrap(), "test");
234        assert!(!result.ok);
235    }
236
237    #[test]
238    fn test_magic_bytes_verifier_match() {
239        let mut f = NamedTempFile::new().unwrap();
240        f.write_all(b"\x89PNG\r\n\x1a\nrest of file").unwrap();
241        let verifier = MagicBytesVerifier::new(b"\x89PNG\r\n\x1a\n".to_vec());
242        let result = verifier.verify(f.path().to_str().unwrap(), "test");
243        assert!(result.ok);
244    }
245
246    #[test]
247    fn test_magic_bytes_verifier_mismatch() {
248        let mut f = NamedTempFile::new().unwrap();
249        f.write_all(b"NOT PNG").unwrap();
250        let verifier = MagicBytesVerifier::new(b"\x89PNG".to_vec());
251        let result = verifier.verify(f.path().to_str().unwrap(), "test");
252        assert!(!result.ok);
253        assert!(result.error.unwrap().contains("mismatch"));
254    }
255
256    #[test]
257    fn test_run_verifier_chain_all_pass() {
258        let v1 = JSONVerifier::new();
259        let mut f = NamedTempFile::new().unwrap();
260        writeln!(f, r#"{{"ok": true}}"#).unwrap();
261        let verifiers: Vec<&dyn Verifier> = vec![&v1];
262        let result = run_verifier_chain(&verifiers, f.path().to_str().unwrap(), "test");
263        assert!(result.ok);
264    }
265
266    #[test]
267    fn test_run_verifier_chain_stops_on_failure() {
268        let v1 = JSONVerifier::new();
269        let mut f = NamedTempFile::new().unwrap();
270        writeln!(f, "not json").unwrap();
271        let verifiers: Vec<&dyn Verifier> = vec![&v1];
272        let result = run_verifier_chain(&verifiers, f.path().to_str().unwrap(), "test");
273        assert!(!result.ok);
274    }
275
276    #[test]
277    fn test_run_verifier_chain_empty() {
278        let verifiers: Vec<&dyn Verifier> = vec![];
279        let result = run_verifier_chain(&verifiers, "", "test");
280        assert!(result.ok);
281    }
282
283    #[test]
284    fn test_yaml_verifier_nonexistent_file() {
285        let result = YAMLVerifier.verify("/tmp/nonexistent_file_abc123.yaml", "test");
286        assert!(!result.ok);
287        assert!(result.error.unwrap().contains("Cannot read file"));
288    }
289
290    #[test]
291    fn test_json_verifier_nonexistent_file() {
292        let result = JSONVerifier::new().verify("/tmp/nonexistent_file_abc123.json", "test");
293        assert!(!result.ok);
294        assert!(result.error.unwrap().contains("Cannot read file"));
295    }
296
297    #[test]
298    fn test_magic_bytes_verifier_file_too_short() {
299        let mut f = NamedTempFile::new().unwrap();
300        f.write_all(b"AB").unwrap();
301        let verifier = MagicBytesVerifier::new(b"ABCDEF".to_vec());
302        let result = verifier.verify(f.path().to_str().unwrap(), "test");
303        assert!(!result.ok);
304        let err = result.error.unwrap();
305        assert!(err.contains("File too short"), "got: {err}");
306        assert!(err.contains("6"), "should mention expected length");
307        assert!(err.contains("2"), "should mention actual length");
308    }
309
310    /// A verifier that panics, used to test catch_unwind in the chain.
311    struct PanickingVerifier;
312
313    impl Verifier for PanickingVerifier {
314        fn verify(&self, _path: &str, _module_id: &str) -> VerifyResult {
315            panic!("verifier exploded");
316        }
317    }
318
319    #[test]
320    fn test_run_verifier_chain_panic_caught() {
321        let bad = PanickingVerifier;
322        let verifiers: Vec<&dyn Verifier> = vec![&bad];
323        let result = run_verifier_chain(&verifiers, "/fake", "test");
324        assert!(!result.ok);
325        let err = result.error.unwrap();
326        assert!(
327            err.contains("Verifier crashed"),
328            "expected crash message, got: {err}"
329        );
330        assert!(
331            err.contains("verifier exploded"),
332            "expected panic message, got: {err}"
333        );
334    }
335
336    /// A verifier that always returns an error, simulating a "crash" without panicking.
337    struct AlwaysFailVerifier {
338        message: String,
339    }
340
341    impl AlwaysFailVerifier {
342        fn new(message: &str) -> Self {
343            Self {
344                message: message.to_string(),
345            }
346        }
347    }
348
349    impl Verifier for AlwaysFailVerifier {
350        fn verify(&self, _path: &str, _module_id: &str) -> VerifyResult {
351            VerifyResult::fail(self.message.clone())
352        }
353    }
354
355    /// A verifier that always passes.
356    struct AlwaysPassVerifier;
357
358    impl Verifier for AlwaysPassVerifier {
359        fn verify(&self, _path: &str, _module_id: &str) -> VerifyResult {
360            VerifyResult::ok()
361        }
362    }
363
364    #[test]
365    fn test_run_verifier_chain_crash_caught() {
366        let bad = AlwaysFailVerifier::new("simulated crash");
367        let verifiers: Vec<&dyn Verifier> = vec![&bad];
368        let result = run_verifier_chain(&verifiers, "/fake", "test");
369        assert!(!result.ok);
370        assert_eq!(result.error.as_deref(), Some("simulated crash"));
371    }
372
373    #[test]
374    fn test_run_verifier_chain_first_failure_stops() {
375        // The first verifier fails, so the second should never matter.
376        let fail_v = AlwaysFailVerifier::new("first failed");
377        let pass_v = AlwaysPassVerifier;
378        let verifiers: Vec<&dyn Verifier> = vec![&fail_v, &pass_v];
379        let result = run_verifier_chain(&verifiers, "/fake", "test");
380        assert!(!result.ok);
381        assert_eq!(result.error.as_deref(), Some("first failed"));
382    }
383}