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 target: String,
28    pub recommendation: &'static str,
29    pub report: Result<RuleReport, RuleError>,
30    pub duration_ms: u128,
31}
32
33pub struct EngineRun {
34    pub results: Vec<EngineResult>,
35    pub total_duration_ms: u128,
36    pub cache_stats: ArtifactCacheStats,
37}
38
39pub struct Engine {
40    rules: Vec<Box<dyn AppStoreRule>>,
41    pub xcode_project: Option<crate::parsers::xcode_parser::XcodeProject>,
42}
43
44impl Engine {
45    pub fn new() -> Self {
46        Self {
47            rules: Vec::new(),
48            xcode_project: None,
49        }
50    }
51
52    pub fn register_rule(&mut self, rule: Box<dyn AppStoreRule>) {
53        self.rules.push(rule);
54    }
55
56    pub fn run<P: AsRef<Path>>(&self, ipa_path: P) -> Result<EngineRun, OrchestratorError> {
57        let run_started = Instant::now();
58        let path = ipa_path.as_ref();
59
60        if path.is_dir() {
61            return self.run_on_bundle(path, run_started);
62        }
63
64        let extracted_ipa = extract_ipa(path)?;
65        let targets = extracted_ipa
66            .discover_targets()
67            .map_err(|e| OrchestratorError::Extraction(ExtractionError::Io(e)))?;
68
69        let mut all_results = Vec::new();
70        let mut total_cache_stats = ArtifactCacheStats::default();
71
72        if targets.is_empty() {
73            // Fallback to original logic if no specific targets found
74            let res = self.run_on_bundle_internal(&extracted_ipa.payload_dir, run_started, None)?;
75            return Ok(res);
76        }
77
78        for (target_path, target_type) in targets {
79            let project_context = if target_type == "app" {
80                // Try to find a project context for this app if it's in a larger folder
81                extracted_ipa.get_project_path().ok().flatten().and_then(|p| {
82                    if p.extension().is_some_and(|e| e == "xcworkspace") {
83                        crate::parsers::xcworkspace_parser::Xcworkspace::from_path(&p)
84                            .ok()
85                            .and_then(|ws| ws.project_paths.first().cloned())
86                            .and_then(|proj_path| {
87                                crate::parsers::xcode_parser::XcodeProject::from_path(proj_path).ok()
88                            })
89                    } else {
90                        crate::parsers::xcode_parser::XcodeProject::from_path(&p).ok()
91                    }
92                })
93            } else if target_type == "project" {
94                crate::parsers::xcode_parser::XcodeProject::from_path(&target_path).ok()
95            } else if target_type == "workspace" {
96                crate::parsers::xcworkspace_parser::Xcworkspace::from_path(&target_path)
97                    .ok()
98                    .and_then(|ws| ws.project_paths.first().cloned())
99                    .and_then(|proj_path| {
100                        crate::parsers::xcode_parser::XcodeProject::from_path(proj_path).ok()
101                    })
102            } else {
103                None
104            };
105
106            let app_results =
107                self.run_on_bundle_internal(&target_path, run_started, project_context)?;
108            
109            // Tag results with target name
110            let target_name = target_path
111                .file_name()
112                .map(|n| n.to_string_lossy().into_owned())
113                .unwrap_or_else(|| "Unknown".to_string());
114
115            for mut res in app_results.results {
116                res.target = target_name.clone();
117                all_results.push(res);
118            }
119
120            // Merge stats (simplified)
121            total_cache_stats.nested_bundles.hits += app_results.cache_stats.nested_bundles.hits;
122            total_cache_stats.nested_bundles.misses += app_results.cache_stats.nested_bundles.misses;
123            // ... other stats could be merged if needed, but for now we focus on results
124        }
125
126        Ok(EngineRun {
127            results: all_results,
128            total_duration_ms: run_started.elapsed().as_millis(),
129            cache_stats: total_cache_stats,
130        })
131    }
132
133    pub fn run_on_bundle<P: AsRef<Path>>(
134        &self,
135        app_bundle_path: P,
136        run_started: Instant,
137    ) -> Result<EngineRun, OrchestratorError> {
138        self.run_on_bundle_internal(app_bundle_path, run_started, None)
139    }
140
141    fn run_on_bundle_internal<P: AsRef<Path>>(
142        &self,
143        app_bundle_path: P,
144        run_started: Instant,
145        project_override: Option<crate::parsers::xcode_parser::XcodeProject>,
146    ) -> Result<EngineRun, OrchestratorError> {
147        let app_bundle_path = app_bundle_path.as_ref();
148        let info_plist_path = app_bundle_path.join("Info.plist");
149        let info_plist = if info_plist_path.exists() {
150            Some(InfoPlist::from_file(&info_plist_path)?)
151        } else {
152            None
153        };
154
155        let context = ArtifactContext::new(
156            app_bundle_path,
157            info_plist.as_ref(),
158            project_override.as_ref().or(self.xcode_project.as_ref()),
159        );
160
161        let mut results = Vec::new();
162
163        for rule in &self.rules {
164            let rule_started = Instant::now();
165            let res = rule.evaluate(&context);
166            results.push(EngineResult {
167                rule_id: rule.id(),
168                rule_name: rule.name(),
169                category: rule.category(),
170                severity: rule.severity(),
171                target: app_bundle_path
172                    .file_name()
173                    .map(|n| n.to_string_lossy().into_owned())
174                    .unwrap_or_else(|| "Bundle".to_string()),
175                recommendation: rule.recommendation(),
176                report: res,
177                duration_ms: rule_started.elapsed().as_millis(),
178            });
179        }
180
181        Ok(EngineRun {
182            results,
183            total_duration_ms: run_started.elapsed().as_millis(),
184            cache_stats: context.cache_stats(),
185        })
186    }
187}
188
189impl Default for Engine {
190    fn default() -> Self {
191        Self::new()
192    }
193}