use chrono::{DateTime, NaiveDateTime};
use serde::{Deserialize, Serialize};
use tokio::io::AsyncWriteExt;
use crate::{Idevice, IdeviceError, IdeviceService, obf};
#[derive(Debug)]
pub struct OsTraceRelayClient {
pub idevice: Idevice,
}
impl IdeviceService for OsTraceRelayClient {
fn service_name() -> std::borrow::Cow<'static, str> {
obf!("com.apple.os_trace_relay")
}
async fn from_stream(idevice: Idevice) -> Result<Self, crate::IdeviceError> {
Ok(Self { idevice })
}
}
#[derive(Debug)]
pub struct OsTraceRelayReceiver {
inner: OsTraceRelayClient,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct OsTraceLog {
pub pid: u32,
pub timestamp: NaiveDateTime,
pub level: LogLevel,
pub image_name: String,
pub filename: String,
pub message: String,
pub label: Option<SyslogLabel>,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct SyslogLabel {
pub subsystem: String,
pub category: String,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize, Copy)]
pub enum LogLevel {
Notice = 0,
Info = 1,
Debug = 2,
Error = 10,
Fault = 11,
}
impl OsTraceRelayClient {
pub async fn start_trace(
mut self,
pid: Option<u32>,
) -> Result<OsTraceRelayReceiver, IdeviceError> {
let pid = match pid {
Some(p) => p as i64,
None => -1,
};
let req = crate::plist!({
"Request": "StartActivity",
"Pid": pid,
"MessageFilter": 65_535,
"StreamFlags": 60
});
self.idevice.send_bplist(req).await?;
self.idevice.read_raw(1).await?;
let res = self.idevice.read_plist().await?;
match res.get("Status").and_then(|x| x.as_string()) {
Some(r) => {
if r == "RequestSuccessful" {
Ok(OsTraceRelayReceiver { inner: self })
} else {
Err(IdeviceError::UnexpectedResponse(
"Status was not RequestSuccessful in StartActivity response".into(),
))
}
}
None => Err(IdeviceError::UnexpectedResponse(
"missing Status in StartActivity response".into(),
)),
}
}
pub async fn get_pid_list(&mut self) -> Result<Vec<u64>, IdeviceError> {
let req = crate::plist!({
"Request": "PidList"
});
self.idevice.send_bplist(req).await?;
self.idevice.read_raw(1).await?;
let res = self.idevice.read_plist().await?;
if let Some(payload) = res.get("Payload").and_then(|x| x.as_dictionary()) {
payload
.keys()
.map(|k| {
k.parse::<u64>().map_err(|_| {
IdeviceError::UnexpectedResponse(format!(
"PidList Payload key is not a valid PID: {k}"
))
})
})
.collect()
} else {
Err(IdeviceError::UnexpectedResponse(
"missing Payload dictionary in PidList response".into(),
))
}
}
pub async fn create_archive<W: tokio::io::AsyncWrite + Unpin>(
&mut self,
out: &mut W,
size_limit: Option<u64>,
age_limit: Option<u64>,
start_time: Option<u64>,
) -> Result<(), IdeviceError> {
let req = crate::plist!({
"Request": "CreateArchive",
"SizeLimit":? size_limit,
"AgeLimit":? age_limit,
"StartTime":? start_time,
});
self.idevice.send_bplist(req).await?;
if self.idevice.read_raw(1).await?[0] != 1 {
return Err(IdeviceError::UnexpectedResponse(
"expected leading byte 0x01 in CreateArchive response".into(),
));
}
let res = self.idevice.read_plist().await?;
match res.get("Status").and_then(|x| x.as_string()) {
Some("RequestSuccessful") => {}
_ => {
return Err(IdeviceError::UnexpectedResponse(
"Status was not RequestSuccessful in CreateArchive response".into(),
));
}
}
loop {
match self.idevice.read_raw(1).await {
Ok(data) if data[0] == 0x03 => {
let length_bytes = self.idevice.read_raw(4).await?;
let length = u32::from_le_bytes([
length_bytes[0],
length_bytes[1],
length_bytes[2],
length_bytes[3],
]);
let data = self.idevice.read_raw(length as usize).await?;
out.write_all(&data).await?;
}
Err(IdeviceError::Socket(_)) => break,
_ => {
return Err(IdeviceError::UnexpectedResponse(
"unexpected data format in archive stream".into(),
));
}
}
}
Ok(())
}
}
impl OsTraceRelayReceiver {
pub async fn next(&mut self) -> Result<OsTraceLog, IdeviceError> {
if self.inner.idevice.read_raw(1).await?[0] != 0x02 {
return Err(IdeviceError::UnexpectedResponse(
"expected leading byte 0x02 at start of log packet".into(),
));
}
let pl = self.inner.idevice.read_raw(4).await?;
let packet_length = u32::from_le_bytes([pl[0], pl[1], pl[2], pl[3]]);
let packet = self.inner.idevice.read_raw(packet_length as usize).await?;
let packet = &packet[9..];
let pid = u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]);
let packet = &packet[4..];
let packet = &packet[42..];
let seconds = u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]);
let packet = &packet[8..]; let microseconds = u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]);
let packet = &packet[4..];
let packet = &packet[1..];
let log_level = packet[0];
let log_level: LogLevel = log_level.try_into()?;
let packet = &packet[1..];
let packet = &packet[38..];
let image_name_size = u16::from_le_bytes([packet[0], packet[1]]) as usize;
let packet = &packet[2..];
let message_size = u16::from_le_bytes([packet[0], packet[1]]) as usize;
let packet = &packet[2..];
let packet = &packet[6..];
let subsystem_size =
u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]) as usize;
let packet = &packet[4..];
let category_size =
u32::from_le_bytes([packet[0], packet[1], packet[2], packet[3]]) as usize;
let packet = &packet[4..];
let packet = &packet[4..];
let filename_end =
packet
.iter()
.position(|&b| b == 0)
.ok_or(IdeviceError::UnexpectedResponse(
"filename not null-terminated in log packet".into(),
))?;
let filename = String::from_utf8_lossy(&packet[..filename_end]).into_owned();
let packet = &packet[filename_end + 1..];
let image_name_bytes = &packet[..image_name_size];
let image_name =
String::from_utf8_lossy(&image_name_bytes[..image_name_bytes.len() - 1]).into_owned();
let packet = &packet[image_name_size..];
let message_bytes = &packet[..message_size];
let message =
String::from_utf8_lossy(&message_bytes[..message_bytes.len() - 1]).into_owned();
let packet = &packet[message_size..];
let label = if subsystem_size > 0 && category_size > 0 && !packet.is_empty() {
let subsystem_bytes = &packet[..subsystem_size];
let subsystem =
String::from_utf8_lossy(&subsystem_bytes[..subsystem_bytes.len() - 1]).into_owned();
let packet = &packet[subsystem_size..];
let category_bytes = &packet[..category_size];
let category =
String::from_utf8_lossy(&category_bytes[..category_bytes.len() - 1]).into_owned();
Some(SyslogLabel {
subsystem,
category,
})
} else {
None
};
let timestamp = match DateTime::from_timestamp(seconds as i64, microseconds) {
Some(t) => t.naive_local(),
None => {
return Err(IdeviceError::UnexpectedResponse(
"invalid timestamp in log packet".into(),
));
}
};
Ok(OsTraceLog {
pid,
timestamp,
level: log_level,
image_name,
filename,
message,
label,
})
}
}
impl TryFrom<u8> for LogLevel {
type Error = IdeviceError;
fn try_from(value: u8) -> Result<Self, IdeviceError> {
Ok(match value {
0 => Self::Notice,
1 => Self::Info,
2 => Self::Debug,
0x10 => Self::Error,
0x11 => Self::Fault,
_ => {
return Err(IdeviceError::UnexpectedResponse(
"unknown log level byte value".into(),
));
}
})
}
}
#[cfg(feature = "rsd")]
impl crate::RsdService for OsTraceRelayClient {
fn rsd_service_name() -> std::borrow::Cow<'static, str> {
crate::obf!("com.apple.os_trace_relay.shim.remote")
}
async fn from_stream(stream: Box<dyn crate::ReadWrite>) -> Result<Self, crate::IdeviceError> {
let mut idevice = crate::Idevice::new(stream, "");
idevice.rsd_checkin().await?;
Ok(Self { idevice })
}
}