diff --git a/Cargo.lock b/Cargo.lock index f11f850..ae3e8c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -337,6 +337,15 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.40" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1a7964611d71df112cb1730f2ee67324fcf4d0fc6606acbbe9bfe06df124637c" +dependencies = [ + "num-traits", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -1124,6 +1133,7 @@ dependencies = [ "base64", "byteorder", "bytes", + "chrono", "env_logger 0.11.7", "futures", "indexmap", @@ -1464,6 +1474,15 @@ version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" +[[package]] +name = "num-traits" +version = "0.2.19" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "071dfc062690e90b734c0b2273ce72ad0ffa95f0c74596bc250dcfd960262841" +dependencies = [ + "autocfg", +] + [[package]] name = "num_threads" version = "0.1.7" diff --git a/idevice/Cargo.toml b/idevice/Cargo.toml index 707056f..fa25dae 100644 --- a/idevice/Cargo.toml +++ b/idevice/Cargo.toml @@ -26,6 +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 } serde_json = { version = "1", optional = true } json = { version = "0.12", optional = true } @@ -43,7 +44,7 @@ tun-rs = { version = "2.0.8", features = ["async_tokio"] } bytes = "1.10.1" [features] -afc = [] +afc = ["dep:chrono"] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] debug_proxy = [] dvt = ["dep:byteorder", "dep:ns-keyed-archive"] diff --git a/idevice/src/afc/mod.rs b/idevice/src/afc/mod.rs index 354ab43..fab3e0f 100644 --- a/idevice/src/afc/mod.rs +++ b/idevice/src/afc/mod.rs @@ -1,6 +1,9 @@ // Jackson Coxson +use std::collections::HashMap; + use errors::AfcError; +use log::warn; use opcode::AfcOpcode; use packet::{AfcPacket, AfcPacketHeader}; @@ -17,6 +20,17 @@ pub struct AfcClient { package_number: u64, } +#[derive(Debug)] +pub struct FileInfo { + pub size: usize, + pub blocks: usize, + pub creation: chrono::NaiveDateTime, + pub modified: chrono::NaiveDateTime, + pub st_nlink: String, + pub st_ifmt: String, + pub st_link_target: Option, +} + impl IdeviceService for AfcClient { fn service_name() -> &'static str { "com.apple.afc" @@ -54,7 +68,7 @@ impl AfcClient { } } - pub async fn list(&mut self, path: impl Into) -> Result, IdeviceError> { + pub async fn list_dir(&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; @@ -86,6 +100,88 @@ impl AfcClient { Ok(strings) } + pub async fn get_file_info( + &mut self, + path: impl Into, + ) -> Result { + 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::GetFileInfo, + }; + self.package_number += 1; + + let packet = AfcPacket { + header, + header_payload, + 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 size = kvs + .remove("st_size") + .and_then(|x| x.parse::().ok()) + .ok_or(IdeviceError::AfcMissingFileAttribute)?; + let blocks = kvs + .remove("st_blocks") + .and_then(|x| x.parse::().ok()) + .ok_or(IdeviceError::AfcMissingFileAttribute)?; + + let creation = kvs + .remove("st_birthtime") + .and_then(|x| x.parse::().ok()) + .ok_or(IdeviceError::AfcMissingFileAttribute)?; + 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)?; + let modified = chrono::DateTime::from_timestamp_nanos(modified).naive_local(); + + let st_nlink = kvs + .remove("st_nlink") + .ok_or(IdeviceError::AfcMissingFileAttribute)?; + let st_ifmt = kvs + .remove("st_ifmt") + .ok_or(IdeviceError::AfcMissingFileAttribute)?; + let st_link_target = kvs.remove("st_link_target"); + + if !kvs.is_empty() { + warn!("File info kvs not empty: {kvs:?}"); + } + + Ok(FileInfo { + size, + blocks, + creation, + modified, + st_nlink, + st_ifmt, + st_link_target, + }) + } + 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/lib.rs b/idevice/src/lib.rs index 7eca65c..5a254e1 100644 --- a/idevice/src/lib.rs +++ b/idevice/src/lib.rs @@ -312,6 +312,10 @@ pub enum IdeviceError { #[error("invalid afc magic")] InvalidAfcMagic, + #[cfg(feature = "afc")] + #[error("missing file attribute")] + AfcMissingFileAttribute, + #[cfg(any(feature = "tss", feature = "tunneld"))] #[error("http reqwest error")] Reqwest(#[from] reqwest::Error), diff --git a/tools/src/afc.rs b/tools/src/afc.rs index 14c8a16..c9a7810 100644 --- a/tools/src/afc.rs +++ b/tools/src/afc.rs @@ -46,6 +46,11 @@ async fn main() { .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)), + ) .get_matches(); if matches.get_flag("about") { @@ -71,10 +76,17 @@ async fn main() { if let Some(matches) = matches.subcommand_matches("list") { let path = matches.get_one::("path").expect("No path passed"); - let res = afc_client.list(path).await.expect("Failed to read dir"); + let res = afc_client.list_dir(path).await.expect("Failed to read dir"); println!("{path}\n{res:#?}"); } else if let Some(matches) = matches.subcommand_matches("remove") { let path = matches.get_one::("id").expect("No path passed"); + } else if let Some(matches) = matches.subcommand_matches("info") { + let path = matches.get_one::("path").expect("No path passed"); + let res = afc_client + .get_file_info(path) + .await + .expect("Failed to get file info"); + println!("{res:#?}"); } else { eprintln!("Invalid usage, pass -h for help"); }