Skip to main content

isideload/sideload/
application.rs

1// This file was made using https://github.com/Dadoum/Sideloader as a reference.
2// I'm planning on redoing this later to better handle entitlements, extensions, etc, but it will do for now
3
4use 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    //pub temp_path: PathBuf,
21}
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        //let mut temp_path = PathBuf::new();
33
34        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                // gather the directory contents as a string for debugging
79                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, /*temp_path*/
100        })
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        //mode: &ExtensionsBehavior,
184        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                // Since the App IDs should never be negative in the first place, it might still be worth trying to register them anyways. Who knows.
213            } else {
214                // We only do the conversion if available is positive, else we get an integral conversion error
215                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
323// impl display
324impl 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}