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).map_err(|e| ImageMounterError::Plist(e.to_string()))?;
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 =
431        plist::from_bytes(&buf).map_err(|e| ImageMounterError::Plist(e.to_string()))?;
432    val.into_dictionary()
433        .ok_or_else(|| ImageMounterError::Protocol("expected plist dictionary".into()))
434}
435
436#[cfg(test)]
437mod tests {
438    use crate::test_util::MockStream;
439
440    use super::*;
441
442    #[tokio::test]
443    async fn query_developer_mode_status_roundtrips_boolean() {
444        let response = Value::Dictionary(plist::Dictionary::from_iter([(
445            "DeveloperModeStatus".to_string(),
446            Value::Boolean(true),
447        )]));
448        let mut stream = MockStream::with_response(response);
449        let mut client = ImageMounterClient::new(&mut stream);
450
451        let enabled = client.query_developer_mode_status().await.unwrap();
452        assert!(enabled);
453
454        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
455        let payload = &stream.written[4..4 + len];
456        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
457        assert_eq!(
458            dict.get("Command").and_then(|v| v.as_string()),
459            Some("QueryDeveloperModeStatus")
460        );
461    }
462
463    #[tokio::test]
464    async fn lookup_image_signatures_roundtrips_data_array() {
465        let response = Value::Dictionary(plist::Dictionary::from_iter([(
466            "ImageSignature".to_string(),
467            Value::Array(vec![Value::Data(vec![0xde, 0xad, 0xbe, 0xef])]),
468        )]));
469        let mut stream = MockStream::with_response(response);
470        let mut client = ImageMounterClient::new(&mut stream);
471
472        let signatures = client.lookup_image_signatures("Developer").await.unwrap();
473        assert_eq!(signatures, vec![vec![0xde, 0xad, 0xbe, 0xef]]);
474
475        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
476        let payload = &stream.written[4..4 + len];
477        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
478        assert_eq!(
479            dict.get("Command").and_then(|v| v.as_string()),
480            Some("LookupImage")
481        );
482        assert_eq!(
483            dict.get("ImageType").and_then(|v| v.as_string()),
484            Some("Developer")
485        );
486    }
487
488    #[tokio::test]
489    async fn is_image_mounted_checks_both_image_types() {
490        let mut stream = MockStream::with_responses(vec![
491            Value::Dictionary(plist::Dictionary::new()),
492            Value::Dictionary(plist::Dictionary::from_iter([(
493                "ImageSignature".to_string(),
494                Value::Array(vec![Value::Data(vec![1, 2, 3])]),
495            )])),
496        ]);
497        let mut client = ImageMounterClient::new(&mut stream);
498
499        let mounted = client.is_image_mounted().await.unwrap();
500        assert!(mounted);
501    }
502
503    #[tokio::test]
504    async fn unmount_image_sends_mount_path() {
505        let response = Value::Dictionary(plist::Dictionary::new());
506        let mut stream = MockStream::with_response(response);
507        let mut client = ImageMounterClient::new(&mut stream);
508
509        client.unmount_image("/System/Developer").await.unwrap();
510
511        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
512        let payload = &stream.written[4..4 + len];
513        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
514        assert_eq!(
515            dict.get("Command").and_then(|v| v.as_string()),
516            Some("UnmountImage")
517        );
518        assert_eq!(
519            dict.get("MountPath").and_then(|v| v.as_string()),
520            Some("/System/Developer")
521        );
522    }
523
524    #[tokio::test]
525    async fn query_nonce_uses_query_nonce_command_and_personalization_nonce() {
526        let response = Value::Dictionary(plist::Dictionary::from_iter([(
527            "PersonalizationNonce".to_string(),
528            Value::Data(vec![0xde, 0xad, 0xbe, 0xef]),
529        )]));
530        let mut stream = MockStream::with_response(response);
531        let mut client = ImageMounterClient::new(&mut stream);
532
533        let nonce = client.query_nonce().await.unwrap();
534        assert_eq!(nonce, vec![0xde, 0xad, 0xbe, 0xef]);
535
536        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
537        let payload = &stream.written[4..4 + len];
538        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
539        assert_eq!(
540            dict.get("Command").and_then(|v| v.as_string()),
541            Some("QueryNonce")
542        );
543        assert_eq!(
544            dict.get("PersonalizedImageType")
545                .and_then(|v| v.as_string()),
546            Some("DeveloperDiskImage")
547        );
548    }
549
550    #[tokio::test]
551    async fn copy_devices_roundtrips_entry_list() {
552        let response = Value::Dictionary(plist::Dictionary::from_iter([(
553            "EntryList".to_string(),
554            Value::Array(vec![Value::Dictionary(plist::Dictionary::from_iter([
555                (
556                    "ImageType".to_string(),
557                    Value::String("Personalized".into()),
558                ),
559                ("ImageSignature".to_string(), Value::Data(vec![0xaa, 0xbb])),
560            ]))]),
561        )]));
562        let mut stream = MockStream::with_response(response);
563        let mut client = ImageMounterClient::new(&mut stream);
564
565        let entries = client.copy_devices().await.unwrap();
566        assert_eq!(entries.len(), 1);
567        assert_eq!(
568            entries[0].get("ImageType").and_then(|v| v.as_string()),
569            Some("Personalized")
570        );
571        assert_eq!(
572            entries[0].get("ImageSignature").and_then(|v| v.as_data()),
573            Some([0xaa, 0xbb].as_slice())
574        );
575
576        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
577        let payload = &stream.written[4..4 + len];
578        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
579        assert_eq!(
580            dict.get("Command").and_then(|v| v.as_string()),
581            Some("CopyDevices")
582        );
583    }
584
585    #[tokio::test]
586    async fn query_personalization_manifest_roundtrips_request_and_manifest_bytes() {
587        let response = Value::Dictionary(plist::Dictionary::from_iter([(
588            "ImageSignature".to_string(),
589            Value::Data(vec![0xfa, 0xce]),
590        )]));
591        let mut stream = MockStream::with_response(response);
592        let mut client = ImageMounterClient::new(&mut stream);
593
594        let manifest = client
595            .query_personalization_manifest("DeveloperDiskImage", &[0xaa, 0xbb])
596            .await
597            .unwrap();
598        assert_eq!(manifest, vec![0xfa, 0xce]);
599
600        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
601        let payload = &stream.written[4..4 + len];
602        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
603        assert_eq!(
604            dict.get("Command").and_then(|v| v.as_string()),
605            Some("QueryPersonalizationManifest")
606        );
607        assert_eq!(
608            dict.get("PersonalizedImageType")
609                .and_then(|v| v.as_string()),
610            Some("DeveloperDiskImage")
611        );
612        assert_eq!(
613            dict.get("ImageType").and_then(|v| v.as_string()),
614            Some("DeveloperDiskImage")
615        );
616        assert_eq!(
617            dict.get("ImageSignature").and_then(|v| v.as_data()),
618            Some([0xaa, 0xbb].as_slice())
619        );
620    }
621
622    #[tokio::test]
623    async fn query_nonce_with_custom_image_type_uses_provided_personalized_image_type() {
624        let response = Value::Dictionary(plist::Dictionary::from_iter([(
625            "PersonalizationNonce".to_string(),
626            Value::Data(vec![0xde, 0xad]),
627        )]));
628        let mut stream = MockStream::with_response(response);
629        let mut client = ImageMounterClient::new(&mut stream);
630
631        let nonce = client.query_nonce_with_type("Cryptex").await.unwrap();
632        assert_eq!(nonce, vec![0xde, 0xad]);
633
634        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
635        let payload = &stream.written[4..4 + len];
636        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
637        assert_eq!(
638            dict.get("Command").and_then(|v| v.as_string()),
639            Some("QueryNonce")
640        );
641        assert_eq!(
642            dict.get("PersonalizedImageType")
643                .and_then(|v| v.as_string()),
644            Some("Cryptex")
645        );
646    }
647
648    #[tokio::test]
649    async fn query_personalization_identifiers_with_custom_type_uses_provided_image_type() {
650        let response = Value::Dictionary(plist::Dictionary::from_iter([(
651            "PersonalizationIdentifiers".to_string(),
652            Value::Dictionary(plist::Dictionary::from_iter([(
653                "BoardId".to_string(),
654                Value::Integer(12.into()),
655            )])),
656        )]));
657        let mut stream = MockStream::with_response(response);
658        let mut client = ImageMounterClient::new(&mut stream);
659
660        let identifiers = client
661            .query_personalization_identifiers_with_type("Cryptex")
662            .await
663            .unwrap();
664        assert_eq!(
665            identifiers
666                .get("BoardId")
667                .and_then(|v| v.as_unsigned_integer()),
668            Some(12)
669        );
670
671        let len = u32::from_be_bytes(stream.written[..4].try_into().unwrap()) as usize;
672        let payload = &stream.written[4..4 + len];
673        let dict: plist::Dictionary = plist::from_bytes(payload).unwrap();
674        assert_eq!(
675            dict.get("Command").and_then(|v| v.as_string()),
676            Some("QueryPersonalizationIdentifiers")
677        );
678        assert_eq!(
679            dict.get("PersonalizedImageType")
680                .and_then(|v| v.as_string()),
681            Some("Cryptex")
682        );
683    }
684}