Skip to main content

ios_core/services/fetchsymbols/
mod.rs

1//! Legacy fetch symbols service client.
2//!
3//! Service: `com.apple.dt.fetchsymbols`
4//! Reference: pymobiledevice3 `dtfetchsymbols.py`
5
6use std::io::Write;
7
8use bytes::BytesMut;
9use indexmap::IndexMap;
10use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
11use uuid::Uuid;
12
13pub const SERVICE_NAME: &str = "com.apple.dt.fetchsymbols";
14pub const REMOTE_SERVICE_NAME: &str = "com.apple.dt.remoteFetchSymbols";
15const CMD_LIST_FILES: u32 = 0x3030_3030;
16const CMD_GET_FILE: u32 = 1;
17const MAX_CHUNK: usize = 10 * 1024 * 1024;
18
19#[derive(Debug, thiserror::Error)]
20pub enum FetchSymbolsError {
21    #[error("IO error: {0}")]
22    Io(#[from] std::io::Error),
23    #[error("plist error: {0}")]
24    Plist(String),
25    #[error("protocol error: {0}")]
26    Protocol(String),
27}
28
29pub struct FetchSymbolsClient<S> {
30    stream: S,
31}
32
33impl<S: AsyncRead + AsyncWrite + Unpin> FetchSymbolsClient<S> {
34    pub fn new(stream: S) -> Self {
35        Self { stream }
36    }
37
38    pub async fn list_files(&mut self) -> Result<Vec<String>, FetchSymbolsError> {
39        self.start_command(CMD_LIST_FILES).await?;
40        let response = recv_plist(&mut self.stream).await?;
41        response
42            .get("files")
43            .and_then(plist::Value::as_array)
44            .map(|files| {
45                files
46                    .iter()
47                    .filter_map(|value| value.as_string().map(ToOwned::to_owned))
48                    .collect()
49            })
50            .ok_or_else(|| FetchSymbolsError::Protocol("missing files array".into()))
51    }
52
53    pub async fn download<W: Write>(
54        &mut self,
55        index: u32,
56        mut writer: W,
57        max_bytes: Option<u64>,
58    ) -> Result<u64, FetchSymbolsError> {
59        self.start_command(CMD_GET_FILE).await?;
60        self.stream.write_all(&index.to_be_bytes()).await?;
61        self.stream.flush().await?;
62
63        let size = self.stream.read_u64().await?;
64        let limit = max_bytes.map_or(size, |limit| limit.min(size));
65
66        let mut remaining = limit;
67        let mut written = 0u64;
68        let mut buf = vec![0u8; MAX_CHUNK];
69        while remaining > 0 {
70            let chunk_size = remaining.min(MAX_CHUNK as u64) as usize;
71            self.stream.read_exact(&mut buf[..chunk_size]).await?;
72            writer.write_all(&buf[..chunk_size])?;
73            written += chunk_size as u64;
74            remaining -= chunk_size as u64;
75        }
76
77        Ok(written)
78    }
79
80    async fn start_command(&mut self, command: u32) -> Result<(), FetchSymbolsError> {
81        let encoded = command.to_be_bytes();
82        self.stream.write_all(&encoded).await?;
83        self.stream.flush().await?;
84
85        let mut ack = [0u8; 4];
86        self.stream.read_exact(&mut ack).await?;
87        if ack != encoded {
88            return Err(FetchSymbolsError::Protocol(format!(
89                "unexpected fetchsymbols ack: expected 0x{command:08x}, got 0x{:08x}",
90                u32::from_be_bytes(ack)
91            )));
92        }
93        Ok(())
94    }
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
98pub struct RemoteSymbolFile {
99    pub path: String,
100    pub size: u64,
101}
102
103pub struct RemoteFetchSymbolsClient<S> {
104    framer: crate::xpc::h2_raw::H2Framer<S>,
105    next_msg_id: u64,
106    pending_control_data: BytesMut,
107}
108
109impl<S: AsyncRead + AsyncWrite + Unpin> RemoteFetchSymbolsClient<S> {
110    pub async fn connect(stream: S) -> Result<Self, FetchSymbolsError> {
111        let mut framer = crate::xpc::h2_raw::H2Framer::connect(stream)
112            .await
113            .map_err(|err| FetchSymbolsError::Protocol(format!("H2 error: {err}")))?;
114        bootstrap_remote_xpc(&mut framer).await?;
115        Ok(Self {
116            framer,
117            next_msg_id: 1,
118            pending_control_data: BytesMut::new(),
119        })
120    }
121
122    pub async fn list_files(&mut self) -> Result<Vec<RemoteSymbolFile>, FetchSymbolsError> {
123        self.send_catalog_request().await?;
124
125        let count = self.recv_catalog_count().await?;
126        let mut files = Vec::with_capacity(count.min(128));
127        for _ in 0..count {
128            files.push(self.recv_catalog_entry().await?);
129        }
130        Ok(files)
131    }
132
133    pub async fn download<W: Write>(
134        &mut self,
135        index: u32,
136        mut writer: W,
137        max_bytes: Option<u64>,
138    ) -> Result<u64, FetchSymbolsError> {
139        let files = self.list_files().await?;
140        let file = files.get(index as usize).ok_or_else(|| {
141            FetchSymbolsError::Protocol(format!("symbol index {index} out of range"))
142        })?;
143
144        let stream_id = (index + 1) * 2;
145        self.framer
146            .write_stream(
147                stream_id,
148                &crate::xpc::message::encode_message(&crate::xpc::XpcMessage {
149                    flags: crate::xpc::message::flags::ALWAYS_SET
150                        | crate::xpc::message::flags::FILE_TX_STREAM_RESPONSE,
151                    msg_id: 0,
152                    body: None,
153                })
154                .map_err(|err| {
155                    FetchSymbolsError::Protocol(format!("file stream encode failed: {err}"))
156                })?,
157            )
158            .await
159            .map_err(|err| {
160                FetchSymbolsError::Protocol(format!("file stream open failed: {err}"))
161            })?;
162
163        let limit = max_bytes.map_or(file.size, |limit| limit.min(file.size));
164        let mut remaining = limit;
165        let mut written = 0u64;
166        let mut buf = vec![0u8; MAX_CHUNK.min(limit.max(1) as usize)];
167
168        while remaining > 0 {
169            let chunk_len = remaining.min(buf.len() as u64) as usize;
170            let chunk = self
171                .framer
172                .read_stream(stream_id, chunk_len)
173                .await
174                .map_err(|err| {
175                    FetchSymbolsError::Protocol(format!("file stream read failed: {err}"))
176                })?;
177            buf[..chunk_len].copy_from_slice(&chunk);
178            writer.write_all(&buf[..chunk_len])?;
179            written += chunk_len as u64;
180            remaining -= chunk_len as u64;
181        }
182
183        Ok(written)
184    }
185
186    async fn send_catalog_request(&mut self) -> Result<(), FetchSymbolsError> {
187        let mut request = IndexMap::new();
188        request.insert(
189            "XPCDictionary_sideChannel".to_string(),
190            crate::xpc::XpcValue::Uuid(*Uuid::new_v4().as_bytes()),
191        );
192        request.insert(
193            "DSCFilePaths".to_string(),
194            crate::xpc::XpcValue::Array(Vec::new()),
195        );
196
197        self.framer
198            .write_client_server(
199                &crate::xpc::message::encode_message(&crate::xpc::XpcMessage {
200                    flags: crate::xpc::message::flags::ALWAYS_SET
201                        | crate::xpc::message::flags::DATA_PRESENT
202                        | crate::xpc::message::flags::WANTING_REPLY,
203                    msg_id: self.next_msg_id,
204                    body: Some(crate::xpc::XpcValue::Dictionary(request)),
205                })
206                .map_err(|err| {
207                    FetchSymbolsError::Protocol(format!("catalog request encode failed: {err}"))
208                })?,
209            )
210            .await
211            .map_err(|err| FetchSymbolsError::Protocol(format!("catalog request failed: {err}")))?;
212        self.next_msg_id += 1;
213        Ok(())
214    }
215
216    async fn recv_control_message(&mut self) -> Result<crate::xpc::XpcMessage, FetchSymbolsError> {
217        loop {
218            if let Some(message) = self.try_take_pending_control_message()? {
219                if message.flags & crate::xpc::message::flags::FILE_TX_STREAM_REQUEST != 0 {
220                    continue;
221                }
222                return Ok(message);
223            }
224
225            let frame = self.framer.read_next_data_frame().await.map_err(|err| {
226                FetchSymbolsError::Protocol(format!("control frame read failed: {err}"))
227            })?;
228            self.pending_control_data.extend_from_slice(&frame.payload);
229        }
230    }
231
232    async fn recv_catalog_count(&mut self) -> Result<usize, FetchSymbolsError> {
233        let mut last_error = None;
234        for _ in 0..32 {
235            let message = self.recv_control_message().await?;
236            match try_parse_catalog_count(&message) {
237                Some(Ok(count)) => return Ok(count),
238                Some(Err(err)) => last_error = Some(err),
239                None => continue,
240            }
241        }
242        Err(last_error.unwrap_or_else(|| {
243            FetchSymbolsError::Protocol("did not receive remote symbols catalog count".into())
244        }))
245    }
246
247    async fn recv_catalog_entry(&mut self) -> Result<RemoteSymbolFile, FetchSymbolsError> {
248        let mut last_error = None;
249        for _ in 0..64 {
250            let message = self.recv_control_message().await?;
251            match try_parse_catalog_entry(&message) {
252                Some(Ok(entry)) => return Ok(entry),
253                Some(Err(err)) => {
254                    tracing::trace!(
255                        "remote fetchsymbols catalog entry parse failed: err={err}; body={:?}",
256                        message.body
257                    );
258                    last_error = Some(err);
259                }
260                None => continue,
261            }
262        }
263        Err(last_error.unwrap_or_else(|| {
264            FetchSymbolsError::Protocol("did not receive remote symbols catalog entry".into())
265        }))
266    }
267}
268
269impl<S> RemoteFetchSymbolsClient<S> {
270    fn try_take_pending_control_message(
271        &mut self,
272    ) -> Result<Option<crate::xpc::XpcMessage>, FetchSymbolsError> {
273        if self.pending_control_data.len() < 24 {
274            return Ok(None);
275        }
276
277        let body_len =
278            u64::from_le_bytes(self.pending_control_data[8..16].try_into().map_err(|_| {
279                FetchSymbolsError::Protocol("invalid control message header".into())
280            })?) as usize;
281        let total_len = 24usize
282            .checked_add(body_len)
283            .ok_or_else(|| FetchSymbolsError::Protocol("control message length overflow".into()))?;
284        if self.pending_control_data.len() < total_len {
285            return Ok(None);
286        }
287
288        let payload = self.pending_control_data.split_to(total_len).freeze();
289        let message = crate::xpc::message::decode_message(payload)
290            .map_err(|err| FetchSymbolsError::Protocol(format!("control decode failed: {err}")))?;
291        Ok(Some(message))
292    }
293}
294
295async fn recv_plist<S: AsyncRead + Unpin>(
296    stream: &mut S,
297) -> Result<plist::Dictionary, FetchSymbolsError> {
298    let len = stream.read_u32().await? as usize;
299    const MAX_PLIST_SIZE: usize = 8 * 1024 * 1024;
300    if len > MAX_PLIST_SIZE {
301        return Err(FetchSymbolsError::Protocol(format!(
302            "plist length {len} exceeds max {MAX_PLIST_SIZE}"
303        )));
304    }
305    let mut buf = vec![0u8; len];
306    stream.read_exact(&mut buf).await?;
307    plist::from_bytes(&buf).map_err(|err| FetchSymbolsError::Plist(err.to_string()))
308}
309
310fn try_parse_catalog_count(
311    message: &crate::xpc::XpcMessage,
312) -> Option<Result<usize, FetchSymbolsError>> {
313    let dict = message.body.as_ref()?.as_dict()?;
314    let value = dict.get("DSCFilePaths")?;
315    Some(as_u64(value).map(|count| count as usize).ok_or_else(|| {
316        FetchSymbolsError::Protocol("catalog response missing DSCFilePaths count".into())
317    }))
318}
319
320fn try_parse_catalog_entry(
321    message: &crate::xpc::XpcMessage,
322) -> Option<Result<RemoteSymbolFile, FetchSymbolsError>> {
323    let dict = message.body.as_ref()?.as_dict()?;
324    let entry = match dict.get("DSCFilePaths") {
325        Some(value) => value.as_dict()?,
326        None => return None,
327    };
328    let path = entry
329        .get("filePath")
330        .and_then(crate::xpc::XpcValue::as_str)
331        .ok_or_else(|| FetchSymbolsError::Protocol("catalog entry missing filePath".into()));
332    let transfer = entry
333        .get("fileTransfer")
334        .ok_or_else(|| FetchSymbolsError::Protocol("catalog entry missing fileTransfer".into()));
335
336    Some((|| {
337        let path = path?.to_string();
338        let size = parse_transfer_size(transfer?)?;
339
340        Ok(RemoteSymbolFile { path, size })
341    })())
342}
343
344fn parse_transfer_size(value: &crate::xpc::XpcValue) -> Result<u64, FetchSymbolsError> {
345    if let Some((_, transfer)) = value.as_file_transfer() {
346        return transfer
347            .as_dict()
348            .and_then(|dict| dict.get("s"))
349            .and_then(as_u64)
350            .ok_or_else(|| {
351                FetchSymbolsError::Protocol("catalog entry missing fileTransfer size".into())
352            });
353    }
354
355    let dict = value.as_dict().ok_or_else(|| {
356        FetchSymbolsError::Protocol("catalog entry fileTransfer has unsupported shape".into())
357    })?;
358    if let Some(size) = dict.get("expectedLength").and_then(as_u64) {
359        return Ok(size);
360    }
361    dict.get("xpcFileTransfer")
362        .ok_or_else(|| FetchSymbolsError::Protocol("catalog entry missing xpcFileTransfer".into()))
363        .and_then(parse_transfer_size)
364}
365
366fn as_u64(value: &crate::xpc::XpcValue) -> Option<u64> {
367    match value {
368        crate::xpc::XpcValue::Uint64(n) => Some(*n),
369        crate::xpc::XpcValue::Int64(n) if *n >= 0 => Some(*n as u64),
370        _ => None,
371    }
372}
373
374async fn bootstrap_remote_xpc<S>(
375    framer: &mut crate::xpc::h2_raw::H2Framer<S>,
376) -> Result<(), FetchSymbolsError>
377where
378    S: AsyncRead + AsyncWrite + Unpin,
379{
380    framer
381        .write_client_server(
382            &crate::xpc::message::encode_message(&crate::xpc::XpcMessage {
383                flags: crate::xpc::message::flags::ALWAYS_SET
384                    | crate::xpc::message::flags::DATA_PRESENT,
385                msg_id: 0,
386                body: Some(crate::xpc::XpcValue::Dictionary(IndexMap::new())),
387            })
388            .map_err(|err| {
389                FetchSymbolsError::Protocol(format!(
390                    "remote XPC bootstrap encode step 1 failed: {err}"
391                ))
392            })?,
393        )
394        .await
395        .map_err(|err| {
396            FetchSymbolsError::Protocol(format!("remote XPC bootstrap step 1 failed: {err}"))
397        })?;
398
399    framer
400        .write_client_server(
401            &crate::xpc::message::encode_message(&crate::xpc::XpcMessage {
402                flags: crate::xpc::message::flags::ALWAYS_SET | crate::xpc::message::flags::REPLY,
403                msg_id: 0,
404                body: None,
405            })
406            .map_err(|err| {
407                FetchSymbolsError::Protocol(format!(
408                    "remote XPC bootstrap encode step 2 failed: {err}"
409                ))
410            })?,
411        )
412        .await
413        .map_err(|err| {
414            FetchSymbolsError::Protocol(format!("remote XPC bootstrap step 2 failed: {err}"))
415        })?;
416
417    framer
418        .write_server_client(
419            &crate::xpc::message::encode_message(&crate::xpc::XpcMessage {
420                flags: crate::xpc::message::flags::ALWAYS_SET
421                    | crate::xpc::message::flags::INIT_HANDSHAKE,
422                msg_id: 0,
423                body: None,
424            })
425            .map_err(|err| {
426                FetchSymbolsError::Protocol(format!(
427                    "remote XPC bootstrap encode step 3 failed: {err}"
428                ))
429            })?,
430        )
431        .await
432        .map_err(|err| {
433            FetchSymbolsError::Protocol(format!("remote XPC bootstrap step 3 failed: {err}"))
434        })?;
435
436    Ok(())
437}