Skip to main content

verifyos_cli/parsers/
bundle_scanner.rs

1use std::fs;
2use std::path::{Path, PathBuf};
3
4const NESTED_BUNDLE_CONTAINER_DIRS: &[&str] = &[
5    "Frameworks",
6    "PlugIns",
7    "Extensions",
8    "AppClips",
9    "Watch",
10    "XPCServices",
11];
12const NESTED_BUNDLE_EXTENSIONS: &[&str] = &["app", "appex", "framework", "xpc"];
13
14#[derive(Debug, thiserror::Error)]
15pub enum BundleScanError {
16    #[error("IO Error: {0}")]
17    Io(#[from] std::io::Error),
18}
19
20#[derive(Debug, Clone)]
21pub struct BundleTarget {
22    pub bundle_path: PathBuf,
23    pub display_name: String,
24}
25
26pub fn find_nested_bundles(app_bundle_path: &Path) -> Result<Vec<BundleTarget>, BundleScanError> {
27    let mut bundles = Vec::new();
28
29    for dir_name in NESTED_BUNDLE_CONTAINER_DIRS {
30        let dir = app_bundle_path.join(dir_name);
31        collect_bundles_in_dir(&dir, &mut bundles)?;
32    }
33
34    bundles.sort_by(|a, b| a.bundle_path.cmp(&b.bundle_path));
35    bundles.dedup_by(|a, b| a.bundle_path == b.bundle_path);
36
37    Ok(bundles)
38}
39
40fn collect_bundles_in_dir(
41    dir: &Path,
42    bundles: &mut Vec<BundleTarget>,
43) -> Result<(), BundleScanError> {
44    if !dir.exists() {
45        return Ok(());
46    }
47
48    for entry in fs::read_dir(dir)? {
49        let entry = entry?;
50        let path = entry.path();
51
52        if is_nested_bundle(&path) {
53            bundles.push(BundleTarget {
54                display_name: path
55                    .file_name()
56                    .and_then(|n| n.to_str())
57                    .unwrap_or("")
58                    .to_string(),
59                bundle_path: path.clone(),
60            });
61        }
62
63        if path.is_dir() {
64            collect_bundles_in_dir(&path, bundles)?;
65        }
66    }
67
68    Ok(())
69}
70
71fn is_nested_bundle(path: &Path) -> bool {
72    path.extension()
73        .and_then(|extension| extension.to_str())
74        .map(|extension| {
75            let extension = extension.to_ascii_lowercase();
76            NESTED_BUNDLE_EXTENSIONS
77                .iter()
78                .any(|expected| expected == &extension)
79        })
80        .unwrap_or(false)
81}
82
83#[cfg(test)]
84mod tests {
85    use super::find_nested_bundles;
86    use std::path::{Path, PathBuf};
87    use tempfile::tempdir;
88
89    #[test]
90    fn finds_nested_bundles_in_additional_container_directories() {
91        let dir = tempdir().expect("temp dir");
92        let app_path = dir.path().join("Demo.app");
93
94        std::fs::create_dir_all(app_path.join("Frameworks/Foo.framework"))
95            .expect("create framework");
96        std::fs::create_dir_all(app_path.join("PlugIns/Share.appex")).expect("create appex");
97        std::fs::create_dir_all(app_path.join("Watch/WatchApp.app")).expect("create watch app");
98        std::fs::create_dir_all(app_path.join("AppClips/Clip.app")).expect("create app clip");
99        std::fs::create_dir_all(app_path.join("XPCServices/Service.xpc")).expect("create xpc");
100
101        let bundles = find_nested_bundles(&app_path).expect("nested bundles");
102        let paths: Vec<PathBuf> = bundles
103            .into_iter()
104            .map(|bundle| {
105                bundle
106                    .bundle_path
107                    .strip_prefix(&app_path)
108                    .expect("relative path")
109                    .to_path_buf()
110            })
111            .collect();
112
113        assert!(paths.contains(&Path::new("Frameworks").join("Foo.framework")));
114        assert!(paths.contains(&Path::new("PlugIns").join("Share.appex")));
115        assert!(paths.contains(&Path::new("Watch").join("WatchApp.app")));
116        assert!(paths.contains(&Path::new("AppClips").join("Clip.app")));
117        assert!(paths.contains(&Path::new("XPCServices").join("Service.xpc")));
118    }
119}