Skip to main content

verifyos_cli/parsers/
plist_reader.rs

1use plist::Value;
2use std::path::Path;
3
4#[derive(Debug, thiserror::Error)]
5pub enum PlistError {
6    #[error("Plist Error: {0}")]
7    ParseError(#[from] plist::Error),
8    #[error("Not a dictionary")]
9    NotADictionary,
10}
11
12#[derive(Debug, Clone)]
13pub struct InfoPlist {
14    pub root: Value,
15}
16
17impl InfoPlist {
18    pub fn from_file<P: AsRef<Path>>(path: P) -> Result<Self, PlistError> {
19        let value = Value::from_file(path)?;
20        if value.as_dictionary().is_none() {
21            return Err(PlistError::NotADictionary);
22        }
23        Ok(Self { root: value })
24    }
25
26    pub fn from_bytes(bytes: &[u8]) -> Result<Self, PlistError> {
27        let value = Value::from_reader(std::io::Cursor::new(bytes))?;
28        if value.as_dictionary().is_none() {
29            return Err(PlistError::NotADictionary);
30        }
31        Ok(Self { root: value })
32    }
33
34    pub fn from_dictionary(dict: plist::Dictionary) -> Self {
35        Self {
36            root: Value::Dictionary(dict),
37        }
38    }
39
40    pub fn get_string(&self, key: &str) -> Option<&str> {
41        self.root
42            .as_dictionary()
43            .and_then(|dict| dict.get(key))
44            .and_then(|v| v.as_string())
45    }
46
47    pub fn get_bool(&self, key: &str) -> Option<bool> {
48        self.root
49            .as_dictionary()
50            .and_then(|dict| dict.get(key))
51            .and_then(|v| v.as_boolean())
52    }
53
54    pub fn has_key(&self, key: &str) -> bool {
55        self.root
56            .as_dictionary()
57            .map(|dict| dict.contains_key(key))
58            .unwrap_or(false)
59    }
60
61    pub fn get_dictionary(&self, key: &str) -> Option<&plist::Dictionary> {
62        self.root
63            .as_dictionary()
64            .and_then(|dict| dict.get(key))
65            .and_then(|v| v.as_dictionary())
66    }
67
68    pub fn get_value(&self, key: &str) -> Option<&Value> {
69        self.root.as_dictionary().and_then(|dict| dict.get(key))
70    }
71
72    pub fn get_array_strings(&self, key: &str) -> Option<Vec<String>> {
73        self.root
74            .as_dictionary()
75            .and_then(|dict| dict.get(key))
76            .and_then(|v| v.as_array())
77            .map(|arr| {
78                arr.iter()
79                    .filter_map(|v| v.as_string().map(|s| s.to_string()))
80                    .collect()
81            })
82    }
83
84    pub fn get_app_icons(&self) -> Vec<String> {
85        let mut icons = Vec::new();
86
87        // 1. Check CFBundleIcons
88        if let Some(icons_dict) = self.get_dictionary("CFBundleIcons") {
89            if let Some(primary_icon) = icons_dict
90                .get("CFBundlePrimaryIcon")
91                .and_then(|v| v.as_dictionary())
92            {
93                if let Some(files) = primary_icon
94                    .get("CFBundleIconFiles")
95                    .and_then(|v| v.as_array())
96                {
97                    for file in files {
98                        if let Some(name) = file.as_string() {
99                            icons.push(name.to_string());
100                        }
101                    }
102                }
103            }
104        }
105
106        // 2. Fallback to CFBundleIconFiles (older style)
107        if icons.is_empty() {
108            if let Some(files) = self.get_array_strings("CFBundleIconFiles") {
109                icons.extend(files);
110            }
111        }
112
113        // 3. Fallback to CFBundleIconFile (even older style)
114        if icons.is_empty() {
115            if let Some(file) = self.get_string("CFBundleIconFile") {
116                icons.push(file.to_string());
117            }
118        }
119
120        icons
121    }
122}