isideload/sideload/
application.rs1use crate::SideloadError;
5use crate::dev::app_ids::{AppId, AppIdsApi};
6use crate::dev::developer_session::DeveloperSession;
7use crate::dev::teams::DeveloperTeam;
8use crate::sideload::bundle::Bundle;
9use crate::sideload::cert_identity::CertificateIdentity;
10use isideload_vfs::fs::File;
11use rootcause::option_ext::OptionExt;
12use rootcause::prelude::*;
13use std::io::Write;
14use std::path::PathBuf;
15use tracing::{info, warn};
16use zip::ZipArchive;
17
18pub struct Application {
19 pub bundle: Bundle,
20 }
22
23impl Application {
24 pub fn new(path: PathBuf) -> Result<Self, Report> {
25 if !isideload_vfs::fs::metadata(&path).is_ok() {
26 bail!(SideloadError::InvalidBundle(
27 "Application path does not exist".to_string(),
28 ));
29 }
30
31 let mut bundle_path = path.clone();
32 if isideload_vfs::fs::metadata(&bundle_path)?.is_file() {
35 let temp_dir = isideload_vfs::fs::temp_dir();
36 let temp_path = temp_dir.join(
37 path.file_name()
38 .ok_or_report()?
39 .to_string_lossy()
40 .to_string()
41 + "_extracted",
42 );
43 if isideload_vfs::fs::metadata(&temp_path).is_ok() {
44 isideload_vfs::fs::remove_dir_all(&temp_path)
45 .context("Failed to remove existing temporary directory")?;
46 }
47 isideload_vfs::fs::create_dir_all(&temp_path)
48 .context("Failed to create temporary directory")?;
49
50 let file = File::open(&path).context("Failed to open application archive")?;
51 let mut archive =
52 ZipArchive::new(file).context("Failed to open application archive")?;
53
54 archive
55 .extract(&temp_path)
56 .context("Failed to extract application archive")?;
57
58 let payload_folder = temp_path.join("Payload");
59 if isideload_vfs::fs::metadata(&payload_folder).is_ok() && payload_folder.is_dir() {
60 let app_dirs: Vec<_> = isideload_vfs::fs::read_dir(&payload_folder)
61 .context("Failed to read Payload directory")?
62 .filter_map(Result::ok)
63 .filter(|entry| entry.file_type().map(|ft| ft.is_dir()).unwrap_or(false))
64 .filter(|entry| entry.path().extension().is_some_and(|ext| ext == "app"))
65 .collect();
66 if app_dirs.len() == 1 {
67 bundle_path = app_dirs[0].path();
68 } else if app_dirs.is_empty() {
69 bail!(SideloadError::InvalidBundle(
70 "No .app directory found in Payload".to_string(),
71 ));
72 } else {
73 bail!(SideloadError::InvalidBundle(
74 "Multiple .app directories found in Payload".to_string(),
75 ));
76 }
77 } else {
78 let mut contents = String::new();
80 if isideload_vfs::fs::metadata(&temp_path).is_ok() && temp_path.is_dir() {
81 let entries = isideload_vfs::fs::read_dir(&temp_path)
82 .context("Failed to read temporary directory for error reporting")?;
83 for entry in entries {
84 if let Ok(entry) = entry {
85 contents
86 .push_str(&format!("{}\n", entry.file_name().to_string_lossy()));
87 }
88 }
89 }
90 bail!(SideloadError::InvalidBundle(format!(
91 "No Payload directory found in the application archive, instead: {}",
92 contents
93 ),));
94 }
95 }
96 let bundle = Bundle::new(bundle_path)?;
97
98 Ok(Application {
99 bundle, })
101 }
102
103 pub fn get_special_app(&self) -> Option<SpecialApp> {
104 let bundle_id = self.bundle.bundle_identifier().unwrap_or("");
105 let special_app = match bundle_id {
106 "com.rileytestut.AltStore" => Some(SpecialApp::AltStore),
107 "com.SideStore.SideStore" => Some(SpecialApp::SideStore),
108 "app.stik.store" => Some(SpecialApp::StikStore),
109 _ => None,
110 };
111 if special_app.is_some() {
112 return special_app;
113 }
114
115 if self
116 .bundle
117 .frameworks()
118 .iter()
119 .any(|f| f.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore")
120 {
121 return Some(SpecialApp::SideStoreLc);
122 }
123
124 if bundle_id == "com.kdt.livecontainer" {
125 return Some(SpecialApp::LiveContainer);
126 }
127
128 None
129 }
130
131 pub fn main_bundle_id(&self) -> Result<String, Report> {
132 let str = self
133 .bundle
134 .bundle_identifier()
135 .ok_or_report()
136 .context("Failed to get main bundle identifier")?
137 .to_string();
138
139 Ok(str)
140 }
141
142 pub fn main_app_name(&self) -> Result<String, Report> {
143 let str = self
144 .bundle
145 .bundle_name()
146 .ok_or_report()
147 .context("Failed to get main app name")?
148 .to_string();
149
150 Ok(str)
151 }
152
153 pub fn update_bundle_id(
154 &mut self,
155 main_app_bundle_id: &str,
156 main_app_id_str: &str,
157 ) -> Result<(), Report> {
158 let extensions = self.bundle.app_extensions_mut();
159 for ext in extensions.iter_mut() {
160 if let Some(id) = ext.bundle_identifier() {
161 if !(id.starts_with(main_app_bundle_id) && id.len() > main_app_bundle_id.len()) {
162 bail!(SideloadError::InvalidBundle(format!(
163 "Extension {} is not part of the main app bundle identifier: {}",
164 ext.bundle_name().unwrap_or("Unknown"),
165 id
166 )));
167 } else {
168 ext.set_bundle_identifier(&format!(
169 "{}{}",
170 main_app_id_str,
171 &id[main_app_bundle_id.len()..]
172 ));
173 }
174 }
175 }
176 self.bundle.set_bundle_identifier(main_app_id_str);
177
178 Ok(())
179 }
180
181 pub async fn register_app_ids(
182 &self,
183 dev_session: &mut DeveloperSession,
185 team: &DeveloperTeam,
186 ) -> Result<Vec<AppId>, Report> {
187 let extension_refs: Vec<_> = self.bundle.app_extensions().iter().collect();
188 let mut bundles_with_app_id = vec![&self.bundle];
189 bundles_with_app_id.extend(extension_refs);
190
191 let list_app_ids_response = dev_session
192 .list_app_ids(team, None)
193 .await
194 .context("Failed to list app IDs for the developer team")?;
195 let app_ids_to_register = bundles_with_app_id
196 .iter()
197 .filter(|bundle| {
198 let bundle_id = bundle.bundle_identifier().unwrap_or("");
199 !list_app_ids_response
200 .app_ids
201 .iter()
202 .any(|app_id| app_id.identifier == bundle_id)
203 })
204 .collect::<Vec<_>>();
205
206 if let Some(available) = list_app_ids_response.available_quantity {
207 if available < 0 {
208 warn!(
209 "Apple reports a negative number of available app IDs ({}), which shouldn't be possible.",
210 available
211 );
212 } else {
214 if app_ids_to_register.len() > available.try_into()? {
216 bail!(
217 "Not enough available app IDs. {} {} required, but only {} {} available.",
218 app_ids_to_register.len(),
219 if app_ids_to_register.len() == 1 {
220 "is"
221 } else {
222 "are"
223 },
224 available,
225 if available == 1 { "is" } else { "are" }
226 );
227 }
228 }
229 }
230
231 for bundle in app_ids_to_register {
232 let id = bundle.bundle_identifier().unwrap_or("");
233 let name = bundle.bundle_name().unwrap_or("");
234 dev_session.add_app_id(team, name, id, None).await?;
235 }
236 let list_app_id_response = dev_session.list_app_ids(team, None).await?;
237 let app_ids: Vec<_> = list_app_id_response
238 .app_ids
239 .into_iter()
240 .filter(|app_id| {
241 bundles_with_app_id
242 .iter()
243 .any(|bundle| app_id.identifier == bundle.bundle_identifier().unwrap_or(""))
244 })
245 .collect();
246
247 info!("Registered app IDs");
248 Ok(app_ids)
249 }
250
251 pub async fn apply_special_app_behavior(
252 &mut self,
253 special: &Option<SpecialApp>,
254 group_identifier: &str,
255 cert: &CertificateIdentity,
256 ) -> Result<(), Report> {
257 let Some(special) = special.as_ref() else {
258 return Ok(());
259 };
260
261 if matches!(
262 special,
263 SpecialApp::SideStoreLc
264 | SpecialApp::SideStore
265 | SpecialApp::AltStore
266 | SpecialApp::StikStore
267 ) {
268 if !matches!(special, SpecialApp::StikStore) {
269 self.bundle.app_info.insert(
270 "ALTAppGroups".to_string(),
271 plist::Value::Array(vec![plist::Value::String(group_identifier.to_string())]),
272 );
273 }
274 info!("Injecting certificate for {}", special);
275
276 let target_bundle =
277 match special {
278 SpecialApp::SideStoreLc => self.bundle.frameworks_mut().iter_mut().find(|fw| {
279 fw.bundle_identifier().unwrap_or("") == "com.SideStore.SideStore"
280 }),
281 _ => Some(&mut self.bundle),
282 };
283
284 if let Some(target_bundle) = target_bundle {
285 let id_key = match special {
286 SpecialApp::StikStore => "MachineID",
287 _ => "ALTCertificateID",
288 };
289 let cert_file_name = match special {
290 SpecialApp::StikStore => "Certificate.p12",
291 _ => "ALTCertificate.p12",
292 };
293 target_bundle.app_info.insert(
294 id_key.to_string(),
295 plist::Value::String(cert.get_serial_number()),
296 );
297
298 let p12_bytes = cert
299 .as_p12(&cert.machine_id)
300 .await
301 .context("Failed to encode cert as p12")?;
302 let alt_cert_path = target_bundle.bundle_dir.join(cert_file_name);
303
304 let mut file = isideload_vfs::fs::File::create(&alt_cert_path)
305 .context(format!("Failed to create {}", cert_file_name))?;
306 file.write_all(&p12_bytes)
307 .context(format!("Failed to write {}", cert_file_name))?;
308 }
309 }
310 Ok(())
311 }
312}
313
314#[derive(Debug, Clone, PartialEq, Eq)]
315pub enum SpecialApp {
316 SideStore,
317 SideStoreLc,
318 LiveContainer,
319 AltStore,
320 StikStore,
321}
322
323impl std::fmt::Display for SpecialApp {
325 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
326 match self {
327 SpecialApp::SideStore => write!(f, "SideStore"),
328 SpecialApp::SideStoreLc => write!(f, "SideStore+LiveContainer"),
329 SpecialApp::LiveContainer => write!(f, "LiveContainer"),
330 SpecialApp::AltStore => write!(f, "AltStore"),
331 SpecialApp::StikStore => write!(f, "StikStore"),
332 }
333 }
334}