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, path_or_ipa: P) -> Result<EngineRun, OrchestratorError> {
57        let run_started = Instant::now();
58        let path = path_or_ipa.as_ref();
59
60        let mut extracted = None;
61        let targets = if path.is_dir() {
62            // Direct directory scan: mock ExtractedIpa discover_targets behavior
63            let mut targets = Vec::new();
64            let mut queue = vec![path.to_path_buf()];
65            while let Some(dir) = queue.pop() {
66                if let Ok(entries) = std::fs::read_dir(dir) {
67                    for entry in entries.flatten() {
68                        let p = entry.path();
69                        if p.is_dir() {
70                            let extension = p.extension().and_then(|e| e.to_str());
71                            match extension {
72                                Some("app") => targets.push((p.clone(), "app".to_string())),
73                                Some("xcodeproj") => {
74                                    targets.push((p.clone(), "project".to_string()))
75                                }
76                                Some("xcworkspace") => {
77                                    targets.push((p.clone(), "workspace".to_string()))
78                                }
79                                _ => queue.push(p),
80                            }
81                        } else if p.extension().and_then(|e| e.to_str()) == Some("ipa") {
82                            targets.push((p.clone(), "ipa".to_string()));
83                        }
84                    }
85                }
86            }
87            targets
88        } else {
89            let extracted_ipa = extract_ipa(path)?;
90            let t = extracted_ipa
91                .discover_targets()
92                .map_err(|e| OrchestratorError::Extraction(ExtractionError::Io(e)))?;
93            extracted = Some(extracted_ipa);
94            t
95        };
96
97        let mut all_results = Vec::new();
98        let mut total_cache_stats = ArtifactCacheStats::default();
99
100        if targets.is_empty() {
101            // Fallback to original logic if no specific targets found
102            let res = if let Some(ref ext) = extracted {
103                self.run_on_bundle_internal(&ext.payload_dir, run_started, None)?
104            } else {
105                self.run_on_bundle_internal(path, run_started, None)?
106            };
107            return Ok(res);
108        }
109
110        for (target_path, target_type) in targets {
111            let project_context = if target_type == "app" {
112                // Try to find a project context for this app if it's in a larger folder
113                let mut p_context = None;
114                if let Some(parent) = target_path.parent() {
115                    if let Ok(entries) = std::fs::read_dir(parent) {
116                        for entry in entries.flatten() {
117                            let p = entry.path();
118                            if p.extension().is_some_and(|e| e == "xcworkspace") {
119                                p_context =
120                                    crate::parsers::xcworkspace_parser::Xcworkspace::from_path(&p)
121                                        .ok()
122                                        .and_then(|ws| ws.project_paths.first().cloned())
123                                        .and_then(|proj_path| {
124                                            crate::parsers::xcode_parser::XcodeProject::from_path(
125                                                proj_path,
126                                            )
127                                            .ok()
128                                        });
129                                break;
130                            } else if p.extension().is_some_and(|e| e == "xcodeproj") {
131                                p_context =
132                                    crate::parsers::xcode_parser::XcodeProject::from_path(&p).ok();
133                            }
134                        }
135                    }
136                }
137                p_context
138            } else if target_type == "project" {
139                crate::parsers::xcode_parser::XcodeProject::from_path(&target_path).ok()
140            } else if target_type == "workspace" {
141                crate::parsers::xcworkspace_parser::Xcworkspace::from_path(&target_path)
142                    .ok()
143                    .and_then(|ws| ws.project_paths.first().cloned())
144                    .and_then(|proj_path| {
145                        crate::parsers::xcode_parser::XcodeProject::from_path(proj_path).ok()
146                    })
147            } else {
148                None
149            };
150
151            let app_results =
152                self.run_on_bundle_internal(&target_path, run_started, project_context)?;
153
154            // Tag results with target name
155            let target_name = target_path
156                .file_name()
157                .map(|n| n.to_string_lossy().into_owned())
158                .unwrap_or_else(|| "Unknown".to_string());
159
160            for mut res in app_results.results {
161                res.target = target_name.clone();
162                all_results.push(res);
163            }
164
165            // Merge stats
166            total_cache_stats.nested_bundles.hits += app_results.cache_stats.nested_bundles.hits;
167            total_cache_stats.nested_bundles.misses +=
168                app_results.cache_stats.nested_bundles.misses;
169        }
170
171        Ok(EngineRun {
172            results: all_results,
173            total_duration_ms: run_started.elapsed().as_millis(),
174            cache_stats: total_cache_stats,
175        })
176    }
177
178    pub fn run_on_bundle<P: AsRef<Path>>(
179        &self,
180        app_bundle_path: P,
181        run_started: Instant,
182    ) -> Result<EngineRun, OrchestratorError> {
183        self.run_on_bundle_internal(app_bundle_path, run_started, None)
184    }
185
186    fn run_on_bundle_internal<P: AsRef<Path>>(
187        &self,
188        app_bundle_path: P,
189        run_started: Instant,
190        project_override: Option<crate::parsers::xcode_parser::XcodeProject>,
191    ) -> Result<EngineRun, OrchestratorError> {
192        let app_bundle_path = app_bundle_path.as_ref();
193        let info_plist_path = app_bundle_path.join("Info.plist");
194        let info_plist = if info_plist_path.exists() {
195            Some(InfoPlist::from_file(&info_plist_path)?)
196        } else {
197            None
198        };
199
200        let context = ArtifactContext::new(
201            app_bundle_path,
202            info_plist.as_ref(),
203            project_override.as_ref().or(self.xcode_project.as_ref()),
204        );
205
206        let mut results = Vec::new();
207
208        for rule in &self.rules {
209            let rule_started = Instant::now();
210            let res = rule.evaluate(&context);
211            results.push(EngineResult {
212                rule_id: rule.id(),
213                rule_name: rule.name(),
214                category: rule.category(),
215                severity: rule.severity(),
216                target: app_bundle_path
217                    .file_name()
218                    .map(|n| n.to_string_lossy().into_owned())
219                    .unwrap_or_else(|| "Bundle".to_string()),
220                recommendation: rule.recommendation(),
221                report: res,
222                duration_ms: rule_started.elapsed().as_millis(),
223            });
224        }
225
226        Ok(EngineRun {
227            results,
228            total_duration_ms: run_started.elapsed().as_millis(),
229            cache_stats: context.cache_stats(),
230        })
231    }
232}
233
234impl Default for Engine {
235    fn default() -> Self {
236        Self::new()
237    }
238}