1use std::collections::HashMap;
14
15use crate::proto::afc::{AfcHeader, AfcOpcode, AFC_MAGIC};
16use bytes::{Bytes, BytesMut};
17use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
18use zerocopy::{FromBytes, IntoBytes};
19
20#[cfg(feature = "house_arrest")]
21pub use super::house_arrest;
22
23pub mod protocol; #[derive(Debug, thiserror::Error)]
28pub enum AfcError {
29 #[error("IO error: {0}")]
30 Io(#[from] std::io::Error),
31 #[error("AFC error: {0}")]
32 Status(AfcStatusCode),
33 #[error("protocol error: {0}")]
34 Protocol(String),
35}
36
37#[derive(Debug, Clone, Copy, PartialEq, Eq)]
39pub enum AfcStatusCode {
40 Success,
41 Unknown,
42 OperationHeaderInvalid,
43 NoResources,
44 ReadError,
45 WriteError,
46 UnknownPacketType,
47 InvalidArgument,
48 ObjectNotFound,
49 ObjectIsDir,
50 PermDenied,
51 ServiceNotConnected,
52 Timeout,
53 TooMuchData,
54 EndOfData,
55 OpNotSupported,
56 ObjectExists,
57 ObjectBusy,
58 NoSpaceLeft,
59 OpWouldBlock,
60 IoError,
61 OpInterrupted,
62 OpInProgress,
63 InternalError,
64 MuxError,
65 NoMem,
66 NotEnoughData,
67 DirNotEmpty,
68 Other(u64),
69}
70
71#[derive(Debug, Clone, PartialEq, Eq)]
72pub struct AfcFileInfo {
73 pub name: Option<String>,
74 pub file_type: Option<String>,
75 pub size: Option<u64>,
76 pub mode: Option<u32>,
77 pub link_target: Option<String>,
78 pub raw: HashMap<String, String>,
79}
80
81impl AfcStatusCode {
82 pub fn from_u64(code: u64) -> Self {
83 match code {
84 0 => Self::Success,
85 1 => Self::Unknown,
86 2 => Self::OperationHeaderInvalid,
87 3 => Self::NoResources,
88 4 => Self::ReadError,
89 5 => Self::WriteError,
90 6 => Self::UnknownPacketType,
91 7 => Self::InvalidArgument,
92 8 => Self::ObjectNotFound,
93 9 => Self::ObjectIsDir,
94 10 => Self::PermDenied,
95 11 => Self::ServiceNotConnected,
96 12 => Self::Timeout,
97 13 => Self::TooMuchData,
98 14 => Self::EndOfData,
99 15 => Self::OpNotSupported,
100 16 => Self::ObjectExists,
101 17 => Self::ObjectBusy,
102 18 => Self::NoSpaceLeft,
103 19 => Self::OpWouldBlock,
104 20 => Self::IoError,
105 21 => Self::OpInterrupted,
106 22 => Self::OpInProgress,
107 23 => Self::InternalError,
108 30 => Self::MuxError,
109 31 => Self::NoMem,
110 32 => Self::NotEnoughData,
111 33 => Self::DirNotEmpty,
112 _ => Self::Other(code),
113 }
114 }
115}
116
117impl std::fmt::Display for AfcStatusCode {
118 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
119 match self {
120 Self::Success => write!(f, "success"),
121 Self::Unknown => write!(f, "unknown error (1)"),
122 Self::OperationHeaderInvalid => write!(f, "operation header invalid (2)"),
123 Self::NoResources => write!(f, "no resources (3)"),
124 Self::ReadError => write!(f, "read error (4)"),
125 Self::WriteError => write!(f, "write error (5)"),
126 Self::UnknownPacketType => write!(f, "unknown packet type (6)"),
127 Self::InvalidArgument => write!(f, "invalid argument (7)"),
128 Self::ObjectNotFound => write!(f, "object not found (8)"),
129 Self::ObjectIsDir => write!(f, "object is directory (9)"),
130 Self::PermDenied => write!(f, "permission denied (10)"),
131 Self::ServiceNotConnected => write!(f, "service not connected (11)"),
132 Self::Timeout => write!(f, "timeout (12)"),
133 Self::TooMuchData => write!(f, "too much data (13)"),
134 Self::EndOfData => write!(f, "end of data (14)"),
135 Self::OpNotSupported => write!(f, "operation not supported (15)"),
136 Self::ObjectExists => write!(f, "object exists (16)"),
137 Self::ObjectBusy => write!(f, "object busy (17)"),
138 Self::NoSpaceLeft => write!(f, "no space left (18)"),
139 Self::OpWouldBlock => write!(f, "operation would block (19)"),
140 Self::IoError => write!(f, "I/O error (20)"),
141 Self::OpInterrupted => write!(f, "operation interrupted (21)"),
142 Self::OpInProgress => write!(f, "operation in progress (22)"),
143 Self::InternalError => write!(f, "internal error (23)"),
144 Self::MuxError => write!(f, "mux error (30)"),
145 Self::NoMem => write!(f, "no memory (31)"),
146 Self::NotEnoughData => write!(f, "not enough data (32)"),
147 Self::DirNotEmpty => write!(f, "directory not empty (33)"),
148 Self::Other(code) => write!(f, "unknown status ({code})"),
149 }
150 }
151}
152
153struct Packet {
156 #[allow(dead_code)]
157 opcode: u64,
158 header_payload: Bytes,
161 payload: Bytes,
164}
165
166pub struct AfcClient<S> {
172 stream: S,
173 packet_num: u64,
174}
175
176impl<S: AsyncRead + AsyncWrite + Unpin> AfcClient<S> {
177 pub const FILE_MODE_READ_ONLY: u64 = 0x00000001;
178 pub const FILE_MODE_READ_WRITE: u64 = 0x00000002;
179 pub const FILE_MODE_WRITE_ONLY_CREATE_TRUNC: u64 = 0x00000003;
180 pub const LOCK_EXCLUSIVE: u64 = 2 | 4;
181 pub const LOCK_UNLOCK: u64 = 8 | 4;
182
183 pub fn new(stream: S) -> Self {
184 Self {
187 stream,
188 packet_num: 1,
189 }
190 }
191
192 fn next_pnum(&mut self) -> u64 {
193 let n = self.packet_num;
194 self.packet_num += 1;
195 n
196 }
197
198 async fn send(
201 &mut self,
202 opcode: AfcOpcode,
203 header_payload: &[u8],
204 payload: &[u8],
205 ) -> Result<(), AfcError> {
206 let pnum = self.next_pnum();
207 let hdr = AfcHeader::new(pnum, opcode, header_payload.len(), payload.len());
208 self.stream.write_all(hdr.as_bytes()).await?;
209 if !header_payload.is_empty() {
210 self.stream.write_all(header_payload).await?;
211 }
212 if !payload.is_empty() {
213 self.stream.write_all(payload).await?;
214 }
215 self.stream.flush().await?;
216 Ok(())
217 }
218
219 async fn recv(&mut self) -> Result<Packet, AfcError> {
222 let mut hdr_buf = [0u8; AfcHeader::SIZE];
223 self.stream.read_exact(&mut hdr_buf).await?;
224
225 let hdr = AfcHeader::ref_from_bytes(&hdr_buf)
226 .map_err(|_| AfcError::Protocol("bad AFC header".into()))?;
227
228 if hdr.magic.get() != AFC_MAGIC {
229 return Err(AfcError::Protocol(format!(
230 "bad AFC magic: 0x{:016X}",
231 hdr.magic.get()
232 )));
233 }
234
235 let entire_len = hdr.entire_len.get() as usize;
236 let this_len = hdr.this_len.get() as usize;
237 let opcode = hdr.operation.get();
238
239 let header_payload_len = this_len.saturating_sub(AfcHeader::SIZE);
240 let payload_len = entire_len.saturating_sub(this_len);
241
242 const MAX_AFC_MSG: usize = 256 * 1024 * 1024; if header_payload_len > MAX_AFC_MSG || payload_len > MAX_AFC_MSG {
245 return Err(AfcError::Protocol(format!(
246 "AFC frame too large: header_payload={header_payload_len} payload={payload_len}"
247 )));
248 }
249
250 let mut header_payload = vec![0u8; header_payload_len];
251 let mut payload = vec![0u8; payload_len];
252
253 if header_payload_len > 0 {
254 self.stream.read_exact(&mut header_payload).await?;
255 }
256 if payload_len > 0 {
257 self.stream.read_exact(&mut payload).await?;
258 }
259
260 if opcode == AfcOpcode::Status as u64 {
262 let code = AfcStatusCode::from_u64(if header_payload.len() >= 8 {
263 u64::from_le_bytes(
264 header_payload[..8]
265 .try_into()
266 .map_err(|_| AfcError::Protocol("bad status code".into()))?,
267 )
268 } else {
269 0
270 });
271 if code != AfcStatusCode::Success {
272 return Err(AfcError::Status(code));
273 }
274 }
275
276 Ok(Packet {
277 opcode,
278 header_payload: Bytes::from(header_payload),
279 payload: Bytes::from(payload),
280 })
281 }
282
283 pub async fn list_dir(&mut self, path: &str) -> Result<Vec<String>, AfcError> {
290 let mut hp = path.as_bytes().to_vec();
291 hp.push(0);
292 self.send(AfcOpcode::ReadDir, &hp, &[]).await?;
293 let pkt = self.recv().await?;
294 let entries = split_null_strings(&pkt.payload)
296 .into_iter()
297 .filter(|s| s != "." && s != "..")
298 .collect();
299 Ok(entries)
300 }
301
302 pub async fn stat(&mut self, path: &str) -> Result<HashMap<String, String>, AfcError> {
304 let mut hp = path.as_bytes().to_vec();
305 hp.push(0);
306 self.send(AfcOpcode::GetFileInfo, &hp, &[]).await?;
307 let pkt = self.recv().await?;
308 Ok(parse_kv_pairs(&pkt.payload))
309 }
310
311 pub async fn stat_info(&mut self, path: &str) -> Result<AfcFileInfo, AfcError> {
316 let raw = self.stat(path).await?;
317 Ok(parse_file_info(path, raw))
318 }
319
320 pub async fn make_dir(&mut self, path: &str) -> Result<(), AfcError> {
322 let mut hp = path.as_bytes().to_vec();
323 hp.push(0);
324 self.send(AfcOpcode::MakePath, &hp, &[]).await?;
325 self.recv().await?;
326 Ok(())
327 }
328
329 pub async fn remove(&mut self, path: &str) -> Result<(), AfcError> {
331 let mut hp = path.as_bytes().to_vec();
332 hp.push(0);
333 self.send(AfcOpcode::RemovePath, &hp, &[]).await?;
334 self.recv().await?;
335 Ok(())
336 }
337
338 pub async fn remove_all(&mut self, path: &str) -> Result<(), AfcError> {
340 let mut hp = path.as_bytes().to_vec();
341 hp.push(0);
342 self.send(AfcOpcode::RemovePathAndContents, &hp, &[])
343 .await?;
344 self.recv().await?;
345 Ok(())
346 }
347
348 pub async fn rename(&mut self, from: &str, to: &str) -> Result<(), AfcError> {
350 let mut hp = from.as_bytes().to_vec();
351 hp.push(0);
352 hp.extend_from_slice(to.as_bytes());
353 hp.push(0);
354 self.send(AfcOpcode::RenamePath, &hp, &[]).await?;
355 self.recv().await?;
356 Ok(())
357 }
358
359 pub async fn read_file(&mut self, path: &str) -> Result<Bytes, AfcError> {
361 let fd = self.file_open(path, Self::FILE_MODE_READ_ONLY).await?; let mut data = BytesMut::new();
363 let chunk = 65536u64;
364 loop {
365 let buf = self.file_read(fd, chunk).await?;
366 if buf.is_empty() {
367 break;
368 }
369 data.extend_from_slice(&buf);
370 }
371 self.file_close(fd).await?;
372 Ok(data.freeze())
373 }
374
375 pub async fn read_file_follow_links(&mut self, path: &str) -> Result<Bytes, AfcError> {
381 let target = self.resolve_read_path(path).await?;
382 self.read_file(&target).await
383 }
384
385 pub async fn write_file(&mut self, path: &str, data: &[u8]) -> Result<(), AfcError> {
387 let mut reader = std::io::Cursor::new(data);
388 self.write_file_from_reader(path, &mut reader).await
389 }
390
391 pub async fn write_file_from_reader<R>(
393 &mut self,
394 path: &str,
395 reader: &mut R,
396 ) -> Result<(), AfcError>
397 where
398 R: AsyncRead + Unpin,
399 {
400 let fd = self
401 .file_open(path, Self::FILE_MODE_WRITE_ONLY_CREATE_TRUNC)
402 .await?;
403 let result = async {
404 let mut buf = vec![0u8; 1024 * 1024];
405 loop {
406 let n = reader.read(&mut buf).await?;
407 if n == 0 {
408 break;
409 }
410 self.file_write(fd, &buf[..n]).await?;
411 }
412 Ok::<(), AfcError>(())
413 }
414 .await;
415 let close_result = self.file_close(fd).await;
416 result?;
417 close_result?;
418 Ok(())
419 }
420
421 pub async fn open_file(&mut self, path: &str, mode: u64) -> Result<u64, AfcError> {
422 self.file_open(path, mode).await
423 }
424
425 pub async fn lock_file(&mut self, fd: u64, operation: u64) -> Result<(), AfcError> {
426 let mut hp = [0u8; 16];
427 hp[..8].copy_from_slice(&fd.to_le_bytes());
428 hp[8..].copy_from_slice(&operation.to_le_bytes());
429 self.send(AfcOpcode::FileRefLock, &hp, &[]).await?;
430 self.recv().await?;
431 Ok(())
432 }
433
434 pub async fn close_file(&mut self, fd: u64) -> Result<(), AfcError> {
435 self.file_close(fd).await
436 }
437
438 pub async fn device_info(&mut self) -> Result<HashMap<String, String>, AfcError> {
440 self.send(AfcOpcode::GetDeviceInfo, &[], &[]).await?;
441 let pkt = self.recv().await?;
442 Ok(parse_kv_pairs(&pkt.payload))
443 }
444
445 async fn file_open(&mut self, path: &str, mode: u64) -> Result<u64, AfcError> {
448 let mut hp = vec![0u8; 8];
449 hp[..8].copy_from_slice(&mode.to_le_bytes());
450 hp.extend_from_slice(path.as_bytes());
451 hp.push(0);
452 self.send(AfcOpcode::FileRefOpen, &hp, &[]).await?;
453 let pkt = self.recv().await?;
454 if pkt.header_payload.len() < 8 {
455 return Err(AfcError::Protocol(
456 "FileRefOpenResult: short response".into(),
457 ));
458 }
459 let fd = u64::from_le_bytes(
460 pkt.header_payload[..8]
461 .try_into()
462 .map_err(|_| AfcError::Protocol("bad file handle".into()))?,
463 );
464 Ok(fd)
465 }
466
467 async fn file_read(&mut self, fd: u64, size: u64) -> Result<Bytes, AfcError> {
468 let mut hp = [0u8; 16];
469 hp[..8].copy_from_slice(&fd.to_le_bytes());
470 hp[8..].copy_from_slice(&size.to_le_bytes());
471 self.send(AfcOpcode::FileRefRead, &hp, &[]).await?;
472 let pkt = self.recv().await?;
473 Ok(pkt.payload)
474 }
475
476 async fn file_write(&mut self, fd: u64, data: &[u8]) -> Result<(), AfcError> {
477 let mut hp = [0u8; 8];
478 hp.copy_from_slice(&fd.to_le_bytes());
479 self.send(AfcOpcode::FileRefWrite, &hp, data).await?;
480 self.recv().await?;
481 Ok(())
482 }
483
484 async fn file_close(&mut self, fd: u64) -> Result<(), AfcError> {
485 let hp = fd.to_le_bytes();
486 self.send(AfcOpcode::FileRefClose, &hp, &[]).await?;
487 self.recv().await?;
488 Ok(())
489 }
490
491 async fn resolve_read_path(&mut self, path: &str) -> Result<String, AfcError> {
492 let info = self.stat_info(path).await?;
493 Ok(resolve_link_target(path, &info))
494 }
495}
496
497fn split_null_strings(data: &[u8]) -> Vec<String> {
501 data.split(|&b| b == 0)
502 .filter(|s| !s.is_empty())
503 .map(|s| String::from_utf8_lossy(s).into_owned())
504 .collect()
505}
506
507fn parse_kv_pairs(data: &[u8]) -> HashMap<String, String> {
509 let parts = split_null_strings(data);
510 let mut map = HashMap::new();
511 let mut it = parts.into_iter();
512 while let (Some(k), Some(v)) = (it.next(), it.next()) {
513 map.insert(k, v);
514 }
515 map
516}
517
518fn parse_file_info(path: &str, raw: HashMap<String, String>) -> AfcFileInfo {
519 let name = path
520 .rsplit('/')
521 .next()
522 .filter(|s| !s.is_empty())
523 .map(str::to_string);
524 let file_type = raw.get("st_ifmt").cloned();
525 let size = raw.get("st_size").and_then(|s| s.parse::<u64>().ok());
526 let mode = raw
527 .get("st_mode")
528 .and_then(|s| u32::from_str_radix(s, 8).ok());
529 let link_target = raw.get("st_linktarget").cloned().filter(|s| !s.is_empty());
530
531 AfcFileInfo {
532 name,
533 file_type,
534 size,
535 mode,
536 link_target,
537 raw,
538 }
539}
540
541fn resolve_link_target(path: &str, info: &AfcFileInfo) -> String {
542 let is_link = matches!(info.file_type.as_deref(), Some("S_IFLNK"));
543 if is_link {
544 if let Some(target) = &info.link_target {
545 return target.clone();
546 }
547 }
548 path.to_string()
549}
550
551pub mod mode {
553 pub const READ_ONLY: u64 = 0x00000001;
554 pub const READ_WRITE_CREATE: u64 = 0x00000002;
555 pub const WRITE_ONLY_CREATE_TRUNC: u64 = 0x00000003;
556 pub const READ_WRITE_CREATE_TRUNC: u64 = 0x00000004;
557 pub const WRITE_ONLY_CREATE_APPEND: u64 = 0x00000005;
558 pub const READ_WRITE_CREATE_APPEND: u64 = 0x00000006;
559}
560
561#[cfg(test)]
562mod tests {
563 use crate::test_util::MockStream;
564
565 use super::*;
566 use zerocopy::FromBytes;
567
568 fn afc_frame(opcode: AfcOpcode, header_payload: &[u8], payload: &[u8]) -> Vec<u8> {
569 let header = AfcHeader::new(1, opcode, header_payload.len(), payload.len());
570 let mut frame = header.as_bytes().to_vec();
571 frame.extend_from_slice(header_payload);
572 frame.extend_from_slice(payload);
573 frame
574 }
575
576 fn afc_status_success_frame() -> Vec<u8> {
577 afc_frame(AfcOpcode::Status, &0u64.to_le_bytes(), &[])
578 }
579
580 fn afc_open_result_frame(fd: u64) -> Vec<u8> {
581 afc_frame(AfcOpcode::FileRefOpenResult, &fd.to_le_bytes(), &[])
582 }
583
584 fn afc_write_success_responses(write_count: usize) -> Vec<u8> {
585 let mut responses = afc_open_result_frame(42);
586 for _ in 0..write_count {
587 responses.extend_from_slice(&afc_status_success_frame());
588 }
589 responses.extend_from_slice(&afc_status_success_frame());
590 responses
591 }
592
593 fn file_write_payloads(written: &[u8]) -> Vec<Vec<u8>> {
594 let mut pos = 0;
595 let mut payloads = Vec::new();
596 while pos + AfcHeader::SIZE <= written.len() {
597 let header = AfcHeader::ref_from_bytes(&written[pos..pos + AfcHeader::SIZE]).unwrap();
598 let entire_len = header.entire_len.get() as usize;
599 let this_len = header.this_len.get() as usize;
600 let payload_start = pos + this_len;
601 let payload_end = pos + entire_len;
602 if header.operation.get() == AfcOpcode::FileRefWrite as u64 {
603 payloads.push(written[payload_start..payload_end].to_vec());
604 }
605 pos += entire_len;
606 }
607 assert_eq!(pos, written.len());
608 payloads
609 }
610
611 #[test]
612 fn test_split_null_strings() {
613 let data = b"foo\0bar\0baz\0";
614 let result = split_null_strings(data);
615 assert_eq!(result, vec!["foo", "bar", "baz"]);
616 }
617
618 #[test]
619 fn test_parse_kv_pairs() {
620 let data = b"st_size\x0012345\0st_ifmt\0S_IFREG\0";
621 let map = parse_kv_pairs(data);
622 assert_eq!(map["st_size"], "12345");
623 assert_eq!(map["st_ifmt"], "S_IFREG");
624 }
625
626 #[test]
627 fn test_afc_header_size() {
628 assert_eq!(std::mem::size_of::<AfcHeader>(), 40);
629 }
630
631 #[test]
632 fn test_afc_header_new() {
633 let hdr = AfcHeader::new(7, AfcOpcode::ReadDir, 5, 10);
634 assert_eq!(hdr.magic.get(), AFC_MAGIC);
635 assert_eq!(hdr.packet_num.get(), 7);
636 assert_eq!(hdr.this_len.get(), 45); assert_eq!(hdr.entire_len.get(), 55); assert_eq!(hdr.operation.get(), AfcOpcode::ReadDir as u64);
639 }
640
641 #[tokio::test]
643 async fn test_list_dir_roundtrip() {
644 use zerocopy::IntoBytes;
645
646 let names = b".\0..\0Photos\0Downloads\0";
649 let hdr = AfcHeader::new(
650 1, AfcOpcode::ReadDir, 0, names.len(), );
655 let mut server_resp = hdr.as_bytes().to_vec();
656 server_resp.extend_from_slice(names); let (client_side, mut server_side) = tokio::io::duplex(4096);
659 tokio::spawn(async move {
660 use tokio::io::{AsyncReadExt, AsyncWriteExt};
661 let mut buf = vec![0u8; 256];
662 let _ = server_side.read(&mut buf).await;
663 server_side.write_all(&server_resp).await.unwrap();
664 });
665
666 let mut afc = AfcClient::new(client_side);
667 let entries = afc.list_dir("/").await.unwrap();
668 assert_eq!(entries, vec!["Photos", "Downloads"]);
670 }
671
672 #[test]
673 fn test_resolve_link_target_uses_st_linktarget_for_symlink() {
674 let mut raw = HashMap::new();
675 raw.insert("st_ifmt".to_string(), "S_IFLNK".to_string());
676 raw.insert(
677 "st_linktarget".to_string(),
678 "/var/mobile/real-file".to_string(),
679 );
680 let info = parse_file_info("/var/mobile/link", raw);
681
682 let resolved = resolve_link_target("/var/mobile/link", &info);
683 assert_eq!(resolved, "/var/mobile/real-file");
684 }
685
686 #[test]
687 fn test_resolve_link_target_keeps_original_path_for_regular_file() {
688 let mut raw = HashMap::new();
689 raw.insert("st_ifmt".to_string(), "S_IFREG".to_string());
690 let info = parse_file_info("/var/mobile/file", raw);
691
692 let resolved = resolve_link_target("/var/mobile/file", &info);
693 assert_eq!(resolved, "/var/mobile/file");
694 }
695
696 #[test]
697 fn test_parse_file_info_parses_st_mode_from_octal() {
698 let mut raw = HashMap::new();
699 raw.insert("st_ifmt".to_string(), "S_IFREG".to_string());
700 raw.insert("st_mode".to_string(), "100644".to_string());
701 raw.insert("st_size".to_string(), "12".to_string());
702
703 let info = parse_file_info("/var/mobile/file.txt", raw);
704 assert_eq!(info.name.as_deref(), Some("file.txt"));
705 assert_eq!(info.file_type.as_deref(), Some("S_IFREG"));
706 assert_eq!(info.size, Some(12));
707 assert_eq!(info.mode, Some(0o100644));
708 }
709
710 #[test]
711 fn test_afc_status_code_mapping_matches_go_ios_upper_status_codes() {
712 assert_eq!(AfcStatusCode::from_u64(24), AfcStatusCode::Other(24));
713 assert_eq!(AfcStatusCode::from_u64(25), AfcStatusCode::Other(25));
714 assert_eq!(AfcStatusCode::from_u64(26), AfcStatusCode::Other(26));
715 assert_eq!(AfcStatusCode::from_u64(27), AfcStatusCode::Other(27));
716 assert_eq!(AfcStatusCode::from_u64(30), AfcStatusCode::MuxError);
717 assert_eq!(AfcStatusCode::from_u64(31), AfcStatusCode::NoMem);
718 assert_eq!(AfcStatusCode::from_u64(32), AfcStatusCode::NotEnoughData);
719 assert_eq!(AfcStatusCode::from_u64(33), AfcStatusCode::DirNotEmpty);
720 }
721
722 #[tokio::test]
723 async fn write_file_from_reader_sends_chunks_without_prebuffering() {
724 let payload = vec![0xAB; 1024 * 1024 + 17];
725 let mut reader = std::io::Cursor::new(payload.clone());
726 let mut stream = MockStream::new(afc_write_success_responses(2));
727 let mut client = AfcClient::new(&mut stream);
728
729 client
730 .write_file_from_reader("/PublicStaging/app.ipa", &mut reader)
731 .await
732 .unwrap();
733
734 let payloads = file_write_payloads(&stream.written);
735 assert_eq!(payloads.len(), 2);
736 assert_eq!(payloads[0].len(), 1024 * 1024);
737 assert_eq!(payloads[1].len(), 17);
738 assert_eq!(
739 payloads.concat(),
740 payload,
741 "AFC file writes should preserve the streamed payload"
742 );
743 }
744}