symbolic_debuginfo/macho/
bcsymbolmap.rs1use std::error::Error;
4use std::fmt;
5use std::io::Cursor;
6use std::iter::FusedIterator;
7use std::path::Path;
8
9use elementtree::Element;
10use symbolic_common::{AsSelf, DebugId, ParseDebugIdError};
11use thiserror::Error;
12
13use super::SWIFT_HIDDEN_PREFIX;
14
15const BC_SYMBOL_MAP_HEADER: &str = "BCSymbolMap Version: 2.0";
16
17#[derive(Debug, Error)]
19#[error("{kind}")]
20pub struct BcSymbolMapError {
21 kind: BcSymbolMapErrorKind,
22 #[source]
23 source: Option<Box<dyn Error + Send + Sync + 'static>>,
24}
25
26#[non_exhaustive]
28#[derive(Clone, Copy, Debug, PartialEq, Eq)]
29enum BcSymbolMapErrorKind {
30 InvalidHeader,
34 InvalidUtf8,
36}
37
38impl fmt::Display for BcSymbolMapErrorKind {
39 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
40 match self {
41 Self::InvalidHeader => write!(f, "no valid BCSymbolMap header was found"),
42 Self::InvalidUtf8 => write!(f, "BCSymbolmap is not valid UTF-8"),
43 }
44 }
45}
46
47#[derive(Clone, Debug)]
65pub struct BcSymbolMap<'d> {
66 names: Vec<&'d str>,
67}
68
69impl From<BcSymbolMapErrorKind> for BcSymbolMapError {
70 fn from(source: BcSymbolMapErrorKind) -> Self {
71 Self {
72 kind: source,
73 source: None,
74 }
75 }
76}
77
78impl<'slf> AsSelf<'slf> for BcSymbolMap<'_> {
79 type Ref = BcSymbolMap<'slf>;
80
81 fn as_self(&'slf self) -> &'slf Self::Ref {
82 self
83 }
84}
85
86impl<'d> BcSymbolMap<'d> {
87 pub fn test(bytes: &[u8]) -> bool {
89 let pattern = BC_SYMBOL_MAP_HEADER.as_bytes();
90 bytes.starts_with(pattern)
91 }
92
93 pub fn parse(data: &'d [u8]) -> Result<Self, BcSymbolMapError> {
98 let content = std::str::from_utf8(data).map_err(|err| BcSymbolMapError {
99 kind: BcSymbolMapErrorKind::InvalidUtf8,
100 source: Some(Box::new(err)),
101 })?;
102
103 let mut lines_iter = content.lines();
104
105 let header = lines_iter
106 .next()
107 .ok_or(BcSymbolMapErrorKind::InvalidHeader)?;
108 if header != BC_SYMBOL_MAP_HEADER {
109 return Err(BcSymbolMapErrorKind::InvalidHeader.into());
110 }
111
112 let names = lines_iter.collect();
113
114 Ok(Self { names })
115 }
116
117 pub fn get(&self, index: usize) -> Option<&'d str> {
134 self.names.get(index).copied()
135 }
136
137 pub fn resolve(&self, mut name: &'d str) -> &'d str {
158 if let Some(tail) = name.strip_prefix(SWIFT_HIDDEN_PREFIX) {
159 if let Some(index_as_string) = tail.strip_suffix('_') {
160 name = index_as_string
161 .parse::<usize>()
162 .ok()
163 .and_then(|index| self.get(index))
164 .unwrap_or(name);
165 }
166 }
167 name
168 }
169
170 pub(crate) fn resolve_opt(&self, name: impl AsRef<[u8]>) -> Option<&str> {
175 let name = std::str::from_utf8(name.as_ref()).ok()?;
176 let tail = name.strip_prefix(SWIFT_HIDDEN_PREFIX)?;
177 let index_as_string = tail.strip_suffix('_')?;
178 let index = index_as_string.parse::<usize>().ok()?;
179 self.get(index)
180 }
181
182 pub fn iter(&self) -> BcSymbolMapIterator<'_, 'd> {
184 BcSymbolMapIterator {
185 iter: self.names.iter(),
186 }
187 }
188}
189
190pub struct BcSymbolMapIterator<'a, 'd> {
194 iter: std::slice::Iter<'a, &'d str>,
195}
196
197impl<'d> Iterator for BcSymbolMapIterator<'_, 'd> {
198 type Item = &'d str;
199
200 fn next(&mut self) -> Option<Self::Item> {
201 self.iter.next().copied()
202 }
203
204 fn size_hint(&self) -> (usize, Option<usize>) {
205 self.iter.size_hint()
206 }
207}
208
209impl FusedIterator for BcSymbolMapIterator<'_, '_> {}
210
211#[derive(Debug, Error)]
213#[error("{kind}")]
214pub struct UuidMappingError {
215 kind: UuidMappingErrorKind,
216 #[source]
217 source: Option<Box<dyn Error + Send + Sync + 'static>>,
218}
219
220impl From<elementtree::Error> for UuidMappingError {
221 fn from(source: elementtree::Error) -> Self {
222 Self {
223 kind: UuidMappingErrorKind::PListParse,
224 source: Some(Box::new(source)),
225 }
226 }
227}
228
229impl From<UuidMappingErrorKind> for UuidMappingError {
230 fn from(kind: UuidMappingErrorKind) -> Self {
231 Self { kind, source: None }
232 }
233}
234
235impl From<ParseDebugIdError> for UuidMappingError {
236 fn from(source: ParseDebugIdError) -> Self {
237 Self {
238 kind: UuidMappingErrorKind::PListParseValue,
239 source: Some(Box::new(source)),
240 }
241 }
242}
243
244#[non_exhaustive]
246#[derive(Clone, Copy, Debug, PartialEq, Eq)]
247enum UuidMappingErrorKind {
248 PListSchema,
250 PListParse,
252 PListParseValue,
254 ParseFilename,
256}
257
258impl fmt::Display for UuidMappingErrorKind {
259 fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
260 match self {
261 Self::PListSchema => write!(f, "XML structure did not match expected schema"),
262 Self::PListParse => write!(f, "Invalid XML"),
263 Self::PListParseValue => write!(f, "Failed to parse a value into the right type"),
264 Self::ParseFilename => write!(f, "Failed to parse UUID from filename"),
265 }
266 }
267}
268
269#[derive(Clone, Copy, Debug)]
279pub struct UuidMapping {
280 dsym_uuid: DebugId,
281 original_uuid: DebugId,
282}
283
284impl UuidMapping {
285 pub fn new(dsym_uuid: DebugId, original_uuid: DebugId) -> Self {
287 Self {
288 dsym_uuid,
289 original_uuid,
290 }
291 }
292
293 pub fn parse_plist(dsym_uuid: DebugId, data: &[u8]) -> Result<Self, UuidMappingError> {
322 Ok(Self {
323 dsym_uuid,
324 original_uuid: uuid_from_plist(data)?,
325 })
326 }
327
328 pub fn parse_plist_with_filename(
360 filename: &Path,
361 data: &[u8],
362 ) -> Result<Self, UuidMappingError> {
363 let dsym_uuid = filename
364 .file_stem()
365 .ok_or_else(|| UuidMappingError::from(UuidMappingErrorKind::ParseFilename))?
366 .to_str()
367 .ok_or_else(|| UuidMappingError::from(UuidMappingErrorKind::ParseFilename))?
368 .parse()?;
369 Self::parse_plist(dsym_uuid, data)
370 }
371
372 pub fn original_uuid(&self) -> DebugId {
374 self.original_uuid
375 }
376
377 pub fn dsym_uuid(&self) -> DebugId {
379 self.dsym_uuid
380 }
381}
382
383fn uuid_from_plist(data: &[u8]) -> Result<DebugId, UuidMappingError> {
384 let plist = Element::from_reader(Cursor::new(data))?;
385
386 let raw = uuid_from_xml_plist(plist)
387 .ok_or_else(|| UuidMappingError::from(UuidMappingErrorKind::PListSchema))?;
388
389 raw.parse().map_err(Into::into)
390}
391
392fn uuid_from_xml_plist(plist: Element) -> Option<String> {
393 let version = plist.get_attr("version")?;
394 if version != "1.0" {
395 return None;
396 }
397 let dict = plist.find("dict")?;
398
399 let mut found_key = false;
400 let mut raw_original = None;
401 for element in dict.children() {
402 if element.tag().name() == "key" && element.text() == "DBGOriginalUUID" {
403 found_key = true;
404 } else if found_key {
405 raw_original = Some(element.text().to_string());
406 break;
407 }
408 }
409
410 raw_original
411}
412
413#[cfg(test)]
414mod tests {
415 use super::*;
416
417 #[test]
418 fn test_bcsymbolmap_test() {
419 assert!(BcSymbolMap::test(b"BCSymbolMap Version: 2.0"));
420 assert!(!BcSymbolMap::test(b"BCSymbolMap Vers"));
421 assert!(!BcSymbolMap::test(b"oops"));
422 }
423
424 #[test]
425 fn test_basic() {
426 let data = std::fs::read_to_string(
427 "tests/fixtures/c8374b6d-6e96-34d8-ae38-efaa5fec424f.bcsymbolmap",
428 )
429 .unwrap();
430
431 assert!(BcSymbolMap::test(data.as_bytes()));
432
433 let map = BcSymbolMap::parse(data.as_bytes()).unwrap();
434 assert_eq!(map.get(2), Some("-[SentryMessage serialize]"))
435 }
436
437 #[test]
438 fn test_iter() {
439 let data = std::fs::read_to_string(
440 "tests/fixtures/c8374b6d-6e96-34d8-ae38-efaa5fec424f.bcsymbolmap",
441 )
442 .unwrap();
443 let map = BcSymbolMap::parse(data.as_bytes()).unwrap();
444
445 let mut map_iter = map.iter();
446
447 let (lower_bound, upper_bound) = map_iter.size_hint();
448 assert!(lower_bound > 0);
449 assert!(upper_bound.is_some());
450
451 let name = map_iter.next();
452 assert_eq!(name.unwrap(), "-[SentryMessage initWithFormatted:]");
453
454 let name = map_iter.next();
455 assert_eq!(name.unwrap(), "-[SentryMessage setMessage:]");
456 }
457
458 #[test]
459 fn test_data_lifetime() {
460 let data = std::fs::read_to_string(
461 "tests/fixtures/c8374b6d-6e96-34d8-ae38-efaa5fec424f.bcsymbolmap",
462 )
463 .unwrap();
464
465 let name = {
466 let map = BcSymbolMap::parse(data.as_bytes()).unwrap();
467 map.get(0).unwrap()
468 };
469
470 assert_eq!(name, "-[SentryMessage initWithFormatted:]");
471 }
472
473 #[test]
474 fn test_resolve() {
475 let data = std::fs::read_to_string(
476 "tests/fixtures/c8374b6d-6e96-34d8-ae38-efaa5fec424f.bcsymbolmap",
477 )
478 .unwrap();
479 let map = BcSymbolMap::parse(data.as_bytes()).unwrap();
480
481 assert_eq!(map.resolve("normal_name"), "normal_name");
482 assert_eq!(map.resolve("__hidden#2_"), "-[SentryMessage serialize]");
483 }
484
485 #[test]
486 fn test_plist() {
487 let uuid: DebugId = "2d10c42f-591d-3265-b147-78ba0868073f".parse().unwrap();
488 let data =
489 std::fs::read("tests/fixtures/2d10c42f-591d-3265-b147-78ba0868073f.plist").unwrap();
490 let map = UuidMapping::parse_plist(uuid, &data).unwrap();
491
492 assert_eq!(map.dsym_uuid(), uuid);
493 assert_eq!(
494 map.original_uuid(),
495 "c8374b6d-6e96-34d8-ae38-efaa5fec424f".parse().unwrap()
496 );
497 }
498}