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