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
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}