diff --git a/examples/minimal/src/main.rs b/examples/minimal/src/main.rs index ae250d0..eb74f6f 100644 --- a/examples/minimal/src/main.rs +++ b/examples/minimal/src/main.rs @@ -59,9 +59,9 @@ async fn main() { .expect("No developer teams available for this account"); let res = dev_session - .list_all_development_certs(team, None) + .list_devices(team, None) .await .expect("Failed to list developer devices"); - println!("{:#?}", res); + println!("{:?}", res); } diff --git a/isideload/src/dev/developer_session.rs b/isideload/src/dev/developer_session.rs index 5e46342..4670b27 100644 --- a/isideload/src/dev/developer_session.rs +++ b/isideload/src/dev/developer_session.rs @@ -2,7 +2,7 @@ use plist::Dictionary; use plist_macro::{plist, plist_to_xml_string}; use rootcause::prelude::*; use serde::de::DeserializeOwned; -use tracing::warn; +use tracing::{error, warn}; use uuid::Uuid; use crate::{ @@ -12,10 +12,8 @@ use crate::{ grandslam::GrandSlam, }, dev::structures::{ - DeveloperDevice, DeveloperDeviceType::{self, *}, - DeveloperTeam, DevelopmentCertificate, ListCertificatesResponse, ListDevicesResponse, - ListTeamsResponse, + *, }, util::plist::PlistDataExtract, }; @@ -64,6 +62,7 @@ impl<'a> DeveloperSession<'a> { pub async fn send_developer_request( &self, url: &str, + result_key: &str, body: impl Into>, ) -> Result { let body = body.into().unwrap_or_else(|| Dictionary::new()); @@ -92,26 +91,51 @@ impl<'a> DeveloperSession<'a> { .await .context("Failed to read developer request response text")?; - let dict: T = plist::from_bytes(text.as_bytes()) + let dict: Dictionary = plist::from_bytes(text.as_bytes()) .context("Failed to parse developer request plist")?; - Ok(dict) + // All this error handling is here to ensure that: + // 1. We always warn/log errors from the server even if it returns the expected data + // 2. We return server errors if the expected data is missing + // 3. We return parsing errors if there is no server error but the expected data is missing + let response_code = dict.get("resultCode").and_then(|v| v.as_signed_integer()); + let mut server_error: Option = None; + if let Some(code) = response_code { + if code != 0 { + server_error = Some(format!( + "{} Code {}: {}", + dict.get("userString") + .and_then(|v| v.as_string()) + .unwrap_or("Developer request failed."), + code, + dict.get("resultString") + .and_then(|v| v.as_string()) + .unwrap_or("No error message given.") + )); + error!(server_error); + } + } else { + warn!("No resultCode in developer request response"); + } + + let result: Result = dict.get_struct(result_key); + + if let Err(_) = &result { + if let Some(err) = server_error { + bail!(err); + } + } + + Ok(result.context("Failed to extract developer request result")?) } pub async fn list_teams(&self) -> Result, Report> { - let response: ListTeamsResponse = self - .send_developer_request(&dev_url("listTeams", Any), None) + let response: Vec = self + .send_developer_request(&dev_url("listTeams", Any), "teams", None) .await .context("Failed to list developer teams")?; - if response.result_code != 0 { - warn!( - "Non-zero list teams response code: {}", - response.result_code - ) - }; - - Ok(response.teams) + Ok(response) } pub async fn list_devices( @@ -123,19 +147,33 @@ impl<'a> DeveloperSession<'a> { "teamId": &team.team_id, }); - let response: ListDevicesResponse = self - .send_developer_request(&dev_url("listDevices", device_type), body) + let devices: Vec = self + .send_developer_request(&dev_url("listDevices", device_type), "devices", body) .await .context("Failed to list developer devices")?; - if response.result_code != 0 { - warn!( - "Non-zero list devices response code: {}", - response.result_code - ) - }; + Ok(devices) + } - Ok(response.devices) + pub async fn add_device( + &self, + team: &DeveloperTeam, + name: &str, + udid: &str, + device_type: impl Into>, + ) -> Result { + let body = plist!(dict { + "teamId": &team.team_id, + "name": name, + "deviceNumber": udid, + }); + + let device: DeveloperDevice = self + .send_developer_request(&dev_url("addDevice", device_type), "device", body) + .await + .context("Failed to add developer device")?; + + Ok(device) } pub async fn list_all_development_certs( @@ -147,19 +185,16 @@ impl<'a> DeveloperSession<'a> { "teamId": &team.team_id, }); - let response: ListCertificatesResponse = self - .send_developer_request(&dev_url("listAllDevelopmentCerts", device_type), body) + let certs: Vec = self + .send_developer_request( + &dev_url("listAllDevelopmentCerts", device_type), + "certificates", + body, + ) .await .context("Failed to list development certificates")?; - if response.result_code != 0 { - warn!( - "Non-zero list development certs response code: {}", - response.result_code - ) - }; - - Ok(response.certificates) + Ok(certs) } } diff --git a/isideload/src/dev/structures.rs b/isideload/src/dev/structures.rs index 76e1ae0..5631f18 100644 --- a/isideload/src/dev/structures.rs +++ b/isideload/src/dev/structures.rs @@ -34,6 +34,7 @@ pub struct DeveloperTeam { pub struct ListTeamsResponse { pub teams: Vec, pub result_code: i64, + pub result_string: Option, } #[derive(Deserialize, Debug, Clone)] @@ -45,14 +46,7 @@ pub struct DeveloperDevice { pub status: Option, } -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ListDevicesResponse { - pub devices: Vec, - pub result_code: i64, -} - -#[derive(Deserialize, Debug, Clone)] +#[derive(Deserialize, Clone)] #[serde(rename_all = "camelCase")] pub struct DevelopmentCertificate { pub name: String, @@ -62,9 +56,22 @@ pub struct DevelopmentCertificate { pub cert_content: Option, } -#[derive(Deserialize, Debug, Clone)] -#[serde(rename_all = "camelCase")] -pub struct ListCertificatesResponse { - pub certificates: Vec, - pub result_code: i64, +// the automatic debug implementation spams the console with the cert content bytes +impl std::fmt::Debug for DevelopmentCertificate { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("DevelopmentCertificate") + .field("name", &self.name) + .field("certificate_id", &self.certificate_id) + .field("serial_number", &self.serial_number) + .field("machine_id", &self.machine_id) + .field( + "cert_content", + &self + .cert_content + .as_ref() + .map(|c| format!("Some([{} bytes])", c.len())) + .unwrap_or("None".to_string()), + ) + .finish() + } } diff --git a/isideload/src/util/plist.rs b/isideload/src/util/plist.rs index c02e0bc..be54ecf 100644 --- a/isideload/src/util/plist.rs +++ b/isideload/src/util/plist.rs @@ -1,6 +1,8 @@ use plist::Dictionary; use plist_macro::pretty_print_dictionary; use rootcause::prelude::*; +use serde::de::DeserializeOwned; +use tracing::error; pub struct SensitivePlistAttachment { pub plist: Dictionary, @@ -11,6 +13,18 @@ impl SensitivePlistAttachment { SensitivePlistAttachment { plist } } + pub fn from_text(text: &str) -> Self { + let dict: Result = plist::from_bytes(text.as_bytes()); + if let Err(e) = &dict { + error!( + "Failed to parse plist text for sensitive attachment, returning empty plist: {:?}", + e + ); + return SensitivePlistAttachment::new(Dictionary::new()); + } + SensitivePlistAttachment::new(dict.unwrap()) + } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { // if env variable DEBUG_SENSITIVE is set, print full plist if std::env::var("DEBUG_SENSITIVE").is_ok() { @@ -41,6 +55,7 @@ pub trait PlistDataExtract { fn get_string(&self, key: &str) -> Result; fn get_signed_integer(&self, key: &str) -> Result; fn get_dict(&self, key: &str) -> Result<&Dictionary, Report>; + fn get_struct(&self, key: &str) -> Result; } impl PlistDataExtract for Dictionary { @@ -85,4 +100,24 @@ impl PlistDataExtract for Dictionary { .attach(SensitivePlistAttachment::new(self.clone())) }) } + + fn get_struct(&self, key: &str) -> Result { + let dict = self.get(key); + if dict.is_none() { + return Err(report!("Plist missing dictionary for key '{}'", key) + .attach(SensitivePlistAttachment::new(self.clone()))); + } + let dict = dict.unwrap(); + let struct_data: T = plist::from_value(dict).map_err(|e| { + report!( + "Failed to deserialize plist struct for key '{}': {:?}", + key, + e + ) + .attach(SensitivePlistAttachment::new( + dict.as_dictionary().cloned().unwrap_or_default(), + )) + })?; + Ok(struct_data) + } }