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