use plist::{Dictionary, Value};
use rootcause::prelude::*;
use std::{
fs,
path::{Path, PathBuf},
};
use crate::SideloadError;
#[derive(Debug, Clone)]
pub struct Bundle {
pub app_info: Dictionary,
pub bundle_dir: PathBuf,
app_extensions: Vec<Bundle>,
frameworks: Vec<Bundle>,
_libraries: Vec<String>,
}
impl Bundle {
pub fn new(bundle_dir: PathBuf) -> Result<Self, Report> {
let mut bundle_path = bundle_dir;
if let Some(path_str) = bundle_path.to_str()
&& (path_str.ends_with('/') || path_str.ends_with('\\'))
{
bundle_path = PathBuf::from(&path_str[..path_str.len() - 1]);
}
let info_plist_path = bundle_path.join("Info.plist");
assert_bundle(
info_plist_path.exists(),
&format!("No Info.plist here: {}", info_plist_path.display()),
)?;
let plist_data = fs::read(&info_plist_path).context(SideloadError::InvalidBundle(
"Failed to read Info.plist".to_string(),
))?;
let app_info = plist::from_bytes(&plist_data).context(SideloadError::InvalidBundle(
"Failed to parse Info.plist".to_string(),
))?;
let plug_ins_dir = bundle_path.join("PlugIns");
let app_extensions = if plug_ins_dir.exists() {
fs::read_dir(&plug_ins_dir)
.context(SideloadError::InvalidBundle(
"Failed to read PlugIns directory".to_string(),
))?
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
&& entry.path().join("Info.plist").exists()
})
.filter_map(|entry| Bundle::new(entry.path()).ok())
.collect()
} else {
Vec::new()
};
let frameworks_dir = bundle_path.join("Frameworks");
let frameworks = if frameworks_dir.exists() {
fs::read_dir(&frameworks_dir)
.context(SideloadError::InvalidBundle(
"Failed to read Frameworks directory".to_string(),
))?
.filter_map(|entry| entry.ok())
.filter(|entry| {
entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false)
&& entry.path().join("Info.plist").exists()
})
.filter_map(|entry| Bundle::new(entry.path()).ok())
.collect()
} else {
Vec::new()
};
let libraries = find_dylibs(&bundle_path, &bundle_path)?;
Ok(Bundle {
app_info,
bundle_dir: bundle_path,
app_extensions,
frameworks,
_libraries: libraries,
})
}
pub fn set_bundle_identifier(&mut self, id: &str) {
self.app_info.insert(
"CFBundleIdentifier".to_string(),
Value::String(id.to_string()),
);
}
pub fn bundle_identifier(&self) -> Option<&str> {
self.app_info
.get("CFBundleIdentifier")
.and_then(|v| v.as_string())
}
pub fn bundle_name(&self) -> Option<&str> {
self.app_info
.get("CFBundleName")
.and_then(|v| v.as_string())
}
pub fn app_extensions(&self) -> &[Bundle] {
&self.app_extensions
}
pub fn app_extensions_mut(&mut self) -> &mut [Bundle] {
&mut self.app_extensions
}
pub fn frameworks(&self) -> &[Bundle] {
&self.frameworks
}
pub fn frameworks_mut(&mut self) -> &mut [Bundle] {
&mut self.frameworks
}
pub fn write_info(&self) -> Result<(), Report> {
let info_plist_path = self.bundle_dir.join("Info.plist");
plist::to_file_binary(&info_plist_path, &self.app_info).context(
SideloadError::InvalidBundle("Failed to write Info.plist".to_string()),
)?;
Ok(())
}
fn from_dylib_path(dylib_path: PathBuf) -> Self {
Self {
app_info: Dictionary::new(),
bundle_dir: dylib_path,
app_extensions: Vec::new(),
frameworks: Vec::new(),
_libraries: Vec::new(),
}
}
fn collect_dylib_bundles(&self) -> Vec<Bundle> {
self._libraries
.iter()
.map(|relative| Self::from_dylib_path(self.bundle_dir.join(relative)))
.collect()
}
fn collect_nested_bundles_into(&self, bundles: &mut Vec<Bundle>) {
for bundle in &self.app_extensions {
bundles.push(bundle.clone());
bundle.collect_nested_bundles_into(bundles);
}
for bundle in &self.frameworks {
bundles.push(bundle.clone());
bundle.collect_nested_bundles_into(bundles);
}
}
pub fn collect_nested_bundles(&self) -> Vec<Bundle> {
let mut bundles = Vec::new();
self.collect_nested_bundles_into(&mut bundles);
bundles.extend(self.collect_dylib_bundles());
bundles
}
pub fn collect_bundles_sorted(&self) -> Vec<Bundle> {
let mut bundles = self.collect_nested_bundles();
bundles.push(self.clone());
bundles.sort_by_key(|b| b.bundle_dir.components().count());
bundles.reverse();
bundles
}
}
fn assert_bundle(condition: bool, msg: &str) -> Result<(), Report> {
if !condition {
bail!(SideloadError::InvalidBundle(msg.to_string()))
} else {
Ok(())
}
}
fn find_dylibs(dir: &Path, bundle_root: &Path) -> Result<Vec<String>, Report> {
let mut libraries = Vec::new();
fn collect_dylibs(
dir: &Path,
bundle_root: &Path,
libraries: &mut Vec<String>,
) -> Result<(), Report> {
let entries = fs::read_dir(dir).context(SideloadError::InvalidBundle(format!(
"Failed to read directory {}",
dir.display()
)))?;
for entry in entries {
let entry = entry.context(SideloadError::InvalidBundle(
"Failed to read directory entry".to_string(),
))?;
let path = entry.path();
let file_type = entry.file_type().context(SideloadError::InvalidBundle(
"Failed to get file type".to_string(),
))?;
if file_type.is_file() {
if let Some(name) = path.file_name().and_then(|n| n.to_str())
&& name.ends_with(".dylib")
{
if let Ok(relative_path) = path.strip_prefix(bundle_root)
&& let Some(relative_str) = relative_path.to_str()
{
libraries.push(relative_str.to_string());
}
}
} else if file_type.is_dir() {
collect_dylibs(&path, bundle_root, libraries)?;
}
}
Ok(())
}
collect_dylibs(dir, bundle_root, &mut libraries)?;
Ok(libraries)
}