diff --git a/CHANGELOG.md b/CHANGELOG.md index cd45b1834..cdc1f0931 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,7 @@ **Features**: - `PortablePdbDebugSession` now returns files referenced in the Portable PDB file. ([#729](https://github.com/getsentry/symbolic/pull/729)) +- `PortablePdbDebugSession` now returns source files embedded in the Portable PDB file. ([#734](https://github.com/getsentry/symbolic/pull/734)) **Breaking changes**: diff --git a/examples/dump_sources/src/main.rs b/examples/dump_sources/src/main.rs index 9e59d829e..35cfeb8f6 100644 --- a/examples/dump_sources/src/main.rs +++ b/examples/dump_sources/src/main.rs @@ -27,7 +27,7 @@ fn write_object_sources(path: &Path, output_path: &Path) -> Result<(), Box { - let out = output_path.join(&format!("{}.zip", &object.debug_id())); + let out = output_path.join(format!("{}.zip", &object.debug_id())); println!(" -> {}", out.display()); let writer = SourceBundleWriter::create(&out)?; writer.write_object(&object, &path.file_name().unwrap().to_string_lossy())?; diff --git a/symbolic-debuginfo/src/base.rs b/symbolic-debuginfo/src/base.rs index 476772ae3..60dd61727 100644 --- a/symbolic-debuginfo/src/base.rs +++ b/symbolic-debuginfo/src/base.rs @@ -513,7 +513,7 @@ impl<'data> FileInfo<'data> { } #[allow(clippy::ptr_arg)] // false positive https://github.com/rust-lang/rust-clippy/issues/9218 -fn from_utf8_cow_lossy<'data>(input: &Cow<'data, [u8]>) -> Cow<'data, str> { +pub(crate) fn from_utf8_cow_lossy<'data>(input: &Cow<'data, [u8]>) -> Cow<'data, str> { // See https://github.com/rust-lang/rust/issues/32669 match input { Cow::Borrowed(bytes) => String::from_utf8_lossy(bytes), diff --git a/symbolic-debuginfo/src/ppdb.rs b/symbolic-debuginfo/src/ppdb.rs index f3ca82f40..852f275de 100644 --- a/symbolic-debuginfo/src/ppdb.rs +++ b/symbolic-debuginfo/src/ppdb.rs @@ -1,9 +1,11 @@ //! Support for Portable PDB Objects. use std::borrow::Cow; +use std::collections::HashMap; use std::fmt; use std::iter; use symbolic_common::{Arch, CodeId, DebugId}; +use symbolic_ppdb::EmbeddedSource; use symbolic_ppdb::{FormatError, PortablePdb}; use crate::base::*; @@ -88,9 +90,7 @@ impl<'data: 'object, 'object> ObjectLike<'data, 'object> for PortablePdbObject<' /// Constructs a debugging session. fn debug_session(&self) -> Result, FormatError> { - Ok(PortablePdbDebugSession { - ppdb: self.ppdb.clone(), - }) + PortablePdbDebugSession::new(&self.ppdb) } /// Determines whether this object contains stack unwinding information. @@ -100,7 +100,10 @@ impl<'data: 'object, 'object> ObjectLike<'data, 'object> for PortablePdbObject<' /// Determines whether this object contains embedded source. fn has_sources(&self) -> bool { - false + match self.ppdb.get_embedded_sources() { + Ok(mut iter) => iter.any(|v| v.is_ok()), + Err(_) => false, + } } /// Determines whether this object is malformed and was only partially parsed. @@ -138,9 +141,24 @@ impl fmt::Debug for PortablePdbObject<'_> { /// A debug session for a Portable PDB object. pub struct PortablePdbDebugSession<'data> { ppdb: PortablePdb<'data>, + sources: HashMap>, } impl<'data> PortablePdbDebugSession<'data> { + fn new(ppdb: &'_ PortablePdb<'data>) -> Result { + let mut sources: HashMap> = HashMap::new(); + for source in ppdb.get_embedded_sources()? { + match source { + Ok(source) => sources.insert(source.get_path().into(), source), + Err(e) => return Err(e), + }; + } + Ok(PortablePdbDebugSession { + ppdb: ppdb.clone(), + sources, + }) + } + /// Returns an iterator over all functions in this debug file. pub fn functions(&self) -> PortablePdbFunctionIterator<'_> { iter::empty() @@ -154,8 +172,13 @@ impl<'data> PortablePdbDebugSession<'data> { /// Looks up a file's source contents by its full canonicalized path. /// /// The given path must be canonicalized. - pub fn source_by_path(&self, _path: &str) -> Result>, FormatError> { - Ok(None) + pub fn source_by_path(&self, path: &str) -> Result>, FormatError> { + match self.sources.get(path) { + None => Ok(None), + Some(source) => source + .get_contents() + .map(|bytes| Some(from_utf8_cow_lossy(&bytes))), + } } } diff --git a/symbolic-debuginfo/tests/test_objects.rs b/symbolic-debuginfo/tests/test_objects.rs index d7e418a2b..4686477db 100644 --- a/symbolic-debuginfo/tests/test_objects.rs +++ b/symbolic-debuginfo/tests/test_objects.rs @@ -670,6 +670,49 @@ fn test_ppdb_functions() -> Result<(), Error> { Ok(()) } +#[test] +fn test_ppdb_has_sources() -> Result<(), Error> { + { + let view = ByteView::open(fixture("windows/portable.pdb"))?; + let object = Object::parse(&view)?; + assert_eq!(object.has_sources(), false); + } + { + let view = ByteView::open(fixture("windows/Sentry.Samples.Console.Basic.pdb"))?; + let object = Object::parse(&view)?; + assert_eq!(object.has_sources(), true); + } + Ok(()) +} + +#[test] +fn test_ppdb_source_by_path() -> Result<(), Error> { + { + let view = ByteView::open(fixture("windows/portable.pdb"))?; + let object = Object::parse(&view)?; + + let session = object.debug_session()?; + let source = session.source_by_path("foo/bar.cs").unwrap(); + assert!(source.is_none()); + } + + { + let view = ByteView::open(fixture("windows/Sentry.Samples.Console.Basic.pdb"))?; + let object = Object::parse(&view)?; + + let session = object.debug_session()?; + let source = session + .source_by_path( + "C:\\dev\\sentry-dotnet\\samples\\Sentry.Samples.Console.Basic\\Program.cs", + ) + .unwrap(); + let source_text = source.unwrap(); + assert_eq!(source_text.len(), 204); + } + + Ok(()) +} + #[test] fn test_wasm_symbols() -> Result<(), Error> { let view = ByteView::open(fixture("wasm/simple.wasm"))?; diff --git a/symbolic-ppdb/Cargo.toml b/symbolic-ppdb/Cargo.toml index dc8e61225..e440d3b90 100644 --- a/symbolic-ppdb/Cargo.toml +++ b/symbolic-ppdb/Cargo.toml @@ -25,6 +25,7 @@ symbolic-common = { version = "10.2.1", path = "../symbolic-common" } watto = { version = "0.1.0", features = ["writer", "strings"] } thiserror = "1.0.31" uuid = "1.0.0" +flate2 = { version ="1.0.13", default-features = false, features = [ "rust_backend" ] } [dev-dependencies] symbolic-testutils = { path = "../symbolic-testutils" } diff --git a/symbolic-ppdb/src/cache/mod.rs b/symbolic-ppdb/src/cache/mod.rs index e555dd9c3..6330e9dad 100644 --- a/symbolic-ppdb/src/cache/mod.rs +++ b/symbolic-ppdb/src/cache/mod.rs @@ -105,7 +105,7 @@ pub struct CacheError { } impl CacheError { - /// Creates a new SymCache error from a known kind of error as well as an + /// Creates a new CacheError from a known kind of error as well as an /// arbitrary error payload. pub(crate) fn new(kind: CacheErrorKind, source: E) -> Self where diff --git a/symbolic-ppdb/src/format/metadata.rs b/symbolic-ppdb/src/format/metadata.rs index 8d8eb539e..ae5f6afe2 100644 --- a/symbolic-ppdb/src/format/metadata.rs +++ b/symbolic-ppdb/src/format/metadata.rs @@ -2,9 +2,10 @@ use std::convert::TryInto; use std::fmt; use std::ops::{Index, IndexMut}; +use symbolic_common::Uuid; use watto::Pod; -use super::{FormatError, FormatErrorKind}; +use super::{FormatError, FormatErrorKind, PortablePdb}; /// An enumeration of all table types in ECMA-335 and Portable PDB. #[repr(usize)] @@ -60,9 +61,63 @@ pub enum TableType { DummyEmpty = 0x3F, } +impl From for TableType { + fn from(value: usize) -> Self { + match value { + x if x == Self::Assembly as usize => Self::Assembly, + x if x == Self::AssemblyProcessor as usize => Self::AssemblyProcessor, + x if x == Self::AssemblyRef as usize => Self::AssemblyRef, + x if x == Self::AssemblyRefOs as usize => Self::AssemblyRefOs, + x if x == Self::AssemblyRefProcessor as usize => Self::AssemblyRefProcessor, + x if x == Self::ClassLayout as usize => Self::ClassLayout, + x if x == Self::Constant as usize => Self::Constant, + x if x == Self::CustomAttribute as usize => Self::CustomAttribute, + x if x == Self::DeclSecurity as usize => Self::DeclSecurity, + x if x == Self::EventMap as usize => Self::EventMap, + x if x == Self::Event as usize => Self::Event, + x if x == Self::ExportedType as usize => Self::ExportedType, + x if x == Self::Field as usize => Self::Field, + x if x == Self::FieldLayout as usize => Self::FieldLayout, + x if x == Self::FieldMarshal as usize => Self::FieldMarshal, + x if x == Self::FieldRVA as usize => Self::FieldRVA, + x if x == Self::File as usize => Self::File, + x if x == Self::GenericParam as usize => Self::GenericParam, + x if x == Self::GenericParamConstraint as usize => Self::GenericParamConstraint, + x if x == Self::ImplMap as usize => Self::ImplMap, + x if x == Self::InterfaceImpl as usize => Self::InterfaceImpl, + x if x == Self::ManifestResource as usize => Self::ManifestResource, + x if x == Self::MemberRef as usize => Self::MemberRef, + x if x == Self::MethodDef as usize => Self::MethodDef, + x if x == Self::MethodImpl as usize => Self::MethodImpl, + x if x == Self::MethodSemantics as usize => Self::MethodSemantics, + x if x == Self::MethodSpec as usize => Self::MethodSpec, + x if x == Self::Module as usize => Self::Module, + x if x == Self::ModuleRef as usize => Self::ModuleRef, + x if x == Self::NestedClass as usize => Self::NestedClass, + x if x == Self::Param as usize => Self::Param, + x if x == Self::Property as usize => Self::Property, + x if x == Self::PropertyMap as usize => Self::PropertyMap, + x if x == Self::StandAloneSig as usize => Self::StandAloneSig, + x if x == Self::TypeDef as usize => Self::TypeDef, + x if x == Self::TypeRef as usize => Self::TypeRef, + x if x == Self::TypeSpec as usize => Self::TypeSpec, + x if x == Self::CustomDebugInformation as usize => Self::CustomDebugInformation, + x if x == Self::Document as usize => Self::Document, + x if x == Self::ImportScope as usize => Self::ImportScope, + x if x == Self::LocalConstant as usize => Self::LocalConstant, + x if x == Self::LocalScope as usize => Self::LocalScope, + x if x == Self::LocalVariable as usize => Self::LocalVariable, + x if x == Self::MethodDebugInformation as usize => Self::MethodDebugInformation, + x if x == Self::StateMachineMethod as usize => Self::StateMachineMethod, + _ => Self::DummyEmpty, + } + } +} + /// A table in a Portable PDB file. -#[derive(Default, Clone, Copy)] +#[derive(Clone, Copy)] pub struct Table<'data> { + type_: TableType, /// The number of rows in the table. pub rows: usize, /// The width in bytes of one table row. @@ -162,9 +217,46 @@ impl<'data> Table<'data> { /// Returns the the bytes of the `idx`th row, if any. /// /// Note that table row indices are 1-based! - fn get_row(&self, idx: usize) -> Option<&'data [u8]> { + pub(crate) fn get_row(&self, idx: usize) -> Result { idx.checked_sub(1) .and_then(|idx| self.contents.get(idx * self.width..(idx + 1) * self.width)) + .map(|data| Row { data, table: self }) + .ok_or_else(|| FormatErrorKind::RowIndexOutOfBounds(self.type_, idx).into()) + } +} + +/// A row in a [Table]. +#[derive(Debug, Clone, Copy)] +pub(crate) struct Row<'data> { + data: &'data [u8], + table: &'data Table<'data>, +} + +impl<'data> Row<'data> { + /// Reads the `col` cell in the given table as a `u32`. + /// + /// This returns an error if the indices are out of bounds for the table + /// or the cell is too wide for a `u32`. + /// + /// Note that row and column indices are 1-based! + pub(crate) fn get_col_u32(&self, col: usize) -> Result { + if !(1..=6).contains(&col) { + return Err(FormatErrorKind::ColIndexOutOfBounds(self.table.type_, col).into()); + } + let Column { offset, width } = self.table.columns[col - 1]; + match width { + 1 => Ok(self.data[offset] as u32), + 2 => { + let bytes = &self.data[offset..offset + 2]; + Ok(u16::from_ne_bytes(bytes.try_into().unwrap()) as u32) + } + 4 => { + let bytes = &self.data[offset..offset + 4]; + Ok(u32::from_ne_bytes(bytes.try_into().unwrap())) + } + + _ => Err(FormatErrorKind::ColumnWidth(self.table.type_, col, width).into()), + } } } @@ -255,8 +347,13 @@ impl<'data> MetadataStream<'data> { // TODO: verify major/minor version // TODO: verify reserved - - let mut tables = [Table::default(); 64]; + let mut tables = [Table { + type_: TableType::DummyEmpty, + rows: usize::default(), + width: usize::default(), + columns: [Column::default(); 6], + contents: <&[u8]>::default(), + }; 64]; for (i, table) in tables.iter_mut().enumerate() { if (header.valid_tables >> i & 1) == 0 { continue; @@ -264,7 +361,7 @@ impl<'data> MetadataStream<'data> { let (len, rest_) = u32::ref_from_prefix(rest).ok_or(FormatErrorKind::InvalidLength)?; rest = rest_; - + table.type_ = TableType::from(i); table.rows = *len as usize; } @@ -289,47 +386,6 @@ impl<'data> MetadataStream<'data> { Ok(result) } - /// Returns the bytes of the `idx`th row of the `table` table, if any. - /// - /// Note that table row indices are 1-based! - fn get_row(&self, table: TableType, idx: usize) -> Option<&'data [u8]> { - self[table].get_row(idx) - } - - /// Reads the `(row, col)` cell in the given table as a `u32`. - /// - /// This returns an error if the indices are out of bounds for the table - /// or the cell is too wide for a `u32`. - /// - /// Note that row and column indices are 1-based! - pub(crate) fn get_table_cell_u32( - &self, - table: TableType, - row: usize, - col: usize, - ) -> Result { - let row = self - .get_row(table, row) - .ok_or(FormatErrorKind::RowIndexOutOfBounds(table, row))?; - if !(1..=6).contains(&col) { - return Err(FormatErrorKind::ColIndexOutOfBounds(table, col).into()); - } - let Column { offset, width } = self[table].columns[col - 1]; - match width { - 1 => Ok(row[offset] as u32), - 2 => { - let bytes = &row[offset..offset + 2]; - Ok(u16::from_ne_bytes(bytes.try_into().unwrap()) as u32) - } - 4 => { - let bytes = &row[offset..offset + 4]; - Ok(u32::from_ne_bytes(bytes.try_into().unwrap())) - } - - _ => Err(FormatErrorKind::ColumnWidth(table, col, width).into()), - } - } - /// Sets the column widths of all tables in this stream. fn set_columns(&mut self, referenced_table_sizes: &[u32; 64]) { use TableType::*; @@ -811,3 +867,146 @@ impl<'data> IndexMut for MetadataStream<'data> { &mut self.tables[index as usize] } } + +/// An iterator over CustomDebugInformation of a specific Kind. +/// See [CustomDebugInformation](https://github.com/dotnet/runtime/blob/main/docs/design/specs/PortablePdb-Metadata.md#customdebuginformation-table-0x37). +#[derive(Debug, Clone)] +pub(crate) struct CustomDebugInformationIterator<'data> { + table: Table<'data>, + /// Which kind of CustomDebugInformation we want to filter. + /// We only store the offset in the GUID table to avoid lookups every time. + kind: Option, + /// Current row in the whole table (not just the filtered kind). + /// Note that the row is 1-based, to align with the rest of the crate APIs. + row: usize, +} + +impl<'data> CustomDebugInformationIterator<'data> { + pub(crate) fn new(ppdb: &PortablePdb<'data>, filter_kind: Uuid) -> Result { + let md_stream = ppdb + .metadata_stream + .as_ref() + .ok_or(FormatErrorKind::NoMetadataStream)?; + + let kind = ppdb + .guid_stream + .as_ref() + .ok_or(FormatErrorKind::NoGuidStream)? + .get_offset(filter_kind); + + Ok(CustomDebugInformationIterator { + table: md_stream[TableType::CustomDebugInformation], + kind, + row: 1, + }) + } +} + +macro_rules! ok_or_return { + ( $a:expr ) => { + match $a { + Ok(value) => value, + Err(err) => return Some(Err(err)), + } + }; +} + +impl<'data> Iterator for CustomDebugInformationIterator<'data> { + type Item = Result; + + fn next(&mut self) -> Option { + let expected_kind_offset = self.kind?; + // Find the first row in the table matching the desired Kind. + while self.row <= self.table.rows { + let row = ok_or_return!(self.table.get_row(self.row)); + self.row += 1; + + let kind_offset = ok_or_return!(row.get_col_u32(2)); + + if kind_offset == expected_kind_offset { + // Column 1 contains a Parent coded with HasCustomDebugInformation on the lower 5 bits + let parent = ok_or_return!(row.get_col_u32(1)); + let value = parent >> 5; + let tag = ok_or_return!(CustomDebugInformationTag::from(parent & 0b11111)); + + let blob = ok_or_return!(row.get_col_u32(3)); + return Some(Ok(CustomDebugInformation { tag, value, blob })); + } + } + None + } +} + +#[derive(Debug, Clone, Copy)] +pub(crate) struct CustomDebugInformation { + pub(crate) tag: CustomDebugInformationTag, + pub(crate) value: u32, + pub(crate) blob: u32, +} + +/// See [CustomDebugInformation](https://github.com/dotnet/runtime/blob/main/docs/design/specs/PortablePdb-Metadata.md#customdebuginformation-table-0x37). +#[derive(Debug, Clone, Copy)] +pub(crate) enum CustomDebugInformationTag { + MethodDef = 0, + Field = 1, + TypeRef = 2, + TypeDef = 3, + Param = 4, + InterfaceImpl = 5, + MemberRef = 6, + Module = 7, + DeclSecurity = 8, + Property = 9, + Event = 10, + StandAloneSig = 11, + ModuleRef = 12, + TypeSpec = 13, + Assembly = 14, + AssemblyRef = 15, + File = 16, + ExportedType = 17, + ManifestResource = 18, + GenericParam = 19, + GenericParamConstraint = 20, + MethodSpec = 21, + Document = 22, + LocalScope = 23, + LocalVariable = 24, + LocalConstant = 25, + ImportScope = 26, +} + +impl CustomDebugInformationTag { + fn from(value: u32) -> Result { + Ok(match value { + x if x == Self::MethodDef as u32 => Self::MethodDef, + x if x == Self::Field as u32 => Self::Field, + x if x == Self::TypeRef as u32 => Self::TypeRef, + x if x == Self::TypeDef as u32 => Self::TypeDef, + x if x == Self::Param as u32 => Self::Param, + x if x == Self::InterfaceImpl as u32 => Self::InterfaceImpl, + x if x == Self::MemberRef as u32 => Self::MemberRef, + x if x == Self::Module as u32 => Self::Module, + x if x == Self::DeclSecurity as u32 => Self::DeclSecurity, + x if x == Self::Property as u32 => Self::Property, + x if x == Self::Event as u32 => Self::Event, + x if x == Self::StandAloneSig as u32 => Self::StandAloneSig, + x if x == Self::ModuleRef as u32 => Self::ModuleRef, + x if x == Self::TypeSpec as u32 => Self::TypeSpec, + x if x == Self::Assembly as u32 => Self::Assembly, + x if x == Self::AssemblyRef as u32 => Self::AssemblyRef, + x if x == Self::File as u32 => Self::File, + x if x == Self::ExportedType as u32 => Self::ExportedType, + x if x == Self::ManifestResource as u32 => Self::ManifestResource, + x if x == Self::GenericParam as u32 => Self::GenericParam, + x if x == Self::GenericParamConstraint as u32 => Self::GenericParamConstraint, + x if x == Self::MethodSpec as u32 => Self::MethodSpec, + x if x == Self::Document as u32 => Self::Document, + x if x == Self::LocalScope as u32 => Self::LocalScope, + x if x == Self::LocalVariable as u32 => Self::LocalVariable, + x if x == Self::LocalConstant as u32 => Self::LocalConstant, + x if x == Self::ImportScope as u32 => Self::ImportScope, + _ => return Err(FormatErrorKind::InvalidCustomDebugInformationTag(value).into()), + }) + } +} diff --git a/symbolic-ppdb/src/format/mod.rs b/symbolic-ppdb/src/format/mod.rs index bf7bba823..1817c67f9 100644 --- a/symbolic-ppdb/src/format/mod.rs +++ b/symbolic-ppdb/src/format/mod.rs @@ -4,14 +4,15 @@ mod sequence_points; mod streams; mod utils; -use std::fmt; +use std::{borrow::Cow, fmt, io::Read}; +use flate2::read::DeflateDecoder; use thiserror::Error; use watto::Pod; use symbolic_common::{DebugId, Language, Uuid}; -use metadata::{MetadataStream, TableType}; +use metadata::{CustomDebugInformationIterator, MetadataStream, Table, TableType}; use streams::{BlobStream, GuidStream, PdbStream, StringStream, UsStream}; /// The kind of a [`FormatError`]. @@ -92,6 +93,12 @@ pub enum FormatErrorKind { /// The given column in the table has an incompatible width. #[error("column {1} in table {0:?} has incompatible width {2}")] ColumnWidth(TableType, usize, usize), + /// Tried to read an custom debug information table item tag. + #[error("invalid custom debug information table item tag {0}")] + InvalidCustomDebugInformationTag(u32), + /// Tried to read contents of a blob in an unknown format. + #[error("invalid blob format {0}")] + InvalidBlobFormat(u32), } /// An error encountered while parsing a [`PortablePdb`] file. @@ -104,7 +111,7 @@ pub struct FormatError { } impl FormatError { - /// Creates a new SymCache error from a known kind of error as well as an + /// Creates a new FormatError error from a known kind of error as well as an /// arbitrary error payload. pub(crate) fn new(kind: FormatErrorKind, source: E) -> Self where @@ -309,17 +316,12 @@ impl<'data> PortablePdb<'data> { /// or the cell is too wide for a `u32`. /// /// Note that row and column indices are 1-based! - pub(crate) fn get_table_cell_u32( - &self, - table: TableType, - row: usize, - col: usize, - ) -> Result { + pub(crate) fn get_table(&self, table: TableType) -> Result { let md_stream = self .metadata_stream .as_ref() .ok_or(FormatErrorKind::NoMetadataStream)?; - md_stream.get_table_cell_u32(table, row, col) + Ok(md_stream[table]) } /// Returns true if this portable pdb file contains method debug information. @@ -333,8 +335,10 @@ impl<'data> PortablePdb<'data> { /// /// Given index must be between 1 and get_documents_count(). pub fn get_document(&self, idx: usize) -> Result { - let name_offset = self.get_table_cell_u32(TableType::Document, idx, 1)?; - let lang_offset = self.get_table_cell_u32(TableType::Document, idx, 4)?; + let table = self.get_table(TableType::Document)?; + let row = table.get_row(idx)?; + let name_offset = row.get_col_u32(1)?; + let lang_offset = row.get_col_u32(4)?; let name = self.get_document_name(name_offset)?; let lang = self.get_document_lang(lang_offset)?; @@ -344,11 +348,13 @@ impl<'data> PortablePdb<'data> { /// Get the number of source files referenced by this PDB. pub fn get_documents_count(&self) -> Result { - let md_stream = self - .metadata_stream - .as_ref() - .ok_or(FormatErrorKind::NoMetadataStream)?; - Ok(md_stream[TableType::Document].rows) + let table = self.get_table(TableType::Document)?; + Ok(table.rows) + } + + /// An iterator over source files contents' embedded in this PDB. + pub fn get_embedded_sources(&self) -> Result, FormatError> { + EmbeddedSourceIterator::new(self) } } @@ -359,3 +365,87 @@ pub struct Document { pub name: String, pub(crate) lang: Language, } + +/// An iterator over Embedded Sources. +#[derive(Debug, Clone)] +pub struct EmbeddedSourceIterator<'object, 'data> { + ppdb: &'object PortablePdb<'data>, + inner_it: CustomDebugInformationIterator<'data>, +} + +impl<'object, 'data> EmbeddedSourceIterator<'object, 'data> { + fn new(ppdb: &'object PortablePdb<'data>) -> Result { + // https://github.com/dotnet/runtime/blob/main/docs/design/specs/PortablePdb-Metadata.md#embedded-source-c-and-vb-compilers + const EMBEDDED_SOURCES_KIND: Uuid = uuid::uuid!("0E8A571B-6926-466E-B4AD-8AB04611F5FE"); + let inner_it = CustomDebugInformationIterator::new(ppdb, EMBEDDED_SOURCES_KIND)?; + Ok(EmbeddedSourceIterator { ppdb, inner_it }) + } +} + +impl<'object, 'data> Iterator for EmbeddedSourceIterator<'object, 'data> { + type Item = Result, FormatError>; + + fn next(&mut self) -> Option { + self.inner_it.next().map(|inner| { + inner.and_then(|info| match info.tag { + // Verify we got the expected tag `Document` here. + metadata::CustomDebugInformationTag::Document => { + let document = self.ppdb.get_document(info.value as usize)?; + let blob = self.ppdb.get_blob(info.blob)?; + Ok(EmbeddedSource { document, blob }) + } + _ => Err(FormatErrorKind::InvalidCustomDebugInformationTag(info.tag as u32).into()), + }) + }) + } +} + +/// Lazy Embedded Source file reader. +#[derive(Debug, Clone)] +pub struct EmbeddedSource<'data> { + document: Document, + blob: &'data [u8], +} + +impl<'data, 'object> EmbeddedSource<'data> { + /// Returns the build-time path associated with this source file. + pub fn get_path(&'object self) -> &'object str { + self.document.name.as_str() + } + + /// Reads the source file contents from the Portable PDB. + pub fn get_contents(&self) -> Result, FormatError> { + // The blob has the following structure: `Blob ::= format content` + // - format - int32 - Indicates how the content is serialized. + // 0 = raw bytes, uncompressed. + // Positive value = compressed by deflate algorithm and value indicates uncompressed size. + // Negative values reserved for future formats. + // - content - format-specific - The text of the document in the specified format. The length is implied by the length of the blob minus four bytes for the format. + if self.blob.len() < 4 { + return Err(FormatErrorKind::InvalidBlobData.into()); + } + let (format_blob, data_blob) = self.blob.split_at(4); + let format = u32::from_ne_bytes(format_blob.try_into().unwrap()); + match format { + 0 => Ok(Cow::Borrowed(data_blob)), + x if x > 0 => self.inflate_contents(format as usize, data_blob), + _ => Err(FormatErrorKind::InvalidBlobFormat(format).into()), + } + } + + fn inflate_contents( + &self, + size: usize, + data: &'data [u8], + ) -> Result, FormatError> { + let mut decoder = DeflateDecoder::new(data); + let mut output = Vec::with_capacity(size); + let read_size = decoder + .read_to_end(&mut output) + .map_err(|e| FormatError::new(FormatErrorKind::InvalidBlobData, e))?; + if read_size != size { + return Err(FormatErrorKind::InvalidLength.into()); + } + Ok(Cow::Owned(output)) + } +} diff --git a/symbolic-ppdb/src/format/sequence_points.rs b/symbolic-ppdb/src/format/sequence_points.rs index b024d26b6..476dad4e3 100644 --- a/symbolic-ppdb/src/format/sequence_points.rs +++ b/symbolic-ppdb/src/format/sequence_points.rs @@ -59,8 +59,10 @@ impl<'data> PortablePdb<'data> { } fn get_sequence_points(&self, idx: usize) -> Result, FormatError> { - let document = self.get_table_cell_u32(TableType::MethodDebugInformation, idx, 1)?; - let offset = self.get_table_cell_u32(TableType::MethodDebugInformation, idx, 2)?; + let table = self.get_table(TableType::MethodDebugInformation)?; + let row = table.get_row(idx)?; + let document = row.get_col_u32(1)?; + let offset = row.get_col_u32(2)?; if offset == 0 { return Ok(Vec::new()); } diff --git a/symbolic-ppdb/src/format/streams.rs b/symbolic-ppdb/src/format/streams.rs index 615064823..67b767c79 100644 --- a/symbolic-ppdb/src/format/streams.rs +++ b/symbolic-ppdb/src/format/streams.rs @@ -127,4 +127,16 @@ impl<'data> GuidStream<'data> { .get(idx.checked_sub(1)? as usize) .map(|bytes| Uuid::from_bytes_le(*bytes)) } + + pub(crate) fn get_offset(&self, value: Uuid) -> Option { + let searched_bytes = value.to_bytes_le(); + let mut index = 1; + for bytes in self.buf.iter() { + if bytes.eq(&searched_bytes) { + return Some(index); + } + index += 1 + } + None + } } diff --git a/symbolic-ppdb/src/lib.rs b/symbolic-ppdb/src/lib.rs index e5d198a3d..7f4c394c9 100644 --- a/symbolic-ppdb/src/lib.rs +++ b/symbolic-ppdb/src/lib.rs @@ -56,4 +56,4 @@ mod format; pub use cache::lookup::LineInfo; pub use cache::writer::PortablePdbCacheConverter; pub use cache::{CacheError, CacheErrorKind, PortablePdbCache}; -pub use format::{FormatError, FormatErrorKind, PortablePdb}; +pub use format::{EmbeddedSource, FormatError, FormatErrorKind, PortablePdb}; diff --git a/symbolic-ppdb/tests/fixtures/contents/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs b/symbolic-ppdb/tests/fixtures/contents/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs new file mode 100644 index 000000000..a8c10efec --- /dev/null +++ b/symbolic-ppdb/tests/fixtures/contents/.NETCoreApp,Version=v6.0.AssemblyAttributes.cs @@ -0,0 +1,4 @@ +// +using System; +using System.Reflection; +[assembly: global::System.Runtime.Versioning.TargetFrameworkAttribute(".NETCoreApp,Version=v6.0", FrameworkDisplayName = ".NET 6.0")] diff --git a/symbolic-ppdb/tests/fixtures/contents/Program.cs b/symbolic-ppdb/tests/fixtures/contents/Program.cs new file mode 100644 index 000000000..ef7ff8d84 --- /dev/null +++ b/symbolic-ppdb/tests/fixtures/contents/Program.cs @@ -0,0 +1,7 @@ +using Sentry; + +using (SentrySdk.Init("https://eb18e953812b41c3aeb042e666fd3b5c@o447951.ingest.sentry.io/5428537")) +{ + // The following exception is captured and sent to Sentry + throw null; +} diff --git a/symbolic-ppdb/tests/fixtures/contents/Sentry.Samples.Console.Basic.AssemblyInfo.cs b/symbolic-ppdb/tests/fixtures/contents/Sentry.Samples.Console.Basic.AssemblyInfo.cs new file mode 100644 index 000000000..f7947ab45 --- /dev/null +++ b/symbolic-ppdb/tests/fixtures/contents/Sentry.Samples.Console.Basic.AssemblyInfo.cs @@ -0,0 +1,22 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +using System; +using System.Reflection; + +[assembly: System.Reflection.AssemblyCompanyAttribute("Sentry.Samples.Console.Basic")] +[assembly: System.Reflection.AssemblyConfigurationAttribute("release")] +[assembly: System.Reflection.AssemblyFileVersionAttribute("9.8.7.0")] +[assembly: System.Reflection.AssemblyInformationalVersionAttribute("9.8.7")] +[assembly: System.Reflection.AssemblyProductAttribute("Sentry.Samples.Console.Basic")] +[assembly: System.Reflection.AssemblyTitleAttribute("Sentry.Samples.Console.Basic")] +[assembly: System.Reflection.AssemblyVersionAttribute("9.8.7.0")] + +// Generated by the MSBuild WriteCodeFragment class. + diff --git a/symbolic-ppdb/tests/fixtures/contents/Sentry.Samples.Console.Basic.GlobalUsings.g.cs b/symbolic-ppdb/tests/fixtures/contents/Sentry.Samples.Console.Basic.GlobalUsings.g.cs new file mode 100644 index 000000000..ac22929d0 --- /dev/null +++ b/symbolic-ppdb/tests/fixtures/contents/Sentry.Samples.Console.Basic.GlobalUsings.g.cs @@ -0,0 +1,8 @@ +// +global using global::System; +global using global::System.Collections.Generic; +global using global::System.IO; +global using global::System.Linq; +global using global::System.Net.Http; +global using global::System.Threading; +global using global::System.Threading.Tasks; diff --git a/symbolic-ppdb/tests/test_ppdb.rs b/symbolic-ppdb/tests/test_ppdb.rs new file mode 100644 index 000000000..a5127d1f2 --- /dev/null +++ b/symbolic-ppdb/tests/test_ppdb.rs @@ -0,0 +1,63 @@ +use symbolic_ppdb::PortablePdb; +use symbolic_testutils::fixture; + +#[test] +fn test_embedded_sources_missing() { + let buf = std::fs::read(fixture("windows/portable.pdb")).unwrap(); + + let ppdb = PortablePdb::parse(&buf).unwrap(); + let mut iter = ppdb.get_embedded_sources().unwrap(); + assert!(iter.next().is_none()); +} + +#[test] +fn test_embedded_sources() { + let buf = std::fs::read(fixture("windows/Sentry.Samples.Console.Basic.pdb")).unwrap(); + + let ppdb = PortablePdb::parse(&buf).unwrap(); + let iter = ppdb.get_embedded_sources().unwrap(); + let items = iter.collect::, _>>().unwrap(); + assert_eq!(items.len(), 4); + + let check_path = |i: usize, expected: &str| { + let repo_root = "C:\\dev\\sentry-dotnet\\samples\\Sentry.Samples.Console.Basic\\"; + assert_eq!(items[i].get_path(), format!("{}{}", repo_root, expected)); + }; + + check_path(0, "Program.cs"); + check_path( + 1, + "obj\\release\\net6.0\\Sentry.Samples.Console.Basic.GlobalUsings.g.cs", + ); + check_path( + 2, + "obj\\release\\net6.0\\.NETCoreApp,Version=v6.0.AssemblyAttributes.cs", + ); + check_path( + 3, + "obj\\release\\net6.0\\Sentry.Samples.Console.Basic.AssemblyInfo.cs", + ); +} + +#[test] +fn test_embedded_sources_contents() { + let buf = std::fs::read(fixture("windows/Sentry.Samples.Console.Basic.pdb")).unwrap(); + + let ppdb = PortablePdb::parse(&buf).unwrap(); + let iter = ppdb.get_embedded_sources().unwrap(); + let items = iter.collect::, _>>().unwrap(); + assert_eq!(items.len(), 4); + + let check_contents = |i: usize, length: usize, name: &str| { + let content = items[i].get_contents().unwrap(); + assert_eq!(content.len(), length); + + let expected = std::fs::read(format!("tests/fixtures/contents/{}", name)).unwrap(); + assert_eq!(content, expected); + }; + + check_contents(0, 204, "Program.cs"); + check_contents(1, 295, "Sentry.Samples.Console.Basic.GlobalUsings.g.cs"); + check_contents(2, 198, ".NETCoreApp,Version=v6.0.AssemblyAttributes.cs"); + check_contents(3, 1019, "Sentry.Samples.Console.Basic.AssemblyInfo.cs"); +} diff --git a/symbolic-testutils/fixtures/windows/Sentry.Samples.Console.Basic.pdb b/symbolic-testutils/fixtures/windows/Sentry.Samples.Console.Basic.pdb new file mode 100644 index 000000000..2d26fb354 Binary files /dev/null and b/symbolic-testutils/fixtures/windows/Sentry.Samples.Console.Basic.pdb differ