register app ids

This commit is contained in:
nab138
2026-02-10 11:11:01 -05:00
parent 0cee867ca2
commit 728c61e4ee
6 changed files with 261 additions and 13 deletions

View File

@@ -8,9 +8,14 @@ This branch is home to isideload-next, the next major version of isideload. It f
## TODO ## TODO
- [ ] Signing apps Things left todo before the rewrite is considered finished
- [ ] Installing apps
- [ ] Remove dependency on ring - Download provisioning profiles
- Signing apps
- Installing apps
(will superceed the original isideload at this point)
- Remove dependency on ring
- More parallelism/cachng for better performance
## Licensing ## Licensing

View File

@@ -4,7 +4,7 @@ use crate::{
device_type::{DeveloperDeviceType, dev_url}, device_type::{DeveloperDeviceType, dev_url},
teams::DeveloperTeam, teams::DeveloperTeam,
}, },
util::plist::SensitivePlistAttachment, util::plist::{PlistDataExtract, SensitivePlistAttachment},
}; };
use plist::{Data, Date, Dictionary, Value}; use plist::{Data, Date, Dictionary, Value};
use plist_macro::plist; use plist_macro::plist;
@@ -17,7 +17,7 @@ pub struct AppId {
pub app_id_id: String, pub app_id_id: String,
pub identifier: String, pub identifier: String,
pub name: String, pub name: String,
pub features: Option<Dictionary>, pub features: Dictionary,
pub expiration_date: Option<Date>, pub expiration_date: Option<Date>,
} }
@@ -178,3 +178,26 @@ impl AppIdsApi for DeveloperSession {
self self
} }
} }
impl AppId {
pub async fn ensure_group_feature(
&mut self,
dev_session: &mut DeveloperSession,
team: &DeveloperTeam,
) -> Result<(), Report> {
let app_group_feature_enabled = self.features.get_bool("APG3427HIY")?;
if !app_group_feature_enabled {
let body = plist!(dict {
"APG3427HIY": true,
});
let new_features = dev_session
.update_app_id(team, self, body, None)
.await?
.features;
self.features = new_features;
}
Ok(())
}
}

View File

@@ -2,7 +2,12 @@
// I'm planning on redoing this later to better handle entitlements, extensions, etc, but it will do for now // I'm planning on redoing this later to better handle entitlements, extensions, etc, but it will do for now
use crate::SideloadError; use crate::SideloadError;
use crate::dev::app_ids::{AppId, AppIdsApi};
use crate::dev::developer_session::DeveloperSession;
use crate::dev::teams::DeveloperTeam;
use crate::sideload::builder::ExtensionsBehavior;
use crate::sideload::bundle::Bundle; use crate::sideload::bundle::Bundle;
use rootcause::option_ext::OptionExt;
use rootcause::prelude::*; use rootcause::prelude::*;
use std::fs::File; use std::fs::File;
use std::path::PathBuf; use std::path::PathBuf;
@@ -73,14 +78,140 @@ impl Application {
}) })
} }
pub fn is_sidestore(&self) -> bool { pub fn get_special_app(&self) -> Option<SpecialApp> {
self.bundle.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore" let special_app = match self.bundle.bundle_identifier().unwrap_or("") {
"com.rileytestut.AltStore" => Some(SpecialApp::AltStore),
"com.SideStore.SideStore" => Some(SpecialApp::SideStore),
_ => None,
};
if special_app.is_some() {
return special_app;
} }
pub fn is_lc_and_sidestore(&self) -> bool { if self
self.bundle .bundle
.frameworks() .frameworks()
.iter() .iter()
.any(|f| f.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore") .any(|f| f.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore")
{
return Some(SpecialApp::SideStoreLc);
}
None
}
pub fn main_bundle_id(&self) -> Result<String, Report> {
let str = self
.bundle
.bundle_identifier()
.ok_or_report()
.context("Failed to get main bundle identifier")?
.to_string();
Ok(str)
}
pub fn main_app_name(&self) -> Result<String, Report> {
let str = self
.bundle
.bundle_name()
.ok_or_report()
.context("Failed to get main app name")?
.to_string();
Ok(str)
}
pub fn update_bundle_id(
&mut self,
main_app_bundle_id: &str,
main_app_id_str: &str,
) -> Result<(), Report> {
let extensions = self.bundle.app_extensions_mut();
for ext in extensions.iter_mut() {
if let Some(id) = ext.bundle_identifier() {
if !(id.starts_with(&main_app_bundle_id) && id.len() > main_app_bundle_id.len()) {
bail!(SideloadError::InvalidBundle(format!(
"Extension {} is not part of the main app bundle identifier: {}",
ext.bundle_name().unwrap_or("Unknown"),
id
)));
} else {
ext.set_bundle_identifier(&format!(
"{}{}",
main_app_id_str,
&id[main_app_bundle_id.len()..]
));
} }
} }
}
self.bundle.set_bundle_identifier(&main_app_id_str);
Ok(())
}
pub async fn register_app_ids(
&self,
mode: &ExtensionsBehavior,
dev_session: &mut DeveloperSession,
team: &DeveloperTeam,
) -> Result<Vec<AppId>, Report> {
let extension_refs: Vec<_> = self.bundle.app_extensions().iter().collect();
let mut bundles_with_app_id = vec![&self.bundle];
bundles_with_app_id.extend(extension_refs);
let list_app_ids_response = dev_session
.list_app_ids(&team, None)
.await
.context("Failed to list app IDs for the developer team")?;
let app_ids_to_register = match mode {
ExtensionsBehavior::RegisterAll => bundles_with_app_id
.iter()
.filter(|bundle| {
let bundle_id = bundle.bundle_identifier().unwrap_or("");
!list_app_ids_response
.app_ids
.iter()
.any(|app_id| app_id.identifier == bundle_id)
})
.collect::<Vec<_>>(),
_ => todo!(),
};
if let Some(available) = list_app_ids_response.available_quantity
&& app_ids_to_register.len() > available.try_into().unwrap()
{
bail!(
"Not enough available app IDs. {} are required, but only {} are available.",
app_ids_to_register.len(),
available
);
}
for bundle in app_ids_to_register {
let id = bundle.bundle_identifier().unwrap_or("");
let name = bundle.bundle_name().unwrap_or("");
dev_session.add_app_id(&team, name, id, None).await?;
}
let list_app_id_response = dev_session.list_app_ids(&team, None).await?;
let app_ids: Vec<_> = list_app_id_response
.app_ids
.into_iter()
.filter(|app_id| {
bundles_with_app_id
.iter()
.any(|bundle| app_id.identifier == bundle.bundle_identifier().unwrap_or(""))
})
.collect();
Ok(app_ids)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum SpecialApp {
SideStore,
SideStoreLc,
AltStore,
StikStore,
}

View File

@@ -39,11 +39,44 @@ pub enum MaxCertsBehavior {
Prompt(fn(&Vec<DevelopmentCertificate>) -> Option<Vec<DevelopmentCertificate>>), Prompt(fn(&Vec<DevelopmentCertificate>) -> Option<Vec<DevelopmentCertificate>>),
} }
/// The actual behavior choices for extensions (non-prompt variants)
pub enum ExtensionsBehaviorChoice {
/// Use the main app id/profile for all sub-bundles
ReuseMain,
/// Create separate app ids/profiles for each sub-bundle
RegisterAll,
/// Remove all sub-bundles
RemoveExtensions,
}
/// Behavior used when an app contains sub bundles
pub enum ExtensionsBehavior {
/// Use the main app id/profile for all sub-bundles
ReuseMain,
/// Create separate app ids/profiles for each sub-bundle
RegisterAll,
/// Remove all sub-bundles
RemoveExtensions,
/// Prompt the user to choose one of the above behaviors
Prompt(fn(&Vec<String>) -> ExtensionsBehaviorChoice),
}
impl From<ExtensionsBehaviorChoice> for ExtensionsBehavior {
fn from(choice: ExtensionsBehaviorChoice) -> Self {
match choice {
ExtensionsBehaviorChoice::ReuseMain => ExtensionsBehavior::ReuseMain,
ExtensionsBehaviorChoice::RegisterAll => ExtensionsBehavior::RegisterAll,
ExtensionsBehaviorChoice::RemoveExtensions => ExtensionsBehavior::RemoveExtensions,
}
}
}
pub struct SideloaderBuilder { pub struct SideloaderBuilder {
developer_session: DeveloperSession, developer_session: DeveloperSession,
apple_email: String, apple_email: String,
team_selection: Option<TeamSelection>, team_selection: Option<TeamSelection>,
max_certs_behavior: Option<MaxCertsBehavior>, max_certs_behavior: Option<MaxCertsBehavior>,
extensions_behavior: Option<ExtensionsBehavior>,
storage: Option<Box<dyn SideloadingStorage>>, storage: Option<Box<dyn SideloadingStorage>>,
machine_name: Option<String>, machine_name: Option<String>,
} }
@@ -57,6 +90,7 @@ impl SideloaderBuilder {
machine_name: None, machine_name: None,
apple_email, apple_email,
max_certs_behavior: None, max_certs_behavior: None,
extensions_behavior: None,
} }
} }
@@ -94,6 +128,11 @@ impl SideloaderBuilder {
self self
} }
pub fn extensions_behavior(mut self, behavior: ExtensionsBehavior) -> Self {
self.extensions_behavior = Some(behavior);
self
}
/// Build the `Sideloader` instance with the provided configuration /// Build the `Sideloader` instance with the provided configuration
pub fn build(self) -> Sideloader { pub fn build(self) -> Sideloader {
Sideloader::new( Sideloader::new(
@@ -104,6 +143,8 @@ impl SideloaderBuilder {
self.machine_name.unwrap_or_else(|| "isideload".to_string()), self.machine_name.unwrap_or_else(|| "isideload".to_string()),
self.storage self.storage
.unwrap_or_else(|| Box::new(crate::util::storage::new_storage())), .unwrap_or_else(|| Box::new(crate::util::storage::new_storage())),
self.extensions_behavior
.unwrap_or(ExtensionsBehavior::RegisterAll),
) )
} }
} }

View File

@@ -5,7 +5,9 @@ use crate::{
teams::{DeveloperTeam, TeamsApi}, teams::{DeveloperTeam, TeamsApi},
}, },
sideload::{ sideload::{
TeamSelection, application::Application, builder::MaxCertsBehavior, TeamSelection,
application::{Application, SpecialApp},
builder::{ExtensionsBehavior, MaxCertsBehavior},
cert_identity::CertificateIdentity, cert_identity::CertificateIdentity,
}, },
util::{device::IdeviceInfo, storage::SideloadingStorage}, util::{device::IdeviceInfo, storage::SideloadingStorage},
@@ -24,6 +26,7 @@ pub struct Sideloader {
machine_name: String, machine_name: String,
apple_email: String, apple_email: String,
max_certs_behavior: MaxCertsBehavior, max_certs_behavior: MaxCertsBehavior,
extensions_behavior: ExtensionsBehavior,
} }
impl Sideloader { impl Sideloader {
@@ -37,6 +40,7 @@ impl Sideloader {
max_certs_behavior: MaxCertsBehavior, max_certs_behavior: MaxCertsBehavior,
machine_name: String, machine_name: String,
storage: Box<dyn SideloadingStorage>, storage: Box<dyn SideloadingStorage>,
extensions_behavior: ExtensionsBehavior,
) -> Self { ) -> Self {
Sideloader { Sideloader {
team_selection, team_selection,
@@ -45,6 +49,7 @@ impl Sideloader {
machine_name, machine_name,
apple_email, apple_email,
max_certs_behavior, max_certs_behavior,
extensions_behavior,
} }
} }
@@ -73,9 +78,44 @@ impl Sideloader {
.await?; .await?;
let mut app = Application::new(app_path)?; let mut app = Application::new(app_path)?;
let special = app.get_special_app();
let is_sidestore = app.is_sidestore(); let main_bundle_id = app.main_bundle_id()?;
let is_lc_and_sidestore = app.is_lc_and_sidestore(); let main_app_name = app.main_app_name()?;
let main_app_id_str = format!("{}.{}", main_bundle_id, team.team_id);
app.update_bundle_id(&main_bundle_id, &main_app_id_str)?;
let mut app_ids = app
.register_app_ids(&self.extensions_behavior, &mut self.dev_session, &team)
.await?;
let main_app_id = match app_ids
.iter()
.find(|app_id| app_id.identifier == main_app_id_str)
{
Some(id) => id,
None => {
bail!(
"Main app ID {} not found in registered app IDs",
main_app_id_str
);
}
};
for app_id in app_ids.iter_mut() {
app_id
.ensure_group_feature(&mut self.dev_session, &team)
.await?;
// TODO: Increased memory entitlement
}
let group_identifier = format!(
"group.{}",
if Some(SpecialApp::SideStoreLc) == special {
format!("com.SideStore.SideStore.{}", team.team_id)
} else {
main_app_id_str.clone()
}
);
Ok(()) Ok(())
} }

View File

@@ -55,6 +55,7 @@ pub trait PlistDataExtract {
fn get_string(&self, key: &str) -> Result<String, Report>; fn get_string(&self, key: &str) -> Result<String, Report>;
fn get_signed_integer(&self, key: &str) -> Result<i64, Report>; fn get_signed_integer(&self, key: &str) -> Result<i64, Report>;
fn get_dict(&self, key: &str) -> Result<&Dictionary, Report>; fn get_dict(&self, key: &str) -> Result<&Dictionary, Report>;
fn get_bool(&self, key: &str) -> Result<bool, Report>;
fn get_struct<T: DeserializeOwned>(&self, key: &str) -> Result<T, Report>; fn get_struct<T: DeserializeOwned>(&self, key: &str) -> Result<T, Report>;
} }
@@ -120,4 +121,11 @@ impl PlistDataExtract for Dictionary {
})?; })?;
Ok(struct_data) Ok(struct_data)
} }
fn get_bool(&self, key: &str) -> Result<bool, Report> {
self.get(key).and_then(|v| v.as_boolean()).ok_or_else(|| {
report!("Plist missing boolean for key '{}'", key)
.attach(SensitivePlistAttachment::new(self.clone()))
})
}
} }