add bundle and application structs

This commit is contained in:
nab138
2026-02-10 08:28:36 -05:00
parent 5f7dc3744b
commit 0cee867ca2
7 changed files with 331 additions and 6 deletions

40
Cargo.lock generated
View File

@@ -704,6 +704,7 @@ checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c"
dependencies = [
"crc32fast",
"miniz_oxide",
"zlib-rs",
]
[[package]]
@@ -1288,6 +1289,7 @@ dependencies = [
"tracing",
"uuid",
"x509-certificate",
"zip",
]
[[package]]
@@ -2740,6 +2742,12 @@ dependencies = [
"utf-8",
]
[[package]]
name = "typed-path"
version = "0.12.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3015e6ce46d5ad8751e4a772543a30c7511468070e98e64e20165f8f81155b64"
[[package]]
name = "typenum"
version = "1.19.0"
@@ -3605,8 +3613,40 @@ dependencies = [
"syn",
]
[[package]]
name = "zip"
version = "7.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc12baa6db2b15a140161ce53d72209dacea594230798c24774139b54ecaa980"
dependencies = [
"crc32fast",
"flate2",
"indexmap",
"memchr",
"typed-path",
"zopfli",
]
[[package]]
name = "zlib-rs"
version = "0.6.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a7948af682ccbc3342b6e9420e8c51c1fe5d7bf7756002b4a3c6cabfe96a7e3c"
[[package]]
name = "zmij"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "3ff05f8caa9038894637571ae6b9e29466c1f4f829d26c9b28f869a29cbe3445"
[[package]]
name = "zopfli"
version = "0.8.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f05cd8797d63865425ff89b5c4a48804f35ba0ce8d125800027ad6017d2b5249"
dependencies = [
"bumpalo",
"crc32fast",
"log",
"simd-adler32",
]

View File

@@ -49,4 +49,5 @@ keyring = { version = "3.6.3", features = ["apple-native", "linux-native-sync-pe
# TODO: Fork to update dependencies (doubt it will ever be updated)
x509-certificate = "0.25"
rcgen = { version = "0.14.7", default-features = false, features = ["aws_lc_rs", "pem"] }
p12-keystore = { optional = true, version = "0.2.0" }
p12-keystore = { optional = true, version = "0.2.0" }
zip = { version = "7.4", default-features = false, features = ["deflate"] }

View File

@@ -22,6 +22,9 @@ pub enum SideloadError {
#[error("Developer error {0}: {1}")]
DeveloperError(i64, String),
#[error("Invalid bundle: {0}")]
InvalidBundle(String),
}
// The default reqwest error formatter sucks and provides no info

View File

@@ -0,0 +1,86 @@
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
// I'm planning on redoing this later to better handle entitlements, extensions, etc, but it will do for now
use crate::SideloadError;
use crate::sideload::bundle::Bundle;
use rootcause::prelude::*;
use std::fs::File;
use std::path::PathBuf;
use zip::ZipArchive;
pub struct Application {
pub bundle: Bundle,
//pub temp_path: PathBuf,
}
impl Application {
pub fn new(path: PathBuf) -> Result<Self, Report> {
if !path.exists() {
bail!(SideloadError::InvalidBundle(
"Application path does not exist".to_string(),
));
}
let mut bundle_path = path.clone();
//let mut temp_path = PathBuf::new();
if path.is_file() {
let temp_dir = std::env::temp_dir();
let temp_path = temp_dir
.join(path.file_name().unwrap().to_string_lossy().to_string() + "_extracted");
if temp_path.exists() {
std::fs::remove_dir_all(&temp_path)
.context("Failed to remove existing temporary directory")?;
}
std::fs::create_dir_all(&temp_path).context("Failed to create temporary directory")?;
let file = File::open(&path).context("Failed to open application archive")?;
let mut archive =
ZipArchive::new(file).context("Failed to open application archive")?;
archive
.extract(&temp_path)
.context("Failed to extract application archive")?;
let payload_folder = temp_path.join("Payload");
if payload_folder.exists() && payload_folder.is_dir() {
let app_dirs: Vec<_> = std::fs::read_dir(&payload_folder)
.context("Failed to read Payload directory")?
.filter_map(Result::ok)
.filter(|entry| entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
.filter(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
.collect();
if app_dirs.len() == 1 {
bundle_path = app_dirs[0].path();
} else if app_dirs.is_empty() {
bail!(SideloadError::InvalidBundle(
"No .app directory found in Payload".to_string(),
));
} else {
bail!(SideloadError::InvalidBundle(
"Multiple .app directories found in Payload".to_string(),
));
}
} else {
bail!(SideloadError::InvalidBundle(
"No Payload directory found in the application archive".to_string(),
));
}
}
let bundle = Bundle::new(bundle_path)?;
Ok(Application {
bundle, /*temp_path*/
})
}
pub fn is_sidestore(&self) -> bool {
self.bundle.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore"
}
pub fn is_lc_and_sidestore(&self) -> bool {
self.bundle
.frameworks()
.iter()
.any(|f| f.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore")
}
}

View File

@@ -0,0 +1,190 @@
// This file was made using https://github.com/Dadoum/Sideloader as a reference.
// I'm planning on redoing this later to better handle entitlements, extensions, etc, but it will do for now
use plist::{Dictionary, Value};
use rootcause::prelude::*;
use std::{
fs,
path::{Path, PathBuf},
};
use crate::SideloadError;
#[derive(Debug)]
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;
// Remove trailing slash/backslash
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(),
))?;
// Load app extensions from PlugIns directory
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()
};
// Load frameworks from Frameworks directory
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()
};
// Find all .dylib files in the bundle directory (recursive)
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 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")
{
// Get relative path from bundle root
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)
}

View File

@@ -1,4 +1,6 @@
pub mod application;
pub mod builder;
pub mod bundle;
pub mod cert_identity;
pub mod sideloader;
pub use builder::{SideloaderBuilder, TeamSelection};

View File

@@ -4,7 +4,10 @@ use crate::{
devices::DevicesApi,
teams::{DeveloperTeam, TeamsApi},
},
sideload::{TeamSelection, builder::MaxCertsBehavior, cert_identity::CertificateIdentity},
sideload::{
TeamSelection, application::Application, builder::MaxCertsBehavior,
cert_identity::CertificateIdentity,
},
util::{device::IdeviceInfo, storage::SideloadingStorage},
};
@@ -69,10 +72,10 @@ impl Sideloader {
)
.await?;
// info!(
// "Using certificate for machine {} with ID {}",
// cert_identity.machine_name, cert_identity.machine_id
// );
let mut app = Application::new(app_path)?;
let is_sidestore = app.is_sidestore();
let is_lc_and_sidestore = app.is_lc_and_sidestore();
Ok(())
}