From 12f2a22b1ec355af6c37996bb8acf9fec155a971 Mon Sep 17 00:00:00 2001 From: Jackson Coxson Date: Sat, 5 Apr 2025 12:07:55 -0600 Subject: [PATCH] afc get device info --- idevice/Cargo.toml | 2 +- idevice/src/afc/mod.rs | 135 ++++++++++++++++++++++++++++++++++++-- idevice/src/afc/opcode.rs | 1 + idevice/src/lib.rs | 2 +- tools/src/afc.rs | 24 +++++-- 5 files changed, 150 insertions(+), 14 deletions(-) diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index fa25dae..15b4f7a 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -26,7 +26,7 @@ indexmap = { version = "2.7", features = ["serde"], optional = true } uuid = { version = "1.12", features = ["serde", "v4"], optional = true } async-recursion = { version = "1.1", optional = true } base64 = { version = "0.22", optional = true } -chrono = { version = "0.4.40", optional = true, default_features = false } +chrono = { version = "0.4.40", optional = true, default-features = false } serde_json = { version = "1", optional = true } json = { version = "0.12", optional = true } diff --git a/idevice/src/afc/mod.rs b/idevice/src/afc/mod.rs index 0840b39..481778b 100644 --- a/idevice/src/afc/mod.rs +++ b/idevice/src/afc/mod.rs @@ -20,7 +20,7 @@ pub struct AfcClient { package_number: u64, } -#[derive(Debug)] +#[derive(Clone, Debug)] pub struct FileInfo { pub size: usize, pub blocks: usize, @@ -31,6 +31,14 @@ pub struct FileInfo { pub st_link_target: Option, } +#[derive(Clone, Debug)] +pub struct DeviceInfo { + model: String, + total_bytes: usize, + free_bytes: usize, + block_size: usize, +} + impl IdeviceService for AfcClient { fn service_name() -> &'static str { "com.apple.afc" @@ -167,30 +175,30 @@ impl AfcClient { let size = kvs .remove("st_size") .and_then(|x| x.parse::().ok()) - .ok_or(IdeviceError::AfcMissingFileAttribute)?; + .ok_or(IdeviceError::AfcMissingAttribute)?; let blocks = kvs .remove("st_blocks") .and_then(|x| x.parse::().ok()) - .ok_or(IdeviceError::AfcMissingFileAttribute)?; + .ok_or(IdeviceError::AfcMissingAttribute)?; let creation = kvs .remove("st_birthtime") .and_then(|x| x.parse::().ok()) - .ok_or(IdeviceError::AfcMissingFileAttribute)?; + .ok_or(IdeviceError::AfcMissingAttribute)?; let creation = chrono::DateTime::from_timestamp_nanos(creation).naive_local(); let modified = kvs .remove("st_mtime") .and_then(|x| x.parse::().ok()) - .ok_or(IdeviceError::AfcMissingFileAttribute)?; + .ok_or(IdeviceError::AfcMissingAttribute)?; let modified = chrono::DateTime::from_timestamp_nanos(modified).naive_local(); let st_nlink = kvs .remove("st_nlink") - .ok_or(IdeviceError::AfcMissingFileAttribute)?; + .ok_or(IdeviceError::AfcMissingAttribute)?; let st_ifmt = kvs .remove("st_ifmt") - .ok_or(IdeviceError::AfcMissingFileAttribute)?; + .ok_or(IdeviceError::AfcMissingAttribute)?; let st_link_target = kvs.remove("st_link_target"); if !kvs.is_empty() { @@ -208,6 +216,119 @@ impl AfcClient { }) } + pub async fn get_device_info(&mut self) -> Result { + let header_len = 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::GetDevInfo, + }; + self.package_number += 1; + + let packet = AfcPacket { + header, + header_payload: Vec::new(), + payload: Vec::new(), + }; + + self.send(packet).await?; + let res = self.read().await?; + + let strings: Vec = res + .payload + .split(|b| *b == 0) + .filter(|s| !s.is_empty()) + .map(|s| String::from_utf8_lossy(s).into_owned()) + .collect(); + + let mut kvs: HashMap = strings + .chunks_exact(2) + .map(|chunk| (chunk[0].clone(), chunk[1].clone())) + .collect(); + + let model = kvs + .remove("Model") + .ok_or(IdeviceError::AfcMissingAttribute)?; + let total_bytes = kvs + .remove("FSTotalBytes") + .and_then(|x| x.parse::().ok()) + .ok_or(IdeviceError::AfcMissingAttribute)?; + let free_bytes = kvs + .remove("FSFreeBytes") + .and_then(|x| x.parse::().ok()) + .ok_or(IdeviceError::AfcMissingAttribute)?; + let block_size = kvs + .remove("FSBlockSize") + .and_then(|x| x.parse::().ok()) + .ok_or(IdeviceError::AfcMissingAttribute)?; + + if !kvs.is_empty() { + warn!("Device info kvs not empty: {kvs:?}"); + } + + Ok(DeviceInfo { + model, + total_bytes, + free_bytes, + block_size, + }) + } + + pub async fn remove(&mut self, path: impl Into) -> Result<(), IdeviceError> { + let path = path.into(); + let header_payload = path.as_bytes().to_vec(); + 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::RemovePath, + }; + self.package_number += 1; + + let packet = AfcPacket { + header, + header_payload, + payload: Vec::new(), + }; + + self.send(packet).await?; + self.read().await?; // read a response to check for errors + + Ok(()) + } + + pub async fn remove_all(&mut self, path: impl Into) -> Result<(), IdeviceError> { + let path = path.into(); + let header_payload = path.as_bytes().to_vec(); + 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::RemovePathAndContents, + }; + self.package_number += 1; + + let packet = AfcPacket { + header, + header_payload, + payload: Vec::new(), + }; + + self.send(packet).await?; + self.read().await?; // read a response to check for errors + + Ok(()) + } + pub async fn read(&mut self) -> Result { let res = AfcPacket::read(&mut self.idevice).await?; if res.header.operation == AfcOpcode::Status { diff --git a/idevice/src/afc/opcode.rs b/idevice/src/afc/opcode.rs index 6f5f88c..e550426 100644 --- a/idevice/src/afc/opcode.rs +++ b/idevice/src/afc/opcode.rs @@ -32,6 +32,7 @@ pub enum AfcOpcode { FileLock = 0x0000001B, // FileRefLock MakeLink = 0x0000001C, // MakeLink SetFileTime = 0x0000001E, // Set st_mtime + RemovePathAndContents = 0x00000022, } pub enum AfcFopenMode { diff --git a/idevice/src/lib.rs b/idevice/src/lib.rs index 5a254e1..28ddc6a 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -314,7 +314,7 @@ pub enum IdeviceError { #[cfg(feature = "afc")] #[error("missing file attribute")] - AfcMissingFileAttribute, + AfcMissingAttribute, #[cfg(any(feature = "tss", feature = "tunneld"))] #[error("http reqwest error")] diff --git a/tools/src/afc.rs b/tools/src/afc.rs index 219361a..0e84759 100644 --- a/tools/src/afc.rs +++ b/tools/src/afc.rs @@ -1,9 +1,7 @@ // Jackson Coxson -use std::path::PathBuf; - -use clap::{arg, value_parser, Arg, Command}; -use idevice::{afc::AfcClient, misagent::MisagentClient, IdeviceService}; +use clap::{Arg, Command}; +use idevice::{afc::AfcClient, IdeviceService}; mod common; @@ -51,11 +49,17 @@ async fn main() { .about("Remove a provisioning profile") .arg(Arg::new("path").required(true).index(1)), ) + .subcommand( + Command::new("remove_all") + .about("Remove a provisioning profile") + .arg(Arg::new("path").required(true).index(1)), + ) .subcommand( Command::new("info") .about("Get info about a file") .arg(Arg::new("path").required(true).index(1)), ) + .subcommand(Command::new("device_info").about("Get info about the device")) .get_matches(); if matches.get_flag("about") { @@ -87,7 +91,11 @@ async fn main() { let path = matches.get_one::("path").expect("No path passed"); afc_client.mk_dir(path).await.expect("Failed to mkdir"); } else if let Some(matches) = matches.subcommand_matches("remove") { - let path = matches.get_one::("id").expect("No path passed"); + let path = matches.get_one::("path").expect("No path passed"); + afc_client.remove(path).await.expect("Failed to remove"); + } else if let Some(matches) = matches.subcommand_matches("remove_all") { + let path = matches.get_one::("path").expect("No path passed"); + afc_client.remove_all(path).await.expect("Failed to remove"); } else if let Some(matches) = matches.subcommand_matches("info") { let path = matches.get_one::("path").expect("No path passed"); let res = afc_client @@ -95,6 +103,12 @@ async fn main() { .await .expect("Failed to get file info"); println!("{res:#?}"); + } else if matches.subcommand_matches("device_info").is_some() { + let res = afc_client + .get_device_info() + .await + .expect("Failed to get file info"); + println!("{res:#?}"); } else { eprintln!("Invalid usage, pass -h for help"); }