1mod function;
2mod layout;
3mod lookup;
4mod owned;
5
6use std::fmt;
7use std::path::Path;
8
9use smallvec::SmallVec;
10use zerocopy::byteorder::{BigEndian, LittleEndian, U16, U32, U64};
11use zerocopy::{FromBytes, Immutable, KnownLayout};
12
13use crate::GsymVersion;
14use crate::endian::{Cursor, Endian};
15use crate::error::{Error, Result};
16use crate::format::function::EncodedFunction;
17use crate::model::{AddressRange, FileIndex};
18
19pub use function::{FunctionRef, Functions};
20use function::{RawFunction, file_at, string_at};
21pub(crate) use layout::ParsedLayout;
22use layout::VersionLayout;
23
24#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31#[non_exhaustive]
32pub struct Header<'data> {
33 pub version: GsymVersion,
35 pub endian: Endian,
37 pub address_offset_size: u8,
39 pub base_address: u64,
41 pub address_count: u32,
43 pub build_id: &'data [u8],
45}
46
47#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub struct LookupOptions {
66 pub line_information: bool,
68 pub inline_frames: bool,
70 pub call_sites: bool,
72}
73
74impl Default for LookupOptions {
75 fn default() -> Self {
76 Self {
77 line_information: true,
78 inline_frames: true,
79 call_sites: true,
80 }
81 }
82}
83
84#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub struct FrameLookupOptions {
91 pub line_information: bool,
93 pub inline_frames: bool,
95}
96
97impl Default for FrameLookupOptions {
98 fn default() -> Self {
99 Self {
100 line_information: true,
101 inline_frames: true,
102 }
103 }
104}
105
106impl From<FrameLookupOptions> for LookupOptions {
107 fn from(options: FrameLookupOptions) -> Self {
108 Self {
109 line_information: options.line_information,
110 inline_frames: options.inline_frames,
111 call_sites: false,
112 }
113 }
114}
115
116#[derive(Default)]
123pub struct LookupScratch {
124 inline_frames: SmallVec<[RawInlineFrame; 4]>,
125}
126
127impl fmt::Debug for LookupScratch {
128 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
129 formatter
130 .debug_struct("LookupScratch")
131 .finish_non_exhaustive()
132 }
133}
134
135impl LookupScratch {
136 #[must_use]
138 pub fn with_capacity(inline_depth: usize) -> Self {
139 Self {
140 inline_frames: SmallVec::with_capacity(inline_depth),
141 }
142 }
143
144 fn clear(&mut self) {
145 self.inline_frames.clear();
146 }
147}
148
149#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
154#[non_exhaustive]
155pub struct VerifyReport {
156 pub functions: usize,
158 pub files: usize,
160 pub strings: usize,
162 pub function_info_bytes: usize,
164}
165
166#[derive(Clone, Copy, Debug)]
167struct RawInlineFrame {
168 name: u64,
169 call_file: FileIndex,
170 call_line: u32,
171 start: u64,
172}
173
174pub struct Gsym<D> {
208 pub(super) data: D,
209 pub(super) layout: ParsedLayout,
210}
211
212impl<D: AsRef<[u8]>> fmt::Debug for Gsym<D> {
213 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
214 formatter
215 .debug_struct("Gsym")
216 .field("byte_len", &self.data.as_ref().len())
217 .field("version", &self.header().version)
218 .field("endian", &self.layout.endian)
219 .field("base_address", &self.layout.base_address)
220 .field("function_count", &self.layout.address_count)
221 .field("build_id_len", &self.layout.build_id.len())
222 .finish_non_exhaustive()
223 }
224}
225
226impl Gsym<Vec<u8>> {
227 pub fn open(path: impl AsRef<Path>) -> Result<Self> {
246 let path = path.as_ref();
247 let data = std::fs::read(path).map_err(|source| Error::IoAtPath {
248 operation: "read GSYM file",
249 path: path.to_path_buf(),
250 source,
251 })?;
252 Self::parse(data)
253 }
254}
255
256impl<D: AsRef<[u8]>> Gsym<D> {
257 pub fn parse(data: D) -> Result<Self> {
264 let layout = layout::parse(data.as_ref())?;
265 Ok(Self { data, layout })
266 }
267
268 #[must_use]
270 pub fn into_inner(self) -> D {
271 self.data
272 }
273
274 #[must_use]
276 pub fn header(&self) -> Header<'_> {
277 Header {
278 version: match self.layout.version {
279 VersionLayout::V1 => GsymVersion::V1,
280 VersionLayout::V2 => GsymVersion::V2,
281 },
282 endian: self.layout.endian,
283 address_offset_size: self.layout.address_offset_size,
284 base_address: self.layout.base_address,
285 address_count: self.layout.address_count,
286 build_id: self.build_id(),
287 }
288 }
289
290 #[must_use]
292 pub fn build_id(&self) -> &[u8] {
293 self.data
294 .as_ref()
295 .get(self.layout.build_id.clone())
296 .unwrap_or_default()
297 }
298
299 #[must_use]
301 pub const fn functions(&self) -> Functions<'_, D> {
302 Functions {
303 gsym: self,
304 next: 0,
305 }
306 }
307
308 pub fn function(&self, index: usize) -> Result<FunctionRef<'_>> {
315 self.get_function(index)?
316 .ok_or(Error::FunctionIndexOutOfBounds {
317 index,
318 count: self.layout.address_count as usize,
319 })
320 }
321
322 pub fn get_function(&self, index: usize) -> Result<Option<FunctionRef<'_>>> {
331 let Some(raw) = self.raw_function(index)? else {
332 return Ok(None);
333 };
334 Ok(Some(FunctionRef {
335 index,
336 name: self.string(raw.name)?,
337 all_data: self.data.as_ref(),
338 raw,
339 layout: &self.layout,
340 }))
341 }
342
343 pub(in crate::reader) fn raw_function(&self, index: usize) -> Result<Option<RawFunction<'_>>> {
344 if index >= self.layout.address_count as usize {
345 return Ok(None);
346 }
347 let start = self.address(index)?;
348 self.raw_function_at(index, start).map(Some)
349 }
350
351 #[inline]
358 pub(in crate::reader) fn raw_function_at(
359 &self,
360 index: usize,
361 start: u64,
362 ) -> Result<RawFunction<'_>> {
363 let offset = self.function_offset(index)?;
364 let section_end = self.layout.function_info.end;
365 if offset < self.layout.function_info.start || offset >= section_end {
366 return Err(Error::InvalidOffset {
367 offset: offset as u64,
368 input_len: self.data.as_ref().len(),
369 });
370 }
371 let data =
372 self.data
373 .as_ref()
374 .get(offset..section_end)
375 .ok_or_else(|| Error::InvalidOffset {
376 offset: offset as u64,
377 input_len: self.data.as_ref().len(),
378 })?;
379 let mut header = Cursor::new(data, self.layout.endian);
380 let size = header.read_u32()?;
381 let name_offset = header.read_uint(self.layout.string_offset_size)?;
382 if name_offset == 0 {
383 return Err(Error::ZeroNameOffset);
384 }
385 let end = start
386 .checked_add(u64::from(size))
387 .ok_or(Error::Overflow("function range"))?;
388 let raw = RawFunction {
389 range: AddressRange::new(start, end),
390 name: name_offset,
391 data,
392 records: data.get(header.position()..).ok_or(Error::InvalidFormat(
393 "function record header overruns its record",
394 ))?,
395 };
396 Ok(raw)
397 }
398
399 pub fn string(&self, offset: u64) -> Result<&[u8]> {
405 string_at(self.data.as_ref(), &self.layout.string_table, offset)
406 }
407
408 pub fn file(&self, index: impl Into<FileIndex>) -> Result<(&[u8], &[u8])> {
414 file_at(
415 self.data.as_ref(),
416 self.layout.endian,
417 self.layout.string_offset_size,
418 &self.layout.file_table,
419 self.layout.file_count,
420 &self.layout.string_table,
421 index.into(),
422 )
423 }
424
425 pub fn verify(&self) -> Result<VerifyReport> {
452 self.verify_with(|_, _| Ok(()))
453 }
454
455 pub(crate) fn decode_all_verified(&self) -> Result<(VerifyReport, Vec<crate::Function>)> {
456 let mut functions = Vec::with_capacity(self.layout.address_count as usize);
457 let report = self.verify_with(|reference, encoded| {
458 functions.push(owned::decode(reference, encoded)?);
459 Ok(())
460 })?;
461 Ok((report, functions))
462 }
463
464 fn verify_with(
465 &self,
466 mut visitor: impl FnMut(&FunctionRef<'_>, EncodedFunction) -> Result<()>,
467 ) -> Result<VerifyReport> {
468 if self
469 .data
470 .as_ref()
471 .get(self.layout.string_table.start)
472 .copied()
473 != Some(0)
474 {
475 return Err(Error::InvalidFormat(
476 "GSYM string table does not begin with an empty string",
477 ));
478 }
479 if self.layout.file_count > 0 {
480 let (directory, basename) = self.file(0_u32)?;
481 if !directory.is_empty() || !basename.is_empty() {
482 return Err(Error::InvalidFormat("file-table index zero must be empty"));
483 }
484 }
485 let mut previous = None;
486 for index in 0..self.layout.address_count as usize {
487 let address = self.address(index)?;
488 if previous.is_some_and(|value| address < value) {
489 return Err(Error::InvalidFormat("address table is not sorted"));
490 }
491 previous = Some(address);
492 let function = self.function(index)?;
493 let decoded = function.decode_encoded()?;
494 owned::validate(&function, &decoded)?;
495 visitor(&function, decoded)?;
496 }
497 for index in 0..self.layout.file_count {
498 let _ = self.file(index)?;
499 }
500 Ok(VerifyReport {
501 functions: self.layout.address_count as usize,
502 files: self.layout.file_count as usize,
503 strings: self
504 .data
505 .as_ref()
506 .get(self.layout.string_table.clone())
507 .unwrap_or_default()
508 .iter()
509 .fold(0_usize, |total, byte| {
510 total.saturating_add(usize::from(*byte == 0))
511 }),
512 function_info_bytes: self.layout.function_info.len(),
513 })
514 }
515
516 pub(super) fn address(&self, index: usize) -> Result<u64> {
521 let width = usize::from(self.layout.address_offset_size);
522 let offset = self
523 .layout
524 .address_offsets
525 .start
526 .checked_add(
527 index
528 .checked_mul(width)
529 .ok_or(Error::Overflow("address table index"))?,
530 )
531 .ok_or(Error::Overflow("address table offset"))?;
532 let mut cursor = Cursor::at(self.data.as_ref(), self.layout.endian, offset)?;
533 self.layout
534 .base_address
535 .checked_add(cursor.read_uint(self.layout.address_offset_size)?)
536 .ok_or(Error::Overflow("function address"))
537 }
538
539 fn function_offset(&self, index: usize) -> Result<usize> {
540 let width: u8 = match self.layout.version {
541 VersionLayout::V1 => 4,
542 VersionLayout::V2 => 8,
543 };
544 let offset = self
545 .layout
546 .address_info_offsets
547 .start
548 .checked_add(
549 index
550 .checked_mul(usize::from(width))
551 .ok_or(Error::Overflow("address-info table index"))?,
552 )
553 .ok_or(Error::Overflow("address-info table offset"))?;
554 let mut cursor = Cursor::at(self.data.as_ref(), self.layout.endian, offset)?;
555 let relative = cursor.read_uint(width)?;
556 let absolute = match self.layout.version {
557 VersionLayout::V1 => relative,
558 VersionLayout::V2 => relative
559 .checked_add(self.layout.function_info.start as u64)
560 .ok_or(Error::Overflow("FunctionInfo offset"))?,
561 };
562 usize::try_from(absolute).map_err(|_| Error::Overflow("FunctionInfo offset conversion"))
563 }
564
565 pub(super) fn find_address_index(&self, address: u64) -> Result<Option<usize>> {
566 if address < self.layout.base_address || self.layout.address_count == 0 {
567 return Ok(None);
568 }
569 let count = self.layout.address_count as usize;
570 let relative = address.saturating_sub(self.layout.base_address);
571 let entries = self
572 .data
573 .as_ref()
574 .get(self.layout.address_offsets.clone())
575 .ok_or_else(|| Error::InvalidOffset {
576 offset: self.layout.address_offsets.start as u64,
577 input_len: self.data.as_ref().len(),
578 })?;
579
580 let low = match (self.layout.address_offset_size, self.layout.endian) {
581 (1, _) => partition_point::<1>(entries, relative, |entry| u64::from(entry[0])),
582 (2, Endian::Little) => {
583 typed_partition_point::<U16<LittleEndian>>(entries, relative, |entry| {
584 u64::from(entry.get())
585 })?
586 }
587 (2, Endian::Big) => {
588 typed_partition_point::<U16<BigEndian>>(entries, relative, |entry| {
589 u64::from(entry.get())
590 })?
591 }
592 (4, Endian::Little) => {
593 typed_partition_point::<U32<LittleEndian>>(entries, relative, |entry| {
594 u64::from(entry.get())
595 })?
596 }
597 (4, Endian::Big) => {
598 typed_partition_point::<U32<BigEndian>>(entries, relative, |entry| {
599 u64::from(entry.get())
600 })?
601 }
602 (8, Endian::Little) => {
603 typed_partition_point::<U64<LittleEndian>>(entries, relative, |entry| entry.get())?
604 }
605 (8, Endian::Big) => {
606 typed_partition_point::<U64<BigEndian>>(entries, relative, |entry| entry.get())?
607 }
608 _ => {
609 let mut low = 0usize;
610 let mut high = count;
611 while low < high {
612 let middle = low.saturating_add(high.saturating_sub(low) / 2);
613 if self.address(middle)? <= address {
614 low = middle.saturating_add(1);
615 } else {
616 high = middle;
617 }
618 }
619 low
620 }
621 };
622 Ok(low.checked_sub(1))
623 }
624}
625
626#[inline]
627fn partition_point<const N: usize>(
628 entries: &[u8],
629 probe: u64,
630 decode: impl Fn([u8; N]) -> u64,
631) -> usize {
632 let (chunks, _) = entries.as_chunks::<N>();
633 chunks.partition_point(|entry| decode(*entry) <= probe)
634}
635
636#[inline]
637fn typed_partition_point<T>(entries: &[u8], probe: u64, decode: impl Fn(&T) -> u64) -> Result<usize>
638where
639 [T]: FromBytes + KnownLayout + Immutable,
640{
641 let entries = <[T]>::ref_from_bytes(entries)
642 .map_err(|_| Error::InvalidFormat("address table has an invalid typed layout"))?;
643 Ok(entries.partition_point(|entry| decode(entry) <= probe))
644}
645
646impl<D: AsRef<[u8]>> AsRef<[u8]> for Gsym<D> {
647 fn as_ref(&self) -> &[u8] {
648 self.data.as_ref()
649 }
650}
651
652#[cfg(test)]
653mod tests {
654 use crate::model::{AddressRange, FileEntry, Function, InlineNode};
655 use crate::{Error, GsymBuilder};
656
657 use super::Gsym;
658
659 #[test]
660 fn verification_rejects_a_non_empty_reserved_file_entry() {
661 let mut builder = GsymBuilder::new();
662 let _ = builder.add_file(FileEntry::new("/src", "main.c")).unwrap();
663 builder
664 .add_function(Function::new(AddressRange::new(0x1000, 0x1010), b"main"))
665 .unwrap();
666 let mut bytes = builder.to_bytes().unwrap();
667
668 let table = Gsym::parse(bytes.as_slice()).unwrap().layout.file_table;
669 let reserved = table.start.saturating_add(4);
670 let first = reserved.saturating_add(8);
671 bytes.copy_within(first..first.saturating_add(8), reserved);
672
673 assert!(matches!(
674 Gsym::parse(bytes.as_slice()).unwrap().verify(),
675 Err(Error::InvalidFormat("file-table index zero must be empty"))
676 ));
677 }
678
679 #[test]
680 fn verification_counts_stored_strings() {
681 let mut builder = GsymBuilder::new();
682 let _ = builder.add_file(FileEntry::new("/src", "main.c")).unwrap();
683 builder
684 .add_function(Function::new(AddressRange::new(0x1000, 0x1010), b"main"))
685 .unwrap();
686 builder
687 .add_function(Function::new(AddressRange::new(0x2000, 0x2010), b"helper"))
688 .unwrap();
689 let bytes = builder.to_bytes().unwrap();
690
691 let report = Gsym::parse(bytes.as_slice()).unwrap().verify().unwrap();
692 assert_eq!(report.functions, 2);
693 assert_eq!(report.files, 2);
694 assert_eq!(report.strings, 5);
695 }
696
697 #[test]
698 fn verification_and_owned_decode_reject_a_missing_inline_file() {
699 let range = AddressRange::new(0x1000, 0x1010);
700 let mut builder = GsymBuilder::new();
701 builder
702 .add_function(Function {
703 inline: Some(InlineNode {
704 ranges: vec![range],
705 name: b"inlined".to_vec(),
706 call_file: 0_u32.into(),
707 ..InlineNode::default()
708 }),
709 ..Function::new(range, b"outer")
710 })
711 .unwrap();
712 let mut bytes = builder.to_bytes().unwrap();
713 let function_offset = Gsym::parse(bytes.as_slice())
714 .unwrap()
715 .function_offset(0)
716 .unwrap();
717 let inline_payload = function_offset.saturating_add(16);
718 let call_file = inline_payload.saturating_add(8);
719 let Some(slot) = bytes.get_mut(call_file) else {
720 panic!("writer omitted the inline call-file field");
721 };
722 *slot = 2;
723
724 let gsym = Gsym::parse(bytes.as_slice()).unwrap();
725 assert!(gsym.verify().is_err());
726 assert!(gsym.function(0).unwrap().decode().is_err());
727 }
728}