Skip to main content

ios_core/services/imagemounter/
protocol.rs

1//! Wire protocol for `com.apple.mobile.mobile_image_mounter`.
2//!
3//! Commands: LookupImage, ReceiveBytes, MountImage, QueryPersonalizationIdentifiers,
4//!           QueryPersonalizationManifest (QueryNonce), Hangup
5//!
6//! Reference: go-ios/ios/imagemounter/imagemounter.go
7
8use std::collections::HashMap;
9
10use plist::Value;
11use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
12
13pub const SERVICE_NAME: &str = "com.apple.mobile.mobile_image_mounter";
14
15service_error!(
16    ImageMounterError,
17    #[error("device error: {0}")]
18    DeviceError(String),
19    #[error("TSS error: {0}")]
20    Tss(String),
21    #[error("download error: {0}")]
22    Download(String),
23);
24
25/// High-level image mounter client.
26pub struct ImageMounterClient<S> {
27    stream: S,
28}
29
30impl<S: AsyncRead + AsyncWrite + Unpin + Send> ImageMounterClient<S> {
31    pub fn new(stream: S) -> Self {
32        Self { stream }
33    }
34
35    /// Return raw mounted image entries reported by mobile_image_mounter.
36    pub async fn copy_devices(&mut self) -> Result<Vec<plist::Dictionary>, ImageMounterError> {
37        let req = plist::Dictionary::from_iter([(
38            "Command".to_string(),
39            Value::String("CopyDevices".into()),
40        )]);
41        send_plist(&mut self.stream, &Value::Dictionary(req)).await?;
42        let resp = recv_plist(&mut self.stream).await?;
43        check_error(&resp)?;
44
45        match resp.get("EntryList") {
46            Some(Value::Array(items)) => items
47                .iter()
48                .map(|value| {
49                    value.as_dictionary().cloned().ok_or_else(|| {
50                        ImageMounterError::Protocol("CopyDevices entry was not a dictionary".into())
51                    })
52                })
53                .collect(),
54            None => Ok(Vec::new()),
55            Some(_) => Err(ImageMounterError::Protocol(
56                "CopyDevices EntryList had unexpected type".into(),
57            )),
58        }
59    }
60
61    /// Check if a developer image is already mounted.
62    pub async fn is_image_mounted(&mut self) -> Result<bool, ImageMounterError> {
63        Ok(!self.lookup_image_signatures("Developer").await?.is_empty()
64            || !self
65                .lookup_image_signatures("Personalized")
66                .await?
67                .is_empty())
68    }
69
70    /// Return mounted image signatures for an image type.
71    pub async fn lookup_image_signatures(
72        &mut self,
73        image_type: &str,
74    ) -> Result<Vec<Vec<u8>>, ImageMounterError> {
75        let req = plist::Dictionary::from_iter([
76            ("Command".to_string(), Value::String("LookupImage".into())),
77            ("ImageType".to_string(), Value::String(image_type.into())),
78        ]);
79        send_plist(&mut self.stream, &Value::Dictionary(req)).await?;
80        let resp = recv_plist(&mut self.stream).await?;
81        check_error(&resp)?;
82
83        match resp.get("ImageSignature") {
84            Some(Value::Array(items)) => items
85                .iter()
86                .map(|value| {
87                    value.as_data().map(|bytes| bytes.to_vec()).ok_or_else(|| {
88                        ImageMounterError::Protocol(
89                            "LookupImage ImageSignature entry was not data".into(),
90                        )
91                    })
92                })
93                .collect(),
94            Some(Value::Data(bytes)) => Ok(vec![bytes.clone()]),
95            None => Ok(Vec::new()),
96            Some(_) => Err(ImageMounterError::Protocol(
97                "LookupImage ImageSignature had unexpected type".into(),
98            )),
99        }
100    }
101
102    /// Mount a standard (pre-iOS 17) developer disk image.
103    ///
104    /// `image_bytes`: the DeveloperDiskImage.dmg contents
105    /// `signature`: the DeveloperDiskImage.dmg.signature contents
106    pub async fn mount_standard(
107        &mut self,
108        image_bytes: &[u8],
109        signature: &[u8],
110    ) -> Result<(), ImageMounterError> {
111        // 1. Upload image via ReceiveBytes
112        self.upload_image(image_bytes, signature).await?;
113
114        // 2. Mount
115        let mount_req = plist::Dictionary::from_iter([
116            ("Command".to_string(), Value::String("MountImage".into())),
117            ("ImageType".to_string(), Value::String("Developer".into())),
118            (
119                "ImagePath".to_string(),
120                Value::String("/private/var/mobile/Media/PublicStaging/staging.dimage".into()),
121            ),
122            (
123                "ImageSignature".to_string(),
124                Value::Data(signature.to_vec()),
125            ),
126        ]);
127        send_plist(&mut self.stream, &Value::Dictionary(mount_req)).await?;
128        let resp = recv_plist(&mut self.stream).await?;
129        check_error(&resp)?;
130        Ok(())
131    }
132
133    /// Mount a personalized (iOS 17+) developer disk image.
134    ///
135    /// `trustcache`: the trust cache data
136    /// `build_manifest`: the BuildManifest.plist bytes
137    /// `image_bytes`: the personalized disk image
138    /// `ticket`: the TSS ticket (ApImg4Ticket)
139    pub async fn mount_personalized(
140        &mut self,
141        trustcache: &[u8],
142        build_manifest: &[u8],
143        image_bytes: &[u8],
144        ticket: &[u8],
145    ) -> Result<(), ImageMounterError> {
146        // 1. Query personalization identifiers
147        let ids = self.query_personalization_identifiers().await?;
148        tracing::debug!(
149            "personalization identifiers: {:?}",
150            ids.keys().collect::<Vec<_>>()
151        );
152
153        // 2. Query nonce
154        let nonce = self.query_nonce().await?;
155        tracing::debug!("personalization nonce: {} bytes", nonce.len());
156
157        // 3. Upload image
158        self.upload_personalized_image(image_bytes, trustcache, build_manifest)
159            .await?;
160
161        // 4. Mount with ticket
162        let mount_req = plist::Dictionary::from_iter([
163            ("Command".to_string(), Value::String("MountImage".into())),
164            (
165                "ImageType".to_string(),
166                Value::String("Personalized".into()),
167            ),
168            (
169                "ImagePath".to_string(),
170                Value::String("/private/var/mobile/Media/PublicStaging/staging.dimage".into()),
171            ),
172            ("ImageSignature".to_string(), Value::Data(ticket.to_vec())),
173        ]);
174        send_plist(&mut self.stream, &Value::Dictionary(mount_req)).await?;
175        let resp = recv_plist(&mut self.stream).await?;
176        check_error(&resp)?;
177        Ok(())
178    }
179
180    /// Query personalization identifiers (board ID, chip ID, etc.)
181    pub async fn query_personalization_identifiers(
182        &mut self,
183    ) -> Result<HashMap<String, Value>, ImageMounterError> {
184        self.query_personalization_identifiers_with_type("DeveloperDiskImage")
185            .await
186    }
187
188    /// Query personalization identifiers for a specific personalized image type.
189    pub async fn query_personalization_identifiers_with_type(
190        &mut self,
191        personalized_image_type: &str,
192    ) -> Result<HashMap<String, Value>, ImageMounterError> {
193        let req = plist::Dictionary::from_iter([
194            (
195                "Command".to_string(),
196                Value::String("QueryPersonalizationIdentifiers".into()),
197            ),
198            (
199                "PersonalizedImageType".to_string(),
200                Value::String(personalized_image_type.into()),
201            ),
202        ]);
203        send_plist(&mut self.stream, &Value::Dictionary(req)).await?;
204        let resp = recv_plist(&mut self.stream).await?;
205        check_error(&resp)?;
206
207        let ids = resp
208            .get("PersonalizationIdentifiers")
209            .and_then(|v| v.as_dictionary())
210            .ok_or_else(|| {
211                ImageMounterError::Protocol("missing PersonalizationIdentifiers".into())
212            })?;
213
214        Ok(ids.iter().map(|(k, v)| (k.clone(), v.clone())).collect())
215    }
216
217    /// Query the personalization manifest associated with a mounted personalized image.
218    pub async fn query_personalization_manifest(
219        &mut self,
220        personalized_image_type: &str,
221        image_signature: &[u8],
222    ) -> Result<Vec<u8>, ImageMounterError> {
223        let req = plist::Dictionary::from_iter([
224            (
225                "Command".to_string(),
226                Value::String("QueryPersonalizationManifest".into()),
227            ),
228            (
229                "PersonalizedImageType".to_string(),
230                Value::String(personalized_image_type.into()),
231            ),
232            (
233                "ImageType".to_string(),
234                Value::String(personalized_image_type.into()),
235            ),
236            (
237                "ImageSignature".to_string(),
238                Value::Data(image_signature.to_vec()),
239            ),
240        ]);
241        send_plist(&mut self.stream, &Value::Dictionary(req)).await?;
242        let resp = recv_plist(&mut self.stream).await?;
243        check_error(&resp)?;
244
245        let manifest = resp
246            .get("ImageSignature")
247            .and_then(|v| v.as_data())
248            .ok_or_else(|| ImageMounterError::Protocol("missing ImageSignature".into()))?;
249
250        Ok(manifest.to_vec())
251    }
252
253    /// Query the personalization nonce.
254    pub async fn query_nonce(&mut self) -> Result<Vec<u8>, ImageMounterError> {
255        self.query_nonce_with_type("DeveloperDiskImage").await
256    }
257
258    /// Query the personalization nonce for a specific personalized image type.
259    pub async fn query_nonce_with_type(
260        &mut self,
261        personalized_image_type: &str,
262    ) -> Result<Vec<u8>, ImageMounterError> {
263        let req = plist::Dictionary::from_iter([
264            ("Command".to_string(), Value::String("QueryNonce".into())),
265            (
266                "PersonalizedImageType".to_string(),
267                Value::String(personalized_image_type.into()),
268            ),
269        ]);
270        send_plist(&mut self.stream, &Value::Dictionary(req)).await?;
271        let resp = recv_plist(&mut self.stream).await?;
272        check_error(&resp)?;
273
274        let nonce = resp
275            .get("PersonalizationNonce")
276            .and_then(|v| v.as_data())
277            .ok_or_else(|| ImageMounterError::Protocol("missing PersonalizationNonce".into()))?;
278
279        Ok(nonce.to_vec())
280    }
281
282    /// Query whether developer mode is enabled on the device.
283    pub async fn query_developer_mode_status(&mut self) -> Result<bool, ImageMounterError> {
284        let req = plist::Dictionary::from_iter([(
285            "Command".to_string(),
286            Value::String("QueryDeveloperModeStatus".into()),
287        )]);
288        send_plist(&mut self.stream, &Value::Dictionary(req)).await?;
289        let resp = recv_plist(&mut self.stream).await?;
290        check_error(&resp)?;
291
292        Ok(resp
293            .get("DeveloperModeStatus")
294            .and_then(|v| v.as_boolean())
295            .unwrap_or(false))
296    }
297
298    /// Unmount a mounted image at a mount path such as `/Developer` or `/System/Developer`.
299    pub async fn unmount_image(&mut self, mount_path: &str) -> Result<(), ImageMounterError> {
300        let req = plist::Dictionary::from_iter([
301            ("Command".to_string(), Value::String("UnmountImage".into())),
302            ("MountPath".to_string(), Value::String(mount_path.into())),
303        ]);
304        send_plist(&mut self.stream, &Value::Dictionary(req)).await?;
305        let resp = recv_plist(&mut self.stream).await?;
306        check_error(&resp)?;
307        Ok(())
308    }
309
310    async fn upload_image(
311        &mut self,
312        image_bytes: &[u8],
313        signature: &[u8],
314    ) -> Result<(), ImageMounterError> {
315        let req = plist::Dictionary::from_iter([
316            ("Command".to_string(), Value::String("ReceiveBytes".into())),
317            ("ImageType".to_string(), Value::String("Developer".into())),
318            (
319                "ImageSize".to_string(),
320                Value::Integer((image_bytes.len() as i64).into()),
321            ),
322            (
323                "ImageSignature".to_string(),
324                Value::Data(signature.to_vec()),
325            ),
326        ]);
327        send_plist(&mut self.stream, &Value::Dictionary(req)).await?;
328        let resp = recv_plist(&mut self.stream).await?;
329
330        let status = resp.get("Status").and_then(|v| v.as_string()).unwrap_or("");
331
332        if status == "ReceiveBytesAck" {
333            // Device wants the image bytes
334            self.stream.write_all(image_bytes).await?;
335            self.stream.flush().await?;
336            let resp2 = recv_plist(&mut self.stream).await?;
337            check_error(&resp2)?;
338        } else {
339            check_error(&resp)?;
340        }
341        Ok(())
342    }
343
344    async fn upload_personalized_image(
345        &mut self,
346        image_bytes: &[u8],
347        trustcache: &[u8],
348        build_manifest: &[u8],
349    ) -> Result<(), ImageMounterError> {
350        let req = plist::Dictionary::from_iter([
351            ("Command".to_string(), Value::String("ReceiveBytes".into())),
352            (
353                "ImageType".to_string(),
354                Value::String("Personalized".into()),
355            ),
356            (
357                "ImageSize".to_string(),
358                Value::Integer((image_bytes.len() as i64).into()),
359            ),
360            (
361                "ImageTrustCache".to_string(),
362                Value::Data(trustcache.to_vec()),
363            ),
364            (
365                "BuildManifest".to_string(),
366                Value::Data(build_manifest.to_vec()),
367            ),
368        ]);
369        send_plist(&mut self.stream, &Value::Dictionary(req)).await?;
370        let resp = recv_plist(&mut self.stream).await?;
371
372        let status = resp.get("Status").and_then(|v| v.as_string()).unwrap_or("");
373
374        if status == "ReceiveBytesAck" {
375            self.stream.write_all(image_bytes).await?;
376            self.stream.flush().await?;
377            let resp2 = recv_plist(&mut self.stream).await?;
378            check_error(&resp2)?;
379        } else {
380            check_error(&resp)?;
381        }
382        Ok(())
383    }
384
385    /// Send Hangup to close the session.
386    pub async fn hangup(&mut self) -> Result<(), ImageMounterError> {
387        let req =
388            plist::Dictionary::from_iter([("Command".to_string(), Value::String("Hangup".into()))]);
389        send_plist(&mut self.stream, &Value::Dictionary(req)).await?;
390        Ok(())
391    }
392}
393
394fn check_error(resp: &plist::Dictionary) -> Result<(), ImageMounterError> {
395    if let Some(err) = resp.get("Error") {
396        let msg = err.as_string().unwrap_or("unknown error");
397        return Err(ImageMounterError::DeviceError(msg.to_string()));
398    }
399    Ok(())
400}
401
402// ── plist framing (4-byte BE length prefix) ──────────────────────────────────
403
404async fn send_plist<S: AsyncWrite + Unpin>(
405    stream: &mut S,
406    value: &Value,
407) -> Result<(), ImageMounterError> {
408    let mut buf = Vec::new();
409    plist::to_writer_xml(&mut buf, value)?;
410    stream.write_all(&(buf.len() as u32).to_be_bytes()).await?;
411    stream.write_all(&buf).await?;
412    stream.flush().await?;
413    Ok(())
414}
415
416async fn recv_plist<S: AsyncRead + Unpin>(
417    stream: &mut S,
418) -> Result<plist::Dictionary, ImageMounterError> {
419    let mut len_buf = [0u8; 4];
420    stream.read_exact(&mut len_buf).await?;
421    let len = u32::from_be_bytes(len_buf) as usize;
422    const MAX_PLIST_SIZE: usize = 4 * 1024 * 1024;
423    if len > MAX_PLIST_SIZE {
424        return Err(ImageMounterError::Protocol(format!(
425            "plist length {len} exceeds maximum of {MAX_PLIST_SIZE}"
426        )));
427    }
428    let mut buf = vec![0u8; len];
429    stream.read_exact(&mut buf).await?;
430    let val: plist::Value = plist::from_bytes(&buf)?;
431    val.into_dictionary()
432        .ok_or_else(|| ImageMounterError::Protocol("expected plist dictionary".into()))
433}
434
435#[cfg(test)]
436mod tests {
437    use crate::test_util::MockStream;
438
439    use super::*;
440
441    #[tokio::test]
442    async fn query_developer_mode_status_roundtrips_boolean() {
443        let response = Value::Dictionary(plist::Dictionary::from_iter([(
444            "DeveloperModeStatus".to_string(),
445            Value::Boolean(true),
446        )]));
447        let mut stream = MockStream::with_response(response);
448        let mut client = ImageMounterClient::new(&mut stream);
449
450        let enabled = client.query_developer_mode_status().await.unwrap();
451        assert!(enabled);
452
453        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
454        let payload = &stream.written[4..4 + len];
455        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
456        assert_eq!(
457            dict.get("Command").and_then(|v| v.as_string()),
458            Some("QueryDeveloperModeStatus")
459        );
460    }
461
462    #[tokio::test]
463    async fn lookup_image_signatures_roundtrips_data_array() {
464        let response = Value::Dictionary(plist::Dictionary::from_iter([(
465            "ImageSignature".to_string(),
466            Value::Array(vec![Value::Data(vec![0xde, 0xad, 0xbe, 0xef])]),
467        )]));
468        let mut stream = MockStream::with_response(response);
469        let mut client = ImageMounterClient::new(&mut stream);
470
471        let signatures = client.lookup_image_signatures("Developer").await.unwrap();
472        assert_eq!(signatures, vec![vec![0xde, 0xad, 0xbe, 0xef]]);
473
474        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
475        let payload = &stream.written[4..4 + len];
476        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
477        assert_eq!(
478            dict.get("Command").and_then(|v| v.as_string()),
479            Some("LookupImage")
480        );
481        assert_eq!(
482            dict.get("ImageType").and_then(|v| v.as_string()),
483            Some("Developer")
484        );
485    }
486
487    #[tokio::test]
488    async fn is_image_mounted_checks_both_image_types() {
489        let mut stream = MockStream::with_responses(vec![
490            Value::Dictionary(plist::Dictionary::new()),
491            Value::Dictionary(plist::Dictionary::from_iter([(
492                "ImageSignature".to_string(),
493                Value::Array(vec![Value::Data(vec![1, 2, 3])]),
494            )])),
495        ]);
496        let mut client = ImageMounterClient::new(&mut stream);
497
498        let mounted = client.is_image_mounted().await.unwrap();
499        assert!(mounted);
500    }
501
502    #[tokio::test]
503    async fn unmount_image_sends_mount_path() {
504        let response = Value::Dictionary(plist::Dictionary::new());
505        let mut stream = MockStream::with_response(response);
506        let mut client = ImageMounterClient::new(&mut stream);
507
508        client.unmount_image("/System/Developer").await.unwrap();
509
510        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
511        let payload = &stream.written[4..4 + len];
512        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
513        assert_eq!(
514            dict.get("Command").and_then(|v| v.as_string()),
515            Some("UnmountImage")
516        );
517        assert_eq!(
518            dict.get("MountPath").and_then(|v| v.as_string()),
519            Some("/System/Developer")
520        );
521    }
522
523    #[tokio::test]
524    async fn query_nonce_uses_query_nonce_command_and_personalization_nonce() {
525        let response = Value::Dictionary(plist::Dictionary::from_iter([(
526            "PersonalizationNonce".to_string(),
527            Value::Data(vec![0xde, 0xad, 0xbe, 0xef]),
528        )]));
529        let mut stream = MockStream::with_response(response);
530        let mut client = ImageMounterClient::new(&mut stream);
531
532        let nonce = client.query_nonce().await.unwrap();
533        assert_eq!(nonce, vec![0xde, 0xad, 0xbe, 0xef]);
534
535        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
536        let payload = &stream.written[4..4 + len];
537        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
538        assert_eq!(
539            dict.get("Command").and_then(|v| v.as_string()),
540            Some("QueryNonce")
541        );
542        assert_eq!(
543            dict.get("PersonalizedImageType")
544                .and_then(|v| v.as_string()),
545            Some("DeveloperDiskImage")
546        );
547    }
548
549    #[tokio::test]
550    async fn copy_devices_roundtrips_entry_list() {
551        let response = Value::Dictionary(plist::Dictionary::from_iter([(
552            "EntryList".to_string(),
553            Value::Array(vec![Value::Dictionary(plist::Dictionary::from_iter([
554                (
555                    "ImageType".to_string(),
556                    Value::String("Personalized".into()),
557                ),
558                ("ImageSignature".to_string(), Value::Data(vec![0xaa, 0xbb])),
559            ]))]),
560        )]));
561        let mut stream = MockStream::with_response(response);
562        let mut client = ImageMounterClient::new(&mut stream);
563
564        let entries = client.copy_devices().await.unwrap();
565        assert_eq!(entries.len(), 1);
566        assert_eq!(
567            entries[0].get("ImageType").and_then(|v| v.as_string()),
568            Some("Personalized")
569        );
570        assert_eq!(
571            entries[0].get("ImageSignature").and_then(|v| v.as_data()),
572            Some([0xaa, 0xbb].as_slice())
573        );
574
575        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
576        let payload = &stream.written[4..4 + len];
577        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
578        assert_eq!(
579            dict.get("Command").and_then(|v| v.as_string()),
580            Some("CopyDevices")
581        );
582    }
583
584    #[tokio::test]
585    async fn query_personalization_manifest_roundtrips_request_and_manifest_bytes() {
586        let response = Value::Dictionary(plist::Dictionary::from_iter([(
587            "ImageSignature".to_string(),
588            Value::Data(vec![0xfa, 0xce]),
589        )]));
590        let mut stream = MockStream::with_response(response);
591        let mut client = ImageMounterClient::new(&mut stream);
592
593        let manifest = client
594            .query_personalization_manifest("DeveloperDiskImage", &[0xaa, 0xbb])
595            .await
596            .unwrap();
597        assert_eq!(manifest, vec![0xfa, 0xce]);
598
599        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
600        let payload = &stream.written[4..4 + len];
601        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
602        assert_eq!(
603            dict.get("Command").and_then(|v| v.as_string()),
604            Some("QueryPersonalizationManifest")
605        );
606        assert_eq!(
607            dict.get("PersonalizedImageType")
608                .and_then(|v| v.as_string()),
609            Some("DeveloperDiskImage")
610        );
611        assert_eq!(
612            dict.get("ImageType").and_then(|v| v.as_string()),
613            Some("DeveloperDiskImage")
614        );
615        assert_eq!(
616            dict.get("ImageSignature").and_then(|v| v.as_data()),
617            Some([0xaa, 0xbb].as_slice())
618        );
619    }
620
621    #[tokio::test]
622    async fn query_nonce_with_custom_image_type_uses_provided_personalized_image_type() {
623        let response = Value::Dictionary(plist::Dictionary::from_iter([(
624            "PersonalizationNonce".to_string(),
625            Value::Data(vec![0xde, 0xad]),
626        )]));
627        let mut stream = MockStream::with_response(response);
628        let mut client = ImageMounterClient::new(&mut stream);
629
630        let nonce = client.query_nonce_with_type("Cryptex").await.unwrap();
631        assert_eq!(nonce, vec![0xde, 0xad]);
632
633        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
634        let payload = &stream.written[4..4 + len];
635        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
636        assert_eq!(
637            dict.get("Command").and_then(|v| v.as_string()),
638            Some("QueryNonce")
639        );
640        assert_eq!(
641            dict.get("PersonalizedImageType")
642                .and_then(|v| v.as_string()),
643            Some("Cryptex")
644        );
645    }
646
647    #[tokio::test]
648    async fn query_personalization_identifiers_with_custom_type_uses_provided_image_type() {
649        let response = Value::Dictionary(plist::Dictionary::from_iter([(
650            "PersonalizationIdentifiers".to_string(),
651            Value::Dictionary(plist::Dictionary::from_iter([(
652                "BoardId".to_string(),
653                Value::Integer(12.into()),
654            )])),
655        )]));
656        let mut stream = MockStream::with_response(response);
657        let mut client = ImageMounterClient::new(&mut stream);
658
659        let identifiers = client
660            .query_personalization_identifiers_with_type("Cryptex")
661            .await
662            .unwrap();
663        assert_eq!(
664            identifiers
665                .get("BoardId")
666                .and_then(|v| v.as_unsigned_integer()),
667            Some(12)
668        );
669
670        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
671        let payload = &stream.written[4..4 + len];
672        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
673        assert_eq!(
674            dict.get("Command").and_then(|v| v.as_string()),
675            Some("QueryPersonalizationIdentifiers")
676        );
677        assert_eq!(
678            dict.get("PersonalizedImageType")
679                .and_then(|v| v.as_string()),
680            Some("Cryptex")
681        );
682    }
683}