diff --git a/CHANGELOG.md b/CHANGELOG.md index ec54c3538..6ccf4cf98 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,10 @@ ## Unreleased +**Breaking changes**: + +- Change `DebugSession::source_by_path()` to return `SourceCode` enum with either file content or a URL to fetch it from. ([#758](https://github.com/getsentry/symbolic/pull/758)) + **Fixes**: - Make sure to parse `PortablePdb` streams in the correct order. ([#760](https://github.com/getsentry/symbolic/pull/760)) diff --git a/symbolic-debuginfo/src/base.rs b/symbolic-debuginfo/src/base.rs index fba9a9644..a21a04bff 100644 --- a/symbolic-debuginfo/src/base.rs +++ b/symbolic-debuginfo/src/base.rs @@ -670,6 +670,17 @@ impl fmt::Debug for Function<'_> { /// A dynamically dispatched iterator over items with the given lifetime. pub type DynIterator<'a, T> = Box + 'a>; +/// Represents a source file referenced by a debug information object file. +#[non_exhaustive] +#[derive(Debug, Clone)] +pub enum SourceCode<'a> { + /// Verbatim source code/file contents. + Content(Cow<'a, str>), + + /// Url (usually https) where the content can be fetched from. + Url(Cow<'a, str>), +} + /// A stateful session for interfacing with debug information. /// /// Debug sessions can be obtained via [`ObjectLike::debug_session`]. Since computing a session may @@ -716,10 +727,9 @@ pub trait DebugSession<'session> { /// Returns an iterator over all source files referenced by this debug file. fn files(&'session self) -> Self::FileIterator; - /// Looks up a file's source contents by its full canonicalized path. - /// - /// The given path must be canonicalized. - fn source_by_path(&self, path: &str) -> Result>, Self::Error>; + /// Looks up a file's source by its full canonicalized path. + /// Returns either source contents, if it was embedded, or a source link. + fn source_by_path(&self, path: &str) -> Result>, Self::Error>; } /// An object containing debug information. diff --git a/symbolic-debuginfo/src/breakpad.rs b/symbolic-debuginfo/src/breakpad.rs index ed843ad75..0e6ccb2ab 100644 --- a/symbolic-debuginfo/src/breakpad.rs +++ b/symbolic-debuginfo/src/breakpad.rs @@ -1276,10 +1276,8 @@ impl<'data> BreakpadDebugSession<'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>, BreakpadError> { + /// See [DebugSession::source_by_path] for more information. + pub fn source_by_path(&self, _path: &str) -> Result>, BreakpadError> { Ok(None) } } @@ -1297,7 +1295,7 @@ impl<'data, 'session> DebugSession<'session> for BreakpadDebugSession<'data> { self.files() } - fn source_by_path(&self, path: &str) -> Result>, Self::Error> { + fn source_by_path(&self, path: &str) -> Result>, Self::Error> { self.source_by_path(path) } } diff --git a/symbolic-debuginfo/src/dwarf.rs b/symbolic-debuginfo/src/dwarf.rs index 30d0ffc5e..17ab0e165 100644 --- a/symbolic-debuginfo/src/dwarf.rs +++ b/symbolic-debuginfo/src/dwarf.rs @@ -1331,10 +1331,8 @@ impl<'data> DwarfDebugSession<'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>, DwarfError> { + /// See [DebugSession::source_by_path] for more information. + pub fn source_by_path(&self, _path: &str) -> Result>, DwarfError> { Ok(None) } } @@ -1352,7 +1350,7 @@ impl<'data, 'session> DebugSession<'session> for DwarfDebugSession<'data> { self.files() } - fn source_by_path(&self, path: &str) -> Result>, Self::Error> { + fn source_by_path(&self, path: &str) -> Result>, Self::Error> { self.source_by_path(path) } } diff --git a/symbolic-debuginfo/src/object.rs b/symbolic-debuginfo/src/object.rs index 7c98c0894..c9419248d 100644 --- a/symbolic-debuginfo/src/object.rs +++ b/symbolic-debuginfo/src/object.rs @@ -1,6 +1,5 @@ //! Generic wrappers over various object file formats. -use std::borrow::Cow; use std::error::Error; use std::fmt; @@ -483,10 +482,9 @@ impl<'d> ObjectDebugSession<'d> { } } - /// 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>, ObjectError> { + /// Looks up a file's source by its full canonicalized path. + /// Returns either source contents, if it was embedded, or a source link. + pub fn source_by_path(&self, path: &str) -> Result>, ObjectError> { match *self { ObjectDebugSession::Breakpad(ref s) => { s.source_by_path(path).map_err(ObjectError::transparent) @@ -520,7 +518,7 @@ impl<'session> DebugSession<'session> for ObjectDebugSession<'_> { self.files() } - fn source_by_path(&self, path: &str) -> Result>, Self::Error> { + fn source_by_path(&self, path: &str) -> Result>, Self::Error> { self.source_by_path(path) } } diff --git a/symbolic-debuginfo/src/pdb.rs b/symbolic-debuginfo/src/pdb.rs index f7b864646..bc906915e 100644 --- a/symbolic-debuginfo/src/pdb.rs +++ b/symbolic-debuginfo/src/pdb.rs @@ -637,10 +637,8 @@ impl<'d> PdbDebugSession<'d> { } } - /// 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>, PdbError> { + /// See [DebugSession::source_by_path] for more information. + pub fn source_by_path(&self, _path: &str) -> Result>, PdbError> { Ok(None) } } @@ -658,7 +656,7 @@ impl<'session> DebugSession<'session> for PdbDebugSession<'_> { self.files() } - fn source_by_path(&self, path: &str) -> Result>, Self::Error> { + fn source_by_path(&self, path: &str) -> Result>, Self::Error> { self.source_by_path(path) } } diff --git a/symbolic-debuginfo/src/ppdb.rs b/symbolic-debuginfo/src/ppdb.rs index 852f275de..e7b9362a0 100644 --- a/symbolic-debuginfo/src/ppdb.rs +++ b/symbolic-debuginfo/src/ppdb.rs @@ -4,9 +4,10 @@ use std::collections::HashMap; use std::fmt; use std::iter; +use lazycell::LazyCell; use symbolic_common::{Arch, CodeId, DebugId}; use symbolic_ppdb::EmbeddedSource; -use symbolic_ppdb::{FormatError, PortablePdb}; +use symbolic_ppdb::{Document, FormatError, PortablePdb}; use crate::base::*; @@ -141,24 +142,44 @@ impl fmt::Debug for PortablePdbObject<'_> { /// A debug session for a Portable PDB object. pub struct PortablePdbDebugSession<'data> { ppdb: PortablePdb<'data>, - sources: HashMap>, + sources: LazyCell>>, +} + +#[derive(Debug, Clone)] +enum PPDBSource<'data> { + Embedded(EmbeddedSource<'data>), + Link(Document), } 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, + sources: LazyCell::new(), }) } + fn init_sources(&self) -> HashMap> { + let count = self.ppdb.get_documents_count().unwrap_or(0); + let mut result = HashMap::with_capacity(count); + + if let Ok(iter) = self.ppdb.get_embedded_sources() { + for source in iter.flatten() { + result.insert(source.get_path().to_string(), PPDBSource::Embedded(source)); + } + }; + + for i in 1..count + 1 { + if let Ok(doc) = self.ppdb.get_document(i) { + if !result.contains_key(&doc.name) { + result.insert(doc.name.clone(), PPDBSource::Link(doc)); + } + } + } + + result + } + /// Returns an iterator over all functions in this debug file. pub fn functions(&self) -> PortablePdbFunctionIterator<'_> { iter::empty() @@ -169,15 +190,17 @@ impl<'data> PortablePdbDebugSession<'data> { PortablePdbFileIterator::new(&self.ppdb) } - /// 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> { - match self.sources.get(path) { + /// See [DebugSession::source_by_path] for more information. + pub fn source_by_path(&self, path: &str) -> Result>, FormatError> { + let sources = self.sources.borrow_with(|| self.init_sources()); + match sources.get(path) { None => Ok(None), - Some(source) => source + Some(PPDBSource::Embedded(source)) => source .get_contents() - .map(|bytes| Some(from_utf8_cow_lossy(&bytes))), + .map(|bytes| Some(SourceCode::Content(from_utf8_cow_lossy(&bytes)))), + Some(PPDBSource::Link(document)) => { + Ok(self.ppdb.get_source_link(document).map(SourceCode::Url)) + } } } } @@ -195,7 +218,7 @@ impl<'data, 'session> DebugSession<'session> for PortablePdbDebugSession<'data> self.files() } - fn source_by_path(&self, path: &str) -> Result>, Self::Error> { + fn source_by_path(&self, path: &str) -> Result>, Self::Error> { self.source_by_path(path) } } diff --git a/symbolic-debuginfo/src/sourcebundle.rs b/symbolic-debuginfo/src/sourcebundle.rs index 8f045b08d..9291f7f97 100644 --- a/symbolic-debuginfo/src/sourcebundle.rs +++ b/symbolic-debuginfo/src/sourcebundle.rs @@ -650,17 +650,15 @@ impl<'data> SourceBundleDebugSession<'data> { Ok(Some(source_content)) } - /// 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>, SourceBundleError> { + /// See [DebugSession::source_by_path] for more information. + pub fn source_by_path(&self, path: &str) -> Result>, SourceBundleError> { let zip_path = match self.zip_path_by_source_path(path) { Some(zip_path) => zip_path, None => return Ok(None), }; - self.source_by_zip_path(zip_path) - .map(|opt| opt.map(Cow::Owned)) + let content = self.source_by_zip_path(zip_path)?; + Ok(content.map(|opt| SourceCode::Content(Cow::Owned(opt)))) } } @@ -677,7 +675,7 @@ impl<'data, 'session> DebugSession<'session> for SourceBundleDebugSession<'data> self.files() } - fn source_by_path(&self, path: &str) -> Result>, Self::Error> { + fn source_by_path(&self, path: &str) -> Result>, Self::Error> { self.source_by_path(path) } } @@ -1152,11 +1150,12 @@ mod tests { .flatten() .flat_map(|f| { let path = f.abs_path_str(); - session - .source_by_path(&path) - .ok() - .flatten() - .map(|c| (path, c.into_owned())) + session.source_by_path(&path).ok().flatten().map(|source| { + let SourceCode::Content(text) = source else { + unreachable!(); + }; + (path, text.into_owned()) + }) }) .collect(); diff --git a/symbolic-debuginfo/tests/test_objects.rs b/symbolic-debuginfo/tests/test_objects.rs index a1fb70670..2346013af 100644 --- a/symbolic-debuginfo/tests/test_objects.rs +++ b/symbolic-debuginfo/tests/test_objects.rs @@ -2,7 +2,7 @@ use std::{env, ffi::CString, fmt, io::BufWriter}; use symbolic_common::ByteView; use symbolic_debuginfo::{ - elf::ElfObject, pe::PeObject, FileEntry, Function, LineInfo, Object, SymbolMap, + elf::ElfObject, pe::PeObject, FileEntry, Function, LineInfo, Object, SourceCode, SymbolMap, }; use symbolic_testutils::fixture; @@ -776,10 +776,51 @@ fn test_ppdb_source_by_path() -> Result<(), Error> { "C:\\dev\\sentry-dotnet\\samples\\Sentry.Samples.Console.Basic\\Program.cs", ) .unwrap(); - let source_text = source.unwrap(); - assert_eq!(source_text.len(), 204); + match source.unwrap() { + SourceCode::Content(text) => assert_eq!(text.len(), 204), + _ => panic!(), + } + } + + Ok(()) +} + +#[test] +fn test_ppdb_source_links() -> Result<(), Error> { + let view = ByteView::open(fixture("ppdb-sourcelink-sample/ppdb-sourcelink-sample.pdb"))?; + let object = Object::parse(&view)?; + let session = object.debug_session()?; + + let known_embedded_sources = vec![ + ".NETStandard,Version=v2.0.AssemblyAttributes.cs", + "ppdb-sourcelink-sample.AssemblyInfo.cs", + ]; + + // Testing this is simple because there's just one prefix rule in this PPDB. + let src_prefix = "C:\\dev\\symbolic\\"; + let url_prefix = "https://raw.githubusercontent.com/getsentry/symbolic/9f7ceefc29da4c45bc802751916dbb3ea72bf08f/"; + + for file in session.files() { + let file = file.unwrap(); + + match session.source_by_path(&file.path_str()).unwrap().unwrap() { + SourceCode::Content(text) => { + assert!(known_embedded_sources.contains(&file.name_str().as_ref())); + assert!(!text.is_empty()); + } + SourceCode::Url(url) => { + // testing this is simple because there's just one prefix rule in this PPDB. + let expected = file + .path_str() + .replace(src_prefix, url_prefix) + .replace('\\', "/"); + assert_eq!(url, expected); + } + _ => panic!(), + } } + assert!(session.source_by_path("c:/non/existent/path.cs")?.is_none()); Ok(()) } diff --git a/symbolic-ppdb/Cargo.toml b/symbolic-ppdb/Cargo.toml index 3e62d0edf..9b29b6f06 100644 --- a/symbolic-ppdb/Cargo.toml +++ b/symbolic-ppdb/Cargo.toml @@ -26,6 +26,7 @@ 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" ] } +serde_json = { version = "1.0.40" } [dev-dependencies] symbolic-debuginfo = { path = "../symbolic-debuginfo" } diff --git a/symbolic-ppdb/src/format/mod.rs b/symbolic-ppdb/src/format/mod.rs index 6021304c7..70dda07f9 100644 --- a/symbolic-ppdb/src/format/mod.rs +++ b/symbolic-ppdb/src/format/mod.rs @@ -1,6 +1,7 @@ mod metadata; mod raw; mod sequence_points; +mod sourcelinks; mod streams; mod utils; @@ -13,8 +14,10 @@ use watto::Pod; use symbolic_common::{DebugId, Language, Uuid}; use metadata::{ - CustomDebugInformation, CustomDebugInformationIterator, MetadataStream, Table, TableType, + CustomDebugInformation, CustomDebugInformationIterator, CustomDebugInformationTag, + MetadataStream, Table, TableType, }; +use sourcelinks::SourceLinkMappings; use streams::{BlobStream, GuidStream, PdbStream, StringStream, UsStream}; /// The kind of a [`FormatError`]. @@ -101,6 +104,9 @@ pub enum FormatErrorKind { /// Tried to read contents of a blob in an unknown format. #[error("invalid blob format {0}")] InvalidBlobFormat(u32), + /// Failed to parse Source Link JSON + #[error("invalid source link JSON")] + InvalidSourceLinkJson, } /// An error encountered while parsing a [`PortablePdb`] file. @@ -160,6 +166,8 @@ pub struct PortablePdb<'data> { blob_stream: Option>, /// The file's #GUID stream, if it exists. guid_stream: Option>, + /// Source link mappings + source_link_mappings: SourceLinkMappings, } impl fmt::Debug for PortablePdb<'_> { @@ -229,6 +237,7 @@ impl<'data> PortablePdb<'data> { us_stream: None, blob_stream: None, guid_stream: None, + source_link_mappings: SourceLinkMappings::default(), }; let mut metadata_stream = None; @@ -281,6 +290,19 @@ impl<'data> PortablePdb<'data> { )?) } + // Read source link mappings. + // https://github.com/dotnet/runtime/blob/main/docs/design/specs/PortablePdb-Metadata.md#source-link-c-and-vb-compilers + const SOURCE_LINK_KIND: Uuid = uuid::uuid!("CC110556-A091-4D38-9FEC-25AB9A351A6A"); + let mut source_link_mappings = Vec::new(); + for cdi in CustomDebugInformationIterator::new(&result, SOURCE_LINK_KIND)? { + let cdi = cdi?; + // Note: only handle module #1 (do we actually handle multiple modules in any way??) + if let (CustomDebugInformationTag::Module, 1) = (cdi.tag, cdi.value) { + source_link_mappings.push(result.get_blob(cdi.blob)?); + } + } + result.source_link_mappings = SourceLinkMappings::new(source_link_mappings)?; + Ok(result) } @@ -363,6 +385,16 @@ impl<'data> PortablePdb<'data> { pub fn get_embedded_sources(&self) -> Result, FormatError> { EmbeddedSourceIterator::new(self) } + + /// Tries to resolve given document as a source link (URL). + /// Make sure to try [Self::get_embedded_sources] first when looking for a source file, because + /// function may return a link that actually doesn't exist (e.g. file is in .gitignore). + /// In that case, it's usually the case that the file is embedded in the PPDB instead. + pub fn get_source_link(&self, document: &Document) -> Option> { + self.source_link_mappings + .resolve(&document.name) + .map(Cow::Owned) + } } /// Represents a source file that is referenced by this PDB. @@ -408,7 +440,7 @@ impl<'object, 'data> Iterator for EmbeddedSourceIterator<'object, 'data> { match row { Err(e) => return Some(Err(e)), Ok(info) => { - if let metadata::CustomDebugInformationTag::Document = info.tag { + if let CustomDebugInformationTag::Document = info.tag { return Some(self.get_source(info)); } } diff --git a/symbolic-ppdb/src/format/sourcelinks.rs b/symbolic-ppdb/src/format/sourcelinks.rs new file mode 100644 index 000000000..8fb6a8de9 --- /dev/null +++ b/symbolic-ppdb/src/format/sourcelinks.rs @@ -0,0 +1,192 @@ +use std::cmp::Ordering; + +use crate::{FormatError, FormatErrorKind}; + +/// See [Source Link PPDB docs](https://github.com/dotnet/designs/blob/main/accepted/2020/diagnostics/source-link.md#source-link-json-schema). +#[derive(Default, Clone)] +pub(crate) struct SourceLinkMappings { + rules: Vec, +} + +#[derive(Clone)] +struct Rule { + pattern: Pattern, + url: String, +} + +#[derive(Clone)] +enum Pattern { + Exact(String), + Prefix(String), +} + +impl SourceLinkMappings { + pub fn new(jsons: Vec<&[u8]>) -> Result { + let mut result = Self { rules: Vec::new() }; + for json in jsons { + result.add_mappings(json)?; + } + result.sort(); + Ok(result) + } + + fn add_mappings(&mut self, json: &[u8]) -> Result<(), FormatError> { + use serde_json::*; + let json: Value = serde_json::from_slice(json) + .map_err(|e| FormatError::new(FormatErrorKind::InvalidSourceLinkJson, e))?; + + let docs = json + .get("documents") + .and_then(|v| v.as_object()) + .ok_or_else(Self::err)?; + + self.rules.reserve(docs.len()); + for (key, url) in docs.iter() { + /* + Each document is defined by a file path and a URL. Original source file paths are compared + case-insensitively to documents and the resulting URL is used to download source. The document + may contain an asterisk to represent a wildcard in order to match anything in the asterisk's + location. The rules for the asterisk are as follows: + 1. The only acceptable wildcard is one and only one '*', which if present will be replaced by a relative path. + 2. If the file path does not contain a *, the URL cannot contain a * and if the file path contains a * the URL must contain a *. + 3. If the file path contains a *, it must be the final character. + 4. If the URL contains a *, it may be anywhere in the URL. + */ + let key = key.to_lowercase(); + let url = url.as_str().ok_or_else(Self::err)?.into(); + let pattern = if let Some(prefix) = key.strip_suffix('*') { + Pattern::Prefix(prefix.into()) + } else { + Pattern::Exact(key) + }; + self.rules.push(Rule { pattern, url }); + } + Ok(()) + } + + fn err() -> FormatError { + FormatError { + kind: FormatErrorKind::InvalidSourceLinkJson, + source: None, + } + } + + /// Sort internal rules. This must be called before [Self::resolve]. + fn sort(&mut self) { + // Put Exact matches first, then sort by the Prefix length, longest to shortest. + self.rules.sort_unstable_by(|a, b| match &a.pattern { + Pattern::Exact(_) => Ordering::Less, + Pattern::Prefix(a) => match &b.pattern { + Pattern::Exact(_) => Ordering::Greater, + Pattern::Prefix(b) => b.len().cmp(&a.len()), + }, + }); + } + + /// Resolve the path to a URL. + pub fn resolve(&self, path: &str) -> Option { + // Note: this is currently quite simple, just pick the first match. If we needed to improve + // performance in the future because we encounter PDBs with too many items, we can do a + // prefix binary search, for example. + let path_lower = path.to_lowercase(); + for rule in &self.rules { + match &rule.pattern { + Pattern::Exact(value) => { + if value == &path_lower { + return Some(rule.url.clone()); + } + } + Pattern::Prefix(value) => { + if path_lower.starts_with(value) { + let replacement = path + .get(value.len()..) + .unwrap_or_default() + .replace('\\', "/"); + return Some(rule.url.replace('*', &replacement)); + } + } + } + } + None + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_invalid_json() { + let mut mappings = SourceLinkMappings::default(); + assert!(mappings.add_mappings(b"").is_err()); + assert!(mappings.add_mappings(b"foo").is_err()); + assert!(mappings + .add_mappings(b"{\"docs\": {\"k\": \"v\"}}") + .is_err()); + assert!(mappings + .add_mappings(b"{\"documents\": [\"k\", \"v\"]}") + .is_err()); + assert_eq!(mappings.rules.len(), 0); + } + + #[test] + fn test_mapping() { + let mappings = SourceLinkMappings::new( + vec![br#" + { + "documents": { + "C:\\src\\*": "http://MyDefaultDomain.com/src/*", + "C:\\src\\fOO\\*": "http://MyFooDomain.com/src/*", + "C:\\src\\foo\\specific.txt": "http://MySpecificFoodDomain.com/src/specific.txt", + "C:\\src\\bar\\*": "http://MyBarDomain.com/src/*" + } + } + "#, br#" + { + "documents": { + "C:\\src\\file.txt": "https://example.com/file.txt" + } + } + "#, br#" + { + "documents": { + "/home/user/src/*": "https://linux.com/*" + } + } + "#] + ).unwrap(); + + assert_eq!(mappings.rules.len(), 6); + + // In this example: + // All files under directory bar will map to a relative URL beginning with http://MyBarDomain.com/src/. + // All files under directory foo will map to a relative URL beginning with http://MyFooDomain.com/src/ EXCEPT foo/specific.txt which will map to http://MySpecificFoodDomain.com/src/specific.txt. + // All other files anywhere under the src directory will map to a relative url beginning with http://MyDefaultDomain.com/src/. + assert!(mappings.resolve("c:\\other\\path").is_none()); + assert!(mappings.resolve("/home/path").is_none()); + assert_eq!( + mappings.resolve("c:\\src\\bAr\\foo\\FiLe.txt").unwrap(), + "http://MyBarDomain.com/src/foo/FiLe.txt" + ); + assert_eq!( + mappings.resolve("c:\\src\\foo\\FiLe.txt").unwrap(), + "http://MyFooDomain.com/src/FiLe.txt" + ); + assert_eq!( + mappings.resolve("c:\\src\\foo\\SpEcIfIc.txt").unwrap(), + "http://MySpecificFoodDomain.com/src/specific.txt" + ); + assert_eq!( + mappings.resolve("c:\\src\\other\\path").unwrap(), + "http://MyDefaultDomain.com/src/other/path" + ); + assert_eq!( + mappings.resolve("c:\\src\\other\\path").unwrap(), + "http://MyDefaultDomain.com/src/other/path" + ); + assert_eq!( + mappings.resolve("/home/user/src/Path/TO/file.txt").unwrap(), + "https://linux.com/Path/TO/file.txt" + ); + } +} diff --git a/symbolic-ppdb/src/lib.rs b/symbolic-ppdb/src/lib.rs index 4b8408d89..d7ddc8179 100644 --- a/symbolic-ppdb/src/lib.rs +++ b/symbolic-ppdb/src/lib.rs @@ -60,4 +60,4 @@ mod format; pub use cache::lookup::LineInfo; pub use cache::writer::PortablePdbCacheConverter; pub use cache::{CacheError, CacheErrorKind, PortablePdbCache}; -pub use format::{EmbeddedSource, FormatError, FormatErrorKind, PortablePdb}; +pub use format::{Document, EmbeddedSource, FormatError, FormatErrorKind, PortablePdb}; diff --git a/symbolic-ppdb/tests/test_ppdb.rs b/symbolic-ppdb/tests/test_ppdb.rs index 9988bb7e1..032b34c19 100644 --- a/symbolic-ppdb/tests/test_ppdb.rs +++ b/symbolic-ppdb/tests/test_ppdb.rs @@ -1,3 +1,5 @@ +use std::path::Path; + use symbolic_debuginfo::pe::PeObject; use symbolic_ppdb::{EmbeddedSource, PortablePdb}; use symbolic_testutils::fixture; @@ -156,3 +158,42 @@ fn test_pe_embedded_ppdb_with_sources() { "Sentry.Samples.Console.Basic.AssemblyInfo.cs", ); } + +#[test] +fn test_source_links() { + let buf = std::fs::read(fixture("ppdb-sourcelink-sample/ppdb-sourcelink-sample.pdb")).unwrap(); + + let ppdb = PortablePdb::parse(&buf).unwrap(); + + // Firstly, let's assert for the sources that are embedded in the PPDB itself because they're not on GitHub. + let embedded_sources = ppdb + .get_embedded_sources() + .unwrap() + .map(|src| { + Path::new(&src.unwrap().get_path().replace('\\', "/")) + .file_name() + .unwrap() + .to_string_lossy() + .to_string() + }) + .collect::>(); + assert_eq!( + embedded_sources, + vec![ + ".NETStandard,Version=v2.0.AssemblyAttributes.cs", + "ppdb-sourcelink-sample.AssemblyInfo.cs" + ] + ); + + // Testing this is simple because there's just one prefix rule in this PPDB. + let src_prefix = "C:\\dev\\symbolic\\"; + let url_prefix = "https://raw.githubusercontent.com/getsentry/symbolic/9f7ceefc29da4c45bc802751916dbb3ea72bf08f/"; + + for i in 1..ppdb.get_documents_count().unwrap() + 1 { + let doc = ppdb.get_document(i).unwrap(); + let url = ppdb.get_source_link(&doc).unwrap(); + + let expected = doc.name.replace(src_prefix, url_prefix).replace('\\', "/"); + assert_eq!(url, expected); + } +} diff --git a/symbolic-testutils/fixtures/ppdb-sourcelink-sample/ppdb-sourcelink-sample.dll b/symbolic-testutils/fixtures/ppdb-sourcelink-sample/ppdb-sourcelink-sample.dll index 14c2d2ace..824204f4a 100644 Binary files a/symbolic-testutils/fixtures/ppdb-sourcelink-sample/ppdb-sourcelink-sample.dll and b/symbolic-testutils/fixtures/ppdb-sourcelink-sample/ppdb-sourcelink-sample.dll differ diff --git a/symbolic-testutils/fixtures/ppdb-sourcelink-sample/ppdb-sourcelink-sample.pdb b/symbolic-testutils/fixtures/ppdb-sourcelink-sample/ppdb-sourcelink-sample.pdb index 2f2389f8b..fea93a3bc 100644 Binary files a/symbolic-testutils/fixtures/ppdb-sourcelink-sample/ppdb-sourcelink-sample.pdb and b/symbolic-testutils/fixtures/ppdb-sourcelink-sample/ppdb-sourcelink-sample.pdb differ diff --git a/symbolic-testutils/fixtures/ppdb-sourcelink-sample/src/ppdb-sourcelink-sample.csproj b/symbolic-testutils/fixtures/ppdb-sourcelink-sample/src/ppdb-sourcelink-sample.csproj index 71d98060a..31dbd34c9 100644 --- a/symbolic-testutils/fixtures/ppdb-sourcelink-sample/src/ppdb-sourcelink-sample.csproj +++ b/symbolic-testutils/fixtures/ppdb-sourcelink-sample/src/ppdb-sourcelink-sample.csproj @@ -2,6 +2,7 @@ netstandard2.0 + true