verifyos_cli/core/
engine.rs1use 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}