Skip to main content

ios_core/services/deviceinfo/
mod.rs

1//! iOS 17+ CoreDevice device info service via XPC/RSD.
2
3use 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}