1mod metadata;
2mod raw;
3mod sequence_points;
4mod streams;
5mod utils;
6
7use std::{borrow::Cow, collections::BTreeMap, fmt, io::Read};
8
9use flate2::read::DeflateDecoder;
10use serde::Deserialize;
11use thiserror::Error;
12use watto::Pod;
13
14use symbolic_common::{DebugId, Language, SourceLinkMappings, Uuid};
15
16use metadata::{
17 CustomDebugInformation, CustomDebugInformationIterator, CustomDebugInformationTag,
18 MetadataStream, Table, TableType,
19};
20use streams::{BlobStream, GuidStream, PdbStream, StringStream, UsStream};
21
22#[derive(Debug, Clone, Copy, Error)]
24#[non_exhaustive]
25pub enum FormatErrorKind {
26 #[error("invalid header")]
28 InvalidHeader,
29 #[error("invalid signature")]
30 InvalidSignature,
32 #[error("invalid length")]
34 InvalidLength,
35 #[error("invalid version string")]
37 InvalidVersionString,
38 #[error("invalid stream header")]
40 InvalidStreamHeader,
41 #[error("invalid stream name")]
43 InvalidStreamName,
44 #[error("file does not contain a #Strings stream")]
46 NoStringsStream,
47 #[error("invalid string offset")]
49 InvalidStringOffset,
50 #[error("invalid string data")]
52 InvalidStringData,
53 #[error("unknown stream")]
55 UnknownStream,
56 #[error("file does not contain a #Guid stream")]
58 NoGuidStream,
59 #[error("invalid guid index")]
61 InvalidGuidIndex,
62 #[error(
64 "insufficient table data: {0} bytes required, but table stream only contains {1} bytes"
65 )]
66 InsufficientTableData(usize, usize),
67 #[error("invalid blob offset")]
69 InvalidBlobOffset,
70 #[error("invalid blob data")]
72 InvalidBlobData,
73 #[error("file does not contain a #Blob stream")]
75 NoBlobStream,
76 #[error("invalid compressed unsigned number")]
78 InvalidCompressedUnsigned,
79 #[error("invalid compressed signed number")]
81 InvalidCompressedSigned,
82 #[error("invalid document name")]
84 InvalidDocumentName,
85 #[error("invalid sequence point")]
87 InvalidSequencePoint,
88 #[error("file does not contain a #~ stream")]
90 NoMetadataStream,
91 #[error("row index {1} is out of bounds for table {0:?}")]
93 RowIndexOutOfBounds(TableType, usize),
94 #[error("column index {1} is out of bounds for table {0:?}")]
96 ColIndexOutOfBounds(TableType, usize),
97 #[error("column {1} in table {0:?} has incompatible width {2}")]
99 ColumnWidth(TableType, usize, usize),
100 #[error("invalid custom debug information table item tag {0}")]
102 InvalidCustomDebugInformationTag(u32),
103 #[error("invalid blob format {0}")]
105 InvalidBlobFormat(u32),
106 #[error("invalid source link JSON")]
108 InvalidSourceLinkJson,
109 #[error("embedded source file size ({0}) exceeds maximum")]
111 EmbeddedSourceFileSizeExceeded(usize),
112}
113
114#[derive(Debug, Error)]
116#[error("{kind}")]
117pub struct FormatError {
118 pub(crate) kind: FormatErrorKind,
119 #[source]
120 pub(crate) source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
121}
122
123impl FormatError {
124 pub(crate) fn new<E>(kind: FormatErrorKind, source: E) -> Self
127 where
128 E: Into<Box<dyn std::error::Error + Send + Sync>>,
129 {
130 let source = Some(source.into());
131 Self { kind, source }
132 }
133
134 pub fn kind(&self) -> FormatErrorKind {
136 self.kind
137 }
138}
139
140impl From<FormatErrorKind> for FormatError {
141 fn from(kind: FormatErrorKind) -> Self {
142 Self { kind, source: None }
143 }
144}
145
146#[derive(Clone)]
152pub struct PortablePdb<'data> {
153 header: &'data raw::Header,
155 version_string: &'data str,
157 header2: &'data raw::HeaderPart2,
159 pdb_stream: Option<PdbStream<'data>>,
161 metadata_stream: Option<MetadataStream<'data>>,
163 string_stream: Option<StringStream<'data>>,
165 us_stream: Option<UsStream<'data>>,
167 blob_stream: Option<BlobStream<'data>>,
169 guid_stream: Option<GuidStream<'data>>,
171 source_link_mappings: SourceLinkMappings,
173}
174
175impl fmt::Debug for PortablePdb<'_> {
176 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
177 f.debug_struct("PortablePdb")
178 .field("header", &self.header)
179 .field("version_string", &self.version_string)
180 .field("header2", &self.header2)
181 .field("has_pdb_stream", &self.pdb_stream.is_some())
182 .field("has_table_stream", &self.metadata_stream.is_some())
183 .field("has_string_stream", &self.string_stream.is_some())
184 .field("has_us_stream", &self.us_stream.is_some())
185 .field("has_blob_stream", &self.blob_stream.is_some())
186 .field("has_guid_stream", &self.guid_stream.is_some())
187 .finish()
188 }
189}
190
191impl<'data> PortablePdb<'data> {
192 pub fn peek(buf: &[u8]) -> bool {
195 if let Some((header, _)) = raw::Header::ref_from_prefix(buf) {
196 return header.signature == raw::METADATA_SIGNATURE;
197 }
198 false
199 }
200
201 pub fn parse(buf: &'data [u8]) -> Result<Self, FormatError> {
203 let (header, rest) =
204 raw::Header::ref_from_prefix(buf).ok_or(FormatErrorKind::InvalidHeader)?;
205
206 if header.signature != raw::METADATA_SIGNATURE {
207 return Err(FormatErrorKind::InvalidSignature.into());
208 }
209
210 let version_length = header.version_length as usize;
213 let version_buf = rest
214 .get(..version_length)
215 .ok_or(FormatErrorKind::InvalidLength)?;
216 let version_buf = version_buf
217 .split(|c| *c == 0)
218 .next()
219 .ok_or(FormatErrorKind::InvalidVersionString)?;
220 let version = std::str::from_utf8(version_buf)
221 .map_err(|e| FormatError::new(FormatErrorKind::InvalidVersionString, e))?;
222
223 let streams_buf = &rest[version_length..];
225 let (header2, mut streams_buf) =
226 raw::HeaderPart2::ref_from_prefix(streams_buf).ok_or(FormatErrorKind::InvalidHeader)?;
227
228 let stream_count = header2.streams;
231
232 let mut result = Self {
233 header,
234 version_string: version,
235 header2,
236 pdb_stream: None,
237 metadata_stream: None,
238 string_stream: None,
239 us_stream: None,
240 blob_stream: None,
241 guid_stream: None,
242 source_link_mappings: SourceLinkMappings::default(),
243 };
244
245 let mut metadata_stream = None;
246 for _ in 0..stream_count {
247 let (header, after_header_buf) = raw::StreamHeader::ref_from_prefix(streams_buf)
248 .ok_or(FormatErrorKind::InvalidStreamHeader)?;
249
250 let name_buf = after_header_buf.get(..32).unwrap_or(after_header_buf);
251 let name_buf = name_buf
252 .split(|c| *c == 0)
253 .next()
254 .ok_or(FormatErrorKind::InvalidStreamName)?;
255 let name = std::str::from_utf8(name_buf)
256 .map_err(|e| FormatError::new(FormatErrorKind::InvalidStreamName, e))?;
257
258 let mut rounded_name_len = name.len() + 1;
259 rounded_name_len = match rounded_name_len % 4 {
260 0 => rounded_name_len,
261 r => rounded_name_len + (4 - r),
262 };
263 streams_buf = after_header_buf
264 .get(rounded_name_len..)
265 .ok_or(FormatErrorKind::InvalidLength)?;
266
267 let offset = header.offset as usize;
268 let size = header.size as usize;
269 let stream_buf = buf
270 .get(offset..offset + size)
271 .ok_or(FormatErrorKind::InvalidLength)?;
272
273 match name {
274 "#Pdb" => result.pdb_stream = Some(PdbStream::parse(stream_buf)?),
275 "#~" => metadata_stream = Some(stream_buf),
277 "#Strings" => result.string_stream = Some(StringStream::new(stream_buf)),
278 "#US" => result.us_stream = Some(UsStream::new(stream_buf)),
279 "#Blob" => result.blob_stream = Some(BlobStream::new(stream_buf)),
280 "#GUID" => result.guid_stream = Some(GuidStream::parse(stream_buf)?),
281 _ => return Err(FormatErrorKind::UnknownStream.into()),
282 }
283 }
284
285 if let Some(stream_buf) = metadata_stream {
286 result.metadata_stream = Some(MetadataStream::parse(
287 stream_buf,
288 result
289 .pdb_stream
290 .as_ref()
291 .map_or([0; 64], |s| s.referenced_table_sizes),
292 )?)
293 }
294
295 const SOURCE_LINK_KIND: Uuid = uuid::uuid!("CC110556-A091-4D38-9FEC-25AB9A351A6A");
298
299 #[derive(Debug, Clone, Deserialize)]
300 struct SourceLinkDocuments {
301 documents: BTreeMap<String, String>,
302 }
303
304 for cdi in CustomDebugInformationIterator::new(&result, SOURCE_LINK_KIND)? {
305 let cdi = cdi?;
306 if let (CustomDebugInformationTag::Module, 1) = (cdi.tag, cdi.value) {
308 let docs: SourceLinkDocuments = serde_json::from_slice(result.get_blob(cdi.blob)?)
309 .map_err(|e| FormatError::new(FormatErrorKind::InvalidSourceLinkJson, e))?;
310 result
311 .source_link_mappings
312 .extend(docs.documents.iter().map(|(k, v)| (&k[..], &v[..])));
313 }
314 }
315
316 Ok(result)
317 }
318
319 #[allow(unused)]
321 fn get_string(&self, offset: u32) -> Result<&'data str, FormatError> {
322 self.string_stream
323 .as_ref()
324 .ok_or(FormatErrorKind::NoStringsStream)?
325 .get_string(offset)
326 }
327
328 fn get_guid(&self, idx: u32) -> Result<Uuid, FormatError> {
332 self.guid_stream
333 .as_ref()
334 .ok_or(FormatErrorKind::NoGuidStream)?
335 .get_guid(idx)
336 .ok_or_else(|| FormatErrorKind::InvalidGuidIndex.into())
337 }
338
339 fn get_blob(&self, offset: u32) -> Result<&'data [u8], FormatError> {
341 self.blob_stream
342 .as_ref()
343 .ok_or(FormatErrorKind::NoBlobStream)?
344 .get_blob(offset)
345 }
346
347 pub fn pdb_id(&self) -> Option<DebugId> {
349 self.pdb_stream.as_ref().map(|stream| stream.id())
350 }
351
352 pub(crate) fn get_table(&self, table: TableType) -> Result<Table<'_>, FormatError> {
359 let md_stream = self
360 .metadata_stream
361 .as_ref()
362 .ok_or(FormatErrorKind::NoMetadataStream)?;
363 Ok(md_stream[table])
364 }
365
366 pub fn has_debug_info(&self) -> bool {
368 self.metadata_stream
369 .as_ref()
370 .is_some_and(|md_stream| md_stream[TableType::MethodDebugInformation].rows > 0)
371 }
372
373 pub fn get_document(&self, idx: usize) -> Result<Document, FormatError> {
377 let table = self.get_table(TableType::Document)?;
378 let row = table.get_row(idx)?;
379 let name_offset = row.get_col_u32(1)?;
380 let lang_offset = row.get_col_u32(4)?;
381
382 let name = self.get_document_name(name_offset)?;
383 let lang = self.get_document_lang(lang_offset)?;
384
385 Ok(Document { name, lang })
386 }
387
388 pub fn get_documents_count(&self) -> Result<usize, FormatError> {
390 let table = self.get_table(TableType::Document)?;
391 Ok(table.rows)
392 }
393
394 pub fn get_embedded_sources(&self) -> Result<EmbeddedSourceIterator<'_, 'data>, FormatError> {
396 EmbeddedSourceIterator::new(self)
397 }
398
399 pub fn has_source_links(&self) -> Result<bool, FormatError> {
401 Ok(!self.source_link_mappings.is_empty() && self.get_documents_count()? > 0)
402 }
403
404 pub fn get_source_link(&self, document: &Document) -> Option<Cow<'_, str>> {
409 self.source_link_mappings
410 .resolve(&document.name)
411 .map(Cow::Owned)
412 }
413}
414
415#[derive(Debug, Clone)]
417pub struct Document {
418 pub name: String,
420 pub(crate) lang: Language,
421}
422
423#[derive(Debug, Clone)]
425pub struct EmbeddedSourceIterator<'object, 'data> {
426 ppdb: &'object PortablePdb<'data>,
427 inner_it: CustomDebugInformationIterator<'data>,
428}
429
430impl<'object, 'data> EmbeddedSourceIterator<'object, 'data> {
431 fn new(ppdb: &'object PortablePdb<'data>) -> Result<Self, FormatError> {
432 const EMBEDDED_SOURCES_KIND: Uuid = uuid::uuid!("0E8A571B-6926-466E-B4AD-8AB04611F5FE");
434 let inner_it = CustomDebugInformationIterator::new(ppdb, EMBEDDED_SOURCES_KIND)?;
435 Ok(EmbeddedSourceIterator { ppdb, inner_it })
436 }
437
438 fn get_source(
439 &mut self,
440 info: CustomDebugInformation,
441 ) -> Result<EmbeddedSource<'data>, FormatError> {
442 let document = self.ppdb.get_document(info.value as usize)?;
443 let blob = self.ppdb.get_blob(info.blob)?;
444 Ok(EmbeddedSource { document, blob })
445 }
446}
447
448impl<'data> Iterator for EmbeddedSourceIterator<'_, 'data> {
449 type Item = Result<EmbeddedSource<'data>, FormatError>;
450
451 fn next(&mut self) -> Option<Self::Item> {
452 while let Some(row) = self.inner_it.next() {
455 match row {
456 Err(e) => return Some(Err(e)),
457 Ok(info) => {
458 if let CustomDebugInformationTag::Document = info.tag {
459 return Some(self.get_source(info));
460 }
461 }
462 }
463 }
464 None
465 }
466}
467
468#[derive(Debug, Clone)]
470pub struct EmbeddedSource<'data> {
471 document: Document,
472 blob: &'data [u8],
473}
474
475impl<'data, 'object> EmbeddedSource<'data> {
476 pub fn get_path(&'object self) -> &'object str {
478 self.document.name.as_str()
479 }
480
481 pub fn get_contents(&self) -> Result<Cow<'data, [u8]>, FormatError> {
483 self.get_contents_bounded(None)
484 }
485
486 pub fn get_contents_bounded(
491 &self,
492 max_decompressed_size: Option<usize>,
493 ) -> Result<Cow<'data, [u8]>, FormatError> {
494 if self.blob.len() < 4 {
501 return Err(FormatErrorKind::InvalidBlobData.into());
502 }
503 let (format_blob, data_blob) = self.blob.split_at(4);
504 let format = u32::from_ne_bytes(format_blob.try_into().unwrap());
505
506 const MAX_VALID_FORMAT: u32 = i32::MAX as u32;
513
514 match format {
515 0 => Ok(Cow::Borrowed(data_blob)),
516 1..=MAX_VALID_FORMAT => {
517 let size = format as usize;
518 if max_decompressed_size.is_some_and(|max| size > max) {
519 return Err(FormatErrorKind::EmbeddedSourceFileSizeExceeded(size).into());
520 }
521 self.inflate_contents(size, data_blob)
522 }
523 _ => Err(FormatErrorKind::InvalidBlobFormat(format).into()),
524 }
525 }
526
527 fn inflate_contents(
528 &self,
529 size: usize,
530 data: &'data [u8],
531 ) -> Result<Cow<'data, [u8]>, FormatError> {
532 let decoder = DeflateDecoder::new(data);
533 let mut output = Vec::with_capacity(size + 1);
534 let read_size = decoder
535 .take(size as u64 + 1)
536 .read_to_end(&mut output)
537 .map_err(|e| FormatError::new(FormatErrorKind::InvalidBlobData, e))?;
538 if read_size != size {
539 return Err(FormatErrorKind::InvalidLength.into());
540 }
541 Ok(Cow::Owned(output))
542 }
543}