|
| 1 | +//! Bundle layout helpers for PolicyWitness.app. |
| 2 | +//! |
| 3 | +//! The controller resolves embedded tools relative to its own executable so the |
| 4 | +//! app bundle can be relocated without rewriting paths. |
| 5 | +
|
| 6 | +use std::path::{Component, Path, PathBuf}; |
| 7 | + |
| 8 | +use crate::plist::plist_key_string; |
| 9 | + |
| 10 | +pub const PW_RUNNER_SERVICE_DIR: &str = "PWRunner"; |
| 11 | + |
| 12 | +#[derive(Clone)] |
| 13 | +pub struct PWRunnerBundleInfo { |
| 14 | + pub bundle_id: String, |
| 15 | + pub executable: String, |
| 16 | +} |
| 17 | + |
| 18 | +pub fn validate_tool_name(tool_name: &str) -> Result<(), String> { |
| 19 | + let mut components = Path::new(tool_name).components(); |
| 20 | + match (components.next(), components.next()) { |
| 21 | + (Some(Component::Normal(_)), None) => Ok(()), |
| 22 | + _ => Err(format!( |
| 23 | + "invalid tool name {tool_name:?} (must be a single path component)" |
| 24 | + )), |
| 25 | + } |
| 26 | +} |
| 27 | + |
| 28 | +pub fn app_root_from_current_exe() -> Result<PathBuf, String> { |
| 29 | + let exe = std::env::current_exe().map_err(|e| format!("current_exe() failed: {e}"))?; |
| 30 | + // Expected layout: PolicyWitness.app/Contents/MacOS/policy-witness |
| 31 | + let contents_dir = exe |
| 32 | + .parent() |
| 33 | + .and_then(|p| p.parent()) |
| 34 | + .ok_or_else(|| format!("unexpected executable location: {}", exe.display()))?; |
| 35 | + let app_root = contents_dir |
| 36 | + .parent() |
| 37 | + .ok_or_else(|| format!("unexpected executable location: {}", exe.display()))?; |
| 38 | + Ok(app_root.to_path_buf()) |
| 39 | +} |
| 40 | + |
| 41 | +pub fn resolve_contents_macos_tool(tool_name: &str) -> Result<PathBuf, String> { |
| 42 | + validate_tool_name(tool_name)?; |
| 43 | + let exe = std::env::current_exe().map_err(|e| format!("current_exe() failed: {e}"))?; |
| 44 | + let contents_dir = exe |
| 45 | + .parent() |
| 46 | + .and_then(|p| p.parent()) |
| 47 | + .ok_or_else(|| format!("unexpected executable location: {}", exe.display()))?; |
| 48 | + // Tools live alongside the controller under Contents/MacOS. |
| 49 | + let candidate = contents_dir.join("MacOS").join(tool_name); |
| 50 | + if candidate.exists() { |
| 51 | + return Ok(candidate); |
| 52 | + } |
| 53 | + Err(format!( |
| 54 | + "embedded tool not found in Contents/MacOS: {tool_name:?} (expected: {})", |
| 55 | + candidate.display() |
| 56 | + )) |
| 57 | +} |
| 58 | + |
| 59 | +pub fn resolve_pw_runner_bundle_info(app_root: &Path) -> Result<PWRunnerBundleInfo, String> { |
| 60 | + let plist = app_root |
| 61 | + .join("Contents") |
| 62 | + .join("XPCServices") |
| 63 | + .join(format!("{PW_RUNNER_SERVICE_DIR}.xpc")) |
| 64 | + .join("Contents") |
| 65 | + .join("Info.plist"); |
| 66 | + if !plist.exists() { |
| 67 | + return Err(format!("missing PWRunner Info.plist: {}", plist.display())); |
| 68 | + } |
| 69 | + let bundle_id = plist_key_string(&plist, "CFBundleIdentifier")?; |
| 70 | + let executable = plist_key_string(&plist, "CFBundleExecutable")?; |
| 71 | + Ok(PWRunnerBundleInfo { |
| 72 | + bundle_id, |
| 73 | + executable, |
| 74 | + }) |
| 75 | +} |
0 commit comments