use std::error::Error;
use std::fmt;
use std::io::Cursor;
use std::iter::FusedIterator;
use std::path::Path;
use elementtree::Element;
use symbolic_common::{AsSelf, DebugId, ParseDebugIdError};
use thiserror::Error;
use super::SWIFT_HIDDEN_PREFIX;
const BC_SYMBOL_MAP_HEADER: &str = "BCSymbolMap Version: 2.0";
#[derive(Debug, Error)]
#[error("{kind}")]
pub struct BcSymbolMapError {
kind: BcSymbolMapErrorKind,
#[source]
source: Option<Box<dyn Error + Send + Sync + 'static>>,
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum BcSymbolMapErrorKind {
InvalidHeader,
InvalidUtf8,
}
impl fmt::Display for BcSymbolMapErrorKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::InvalidHeader => write!(f, "no valid BCSymbolMap header was found"),
Self::InvalidUtf8 => write!(f, "BCSymbolmap is not valid UTF-8"),
}
}
}
#[derive(Clone, Debug)]
pub struct BcSymbolMap<'d> {
names: Vec<&'d str>,
}
impl From<BcSymbolMapErrorKind> for BcSymbolMapError {
fn from(source: BcSymbolMapErrorKind) -> Self {
Self {
kind: source,
source: None,
}
}
}
impl<'slf> AsSelf<'slf> for BcSymbolMap<'_> {
type Ref = BcSymbolMap<'slf>;
fn as_self(&'slf self) -> &'slf Self::Ref {
self
}
}
impl<'d> BcSymbolMap<'d> {
pub fn test(bytes: &[u8]) -> bool {
let pattern = BC_SYMBOL_MAP_HEADER.as_bytes();
bytes.starts_with(pattern)
}
pub fn parse(data: &'d [u8]) -> Result<Self, BcSymbolMapError> {
let content = std::str::from_utf8(data).map_err(|err| BcSymbolMapError {
kind: BcSymbolMapErrorKind::InvalidUtf8,
source: Some(Box::new(err)),
})?;
let mut lines_iter = content.lines();
let header = lines_iter
.next()
.ok_or(BcSymbolMapErrorKind::InvalidHeader)?;
if header != BC_SYMBOL_MAP_HEADER {
return Err(BcSymbolMapErrorKind::InvalidHeader.into());
}
let names = lines_iter.collect();
Ok(Self { names })
}
pub fn get(&self, index: usize) -> Option<&'d str> {
self.names.get(index).copied()
}
pub fn resolve(&self, mut name: &'d str) -> &'d str {
if let Some(tail) = name.strip_prefix(SWIFT_HIDDEN_PREFIX) {
if let Some(index_as_string) = tail.strip_suffix('_') {
name = index_as_string
.parse::<usize>()
.ok()
.and_then(|index| self.get(index))
.unwrap_or(name);
}
}
name
}
pub(crate) fn resolve_opt(&self, name: impl AsRef<[u8]>) -> Option<&str> {
let name = std::str::from_utf8(name.as_ref()).ok()?;
let tail = name.strip_prefix(SWIFT_HIDDEN_PREFIX)?;
let index_as_string = tail.strip_suffix('_')?;
let index = index_as_string.parse::<usize>().ok()?;
self.get(index)
}
pub fn iter(&self) -> BcSymbolMapIterator<'_, 'd> {
BcSymbolMapIterator {
iter: self.names.iter(),
}
}
}
pub struct BcSymbolMapIterator<'a, 'd> {
iter: std::slice::Iter<'a, &'d str>,
}
impl<'d> Iterator for BcSymbolMapIterator<'_, 'd> {
type Item = &'d str;
fn next(&mut self) -> Option<Self::Item> {
self.iter.next().copied()
}
fn size_hint(&self) -> (usize, Option<usize>) {
self.iter.size_hint()
}
}
impl FusedIterator for BcSymbolMapIterator<'_, '_> {}
#[derive(Debug, Error)]
#[error("{kind}")]
pub struct UuidMappingError {
kind: UuidMappingErrorKind,
#[source]
source: Option<Box<dyn Error + Send + Sync + 'static>>,
}
impl From<elementtree::Error> for UuidMappingError {
fn from(source: elementtree::Error) -> Self {
Self {
kind: UuidMappingErrorKind::PListParse,
source: Some(Box::new(source)),
}
}
}
impl From<UuidMappingErrorKind> for UuidMappingError {
fn from(kind: UuidMappingErrorKind) -> Self {
Self { kind, source: None }
}
}
impl From<ParseDebugIdError> for UuidMappingError {
fn from(source: ParseDebugIdError) -> Self {
Self {
kind: UuidMappingErrorKind::PListParseValue,
source: Some(Box::new(source)),
}
}
}
#[non_exhaustive]
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum UuidMappingErrorKind {
PListSchema,
PListParse,
PListParseValue,
ParseFilename,
}
impl fmt::Display for UuidMappingErrorKind {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match self {
Self::PListSchema => write!(f, "XML structure did not match expected schema"),
Self::PListParse => write!(f, "Invalid XML"),
Self::PListParseValue => write!(f, "Failed to parse a value into the right type"),
Self::ParseFilename => write!(f, "Failed to parse UUID from filename"),
}
}
}
#[derive(Clone, Copy, Debug)]
pub struct UuidMapping {
dsym_uuid: DebugId,
original_uuid: DebugId,
}
impl UuidMapping {
pub fn new(dsym_uuid: DebugId, original_uuid: DebugId) -> Self {
Self {
dsym_uuid,
original_uuid,
}
}
pub fn parse_plist(dsym_uuid: DebugId, data: &[u8]) -> Result<Self, UuidMappingError> {
Ok(Self {
dsym_uuid,
original_uuid: uuid_from_plist(data)?,
})
}
pub fn parse_plist_with_filename(
filename: &Path,
data: &[u8],
) -> Result<Self, UuidMappingError> {
let dsym_uuid = filename
.file_stem()
.ok_or_else(|| UuidMappingError::from(UuidMappingErrorKind::ParseFilename))?
.to_str()
.ok_or_else(|| UuidMappingError::from(UuidMappingErrorKind::ParseFilename))?
.parse()?;
Self::parse_plist(dsym_uuid, data)
}
pub fn original_uuid(&self) -> DebugId {
self.original_uuid
}
pub fn dsym_uuid(&self) -> DebugId {
self.dsym_uuid
}
}
fn uuid_from_plist(data: &[u8]) -> Result<DebugId, UuidMappingError> {
let plist = Element::from_reader(Cursor::new(data))?;
let raw = uuid_from_xml_plist(plist)
.ok_or_else(|| UuidMappingError::from(UuidMappingErrorKind::PListSchema))?;
raw.parse().map_err(Into::into)
}
fn uuid_from_xml_plist(plist: Element) -> Option<String> {
let version = plist.get_attr("version")?;
if version != "1.0" {
return None;
}
let dict = plist.find("dict")?;
let mut found_key = false;
let mut raw_original = None;
for element in dict.children() {
if element.tag().name() == "key" && element.text() == "DBGOriginalUUID" {
found_key = true;
} else if found_key {
raw_original = Some(element.text().to_string());
break;
}
}
raw_original
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_bcsymbolmap_test() {
assert!(BcSymbolMap::test(b"BCSymbolMap Version: 2.0"));
assert!(!BcSymbolMap::test(b"BCSymbolMap Vers"));
assert!(!BcSymbolMap::test(b"oops"));
}
#[test]
fn test_basic() {
let data = std::fs::read_to_string(
"tests/fixtures/c8374b6d-6e96-34d8-ae38-efaa5fec424f.bcsymbolmap",
)
.unwrap();
assert!(BcSymbolMap::test(data.as_bytes()));
let map = BcSymbolMap::parse(data.as_bytes()).unwrap();
assert_eq!(map.get(2), Some("-[SentryMessage serialize]"))
}
#[test]
fn test_iter() {
let data = std::fs::read_to_string(
"tests/fixtures/c8374b6d-6e96-34d8-ae38-efaa5fec424f.bcsymbolmap",
)
.unwrap();
let map = BcSymbolMap::parse(data.as_bytes()).unwrap();
let mut map_iter = map.iter();
let (lower_bound, upper_bound) = map_iter.size_hint();
assert!(lower_bound > 0);
assert!(upper_bound.is_some());
let name = map_iter.next();
assert_eq!(name.unwrap(), "-[SentryMessage initWithFormatted:]");
let name = map_iter.next();
assert_eq!(name.unwrap(), "-[SentryMessage setMessage:]");
}
#[test]
fn test_data_lifetime() {
let data = std::fs::read_to_string(
"tests/fixtures/c8374b6d-6e96-34d8-ae38-efaa5fec424f.bcsymbolmap",
)
.unwrap();
let name = {
let map = BcSymbolMap::parse(data.as_bytes()).unwrap();
map.get(0).unwrap()
};
assert_eq!(name, "-[SentryMessage initWithFormatted:]");
}
#[test]
fn test_resolve() {
let data = std::fs::read_to_string(
"tests/fixtures/c8374b6d-6e96-34d8-ae38-efaa5fec424f.bcsymbolmap",
)
.unwrap();
let map = BcSymbolMap::parse(data.as_bytes()).unwrap();
assert_eq!(map.resolve("normal_name"), "normal_name");
assert_eq!(map.resolve("__hidden#2_"), "-[SentryMessage serialize]");
}
#[test]
fn test_plist() {
let uuid: DebugId = "2d10c42f-591d-3265-b147-78ba0868073f".parse().unwrap();
let data =
std::fs::read("tests/fixtures/2d10c42f-591d-3265-b147-78ba0868073f.plist").unwrap();
let map = UuidMapping::parse_plist(uuid, &data).unwrap();
assert_eq!(map.dsym_uuid(), uuid);
assert_eq!(
map.original_uuid(),
"c8374b6d-6e96-34d8-ae38-efaa5fec424f".parse().unwrap()
);
}
}