use async_zip::base::read::seek::ZipFileReader;
use futures::AsyncReadExt as _;
use plist_macro::plist;
use std::{io::Cursor, path::Path};
use tokio::io::{AsyncBufRead, AsyncSeek, BufReader};
use crate::{
IdeviceError, IdeviceService,
afc::{AfcClient, opcode::AfcFopenMode},
installation_proxy::InstallationProxyError,
provider::IdeviceProvider,
};
pub const PUBLIC_STAGING: &str = "PublicStaging";
pub const IPCC_REMOTE_FILE: &str = "idevice.ipcc";
pub const IPA_REMOTE_FILE: &str = "idevice.ipa";
pub struct InstallPackage {
pub remote_package_path: String,
pub options: plist::Value,
}
pub enum PackageType {
Ipcc, Ipa(String), 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(InstallationProxyError::OperationFailed("invalid package".into()).into())
}
}
}
}
pub async fn ensure_public_staging(afc: &mut AfcClient) -> Result<(), IdeviceError> {
match afc.get_file_info(PUBLIC_STAGING).await {
Ok(_) => Ok(()),
Err(_) => afc.mk_dir(PUBLIC_STAGING).await,
}
}
pub async fn get_bundle_id<T>(file: &mut T) -> Result<String, IdeviceError>
where
T: AsyncBufRead + AsyncSeek + Unpin,
{
let mut zip_file = ZipFileReader::with_tokio(file)
.await
.map_err(InstallationProxyError::from)?;
for i in 0..zip_file.file().entries().len() {
let mut entry_reader = zip_file
.reader_with_entry(i)
.await
.map_err(InstallationProxyError::from)?;
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();
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)
}
pub async fn determine_package_type<P: AsRef<[u8]>>(
package: &P,
) -> Result<PackageType, IdeviceError> {
let mut package_cursor = BufReader::new(Cursor::new(package.as_ref()));
let mut archive = ZipFileReader::with_tokio(&mut package_cursor)
.await
.map_err(InstallationProxyError::from)?;
let inside_folder = archive
.reader_with_entry(1)
.await
.map_err(InstallationProxyError::from)?;
let folder_name = inside_folder
.entry()
.filename()
.as_str()
.map_err(|_| IdeviceError::Utf8Error)?
.split('/')
.nth(1)
.ok_or(InstallationProxyError::from(
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)
}
}
pub async fn afc_upload_file<F: AsRef<[u8]>>(
afc: &mut AfcClient,
file: F,
remote_path: &str,
) -> Result<(), IdeviceError> {
let mut fd = afc.open(remote_path, AfcFopenMode::WrOnly).await?;
fd.write_entire(file.as_ref()).await?;
fd.close().await
}
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(())
}
async fn upload_file_to_public_staging<P: AsRef<[u8]>>(
provider: &dyn IdeviceProvider,
file: P,
) -> Result<InstallPackage, IdeviceError> {
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,
})
}
async fn upload_dir_to_public_staging<P: AsRef<Path>>(
provider: &dyn IdeviceProvider,
file: P,
) -> Result<InstallPackage, IdeviceError> {
let mut afc = AfcClient::connect(provider).await?;
ensure_public_staging(&mut afc).await?;
let file = file.as_ref();
let remote_folder_name = file
.iter()
.next_back()
.map(|x| x.to_string_lossy().to_string())
.unwrap_or(IPA_REMOTE_FILE.to_string());
let remote_path = format!("{PUBLIC_STAGING}/{remote_folder_name}");
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<plist::Value>,
) -> Result<InstallPackage, IdeviceError> {
let InstallPackage {
remote_package_path,
options,
} = upload_file_to_public_staging(provider, data).await?;
let full_options = plist!({
:<? caller_options,
:< options,
});
Ok(InstallPackage {
remote_package_path,
options: full_options,
})
}
pub async fn prepare_dir_upload(
provider: &dyn IdeviceProvider,
local_path: impl AsRef<Path>,
caller_options: Option<plist::Value>,
) -> Result<InstallPackage, IdeviceError> {
let InstallPackage {
remote_package_path,
options,
} = upload_dir_to_public_staging(provider, &local_path).await?;
let full_options = plist!({
:<? caller_options,
:< options,
});
Ok(InstallPackage {
remote_package_path,
options: full_options,
})
}