From 9e8abb7d374f98a04858344046f8772ba6173d56 Mon Sep 17 00:00:00 2001 From: Abdullah Al-Banna Date: Tue, 9 Dec 2025 22:58:09 +0300 Subject: [PATCH] open an owned file in AFC (#44) * ability to open an owned file Signed-off-by: Abdullah Al-Banna * get the inner afc of an owned file without closing the file Signed-off-by: Abdullah Al-Banna --------- Signed-off-by: Abdullah Al-Banna --- idevice/src/services/afc/file.rs | 84 ++++++++--- idevice/src/services/afc/inner_file.rs | 135 ++++++++++++++---- .../src/services/afc/inner_file_impl_macro.rs | 23 +++ idevice/src/services/afc/mod.rs | 53 ++++++- 4 files changed, 242 insertions(+), 53 deletions(-) create mode 100644 idevice/src/services/afc/inner_file_impl_macro.rs diff --git a/idevice/src/services/afc/file.rs b/idevice/src/services/afc/file.rs index e31b3b4..4b56f7f 100644 --- a/idevice/src/services/afc/file.rs +++ b/idevice/src/services/afc/file.rs @@ -4,21 +4,31 @@ use std::{io::SeekFrom, marker::PhantomPinned, pin::Pin}; use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite}; -use super::inner_file::InnerFileDescriptor; -use crate::IdeviceError; +use crate::{ + IdeviceError, + afc::{ + AfcClient, + inner_file::{InnerFileDescriptor, OwnedInnerFileDescriptor}, + }, +}; #[derive(Debug)] pub struct FileDescriptor<'a> { inner: Pin>>, } +#[derive(Debug)] +pub struct OwnedFileDescriptor { + inner: Pin>, +} + impl<'a> FileDescriptor<'a> { /// create a new FileDescriptor from a raw fd /// /// # Safety /// make sure the fd is an opened file, and that you got it from a previous /// FileDescriptor via `as_raw_fd()` method - pub unsafe fn new(client: &'a mut super::AfcClient, fd: u64, path: String) -> Self { + pub unsafe fn new(client: &'a mut AfcClient, fd: u64, path: String) -> Self { Self { inner: Box::pin(InnerFileDescriptor { client, @@ -30,20 +40,54 @@ impl<'a> FileDescriptor<'a> { } } - pub fn as_raw_fd(&self) -> u64 { - self.inner.fd - } -} -impl FileDescriptor<'_> { - /// Returns the current cursor position for the file - pub async fn seek_tell(&mut self) -> Result { - self.inner.as_mut().seek_tell().await - } - /// Closes the file descriptor pub async fn close(self) -> Result<(), IdeviceError> { self.inner.close().await } +} + +impl OwnedFileDescriptor { + /// create a new OwnedFileDescriptor from a raw fd + /// + /// # Safety + /// make sure the fd is an opened file, and that you got it from a previous + /// OwnedFileDescriptor via `as_raw_fd()` method + pub unsafe fn new(client: AfcClient, fd: u64, path: String) -> Self { + Self { + inner: Box::pin(OwnedInnerFileDescriptor { + client, + fd, + path, + pending_fut: None, + _m: PhantomPinned, + }), + } + } + + /// Closes the file descriptor + pub async fn close(self) -> Result { + self.inner.close().await + } + + /// gets the owned afc + /// + /// # Safety + /// this get's the afc out without closing, if you want to get the afc and close the file, use + /// `.close()` + pub unsafe fn get_inner_afc(self) -> AfcClient { + self.inner.get_inner_afc() + } +} + +crate::impl_to_structs!(FileDescriptor<'_>, OwnedFileDescriptor; { + pub fn as_raw_fd(&self) -> u64 { + self.inner.fd + } + + /// Returns the current cursor position for the file + pub async fn seek_tell(&mut self) -> Result { + self.inner.as_mut().seek_tell().await + } /// Reads the entire contents of the file /// @@ -60,9 +104,9 @@ impl FileDescriptor<'_> { pub async fn write_entire(&mut self, bytes: &[u8]) -> Result<(), IdeviceError> { self.inner.as_mut().write(bytes).await } -} +}); -impl AsyncRead for FileDescriptor<'_> { +crate::impl_trait_to_structs!(AsyncRead for FileDescriptor<'_>, OwnedFileDescriptor; { fn poll_read( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, @@ -71,9 +115,9 @@ impl AsyncRead for FileDescriptor<'_> { let inner = self.inner.as_mut(); inner.poll_read(cx, buf) } -} +}); -impl AsyncWrite for FileDescriptor<'_> { +crate::impl_trait_to_structs!(AsyncWrite for FileDescriptor<'_>, OwnedFileDescriptor; { fn poll_write( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, @@ -98,9 +142,9 @@ impl AsyncWrite for FileDescriptor<'_> { let inner = self.inner.as_mut(); inner.poll_shutdown(cx) } -} +}); -impl AsyncSeek for FileDescriptor<'_> { +crate::impl_trait_to_structs!(AsyncSeek for FileDescriptor<'_>, OwnedFileDescriptor; { fn start_seek(mut self: Pin<&mut Self>, position: SeekFrom) -> std::io::Result<()> { let this = self.inner.as_mut(); this.start_seek(position) @@ -113,4 +157,4 @@ impl AsyncSeek for FileDescriptor<'_> { let this = self.inner.as_mut(); this.poll_complete(cx) } -} +}); diff --git a/idevice/src/services/afc/inner_file.rs b/idevice/src/services/afc/inner_file.rs index 270b140..a245fed 100644 --- a/idevice/src/services/afc/inner_file.rs +++ b/idevice/src/services/afc/inner_file.rs @@ -5,11 +5,13 @@ use std::{io::SeekFrom, pin::Pin}; use futures::{FutureExt, future::BoxFuture}; use tokio::io::{AsyncRead, AsyncSeek, AsyncWrite}; -use crate::IdeviceError; - -use super::{ - opcode::AfcOpcode, - packet::{AfcPacket, AfcPacketHeader}, +use crate::{ + IdeviceError, + afc::{ + AfcClient, MAGIC, + opcode::AfcOpcode, + packet::{AfcPacket, AfcPacketHeader}, + }, }; /// Maximum transfer size for file operations (1MB) @@ -32,19 +34,31 @@ pub(crate) enum PendingResult { Bytes(Vec), } +type OwnedBoxFuture = Pin> + Send>>; + /// Handle for an open file on the device. /// Call close before dropping pub(crate) struct InnerFileDescriptor<'a> { - pub(crate) client: &'a mut super::AfcClient, + pub(crate) client: &'a mut AfcClient, pub(crate) fd: u64, pub(crate) path: String, pub(crate) pending_fut: Option>>, - pub(crate) _m: std::marker::PhantomPinned, } -impl InnerFileDescriptor<'_> { +/// Handle for an owned open file on the device. +/// Call close before dropping +pub(crate) struct OwnedInnerFileDescriptor { + pub(crate) client: AfcClient, + pub(crate) fd: u64, + pub(crate) path: String, + + pub(crate) pending_fut: Option, + pub(crate) _m: std::marker::PhantomPinned, +} + +crate::impl_to_structs!(InnerFileDescriptor<'_>, OwnedInnerFileDescriptor; { /// Generic helper to send an AFC packet and read the response pub async fn send_packet( self: Pin<&mut Self>, @@ -57,7 +71,7 @@ impl InnerFileDescriptor<'_> { let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN; let header = AfcPacketHeader { - magic: super::MAGIC, + magic: MAGIC, entire_len: header_len + payload.len() as u64, header_payload_len: header_len, packet_num: this.client.package_number, @@ -115,15 +129,6 @@ impl InnerFileDescriptor<'_> { self.as_mut().seek_tell().await } - /// Closes the file descriptor - pub async fn close(mut self: Pin>) -> Result<(), IdeviceError> { - let header_payload = self.fd.to_le_bytes().to_vec(); - - self.as_mut() - .send_packet(AfcOpcode::FileClose, header_payload, Vec::new()) - .await?; - Ok(()) - } /// Reads n size of contents from the file /// @@ -188,7 +193,7 @@ impl InnerFileDescriptor<'_> { fn store_pending_read(mut self: Pin<&mut Self>, buf_rem: usize) { unsafe { - let this = self.as_mut().get_unchecked_mut() as *mut InnerFileDescriptor; + let this = self.as_mut().get_unchecked_mut() as *mut Self; let fut = Some( // SAFETY: we already know that self is pinned @@ -204,7 +209,7 @@ impl InnerFileDescriptor<'_> { fn store_pending_seek(mut self: Pin<&mut Self>, position: std::io::SeekFrom) { unsafe { - let this = self.as_mut().get_unchecked_mut() as *mut InnerFileDescriptor; + let this = self.as_mut().get_unchecked_mut() as *mut Self; let fut = Some( Pin::new_unchecked(&mut *this) @@ -221,7 +226,7 @@ impl InnerFileDescriptor<'_> { unsafe { let this = self.as_mut().get_unchecked_mut(); - let this = this as *mut InnerFileDescriptor; + let this = this as *mut Self; // move the entire buffer into the future so we don't have to store it somewhere let pined_this = Pin::new_unchecked(&mut *this); @@ -232,7 +237,7 @@ impl InnerFileDescriptor<'_> { (&mut *this).pending_fut = Some(fut); } } -} +}); impl<'a> InnerFileDescriptor<'a> { fn get_or_init_read_fut( @@ -268,9 +273,63 @@ impl<'a> InnerFileDescriptor<'a> { self.as_mut().get_unchecked_mut().pending_fut.take(); } } + + /// Closes the file descriptor + pub async fn close(mut self: Pin>) -> Result<(), IdeviceError> { + let header_payload = self.fd.to_le_bytes().to_vec(); + + self.as_mut() + .send_packet(AfcOpcode::FileClose, header_payload, Vec::new()) + .await?; + Ok(()) + } } -impl AsyncRead for InnerFileDescriptor<'_> { +impl OwnedInnerFileDescriptor { + fn get_or_init_read_fut(mut self: Pin<&mut Self>, buf_rem: usize) -> &mut OwnedBoxFuture { + if self.as_ref().pending_fut.is_none() { + self.as_mut().store_pending_read(buf_rem); + } + + unsafe { self.get_unchecked_mut().pending_fut.as_mut().unwrap() } + } + + fn get_or_init_write_fut(mut self: Pin<&mut Self>, buf: &'_ [u8]) -> &mut OwnedBoxFuture { + if self.as_ref().pending_fut.is_none() { + self.as_mut().store_pending_write(buf); + } + + unsafe { self.get_unchecked_mut().pending_fut.as_mut().unwrap() } + } + + fn get_seek_fut(self: Pin<&mut Self>) -> Option<&mut OwnedBoxFuture> { + unsafe { self.get_unchecked_mut().pending_fut.as_mut() } + } + + fn remove_pending_fut(mut self: Pin<&mut Self>) { + unsafe { + self.as_mut().get_unchecked_mut().pending_fut.take(); + } + } + + /// Closes the file descriptor + pub async fn close(mut self: Pin>) -> Result { + let header_payload = self.fd.to_le_bytes().to_vec(); + + self.as_mut() + .send_packet(AfcOpcode::FileClose, header_payload, Vec::new()) + .await?; + + // we don't need it to be pinned anymore + Ok(unsafe { Pin::into_inner_unchecked(self) }.client) + } + + pub fn get_inner_afc(self: Pin>) -> AfcClient { + unsafe { Pin::into_inner_unchecked(self).client } + } +} + +crate::impl_trait_to_structs!(AsyncRead for InnerFileDescriptor<'_>, OwnedInnerFileDescriptor; { fn poll_read( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, @@ -293,9 +352,9 @@ impl AsyncRead for InnerFileDescriptor<'_> { std::task::Poll::Ready(Ok(())) } -} +}); -impl AsyncWrite for InnerFileDescriptor<'_> { +crate::impl_trait_to_structs!(AsyncWrite for InnerFileDescriptor<'_>, OwnedInnerFileDescriptor; { fn poll_write( mut self: std::pin::Pin<&mut Self>, cx: &mut std::task::Context<'_>, @@ -329,9 +388,10 @@ impl AsyncWrite for InnerFileDescriptor<'_> { ) -> std::task::Poll> { std::task::Poll::Ready(Ok(())) } -} -impl AsyncSeek for InnerFileDescriptor<'_> { +}); + +crate::impl_trait_to_structs!(AsyncSeek for InnerFileDescriptor<'_>, OwnedInnerFileDescriptor; { fn start_seek(self: Pin<&mut Self>, position: SeekFrom) -> std::io::Result<()> { self.store_pending_seek(position); @@ -356,7 +416,7 @@ impl AsyncSeek for InnerFileDescriptor<'_> { _ => unreachable!("a non seek future was stored, this shouldn't happen"), } } -} +}); impl std::fmt::Debug for InnerFileDescriptor<'_> { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { @@ -368,18 +428,31 @@ impl std::fmt::Debug for InnerFileDescriptor<'_> { } } +impl std::fmt::Debug for OwnedInnerFileDescriptor { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("OwnedInnerFileDescriptor") + .field("client", &self.client) + .field("fd", &self.fd) + .field("path", &self.path) + .finish() + } +} + #[cfg(test)] mod tests { use std::sync::Arc; use tokio::io::{AsyncReadExt, AsyncSeekExt, AsyncWriteExt}; - use crate::usbmuxd::{UsbmuxdAddr, UsbmuxdConnection}; + use crate::{ + IdeviceService as _, + afc::opcode::AfcFopenMode, + usbmuxd::{UsbmuxdAddr, UsbmuxdConnection}, + }; - use super::super::*; use super::*; - async fn make_client() -> super::super::AfcClient { + async fn make_client() -> AfcClient { let mut u = UsbmuxdConnection::default() .await .expect("failed to connect to usbmuxd"); diff --git a/idevice/src/services/afc/inner_file_impl_macro.rs b/idevice/src/services/afc/inner_file_impl_macro.rs new file mode 100644 index 0000000..f169ac2 --- /dev/null +++ b/idevice/src/services/afc/inner_file_impl_macro.rs @@ -0,0 +1,23 @@ +#[macro_export] +macro_rules! impl_to_structs { + ( + $( $name:ident $(<$li:lifetime>)? ),+; + $body:tt + ) => { + $( + impl $name $(<$li>)? $body + )+ + }; +} + +#[macro_export] +macro_rules! impl_trait_to_structs { + ( + $trit:ident for $( $name:ident $(<$li:lifetime>)? ),+; + $body:tt + ) => { + $( + impl $trit for $name $(<$li>)? $body + )+ + }; +} diff --git a/idevice/src/services/afc/mod.rs b/idevice/src/services/afc/mod.rs index 90927d1..f868585 100644 --- a/idevice/src/services/afc/mod.rs +++ b/idevice/src/services/afc/mod.rs @@ -6,16 +6,20 @@ use std::collections::HashMap; use errors::AfcError; -use file::FileDescriptor; use opcode::{AfcFopenMode, AfcOpcode}; use packet::{AfcPacket, AfcPacketHeader}; use tracing::warn; -use crate::{Idevice, IdeviceError, IdeviceService, obf}; +use crate::{ + Idevice, IdeviceError, IdeviceService, + afc::file::{FileDescriptor, OwnedFileDescriptor}, + obf, +}; pub mod errors; pub mod file; mod inner_file; +mod inner_file_impl_macro; pub mod opcode; pub mod packet; @@ -415,6 +419,51 @@ impl AfcClient { Ok(unsafe { FileDescriptor::new(self, fd, path) }) } + /// Opens an owned file on the device + /// + /// # Arguments + /// * `path` - Path to the file to open + /// * `mode` - Opening mode (read, write, etc.) + /// + /// # Returns + /// A `OwnedFileDescriptor` struct for the opened file + pub async fn open_owned( + mut self, + path: impl Into, + mode: AfcFopenMode, + ) -> Result { + let path = path.into(); + let mut header_payload = (mode as u64).to_le_bytes().to_vec(); + header_payload.extend(path.as_bytes()); + let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN; + + let header = AfcPacketHeader { + magic: MAGIC, + entire_len: header_len, // it's the same since the payload is empty for this + header_payload_len: header_len, + packet_num: self.package_number, + operation: AfcOpcode::FileOpen, + }; + self.package_number += 1; + + let packet = AfcPacket { + header, + header_payload, + payload: Vec::new(), + }; + + self.send(packet).await?; + let res = self.read().await?; + if res.header_payload.len() < 8 { + warn!("Header payload fd is less than 8 bytes"); + return Err(IdeviceError::UnexpectedResponse); + } + let fd = u64::from_le_bytes(res.header_payload[..8].try_into().unwrap()); + + // we know it's a valid fd + Ok(unsafe { OwnedFileDescriptor::new(self, fd, path) }) + } + /// Creates a hard or symbolic link /// /// # Arguments