Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 13 additions & 1 deletion isideload/src/dev/developer_session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,7 @@ pub struct DeveloperSession {
adsid: String,
client: Arc<GrandSlam>,
anisette_generator: AnisetteDataGenerator,
device_type: DeveloperDeviceType,
}

impl DeveloperSession {
Expand All @@ -45,6 +46,7 @@ impl DeveloperSession {
adsid,
client,
anisette_generator,
device_type: DeveloperDeviceType::Ios,
}
}

Expand All @@ -67,6 +69,15 @@ impl DeveloperSession {
))
}

/// Set the platform selected for subsequent developer service requests.
pub fn set_device_type(&mut self, device_type: DeveloperDeviceType) {
self.device_type = device_type.clone();
}

pub fn device_type(&self) -> DeveloperDeviceType {
self.device_type.clone()
}

pub async fn get_headers(&mut self) -> Result<HeaderMap, Report> {
let mut headers = self
.anisette_generator
Expand Down Expand Up @@ -101,7 +112,8 @@ impl DeveloperSession {
"userLocale": ["en_US"],
});

let body = base.into_iter().chain(body.into_iter()).collect();
let mut body = base.into_iter().chain(body.into_iter()).collect();
self.device_type.apply_platform_fields(&mut body);

let text = self
.client
Expand Down
28 changes: 26 additions & 2 deletions isideload/src/dev/device_type.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,6 @@
#[derive(Debug, Clone)]
use plist::{Dictionary, Value};

#[derive(Debug, Clone, PartialEq, Eq)]
pub enum DeveloperDeviceType {
Any,
Ios,
Expand All @@ -11,10 +13,32 @@ impl DeveloperDeviceType {
match self {
DeveloperDeviceType::Any => "",
DeveloperDeviceType::Ios => "ios/",
DeveloperDeviceType::Tvos => "tvos/",
// tvOS is served by the iOS service path and selects itself with the
// `subPlatform` request field instead.
DeveloperDeviceType::Tvos => "ios/",
DeveloperDeviceType::Watchos => "watchos/",
}
}

/// Add the request fields that select this platform.
pub fn apply_platform_fields(&self, body: &mut Dictionary) {
if let DeveloperDeviceType::Tvos = self {
body.insert("subPlatform".into(), Value::String("tvOS".into()));
}
}
}

impl From<Option<&str>> for DeveloperDeviceType {
/// Map a lockdown `ProductType` (e.g. `AppleTV6,2`) to a developer device type.
///
/// A missing `ProductType` is treated as iOS.
fn from(product_type: Option<&str>) -> Self {
match product_type {
Some(product_type) if product_type.starts_with("AppleTV") => Self::Tvos,
Some(product_type) if product_type.starts_with("Watch") => Self::Watchos,
_ => Self::Ios,
}
}
}

pub fn dev_url(endpoint: &str, device_type: impl Into<Option<DeveloperDeviceType>>) -> String {
Expand Down
67 changes: 67 additions & 0 deletions isideload/src/sideload/application.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,18 @@ impl Application {
})
}

// tvOS rejects bundles whose directory or executable path contains a space
// during signature verification (0xe8008015), while iOS does not. Rename the
// bundle directory and its executables to strip spaces for signing, keeping
// the plist bundle name (CFBundleName/CFBundleDisplayName) untouched.
pub fn sanitize_signing_names(&mut self) -> Result<(), Report> {
sanitize_bundle_paths(&mut self.bundle)?;
for bundle in self.bundle.app_extensions_mut() {
sanitize_bundle_paths(bundle)?;
}
Ok(())
}

pub fn get_special_app(&self) -> Option<SpecialApp> {
let bundle_id = self.bundle.bundle_identifier().unwrap_or("");
let special_app = match bundle_id {
Expand Down Expand Up @@ -332,3 +344,58 @@ impl std::fmt::Display for SpecialApp {
}
}
}

fn sanitize_bundle_paths(bundle: &mut Bundle) -> Result<(), Report> {
let parent = bundle.bundle_dir.parent().ok_or_report()?.to_path_buf();
let folder_name = bundle
.bundle_dir
.file_stem()
.ok_or_report()?
.to_string_lossy()
.to_string();
let clean_folder = sanitize_name(&folder_name);
if clean_folder != folder_name {
let new_dir = parent.join(format!("{}.app", clean_folder));
if new_dir != bundle.bundle_dir {
let old_dir = bundle.bundle_dir.clone();
isideload_vfs::fs::rename(&old_dir, &new_dir)
.context(format!("Failed to rename bundle directory {:?}", old_dir))?;
bundle.rebase_bundle_dir(&old_dir, &new_dir);
}
}

let executable = bundle
.app_info
.get("CFBundleExecutable")
.and_then(|v| v.as_string())
.map(str::to_string)
.unwrap_or_else(|| folder_name.clone());
let clean_exec = sanitize_name(&executable);
if clean_exec != executable {
let new_executable = bundle.bundle_dir.join(&clean_exec);
let old_executable = bundle.bundle_dir.join(&executable);
if isideload_vfs::fs::metadata(&old_executable).is_ok() && old_executable != new_executable
{
isideload_vfs::fs::rename(&old_executable, &new_executable)
.context(format!("Failed to rename executable {:?}", old_executable))?;
}
bundle.app_info.insert(
"CFBundleExecutable".to_string(),
plist::Value::String(clean_exec),
);
}

Ok(())
}

fn sanitize_name(name: &str) -> String {
let clean: String = name
.chars()
.filter(|c| c.is_ascii_alphanumeric() || *c == '.' || *c == '-')
.collect();
if clean.is_empty() {
"App".to_string()
} else {
clean
}
}
15 changes: 15 additions & 0 deletions isideload/src/sideload/bundle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -124,6 +124,21 @@ impl Bundle {
&self.frameworks
}

// Rewrites the stored path of this bundle and its sub-bundles when a parent
// directory has been renamed on disk, so that they keep pointing at valid
// locations.
pub fn rebase_bundle_dir(&mut self, old_dir: &std::path::Path, new_dir: &std::path::Path) {
if let Ok(rel) = self.bundle_dir.strip_prefix(old_dir) {
self.bundle_dir = new_dir.join(rel);
}
for ext in &mut self.app_extensions {
ext.rebase_bundle_dir(old_dir, new_dir);
}
for fw in &mut self.frameworks {
fw.rebase_bundle_dir(old_dir, new_dir);
}
}

pub fn frameworks_mut(&mut self) -> &mut [Bundle] {
&mut self.frameworks
}
Expand Down
6 changes: 6 additions & 0 deletions isideload/src/sideload/sideloader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ impl<C: MaxCertsCallback> Sideloader<C> {
}

let mut app = Application::new(app_path)?;
if self.dev_session.device_type() == crate::dev::device_type::DeveloperDeviceType::Tvos {
app.sanitize_signing_names()?;
}
let special = app.get_special_app();

let main_bundle_id = app.main_bundle_id()?;
Expand Down Expand Up @@ -247,6 +250,9 @@ impl<C: MaxCertsCallback> Sideloader<C> {
{
let device_info = IdeviceInfo::from_device(device_provider).await?;

self.dev_session
.set_device_type(device_info.product_type.as_deref().into());

let team = self.get_team().await?;
self.dev_session
.ensure_device_registered(&team, &device_info.name, &device_info.udid, None)
Expand Down
19 changes: 17 additions & 2 deletions isideload/src/util/device.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,16 @@ use rootcause::prelude::*;
pub struct IdeviceInfo {
pub name: String,
pub udid: String,
pub product_type: Option<String>,
}

impl IdeviceInfo {
pub fn new(name: String, udid: String) -> Self {
Self { name, udid }
Self {
name,
udid,
product_type: None,
}
}

pub async fn from_device(device: &impl IdeviceProvider) -> Result<Self, Report> {
Expand Down Expand Up @@ -39,6 +44,16 @@ impl IdeviceInfo {
.ok_or_else(|| report!("Device UDID is not a string"))?
.to_string();

Ok(Self::new(device_name, device_udid))
let product_type = lockdown
.get_value(Some("ProductType"), None)
.await
.ok()
.and_then(|value| value.as_string().map(str::to_string));

Ok(Self {
name: device_name,
udid: device_udid,
product_type,
})
}
}