tugger_apple_bundle/
directory_bundle.rs1use {
8 crate::BundlePackageType,
9 anyhow::{anyhow, Context, Result},
10 std::path::{Path, PathBuf},
11};
12
13pub struct DirectoryBundle {
18 root: PathBuf,
20
21 root_name: String,
23
24 shallow: bool,
28
29 package_type: BundlePackageType,
31
32 info_plist: plist::Dictionary,
34}
35
36impl DirectoryBundle {
37 pub fn new_from_path(directory: &Path) -> Result<Self> {
45 if !directory.is_dir() {
46 return Err(anyhow!("{} is not a directory", directory.display()));
47 }
48
49 let root_name = directory
50 .file_name()
51 .ok_or_else(|| anyhow!("unable to resolve root directory name"))?
52 .to_string_lossy()
53 .to_string();
54
55 let contents = directory.join("Contents");
56 let shallow = !contents.is_dir();
57
58 let app_plist = if shallow {
59 directory.join("Info.plist")
60 } else {
61 contents.join("Info.plist")
62 };
63
64 let framework_plist = directory.join("Resources").join("Info.plist");
65
66 let (package_type, info_plist_path) = if app_plist.is_file() {
67 if root_name.ends_with(".app") {
68 (BundlePackageType::App, app_plist)
69 } else {
70 (BundlePackageType::Bundle, app_plist)
71 }
72 } else if framework_plist.is_file() {
73 if root_name.ends_with(".framework") {
74 (BundlePackageType::Framework, framework_plist)
75 } else {
76 (BundlePackageType::Bundle, framework_plist)
77 }
78 } else {
79 return Err(anyhow!("Info.plist not found; not a valid bundle"));
80 };
81
82 let info_plist_data = std::fs::read(&info_plist_path)?;
83 let cursor = std::io::Cursor::new(info_plist_data);
84 let value = plist::Value::from_reader_xml(cursor).context("parsing Info.plist XML")?;
85 let info_plist = value
86 .into_dictionary()
87 .ok_or_else(|| anyhow!("{} is not a dictionary", info_plist_path.display()))?;
88
89 Ok(Self {
90 root: directory.to_path_buf(),
91 root_name,
92 shallow,
93 package_type,
94 info_plist,
95 })
96 }
97
98 pub fn resolve_path(&self, path: impl AsRef<Path>) -> PathBuf {
100 if self.shallow {
101 self.root.join(path.as_ref())
102 } else {
103 self.root.join("Contents").join(path.as_ref())
104 }
105 }
106
107 pub fn root_dir(&self) -> &Path {
109 &self.root
110 }
111
112 pub fn name(&self) -> &str {
117 &self.root_name
118 }
119
120 pub fn shallow(&self) -> bool {
124 self.shallow
125 }
126
127 pub fn info_plist_path(&self) -> PathBuf {
129 match self.package_type {
130 BundlePackageType::App | BundlePackageType::Bundle => self.resolve_path("Info.plist"),
131 BundlePackageType::Framework => self.root.join("Resources").join("Info.plist"),
132 }
133 }
134
135 pub fn info_plist(&self) -> &plist::Dictionary {
137 &self.info_plist
138 }
139
140 pub fn info_plist_key_string(&self, key: &str) -> Result<Option<String>> {
145 if let Some(value) = self.info_plist.get(key) {
146 Ok(Some(
147 value
148 .as_string()
149 .ok_or_else(|| anyhow!("key {} is not a string", key))?
150 .to_string(),
151 ))
152 } else {
153 Ok(None)
154 }
155 }
156
157 pub fn package_type(&self) -> BundlePackageType {
159 self.package_type
160 }
161
162 pub fn display_name(&self) -> Result<Option<String>> {
166 self.info_plist_key_string("CFBundleDisplayName")
167 }
168
169 pub fn identifier(&self) -> Result<Option<String>> {
173 self.info_plist_key_string("CFBundleIdentifier")
174 }
175
176 pub fn version(&self) -> Result<Option<String>> {
180 self.info_plist_key_string("CFBundleVersion")
181 }
182
183 pub fn main_executable(&self) -> Result<Option<String>> {
187 self.info_plist_key_string("CFBundleExecutable")
188 }
189
190 pub fn icon_files(&self) -> Result<Option<Vec<String>>> {
194 if let Some(value) = self.info_plist.get("CFBundleIconFiles") {
195 let values = value
196 .as_array()
197 .ok_or_else(|| anyhow!("CFBundleIconFiles not an array"))?;
198
199 Ok(Some(
200 values
201 .iter()
202 .map(|x| {
203 Ok(x.as_string()
204 .ok_or_else(|| anyhow!("CFBundleIconFiles value not a string"))?
205 .to_string())
206 })
207 .collect::<Result<Vec<_>>>()?,
208 ))
209 } else {
210 Ok(None)
211 }
212 }
213
214 pub fn files(&self, traverse_nested: bool) -> Result<Vec<DirectoryBundleFile<'_>>> {
220 let nested_dirs = self
221 .nested_bundles()?
222 .into_iter()
223 .map(|(_, bundle)| bundle.root_dir().to_path_buf())
224 .collect::<Vec<_>>();
225
226 Ok(walkdir::WalkDir::new(&self.root)
227 .sort_by(|a, b| a.file_name().cmp(b.file_name()))
228 .into_iter()
229 .map(|entry| {
230 let entry = entry?;
231
232 Ok(entry.path().to_path_buf())
233 })
234 .collect::<Result<Vec<_>>>()?
235 .into_iter()
236 .filter_map(|path| {
237 if path.is_dir()
238 || (!traverse_nested
239 && nested_dirs
240 .iter()
241 .any(|prefix| path.strip_prefix(prefix).is_ok()))
242 {
243 None
244 } else {
245 Some(DirectoryBundleFile::new(self, path))
246 }
247 })
248 .collect::<Vec<_>>())
249 }
250
251 pub fn nested_bundles(&self) -> Result<Vec<(String, Self)>> {
259 Ok(walkdir::WalkDir::new(&self.root)
260 .sort_by(|a, b| a.file_name().cmp(b.file_name()))
261 .into_iter()
262 .map(|entry| {
263 let entry = entry?;
264
265 Ok(entry.path().to_path_buf())
266 })
267 .collect::<Result<Vec<_>>>()?
268 .into_iter()
269 .filter_map(|p| {
270 let file_name = p.file_name().map(|x| x.to_string_lossy());
271
272 if p.is_dir() && file_name != Some("Contents".into()) && p != self.root {
273 if let Ok(bundle) = Self::new_from_path(&p) {
274 let rel = bundle
275 .root
276 .strip_prefix(&self.root)
277 .expect("nested bundle should be in sub-directory of main");
278
279 Some((rel.to_string_lossy().to_string(), bundle))
280 } else {
281 None
282 }
283 } else {
284 None
285 }
286 })
287 .collect::<Vec<_>>())
288 }
289}
290
291pub struct DirectoryBundleFile<'a> {
293 bundle: &'a DirectoryBundle,
294 absolute_path: PathBuf,
295 relative_path: PathBuf,
296}
297
298impl<'a> DirectoryBundleFile<'a> {
299 fn new(bundle: &'a DirectoryBundle, absolute_path: PathBuf) -> Self {
300 let relative_path = absolute_path
301 .strip_prefix(&bundle.root)
302 .expect("path prefix strip should have worked")
303 .to_path_buf();
304
305 Self {
306 bundle,
307 absolute_path,
308 relative_path,
309 }
310 }
311
312 pub fn absolute_path(&self) -> &Path {
314 &self.absolute_path
315 }
316
317 pub fn relative_path(&self) -> &Path {
319 &self.relative_path
320 }
321
322 pub fn is_info_plist(&self) -> bool {
324 self.absolute_path == self.bundle.info_plist_path()
325 }
326
327 pub fn is_main_executable(&self) -> Result<bool> {
329 if let Some(main) = self.bundle.main_executable()? {
330 if self.bundle.shallow() {
331 Ok(self.absolute_path == self.bundle.resolve_path(main))
332 } else {
333 Ok(self.absolute_path == self.bundle.resolve_path(format!("MacOS/{}", main)))
334 }
335 } else {
336 Ok(false)
337 }
338 }
339
340 pub fn is_in_code_signature_directory(&self) -> bool {
342 let prefix = self.bundle.resolve_path("_CodeSignature");
343
344 self.absolute_path.starts_with(&prefix)
345 }
346
347 pub fn symlink_target(&self) -> Result<Option<PathBuf>> {
351 let metadata = self.absolute_path.metadata()?;
352
353 if metadata.file_type().is_symlink() {
354 Ok(Some(std::fs::read_link(&self.absolute_path)?))
355 } else {
356 Ok(None)
357 }
358 }
359}