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