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