Skip to main content

verifyos_cli/parsers/
zip_extractor.rs

1use std::fs;
2use std::io;
3use std::path::{Path, PathBuf};
4use tempfile::TempDir;
5use zip::ZipArchive;
6
7#[derive(Debug, thiserror::Error)]
8pub enum ExtractionError {
9    #[error("IO Error: {0}")]
10    Io(#[from] io::Error),
11    #[error("Zip Error: {0}")]
12    Zip(#[from] zip::result::ZipError),
13    #[error("Invalid IPA path: {0}")]
14    InvalidPath(PathBuf),
15}
16
17pub struct ExtractedIpa {
18    pub temp_dir: TempDir,
19    pub payload_dir: PathBuf,
20}
21
22impl ExtractedIpa {
23    pub fn get_app_bundle_path(&self) -> io::Result<Option<PathBuf>> {
24        if !self.payload_dir.exists() {
25            return Ok(None);
26        }
27
28        // Try direct root search first
29        for entry in fs::read_dir(&self.payload_dir)? {
30            let entry = entry?;
31            let path = entry.path();
32            if path.extension().and_then(|e| e.to_str()) == Some("app") {
33                return Ok(Some(path));
34            }
35        }
36
37        // Fallback: recursive search
38        let mut queue = vec![self.payload_dir.clone()];
39        while let Some(dir) = queue.pop() {
40            if let Ok(entries) = fs::read_dir(dir) {
41                for entry in entries.flatten() {
42                    let path = entry.path();
43                    if path.is_dir() {
44                        if path.extension().and_then(|e| e.to_str()) == Some("app") {
45                            return Ok(Some(path));
46                        }
47                        queue.push(path);
48                    }
49                }
50            }
51        }
52
53        Ok(None)
54    }
55
56    pub fn get_project_path(&self) -> io::Result<Option<PathBuf>> {
57        let mut project = None;
58        let mut queue = vec![self.payload_dir.clone()];
59
60        while let Some(dir) = queue.pop() {
61            if let Ok(entries) = fs::read_dir(dir) {
62                for entry in entries.flatten() {
63                    let path = entry.path();
64                    if path.is_dir() {
65                        let extension = path.extension().and_then(|e| e.to_str());
66                        if extension == Some("xcworkspace") {
67                            return Ok(Some(path));
68                        }
69                        if project.is_none() && extension == Some("xcodeproj") {
70                            project = Some(path.clone());
71                        }
72                        queue.push(path);
73                    }
74                }
75            }
76        }
77
78        Ok(project)
79    }
80
81    pub fn discover_targets(&self) -> io::Result<Vec<(PathBuf, String)>> {
82        let mut targets = Vec::new();
83        let mut queue = vec![self.payload_dir.clone()];
84
85        while let Some(dir) = queue.pop() {
86            if let Ok(entries) = fs::read_dir(dir) {
87                for entry in entries.flatten() {
88                    let path = entry.path();
89                    let extension = path.extension().and_then(|e| e.to_str());
90
91                    if path.is_dir() {
92                        match extension {
93                            Some("app") => targets.push((path.clone(), "app".to_string())),
94                            Some("xcodeproj") => {
95                                targets.push((path.clone(), "project".to_string()))
96                            }
97                            Some("xcworkspace") => {
98                                targets.push((path.clone(), "workspace".to_string()))
99                            }
100                            _ => {
101                                // Skip common non-source directories
102                                let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
103                                if name != "node_modules"
104                                    && name != ".git"
105                                    && name != "DerivedData"
106                                    && name != "build"
107                                {
108                                    queue.push(path);
109                                }
110                            }
111                        }
112                    } else if extension == Some("ipa") {
113                        targets.push((path.clone(), "ipa".to_string()));
114                    }
115                }
116            }
117        }
118        Ok(targets)
119    }
120}
121
122pub fn extract_ipa<P: AsRef<Path>>(ipa_path: P) -> Result<ExtractedIpa, ExtractionError> {
123    let path = ipa_path.as_ref();
124    if !path.exists() {
125        return Err(ExtractionError::InvalidPath(path.to_path_buf()));
126    }
127    let file = fs::File::open(path)?;
128    let mut archive = ZipArchive::new(file)?;
129    let temp_dir = tempfile::tempdir()?;
130    let extract_path = temp_dir.path();
131
132    archive.extract(extract_path)?;
133
134    let mut payload_dir = extract_path.join("Payload");
135    if !payload_dir.exists() {
136        payload_dir = extract_path.to_path_buf();
137    }
138
139    Ok(ExtractedIpa {
140        temp_dir,
141        payload_dir,
142    })
143}