Skip to main content

verifyos_cli/core/
engine.rs

1use crate::parsers::plist_reader::{InfoPlist, PlistError};
2use crate::parsers::zip_extractor::{extract_ipa, ExtractionError};
3use crate::rules::core::{
4    AppStoreRule, ArtifactCacheStats, ArtifactContext, RuleCategory, RuleError, RuleReport,
5    Severity,
6};
7use std::path::Path;
8use std::time::Instant;
9
10#[derive(Debug, thiserror::Error)]
11pub enum OrchestratorError {
12    #[error("Extraction failed: {0}")]
13    Extraction(#[from] ExtractionError),
14    #[error("Failed to parse Info.plist: {0}")]
15    PlistParse(#[from] PlistError),
16    #[error("Could not locate App Bundle (.app) inside {0}. Found entries: {1}")]
17    AppBundleNotFoundWithContext(String, String),
18    #[error("Could not locate App Bundle (.app) inside IPAPayload")]
19    AppBundleNotFound,
20}
21
22pub struct EngineResult {
23    pub rule_id: &'static str,
24    pub rule_name: &'static str,
25    pub category: RuleCategory,
26    pub severity: Severity,
27    pub recommendation: &'static str,
28    pub report: Result<RuleReport, RuleError>,
29    pub duration_ms: u128,
30}
31
32pub struct EngineRun {
33    pub results: Vec<EngineResult>,
34    pub total_duration_ms: u128,
35    pub cache_stats: ArtifactCacheStats,
36}
37
38pub struct Engine {
39    rules: Vec<Box<dyn AppStoreRule>>,
40    pub xcode_project: Option<crate::parsers::xcode_parser::XcodeProject>,
41}
42
43impl Engine {
44    pub fn new() -> Self {
45        Self {
46            rules: Vec::new(),
47            xcode_project: None,
48        }
49    }
50
51    pub fn register_rule(&mut self, rule: Box<dyn AppStoreRule>) {
52        self.rules.push(rule);
53    }
54
55    pub fn run<P: AsRef<Path>>(&self, ipa_path: P) -> Result<EngineRun, OrchestratorError> {
56        let run_started = Instant::now();
57        let path = ipa_path.as_ref();
58
59        if path.is_dir() {
60            return self.run_on_bundle(path, run_started);
61        }
62
63        let extracted_ipa = extract_ipa(path)?;
64
65        let app_bundle_path = extracted_ipa
66            .get_app_bundle_path()
67            .map_err(|e| OrchestratorError::Extraction(ExtractionError::Io(e)))?;
68
69        let app_bundle_path = match app_bundle_path {
70            Some(p) => p,
71            None => {
72                let mut entries = Vec::new();
73                if let Ok(rd) = std::fs::read_dir(&extracted_ipa.payload_dir) {
74                    for entry in rd.flatten().take(10) {
75                        entries.push(entry.file_name().to_string_lossy().into_owned());
76                    }
77                }
78                return Err(OrchestratorError::AppBundleNotFoundWithContext(
79                    extracted_ipa.payload_dir.display().to_string(),
80                    entries.join(", "),
81                ));
82            }
83        };
84
85        self.run_on_bundle(&app_bundle_path, run_started)
86    }
87
88    pub fn run_on_bundle<P: AsRef<Path>>(
89        &self,
90        app_bundle_path: P,
91        run_started: Instant,
92    ) -> Result<EngineRun, OrchestratorError> {
93        let app_bundle_path = app_bundle_path.as_ref();
94        let info_plist_path = app_bundle_path.join("Info.plist");
95        let info_plist = if info_plist_path.exists() {
96            Some(InfoPlist::from_file(&info_plist_path)?)
97        } else {
98            None
99        };
100
101        let context = ArtifactContext::new(
102            app_bundle_path,
103            info_plist.as_ref(),
104            self.xcode_project.as_ref(),
105        );
106
107        let mut results = Vec::new();
108
109        for rule in &self.rules {
110            let rule_started = Instant::now();
111            let res = rule.evaluate(&context);
112            results.push(EngineResult {
113                rule_id: rule.id(),
114                rule_name: rule.name(),
115                category: rule.category(),
116                severity: rule.severity(),
117                recommendation: rule.recommendation(),
118                report: res,
119                duration_ms: rule_started.elapsed().as_millis(),
120            });
121        }
122
123        Ok(EngineRun {
124            results,
125            total_duration_ms: run_started.elapsed().as_millis(),
126            cache_stats: context.cache_stats(),
127        })
128    }
129}
130
131impl Default for Engine {
132    fn default() -> Self {
133        Self::new()
134    }
135}