Improve developer session layout

This commit is contained in:
nab138
2026-01-29 20:51:13 -05:00
parent c639ed2f2f
commit c0f02994ec
4 changed files with 127 additions and 50 deletions

View File

@@ -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<T: DeserializeOwned>(
&self,
url: &str,
result_key: &str,
body: impl Into<Option<Dictionary>>,
) -> Result<T, Report> {
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<String> = 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<T, _> = 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<Vec<DeveloperTeam>, Report> {
let response: ListTeamsResponse = self
.send_developer_request(&dev_url("listTeams", Any), None)
let response: Vec<DeveloperTeam> = 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<DeveloperDevice> = 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<Option<DeveloperDeviceType>>,
) -> Result<DeveloperDevice, Report> {
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<DevelopmentCertificate> = 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)
}
}

View File

@@ -34,6 +34,7 @@ pub struct DeveloperTeam {
pub struct ListTeamsResponse {
pub teams: Vec<DeveloperTeam>,
pub result_code: i64,
pub result_string: Option<String>,
}
#[derive(Deserialize, Debug, Clone)]
@@ -45,14 +46,7 @@ pub struct DeveloperDevice {
pub status: Option<String>,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ListDevicesResponse {
pub devices: Vec<DeveloperDevice>,
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<ByteBuf>,
}
#[derive(Deserialize, Debug, Clone)]
#[serde(rename_all = "camelCase")]
pub struct ListCertificatesResponse {
pub certificates: Vec<DevelopmentCertificate>,
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()
}
}

View File

@@ -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<Dictionary, _> = 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<String, Report>;
fn get_signed_integer(&self, key: &str) -> Result<i64, Report>;
fn get_dict(&self, key: &str) -> Result<&Dictionary, Report>;
fn get_struct<T: DeserializeOwned>(&self, key: &str) -> Result<T, Report>;
}
impl PlistDataExtract for Dictionary {
@@ -85,4 +100,24 @@ impl PlistDataExtract for Dictionary {
.attach(SensitivePlistAttachment::new(self.clone()))
})
}
fn get_struct<T: DeserializeOwned>(&self, key: &str) -> Result<T, Report> {
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)
}
}