From da17fa01dcab86250d20c93e033c7bdf899a073c Mon Sep 17 00:00:00 2001 From: Abdullah Al-Banna Date: Tue, 16 Sep 2025 22:54:00 +0300 Subject: [PATCH] support installing .ipcc packages (#25) * resolve conflicts Signed-off-by: abdullah-albanna * resolve conflicts Signed-off-by: abdullah-albanna * fix typos * fix clippy --------- Signed-off-by: abdullah-albanna --- Cargo.lock | 66 +++++ idevice/Cargo.toml | 10 +- idevice/src/lib.rs | 7 + idevice/src/utils/installation.rs | 306 ---------------------- idevice/src/utils/installation/helpers.rs | 273 +++++++++++++++++++ idevice/src/utils/installation/mod.rs | 186 +++++++++++++ 6 files changed, 541 insertions(+), 307 deletions(-) delete mode 100644 idevice/src/utils/installation.rs create mode 100644 idevice/src/utils/installation/helpers.rs create mode 100644 idevice/src/utils/installation/mod.rs diff --git a/Cargo.lock b/Cargo.lock index cdd6a56..ae33fa5 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -109,6 +109,19 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "async-compression" +version = "0.4.27" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddb939d66e4ae03cee6091612804ba446b12878410cfa17f785f4dd67d4014e8" +dependencies = [ + "flate2", + "futures-core", + "futures-io", + "memchr", + "pin-project-lite", +] + [[package]] name = "async-stream" version = "0.3.6" @@ -137,6 +150,21 @@ version = "4.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8b75356056920673b02621b35afd0f7dda9306d03c79a30f5c56c44cf256e3de" +[[package]] +name = "async_zip" +version = "0.0.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0d8c50d65ce1b0e0cb65a785ff615f78860d7754290647d3b983208daa4f85e6" +dependencies = [ + "async-compression", + "crc32fast", + "futures-lite", + "pin-project", + "thiserror 2.0.12", + "tokio", + "tokio-util", +] + [[package]] name = "atomic-waker" version = "1.1.2" @@ -814,7 +842,10 @@ version = "2.6.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f5edaec856126859abb19ed65f39e90fea3a9574b9707f13539acf4abf7eb532" dependencies = [ + "fastrand", "futures-core", + "futures-io", + "parking", "pin-project-lite", ] @@ -1136,6 +1167,7 @@ name = "idevice" version = "0.1.42" dependencies = [ "async-stream", + "async_zip", "base64", "byteorder", "bytes", @@ -1714,6 +1746,26 @@ version = "2.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3148f5046208a5d56bcfc03053e3ca6334e51da8dfb19b6cdc8b306fae3283e" +[[package]] +name = "pin-project" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677f1add503faace112b9f1373e43e9e054bfdd22ff1a63c1bc485eaec6a6a8a" +dependencies = [ + "pin-project-internal", +] + +[[package]] +name = "pin-project-internal" +version = "1.1.10" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6e918e4ff8c4549eb882f14b3a4bc8c8bc93de829416eacf579f1207a8fbf861" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.104", +] + [[package]] name = "pin-project-lite" version = "0.2.16" @@ -2531,6 +2583,20 @@ dependencies = [ "tokio", ] +[[package]] +name = "tokio-util" +version = "0.7.16" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "14307c986784f72ef81c89db7d9e28d6ac26d16213b109ea501696195e6e3ce5" +dependencies = [ + "bytes", + "futures-core", + "futures-io", + "futures-sink", + "pin-project-lite", + "tokio", +] + [[package]] name = "toml" version = "0.8.23" diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 87165fe..4a255e5 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -56,6 +56,8 @@ x509-cert = { version = "0.2", optional = true, features = [ obfstr = { version = "0.4", optional = true } +async_zip = { version = "0.0.18", optional = true } + [dev-dependencies] tokio = { version = "1.43", features = ["full"] } tun-rs = { version = "2.0.8", features = ["async_tokio"] } @@ -78,7 +80,13 @@ diagnostics_relay = [] dvt = ["dep:byteorder", "dep:ns-keyed-archive"] heartbeat = ["tokio/macros", "tokio/time"] house_arrest = ["afc"] -installation_proxy = [] +installation_proxy = [ + "dep:async_zip", + "dep:futures", + "async_zip/tokio", + "async_zip/deflate", + "tokio/fs" +] springboardservices = [] misagent = [] mobile_image_mounter = ["dep:sha2"] diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 33fb862..fb55bc2 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -713,6 +713,10 @@ pub enum IdeviceError { IntegerOverflow = -65, #[error("canceled by user")] CanceledByUser = -66, + + #[cfg(feature = "installation_proxy")] + #[error("malformed package archive: {0}")] + MalformedPackageArchive(#[from] async_zip::error::ZipError) = -67, } impl IdeviceError { @@ -868,6 +872,9 @@ impl IdeviceError { IdeviceError::MalformedCommand => -64, IdeviceError::IntegerOverflow => -65, IdeviceError::CanceledByUser => -66, + + #[cfg(feature = "installation_proxy")] + IdeviceError::MalformedPackageArchive(_) => -67, } } } diff --git a/idevice/src/utils/installation.rs b/idevice/src/utils/installation.rs deleted file mode 100644 index 748363a..0000000 --- a/idevice/src/utils/installation.rs +++ /dev/null @@ -1,306 +0,0 @@ -//! High-level install/upgrade helpers -//! -//! This module provides convenient wrappers that mirror ideviceinstaller's -//! behavior by uploading a package to `PublicStaging` via AFC and then -//! issuing `Install`/`Upgrade` commands through InstallationProxy. -//! -//! Notes: -//! - The package path used by InstallationProxy must be a path inside the -//! AFC jail (e.g. `PublicStaging/`) -//! - For `.ipa` files, we upload the whole file to `PublicStaging/` -//! - For directories (developer bundles), we recursively mirror the directory -//! into `PublicStaging/` and pass that directory path. - -use std::path::Path; - -use crate::{ - IdeviceError, IdeviceService, - provider::IdeviceProvider, - services::{ - afc::{AfcClient, opcode::AfcFopenMode}, - installation_proxy::InstallationProxyClient, - }, -}; - -const PUBLIC_STAGING: &str = "PublicStaging"; - -/// Result of a prepared upload, containing the remote path to use in Install/Upgrade -struct UploadedPackageInfo { - /// Path inside the AFC jail for InstallationProxy `PackagePath` - remote_package_path: String, -} - -/// Ensure `PublicStaging` exists on device via AFC -async fn ensure_public_staging(afc: &mut AfcClient) -> Result<(), IdeviceError> { - // Try to stat and if it fails, create directory - match afc.get_file_info(PUBLIC_STAGING).await { - Ok(_) => Ok(()), - Err(_) => afc.mk_dir(PUBLIC_STAGING).await, - } -} - -/// Upload a single file to a destination path on device using AFC -async fn afc_upload_file( - afc: &mut AfcClient, - local_path: &Path, - remote_path: &str, -) -> Result<(), IdeviceError> { - let mut fd = afc.open(remote_path, AfcFopenMode::WrOnly).await?; - let bytes = tokio::fs::read(local_path).await?; - fd.write(&bytes).await?; - fd.close().await -} - -/// Recursively upload a directory to device via AFC (mirror contents) -async fn afc_upload_dir( - afc: &mut AfcClient, - local_dir: &Path, - remote_dir: &str, -) -> Result<(), IdeviceError> { - use std::collections::VecDeque; - afc.mk_dir(remote_dir).await.ok(); - - let mut queue: VecDeque<(std::path::PathBuf, String)> = VecDeque::new(); - queue.push_back((local_dir.to_path_buf(), remote_dir.to_string())); - - while let Some((cur_local, cur_remote)) = queue.pop_front() { - let mut rd = tokio::fs::read_dir(&cur_local).await?; - while let Some(entry) = rd.next_entry().await? { - let meta = entry.metadata().await?; - let name = entry.file_name(); - let name = name.to_string_lossy().into_owned(); - if name == "." || name == ".." { - continue; - } - let child_local = entry.path(); - let child_remote = format!("{}/{}", cur_remote, name); - if meta.is_dir() { - afc.mk_dir(&child_remote).await.ok(); - queue.push_back((child_local, child_remote)); - } else if meta.is_file() { - afc_upload_file(afc, &child_local, &child_remote).await?; - } - } - } - Ok(()) -} - -/// Upload a package to `PublicStaging` and return its InstallationProxy path -/// -/// - If `local_path` is a file, it will be uploaded to `PublicStaging/` -/// - If it is a directory, it will be mirrored to `PublicStaging/` -async fn upload_package_to_public_staging>( - provider: &dyn IdeviceProvider, - local_path: P, -) -> Result { - // Connect to AFC via the generic service connector - let mut afc = AfcClient::connect(provider).await?; - - ensure_public_staging(&mut afc).await?; - - let local_path = local_path.as_ref(); - let file_name: String = local_path - .file_name() - .map(|s| s.to_string_lossy().into_owned()) - .ok_or_else(|| IdeviceError::InvalidArgument)?; - let remote_path = format!("{}/{}", PUBLIC_STAGING, file_name); - - let meta = tokio::fs::metadata(local_path).await?; - if meta.is_dir() { - afc_upload_dir(&mut afc, local_path, &remote_path).await?; - } else { - afc_upload_file(&mut afc, local_path, &remote_path).await?; - } - - Ok(UploadedPackageInfo { - remote_package_path: remote_path, - }) -} - -/// Install an application by first uploading the local package and then invoking InstallationProxy. -/// -/// - Accepts a local file path or directory path. -/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. -pub async fn install_package>( - provider: &dyn IdeviceProvider, - local_path: P, - options: Option, -) -> Result<(), IdeviceError> { - let UploadedPackageInfo { - remote_package_path, - } = upload_package_to_public_staging(provider, local_path).await?; - - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.install(remote_package_path, options).await -} - -/// Upgrade an application by first uploading the local package and then invoking InstallationProxy. -/// -/// - Accepts a local file path or directory path. -/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. -pub async fn upgrade_package>( - provider: &dyn IdeviceProvider, - local_path: P, - options: Option, -) -> Result<(), IdeviceError> { - let UploadedPackageInfo { - remote_package_path, - } = upload_package_to_public_staging(provider, local_path).await?; - - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.upgrade(remote_package_path, options).await -} - -/// Same as `install_package` but providing a callback that receives `(percent_complete, state)` -/// updates while InstallationProxy performs the operation. -pub async fn install_package_with_callback, Fut, S>( - provider: &dyn IdeviceProvider, - local_path: P, - options: Option, - callback: impl Fn((u64, S)) -> Fut, - state: S, -) -> Result<(), IdeviceError> -where - Fut: std::future::Future, - S: Clone, -{ - let UploadedPackageInfo { - remote_package_path, - } = upload_package_to_public_staging(provider, local_path).await?; - - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.install_with_callback(remote_package_path, options, callback, state) - .await -} - -/// Same as `upgrade_package` but providing a callback that receives `(percent_complete, state)` -/// updates while InstallationProxy performs the operation. -pub async fn upgrade_package_with_callback, Fut, S>( - provider: &dyn IdeviceProvider, - local_path: P, - options: Option, - callback: impl Fn((u64, S)) -> Fut, - state: S, -) -> Result<(), IdeviceError> -where - Fut: std::future::Future, - S: Clone, -{ - let UploadedPackageInfo { - remote_package_path, - } = upload_package_to_public_staging(provider, local_path).await?; - - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.upgrade_with_callback(remote_package_path, options, callback, state) - .await -} - -/// Upload raw bytes to `PublicStaging/` via AFC and return the remote package path. -/// -/// - This is useful when the package is not present on disk or is generated in-memory. -async fn upload_bytes_to_public_staging( - provider: &dyn IdeviceProvider, - data: impl AsRef<[u8]>, - remote_name: &str, -) -> Result { - // Connect to AFC - let mut afc = AfcClient::connect(provider).await?; - ensure_public_staging(&mut afc).await?; - - let remote_path = format!("{}/{}", PUBLIC_STAGING, remote_name); - let mut fd = afc.open(&remote_path, AfcFopenMode::WrOnly).await?; - fd.write(data.as_ref()).await?; - fd.close().await?; - - Ok(UploadedPackageInfo { - remote_package_path: remote_path, - }) -} - -/// Install an application from raw bytes by first uploading them to `PublicStaging` and then -/// invoking InstallationProxy `Install`. -/// -/// - `remote_name` determines the remote filename under `PublicStaging`. -/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. -pub async fn install_bytes( - provider: &dyn IdeviceProvider, - data: impl AsRef<[u8]>, - remote_name: &str, - options: Option, -) -> Result<(), IdeviceError> { - let UploadedPackageInfo { - remote_package_path, - } = upload_bytes_to_public_staging(provider, data, remote_name).await?; - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.install(remote_package_path, options).await -} - -/// Same as `install_bytes` but providing a callback that receives `(percent_complete, state)` -/// updates while InstallationProxy performs the install operation. -/// -/// Tip: -/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")` -/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`). -pub async fn install_bytes_with_callback( - provider: &dyn IdeviceProvider, - data: impl AsRef<[u8]>, - remote_name: &str, - options: Option, - callback: impl Fn((u64, S)) -> Fut, - state: S, -) -> Result<(), IdeviceError> -where - Fut: std::future::Future, - S: Clone, -{ - let UploadedPackageInfo { - remote_package_path, - } = upload_bytes_to_public_staging(provider, data, remote_name).await?; - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.install_with_callback(remote_package_path, options, callback, state) - .await -} - -/// Upgrade an application from raw bytes by first uploading them to `PublicStaging` and then -/// invoking InstallationProxy `Upgrade`. -/// -/// - `remote_name` determines the remote filename under `PublicStaging`. -/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. -pub async fn upgrade_bytes( - provider: &dyn IdeviceProvider, - data: impl AsRef<[u8]>, - remote_name: &str, - options: Option, -) -> Result<(), IdeviceError> { - let UploadedPackageInfo { - remote_package_path, - } = upload_bytes_to_public_staging(provider, data, remote_name).await?; - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.upgrade(remote_package_path, options).await -} - -/// Same as `upgrade_bytes` but providing a callback that receives `(percent_complete, state)` -/// updates while InstallationProxy performs the upgrade operation. -/// -/// Tip: -/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")` -/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`). -pub async fn upgrade_bytes_with_callback( - provider: &dyn IdeviceProvider, - data: impl AsRef<[u8]>, - remote_name: &str, - options: Option, - callback: impl Fn((u64, S)) -> Fut, - state: S, -) -> Result<(), IdeviceError> -where - Fut: std::future::Future, - S: Clone, -{ - let UploadedPackageInfo { - remote_package_path, - } = upload_bytes_to_public_staging(provider, data, remote_name).await?; - let mut inst = InstallationProxyClient::connect(provider).await?; - inst.upgrade_with_callback(remote_package_path, options, callback, state) - .await -} diff --git a/idevice/src/utils/installation/helpers.rs b/idevice/src/utils/installation/helpers.rs new file mode 100644 index 0000000..bf69c48 --- /dev/null +++ b/idevice/src/utils/installation/helpers.rs @@ -0,0 +1,273 @@ +use std::{io::Cursor, path::Path}; + +use async_zip::base::read::seek::ZipFileReader; +use futures::AsyncReadExt as _; +use tokio::io::{AsyncBufRead, AsyncSeek, BufReader}; + +use crate::{ + IdeviceError, IdeviceService, + afc::{AfcClient, opcode::AfcFopenMode}, + plist, + provider::IdeviceProvider, +}; + +pub const PUBLIC_STAGING: &str = "PublicStaging"; + +pub const IPCC_REMOTE_FILE: &str = "idevice.ipcc"; + +pub const IPA_REMOTE_FILE: &str = "idevice.ipa"; + +/// Result of a prepared upload, containing the remote path to use in Install/Upgrade +pub struct InstallPackage { + /// Path inside the AFC jail for InstallationProxy `PackagePath` + pub remote_package_path: String, + + // Each package type has a special option that has to be passed + pub options: plist::Value, +} + +/// Represent the type of package being installed. +pub enum PackageType { + Ipcc, // Carrier bundle package + // an IPA package needs the build id to be installed + Ipa(String), // iOS app package + Unknown, +} + +impl PackageType { + pub fn get_remote_file(&self) -> Result<&'static str, IdeviceError> { + match self { + Self::Ipcc => Ok(IPCC_REMOTE_FILE), + Self::Ipa(_) => Ok(IPA_REMOTE_FILE), + Self::Unknown => Err(IdeviceError::InstallationProxyOperationFailed( + "invalid package".into(), + )), + } + } +} + +/// Ensure `PublicStaging` exists on device via AFC +pub async fn ensure_public_staging(afc: &mut AfcClient) -> Result<(), IdeviceError> { + // Try to stat and if it fails, create directory + match afc.get_file_info(PUBLIC_STAGING).await { + Ok(_) => Ok(()), + Err(_) => afc.mk_dir(PUBLIC_STAGING).await, + } +} + +// Get the bundle id of a package by looping through it's files and looking inside of the +// `Info.plist` +pub async fn get_bundle_id(file: &mut T) -> Result +where + T: AsyncBufRead + AsyncSeek + Unpin, +{ + let mut zip_file = ZipFileReader::with_tokio(file).await?; + + for i in 0..zip_file.file().entries().len() { + let mut entry_reader = zip_file.reader_with_entry(i).await?; + let entry = entry_reader.entry(); + + let inner_file_path = entry + .filename() + .as_str() + .map_err(|_| IdeviceError::Utf8Error)? + .trim_end_matches('/'); + + let path_segments_count = inner_file_path.split('/').count(); + + // there's multiple `Info.plist` files, we only need the one that's in the root of the + // package + // + // 1 2 3 + // which is in this case: Playload -> APP_NAME.app -> Info.plist + if inner_file_path.ends_with("Info.plist") && path_segments_count == 3 { + let mut info_plist_bytes = Vec::new(); + entry_reader.read_to_end(&mut info_plist_bytes).await?; + + let info_plist: plist::Value = plist::from_bytes(&info_plist_bytes)?; + + if let Some(bundle_id) = info_plist + .as_dictionary() + .and_then(|dict| dict.get("CFBundleIdentifier")) + .and_then(|v| v.as_string()) + { + return Ok(bundle_id.to_string()); + } + } + } + + Err(IdeviceError::NotFound) +} + +/// Determines the type of package based on its content (IPA or IPCC). +pub async fn determine_package_type>( + package: &P, +) -> Result { + let mut package_cursor = BufReader::new(Cursor::new(package.as_ref())); + + let mut archive = ZipFileReader::with_tokio(&mut package_cursor).await?; + + // the first index is the first folder name, which is probably `Payload` + // + // we need the folder inside of that `Payload`, which has an extension that we can + // determine the type of the package from it, hence the second index + let inside_folder = archive.reader_with_entry(1).await?; + + let folder_name = inside_folder + .entry() + .filename() + .as_str() + .map_err(|_| IdeviceError::Utf8Error)? + .split('/') + .nth(1) + // only if the package does not have anything inside of the `Payload` folder + .ok_or(async_zip::error::ZipError::EntryIndexOutOfBounds)? + .to_string(); + + let bundle_id = get_bundle_id(&mut package_cursor).await?; + + if folder_name.ends_with(".bundle") { + Ok(PackageType::Ipcc) + } else if folder_name.ends_with(".app") { + Ok(PackageType::Ipa(bundle_id)) + } else { + Ok(PackageType::Unknown) + } +} + +/// Upload a single file to a destination path on device using AFC +pub async fn afc_upload_file>( + afc: &mut AfcClient, + file: F, + remote_path: &str, +) -> Result<(), IdeviceError> { + let mut fd = afc.open(remote_path, AfcFopenMode::WrOnly).await?; + fd.write(file.as_ref()).await?; + fd.close().await +} + +/// Recursively upload a directory to device via AFC (mirror contents) +pub async fn afc_upload_dir( + afc: &mut AfcClient, + local_dir: &Path, + remote_dir: &str, +) -> Result<(), IdeviceError> { + use std::collections::VecDeque; + afc.mk_dir(remote_dir).await.ok(); + + let mut queue: VecDeque<(std::path::PathBuf, String)> = VecDeque::new(); + queue.push_back((local_dir.to_path_buf(), remote_dir.to_string())); + + while let Some((cur_local, cur_remote)) = queue.pop_front() { + let mut rd = tokio::fs::read_dir(&cur_local).await?; + while let Some(entry) = rd.next_entry().await? { + let meta = entry.metadata().await?; + let name = entry.file_name(); + let name = name.to_string_lossy().into_owned(); + if name == "." || name == ".." { + continue; + } + let child_local = entry.path(); + let child_remote = format!("{cur_remote}/{name}"); + if meta.is_dir() { + afc.mk_dir(&child_remote).await.ok(); + queue.push_back((child_local, child_remote)); + } else if meta.is_file() { + afc_upload_file(afc, tokio::fs::read(&child_local).await?, &child_remote).await?; + } + } + } + Ok(()) +} + +/// Upload a file to `PublicStaging` and return its InstallationProxy path +async fn upload_file_to_public_staging>( + provider: &dyn IdeviceProvider, + file: P, +) -> Result { + // Connect to AFC via the generic service connector + let mut afc = AfcClient::connect(provider).await?; + + ensure_public_staging(&mut afc).await?; + + let file = file.as_ref(); + + let package_type = determine_package_type(&file).await?; + + let remote_path = format!("{PUBLIC_STAGING}/{}", package_type.get_remote_file()?); + + afc_upload_file(&mut afc, file, &remote_path).await?; + + let options = match package_type { + PackageType::Ipcc => plist!({"PackageType": "CarrierBundle"}), + PackageType::Ipa(build_id) => plist!({"CFBundleIdentifier": build_id}), + PackageType::Unknown => plist!({}), + }; + + Ok(InstallPackage { + remote_package_path: remote_path, + options, + }) +} + +/// Recursively Upload a directory of file to `PublicStaging` +async fn upload_dir_to_public_staging>( + provider: &dyn IdeviceProvider, + file: P, +) -> Result { + let mut afc = AfcClient::connect(provider).await?; + + ensure_public_staging(&mut afc).await?; + + let file = file.as_ref(); + + let remote_path = format!("{PUBLIC_STAGING}/{IPA_REMOTE_FILE}"); + + afc_upload_dir(&mut afc, file, &remote_path).await?; + + Ok(InstallPackage { + remote_package_path: remote_path, + options: plist!({"PackageType": "Developer"}), + }) +} + +pub async fn prepare_file_upload( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + caller_options: Option, +) -> Result { + let InstallPackage { + remote_package_path, + options, + } = upload_file_to_public_staging(provider, data).await?; + let full_options = plist!({ + :, + caller_options: Option, +) -> Result { + let InstallPackage { + remote_package_path, + options, + } = upload_dir_to_public_staging(provider, &local_path).await?; + + let full_options = plist!({ + :`) +//! - For `.ipa` files, we upload the whole file to `PublicStaging/` +//! - For directories (developer bundles), we recursively mirror the directory +//! into `PublicStaging/` and pass that directory path. + +mod helpers; +use std::path::Path; + +use helpers::{InstallPackage, prepare_dir_upload, prepare_file_upload}; + +use crate::{ + IdeviceError, IdeviceService, provider::IdeviceProvider, + services::installation_proxy::InstallationProxyClient, +}; + +/// Install an application by first uploading the local package and then invoking InstallationProxy. +/// +/// - Accepts a local file path or directory path. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn install_package>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, +) -> Result<(), IdeviceError> { + install_package_with_callback(provider, local_path, options, |_| async {}, ()).await +} + +/// Same as `install_package` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the operation. +pub async fn install_package_with_callback, Fut, S>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let metadata = tokio::fs::metadata(&local_path).await?; + + if metadata.is_dir() { + let InstallPackage { + remote_package_path, + options, + } = prepare_dir_upload(provider, local_path, options).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + + inst.upgrade_with_callback(remote_package_path, Some(options), callback, state) + .await + } else { + let data = tokio::fs::read(&local_path).await?; + install_bytes_with_callback(provider, data, options, callback, state).await + } +} + +/// Upgrade an application by first uploading the local package and then invoking InstallationProxy. +/// +/// - Accepts a local file path or directory path. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn upgrade_package>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, +) -> Result<(), IdeviceError> { + upgrade_package_with_callback(provider, local_path, options, |_| async {}, ()).await +} + +/// Same as `upgrade_package` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the operation. +pub async fn upgrade_package_with_callback, Fut, S>( + provider: &dyn IdeviceProvider, + local_path: P, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let metadata = tokio::fs::metadata(&local_path).await?; + + if metadata.is_dir() { + let InstallPackage { + remote_package_path, + options, + } = prepare_dir_upload(provider, local_path, options).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + + inst.upgrade_with_callback(remote_package_path, Some(options), callback, state) + .await + } else { + let data = tokio::fs::read(&local_path).await?; + upgrade_bytes_with_callback(provider, data, options, callback, state).await + } +} + +/// Install an application from raw bytes by first uploading them to `PublicStaging` and then +/// invoking InstallationProxy `Install`. +/// +/// - `remote_name` determines the remote filename under `PublicStaging`. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn install_bytes( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + options: Option, +) -> Result<(), IdeviceError> { + install_bytes_with_callback(provider, data, options, |_| async {}, ()).await +} + +/// Same as `install_bytes` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the install operation. +/// +/// Tip: +/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")` +/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`). +pub async fn install_bytes_with_callback( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let InstallPackage { + remote_package_path, + options, + } = prepare_file_upload(provider, data, options).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + + inst.install_with_callback(remote_package_path, Some(options), callback, state) + .await +} + +/// Upgrade an application from raw bytes by first uploading them to `PublicStaging` and then +/// invoking InstallationProxy `Upgrade`. +/// +/// - `remote_name` determines the remote filename under `PublicStaging`. +/// - `options` is an InstallationProxy ClientOptions dictionary; pass `None` for defaults. +pub async fn upgrade_bytes( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + options: Option, +) -> Result<(), IdeviceError> { + upgrade_bytes_with_callback(provider, data, options, |_| async {}, ()).await +} + +/// Same as `upgrade_bytes` but providing a callback that receives `(percent_complete, state)` +/// updates while InstallationProxy performs the upgrade operation. +/// +/// Tip: +/// - When embedding assets into the binary, you can pass `include_bytes!("path/to/app.ipa")` +/// as the `data` argument and choose a desired `remote_name` (e.g. `"MyApp.ipa"`). +pub async fn upgrade_bytes_with_callback( + provider: &dyn IdeviceProvider, + data: impl AsRef<[u8]>, + options: Option, + callback: impl Fn((u64, S)) -> Fut, + state: S, +) -> Result<(), IdeviceError> +where + Fut: std::future::Future, + S: Clone, +{ + let InstallPackage { + remote_package_path, + options, + } = prepare_file_upload(provider, data, options).await?; + let mut inst = InstallationProxyClient::connect(provider).await?; + + inst.upgrade_with_callback(remote_package_path, Some(options), callback, state) + .await +}