1use 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
31pub 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 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 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 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 pub async fn mount_standard(
113 &mut self,
114 image_bytes: &[u8],
115 signature: &[u8],
116 ) -> Result<(), ImageMounterError> {
117 self.upload_image(image_bytes, signature).await?;
119
120 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 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 let ids = self.query_personalization_identifiers().await?;
154 tracing::debug!(
155 "personalization identifiers: {:?}",
156 ids.keys().collect::<Vec<_>>()
157 );
158
159 let nonce = self.query_nonce().await?;
161 tracing::debug!("personalization nonce: {} bytes", nonce.len());
162
163 self.upload_personalized_image(image_bytes, trustcache, build_manifest)
165 .await?;
166
167 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 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 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 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 pub async fn query_nonce(&mut self) -> Result<Vec<u8>, ImageMounterError> {
261 self.query_nonce_with_type("DeveloperDiskImage").await
262 }
263
264 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 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 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 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 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
408async 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}