Add get file info to afc

This commit is contained in:
Jackson Coxson
2025-04-05 11:16:43 -06:00
parent 88d031edd7
commit 4d893e9d8c
5 changed files with 135 additions and 3 deletions

19
Cargo.lock generated
View File

@@ -337,6 +337,15 @@ version = "0.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" 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]] [[package]]
name = "clang-sys" name = "clang-sys"
version = "1.8.1" version = "1.8.1"
@@ -1124,6 +1133,7 @@ dependencies = [
"base64", "base64",
"byteorder", "byteorder",
"bytes", "bytes",
"chrono",
"env_logger 0.11.7", "env_logger 0.11.7",
"futures", "futures",
"indexmap", "indexmap",
@@ -1464,6 +1474,15 @@ version = "0.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "51d515d32fb182ee37cda2ccdcb92950d6a3c2893aa280e540671c2cd0f3b1d9" 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]] [[package]]
name = "num_threads" name = "num_threads"
version = "0.1.7" version = "0.1.7"

View File

@@ -26,6 +26,7 @@ indexmap = { version = "2.7", features = ["serde"], optional = true }
uuid = { version = "1.12", features = ["serde", "v4"], optional = true } uuid = { version = "1.12", features = ["serde", "v4"], optional = true }
async-recursion = { version = "1.1", optional = true } async-recursion = { version = "1.1", optional = true }
base64 = { version = "0.22", optional = true } base64 = { version = "0.22", optional = true }
chrono = { version = "0.4.40", optional = true, default_features = false }
serde_json = { version = "1", optional = true } serde_json = { version = "1", optional = true }
json = { version = "0.12", 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" bytes = "1.10.1"
[features] [features]
afc = [] afc = ["dep:chrono"]
core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"] core_device_proxy = ["dep:serde_json", "dep:json", "dep:byteorder"]
debug_proxy = [] debug_proxy = []
dvt = ["dep:byteorder", "dep:ns-keyed-archive"] dvt = ["dep:byteorder", "dep:ns-keyed-archive"]

View File

@@ -1,6 +1,9 @@
// Jackson Coxson // Jackson Coxson
use std::collections::HashMap;
use errors::AfcError; use errors::AfcError;
use log::warn;
use opcode::AfcOpcode; use opcode::AfcOpcode;
use packet::{AfcPacket, AfcPacketHeader}; use packet::{AfcPacket, AfcPacketHeader};
@@ -17,6 +20,17 @@ pub struct AfcClient {
package_number: u64, 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<String>,
}
impl IdeviceService for AfcClient { impl IdeviceService for AfcClient {
fn service_name() -> &'static str { fn service_name() -> &'static str {
"com.apple.afc" "com.apple.afc"
@@ -54,7 +68,7 @@ impl AfcClient {
} }
} }
pub async fn list(&mut self, path: impl Into<String>) -> Result<Vec<String>, IdeviceError> { pub async fn list_dir(&mut self, path: impl Into<String>) -> Result<Vec<String>, IdeviceError> {
let path = path.into(); let path = path.into();
let header_payload = path.as_bytes().to_vec(); let header_payload = path.as_bytes().to_vec();
let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN; let header_len = header_payload.len() as u64 + AfcPacketHeader::LEN;
@@ -86,6 +100,88 @@ impl AfcClient {
Ok(strings) Ok(strings)
} }
pub async fn get_file_info(
&mut self,
path: impl Into<String>,
) -> Result<FileInfo, 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::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<String> = 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<String, String> = strings
.chunks_exact(2)
.map(|chunk| (chunk[0].clone(), chunk[1].clone()))
.collect();
let size = kvs
.remove("st_size")
.and_then(|x| x.parse::<usize>().ok())
.ok_or(IdeviceError::AfcMissingFileAttribute)?;
let blocks = kvs
.remove("st_blocks")
.and_then(|x| x.parse::<usize>().ok())
.ok_or(IdeviceError::AfcMissingFileAttribute)?;
let creation = kvs
.remove("st_birthtime")
.and_then(|x| x.parse::<i64>().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::<i64>().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<AfcPacket, IdeviceError> { pub async fn read(&mut self) -> Result<AfcPacket, IdeviceError> {
let res = AfcPacket::read(&mut self.idevice).await?; let res = AfcPacket::read(&mut self.idevice).await?;
if res.header.operation == AfcOpcode::Status { if res.header.operation == AfcOpcode::Status {

View File

@@ -312,6 +312,10 @@ pub enum IdeviceError {
#[error("invalid afc magic")] #[error("invalid afc magic")]
InvalidAfcMagic, InvalidAfcMagic,
#[cfg(feature = "afc")]
#[error("missing file attribute")]
AfcMissingFileAttribute,
#[cfg(any(feature = "tss", feature = "tunneld"))] #[cfg(any(feature = "tss", feature = "tunneld"))]
#[error("http reqwest error")] #[error("http reqwest error")]
Reqwest(#[from] reqwest::Error), Reqwest(#[from] reqwest::Error),

View File

@@ -46,6 +46,11 @@ async fn main() {
.about("Remove a provisioning profile") .about("Remove a provisioning profile")
.arg(Arg::new("path").required(true).index(1)), .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(); .get_matches();
if matches.get_flag("about") { if matches.get_flag("about") {
@@ -71,10 +76,17 @@ async fn main() {
if let Some(matches) = matches.subcommand_matches("list") { if let Some(matches) = matches.subcommand_matches("list") {
let path = matches.get_one::<String>("path").expect("No path passed"); let path = matches.get_one::<String>("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:#?}"); println!("{path}\n{res:#?}");
} else if let Some(matches) = matches.subcommand_matches("remove") { } else if let Some(matches) = matches.subcommand_matches("remove") {
let path = matches.get_one::<String>("id").expect("No path passed"); let path = matches.get_one::<String>("id").expect("No path passed");
} else if let Some(matches) = matches.subcommand_matches("info") {
let path = matches.get_one::<String>("path").expect("No path passed");
let res = afc_client
.get_file_info(path)
.await
.expect("Failed to get file info");
println!("{res:#?}");
} else { } else {
eprintln!("Invalid usage, pass -h for help"); eprintln!("Invalid usage, pass -h for help");
} }