ios_core/services/deviceinfo/
mod.rs1use indexmap::IndexMap;
4
5use crate::xpc::{XpcClient, XpcError, XpcValue};
6
7pub const SERVICE_NAME: &str = "com.apple.coredevice.deviceinfo";
8
9const FEATURE_GET_DEVICE_INFO: &str = "com.apple.coredevice.feature.getdeviceinfo";
10const FEATURE_GET_DISPLAY_INFO: &str = "com.apple.coredevice.feature.getdisplayinfo";
11const FEATURE_QUERY_MOBILEGESTALT: &str = "com.apple.coredevice.feature.querymobilegestalt";
12const FEATURE_GET_LOCKSTATE: &str = "com.apple.coredevice.feature.getlockstate";
13
14#[derive(Debug, thiserror::Error)]
15pub enum DeviceInfoError {
16 #[error("xpc error: {0}")]
17 Xpc(#[from] XpcError),
18 #[error("protocol error: {0}")]
19 Protocol(String),
20}
21
22pub struct DeviceInfoClient {
23 client: XpcClient,
24 device_identifier: String,
25}
26
27impl DeviceInfoClient {
28 pub fn new(client: XpcClient, device_identifier: impl Into<String>) -> Self {
29 Self {
30 client,
31 device_identifier: device_identifier.into(),
32 }
33 }
34
35 pub async fn get_device_info(&mut self) -> Result<XpcValue, DeviceInfoError> {
36 self.invoke(
37 FEATURE_GET_DEVICE_INFO,
38 XpcValue::Dictionary(IndexMap::new()),
39 )
40 .await
41 }
42
43 pub async fn get_display_info(&mut self) -> Result<XpcValue, DeviceInfoError> {
44 self.invoke(
45 FEATURE_GET_DISPLAY_INFO,
46 XpcValue::Dictionary(IndexMap::new()),
47 )
48 .await
49 }
50
51 pub async fn query_mobilegestalt(
52 &mut self,
53 keys: &[&str],
54 ) -> Result<XpcValue, DeviceInfoError> {
55 self.invoke(
56 FEATURE_QUERY_MOBILEGESTALT,
57 build_query_mobilegestalt_input(keys),
58 )
59 .await
60 }
61
62 pub async fn get_lockstate(&mut self) -> Result<XpcValue, DeviceInfoError> {
63 self.invoke(FEATURE_GET_LOCKSTATE, XpcValue::Dictionary(IndexMap::new()))
64 .await
65 }
66
67 pub async fn invoke(
68 &mut self,
69 feature_identifier: &str,
70 input: XpcValue,
71 ) -> Result<XpcValue, DeviceInfoError> {
72 let response = self
73 .client
74 .call(build_request(
75 &self.device_identifier,
76 feature_identifier,
77 input,
78 ))
79 .await?;
80 crate::services::coredevice::parse_output(response).map_err(DeviceInfoError::Protocol)
81 }
82}
83
84fn build_query_mobilegestalt_input(keys: &[&str]) -> XpcValue {
85 XpcValue::Dictionary(IndexMap::from([(
86 "keys".to_string(),
87 XpcValue::Array(
88 keys.iter()
89 .map(|key| XpcValue::String((*key).to_string()))
90 .collect(),
91 ),
92 )]))
93}
94
95fn build_request(device_identifier: &str, feature_identifier: &str, input: XpcValue) -> XpcValue {
96 crate::services::coredevice::build_request(device_identifier, feature_identifier, input)
97}
98
99pub fn xpc_value_to_plist(value: &XpcValue) -> plist::Value {
100 match value {
101 XpcValue::Null => plist::Value::String("null".into()),
102 XpcValue::Bool(value) => plist::Value::Boolean(*value),
103 XpcValue::Int64(value) => plist::Value::Integer((*value).into()),
104 XpcValue::Uint64(value) => plist::Value::Integer((*value).into()),
105 XpcValue::Double(value) => plist::Value::Real(*value),
106 XpcValue::Date(value) => plist::Value::Integer((*value).into()),
107 XpcValue::Data(bytes) => plist::Value::Data(bytes.to_vec()),
108 XpcValue::String(value) => plist::Value::String(value.clone()),
109 XpcValue::Uuid(bytes) => plist::Value::String(uuid::Uuid::from_bytes(*bytes).to_string()),
110 XpcValue::Array(values) => {
111 plist::Value::Array(values.iter().map(xpc_value_to_plist).collect())
112 }
113 XpcValue::Dictionary(values) => plist::Value::Dictionary(
114 values
115 .iter()
116 .map(|(key, value)| (key.clone(), xpc_value_to_plist(value)))
117 .collect(),
118 ),
119 XpcValue::FileTransfer { msg_id, data } => {
120 plist::Value::Dictionary(plist::Dictionary::from_iter([
121 (
122 "msg_id".to_string(),
123 plist::Value::Integer((*msg_id).into()),
124 ),
125 ("data".to_string(), xpc_value_to_plist(data)),
126 ]))
127 }
128 }
129}
130
131#[cfg(test)]
132mod tests {
133 use crate::xpc::{XpcMessage, XpcValue};
134
135 use super::*;
136
137 #[test]
138 fn build_query_mobilegestalt_request_uses_coredevice_envelope() {
139 let request = build_request(
140 "DEVICE-ID",
141 FEATURE_QUERY_MOBILEGESTALT,
142 build_query_mobilegestalt_input(&["ProductVersion", "MainScreenCanvasSizes"]),
143 );
144 let dict = request.as_dict().expect("request should be a dictionary");
145
146 assert_eq!(
147 dict["CoreDevice.featureIdentifier"].as_str(),
148 Some(FEATURE_QUERY_MOBILEGESTALT)
149 );
150 assert_eq!(
151 dict["CoreDevice.deviceIdentifier"].as_str(),
152 Some("DEVICE-ID")
153 );
154
155 let input = dict["CoreDevice.input"]
156 .as_dict()
157 .expect("CoreDevice.input should be a dictionary");
158 assert_eq!(
159 input["keys"],
160 XpcValue::Array(vec![
161 XpcValue::String("ProductVersion".into()),
162 XpcValue::String("MainScreenCanvasSizes".into()),
163 ])
164 );
165 }
166
167 #[test]
168 fn coredevice_output_extracts_success_payload() {
169 let response = XpcMessage {
170 flags: 0,
171 msg_id: 1,
172 body: Some(XpcValue::Dictionary(IndexMap::from([(
173 "CoreDevice.output".to_string(),
174 XpcValue::Dictionary(IndexMap::from([(
175 "ProductVersion".to_string(),
176 XpcValue::String("17.4".into()),
177 )])),
178 )]))),
179 };
180
181 let output =
182 crate::services::coredevice::parse_output(response).expect("output should parse");
183 let dict = output.as_dict().expect("output should be a dictionary");
184 assert_eq!(dict["ProductVersion"].as_str(), Some("17.4"));
185 }
186
187 #[test]
188 fn coredevice_output_reports_error_payload() {
189 let response = XpcMessage {
190 flags: 0,
191 msg_id: 1,
192 body: Some(XpcValue::Dictionary(IndexMap::from([(
193 "CoreDevice.error".to_string(),
194 XpcValue::Dictionary(IndexMap::from([(
195 "localizedDescription".to_string(),
196 XpcValue::String("denied".into()),
197 )])),
198 )]))),
199 };
200
201 let err = crate::services::coredevice::parse_output(response)
202 .expect_err("errors should be surfaced");
203 assert!(err.to_string().contains("denied"));
204 }
205
206 #[test]
207 fn xpc_value_to_plist_preserves_mobilegestalt_dictionary_shape() {
208 let value = XpcValue::Dictionary(IndexMap::from([
209 (
210 "ProductVersion".to_string(),
211 XpcValue::String("17.4".into()),
212 ),
213 (
214 "MainScreenCanvasSizes".to_string(),
215 XpcValue::Array(vec![XpcValue::Dictionary(IndexMap::from([(
216 "Width".to_string(),
217 XpcValue::Uint64(1290),
218 )]))]),
219 ),
220 ]));
221
222 let plist = xpc_value_to_plist(&value);
223 let dict = plist
224 .as_dictionary()
225 .expect("converted mobilegestalt should be a dictionary");
226 assert_eq!(
227 dict.get("ProductVersion").and_then(plist::Value::as_string),
228 Some("17.4")
229 );
230 assert!(dict
231 .get("MainScreenCanvasSizes")
232 .and_then(plist::Value::as_array)
233 .is_some());
234 }
235}