ios_core/services/fetchsymbols/
mod.rs1use 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}