Skip to main content

symbolic_debuginfo/
ppdb.rs

1//! Support for Portable PDB Objects.
2use std::borrow::Cow;
3use std::collections::HashMap;
4use std::fmt;
5use std::iter;
6use std::sync::OnceLock;
7
8use symbolic_common::{Arch, CodeId, DebugId};
9use symbolic_ppdb::EmbeddedSource;
10use symbolic_ppdb::{Document, FormatError, PortablePdb};
11
12use crate::base::*;
13use crate::sourcebundle::SourceFileDescriptor;
14use crate::ParseObjectOptions;
15
16/// An iterator over symbols in a [`PortablePdbObject`].
17pub type PortablePdbSymbolIterator<'data> = iter::Empty<Symbol<'data>>;
18/// An iterator over functions in a [`PortablePdbObject`].
19pub type PortablePdbFunctionIterator<'session> =
20    iter::Empty<Result<Function<'session>, FormatError>>;
21
22/// An object wrapping a Portable PDB file.
23pub struct PortablePdbObject<'data> {
24    data: &'data [u8],
25    ppdb: PortablePdb<'data>,
26    max_decompressed_embedded_source_size: Option<usize>,
27}
28
29impl<'data> PortablePdbObject<'data> {
30    /// Tries to parse a Portable PDB object from the given slice, with default options.
31    pub fn parse(data: &'data [u8]) -> Result<Self, FormatError> {
32        Self::parse_with_opts(data, Default::default())
33    }
34
35    /// Tries to parse a Portable PDB object from the given slice.
36    pub fn parse_with_opts(
37        data: &'data [u8],
38        opts: ParseObjectOptions,
39    ) -> Result<Self, FormatError> {
40        let ppdb = PortablePdb::parse(data)?;
41        Ok(Self {
42            data,
43            ppdb,
44            max_decompressed_embedded_source_size: opts.max_decompressed_embedded_source_size,
45        })
46    }
47
48    /// Returns the Portable PDB contained in this object.
49    pub fn portable_pdb(&self) -> &PortablePdb<'_> {
50        &self.ppdb
51    }
52
53    /// Returns the raw data of the Portable PDB file.
54    pub fn data(&self) -> &'data [u8] {
55        self.data
56    }
57}
58
59impl<'data: 'object, 'object> ObjectLike<'data, 'object> for PortablePdbObject<'data> {
60    type Error = FormatError;
61    type Session = PortablePdbDebugSession<'data>;
62    type SymbolIterator = PortablePdbSymbolIterator<'data>;
63
64    /// The debug information identifier of a Portable PDB file.
65    fn debug_id(&self) -> DebugId {
66        self.ppdb.pdb_id().unwrap_or_default()
67    }
68
69    /// The code identifier of this object.
70    ///
71    /// Portable PDB does not provide code identifiers.
72    fn code_id(&self) -> Option<CodeId> {
73        None
74    }
75
76    /// The CPU architecture of this object.
77    fn arch(&self) -> Arch {
78        Arch::Unknown
79    }
80
81    /// The kind of this object.
82    fn kind(&self) -> ObjectKind {
83        ObjectKind::Debug
84    }
85
86    /// The address at which the image prefers to be loaded into memory.
87    ///
88    /// This is always 0 as this does not really apply to Portable PDB.
89    fn load_address(&self) -> u64 {
90        0
91    }
92
93    /// Returns true if this object exposes a public symbol table.
94    fn has_symbols(&self) -> bool {
95        false
96    }
97
98    /// Returns an iterator over symbols in the public symbol table.
99    fn symbols(&self) -> PortablePdbSymbolIterator<'data> {
100        iter::empty()
101    }
102
103    /// Returns an ordered map of symbols in the symbol table.
104    fn symbol_map(&self) -> SymbolMap<'data> {
105        SymbolMap::new()
106    }
107
108    /// Determines whether this object contains debug information.
109    fn has_debug_info(&self) -> bool {
110        self.ppdb.has_debug_info()
111    }
112
113    /// Constructs a debugging session.
114    fn debug_session(&self) -> Result<PortablePdbDebugSession<'data>, FormatError> {
115        PortablePdbDebugSession::new(&self.ppdb, self.max_decompressed_embedded_source_size)
116    }
117
118    /// Determines whether this object contains stack unwinding information.
119    fn has_unwind_info(&self) -> bool {
120        false
121    }
122
123    /// Determines whether this object contains embedded or linked sources.
124    fn has_sources(&self) -> bool {
125        self.ppdb.has_source_links().unwrap_or(false)
126            || match self.ppdb.get_embedded_sources() {
127                Ok(mut iter) => iter.any(|v| v.is_ok()),
128                Err(_) => false,
129            }
130    }
131
132    /// Determines whether this object is malformed and was only partially parsed.
133    fn is_malformed(&self) -> bool {
134        false
135    }
136
137    /// The container file format, which currently is always `FileFormat::PortablePdb`.
138    fn file_format(&self) -> FileFormat {
139        FileFormat::PortablePdb
140    }
141}
142
143impl<'data> Parse<'data> for PortablePdbObject<'data> {
144    type Error = FormatError;
145
146    fn test(data: &[u8]) -> bool {
147        PortablePdb::peek(data)
148    }
149
150    fn parse_with_opts(data: &'data [u8], opts: ParseObjectOptions) -> Result<Self, Self::Error> {
151        Self::parse_with_opts(data, opts)
152    }
153}
154
155impl fmt::Debug for PortablePdbObject<'_> {
156    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
157        f.debug_struct("PortablePdbObject")
158            .field("portable_pdb", &self.portable_pdb())
159            .finish()
160    }
161}
162
163/// A debug session for a Portable PDB object.
164pub struct PortablePdbDebugSession<'data> {
165    ppdb: PortablePdb<'data>,
166    sources: OnceLock<HashMap<String, PPDBSource<'data>>>,
167    max_decompressed_embedded_source_size: Option<usize>,
168}
169
170#[derive(Debug, Clone)]
171enum PPDBSource<'data> {
172    Embedded(EmbeddedSource<'data>),
173    Link(Document),
174}
175
176impl<'data> PortablePdbDebugSession<'data> {
177    fn new(
178        ppdb: &'_ PortablePdb<'data>,
179        max_decompressed_embedded_source_size: Option<usize>,
180    ) -> Result<Self, FormatError> {
181        Ok(PortablePdbDebugSession {
182            ppdb: ppdb.clone(),
183            sources: OnceLock::new(),
184            max_decompressed_embedded_source_size,
185        })
186    }
187
188    fn init_sources(&self) -> HashMap<String, PPDBSource<'data>> {
189        let count = self.ppdb.get_documents_count().unwrap_or(0);
190        let mut result = HashMap::with_capacity(count);
191
192        if let Ok(iter) = self.ppdb.get_embedded_sources() {
193            for source in iter.flatten() {
194                result.insert(source.get_path().to_string(), PPDBSource::Embedded(source));
195            }
196        };
197
198        for i in 1..count + 1 {
199            if let Ok(doc) = self.ppdb.get_document(i) {
200                if !result.contains_key(&doc.name) {
201                    result.insert(doc.name.clone(), PPDBSource::Link(doc));
202                }
203            }
204        }
205
206        result
207    }
208
209    /// Returns an iterator over all functions in this debug file.
210    pub fn functions(&self) -> PortablePdbFunctionIterator<'_> {
211        iter::empty()
212    }
213
214    /// Returns an iterator over all source files in this debug file.
215    pub fn files(&self) -> PortablePdbFileIterator<'_> {
216        PortablePdbFileIterator::new(&self.ppdb)
217    }
218
219    /// See [DebugSession::source_by_path] for more information.
220    pub fn source_by_path(
221        &self,
222        path: &str,
223    ) -> Result<Option<SourceFileDescriptor<'_>>, FormatError> {
224        let sources = self.sources.get_or_init(|| self.init_sources());
225        match sources.get(path) {
226            None => Ok(None),
227            Some(PPDBSource::Embedded(source)) => source
228                .get_contents_bounded(self.max_decompressed_embedded_source_size)
229                .map(|bytes| {
230                    Some(SourceFileDescriptor::new_embedded(
231                        from_utf8_cow_lossy(&bytes),
232                        None,
233                    ))
234                }),
235            Some(PPDBSource::Link(document)) => Ok(self
236                .ppdb
237                .get_source_link(document)
238                .map(SourceFileDescriptor::new_remote)),
239        }
240    }
241}
242
243impl<'session> DebugSession<'session> for PortablePdbDebugSession<'_> {
244    type Error = FormatError;
245    type FunctionIterator = PortablePdbFunctionIterator<'session>;
246    type FileIterator = PortablePdbFileIterator<'session>;
247
248    fn functions(&'session self) -> Self::FunctionIterator {
249        self.functions()
250    }
251
252    fn files(&'session self) -> Self::FileIterator {
253        self.files()
254    }
255
256    fn source_by_path(&self, path: &str) -> Result<Option<SourceFileDescriptor<'_>>, Self::Error> {
257        self.source_by_path(path)
258    }
259}
260
261/// An iterator over source files in a Portable PDB file.
262pub struct PortablePdbFileIterator<'s> {
263    ppdb: &'s PortablePdb<'s>,
264    row: usize,
265    size: usize,
266}
267
268impl<'s> PortablePdbFileIterator<'s> {
269    fn new(ppdb: &'s PortablePdb<'s>) -> Self {
270        PortablePdbFileIterator {
271            ppdb,
272            // ppdb.get_document(index) - index is 1-based
273            row: 1,
274            // Zero indicates the value is unknown and must be read during the first next() call.
275            // We do it this way so that we can return a FormatError in case one occurs when determining the size.
276            size: 0,
277        }
278    }
279}
280
281impl<'s> Iterator for PortablePdbFileIterator<'s> {
282    type Item = Result<FileEntry<'s>, FormatError>;
283
284    fn next(&mut self) -> Option<Self::Item> {
285        if self.size == 0 {
286            match self.ppdb.get_documents_count() {
287                Ok(size) => {
288                    debug_assert!(size != usize::MAX);
289                    self.size = size;
290                }
291                Err(e) => {
292                    return Some(Err(e));
293                }
294            }
295        }
296
297        if self.row > self.size {
298            return None;
299        }
300
301        let index = self.row;
302        self.row += 1;
303
304        let document = match self.ppdb.get_document(index) {
305            Ok(doc) => doc,
306            Err(e) => {
307                return Some(Err(e));
308            }
309        };
310        Some(Ok(FileEntry::new(
311            Cow::default(),
312            FileInfo::from_path_owned(document.name.as_bytes()),
313        )))
314    }
315}
316
317#[cfg(test)]
318mod tests {
319    use symbolic_common::ByteView;
320    use symbolic_ppdb::FormatErrorKind as PpdbErrorKind;
321    use symbolic_testutils::fixture;
322
323    use crate::ppdb::PortablePdbObject;
324    use crate::{ObjectLike, ParseObjectOptions};
325
326    #[test]
327    fn test_ppdb_source_by_path_size_limit() {
328        let opts = ParseObjectOptions {
329            max_decompressed_embedded_source_size: Some(200),
330            ..Default::default()
331        };
332
333        let view = ByteView::open(fixture("windows/Sentry.Samples.Console.Basic.pdb")).unwrap();
334        let object = PortablePdbObject::parse_with_opts(&view, opts).unwrap();
335
336        let session = object.debug_session().unwrap();
337        let err = session
338            .source_by_path(
339                "C:\\dev\\sentry-dotnet\\samples\\Sentry.Samples.Console.Basic\\Program.cs",
340            )
341            .unwrap_err();
342
343        assert!(matches!(
344            err.kind(),
345            PpdbErrorKind::EmbeddedSourceFileSizeExceeded(204)
346        ));
347    }
348}