Skip to main content

gsym/
reader.rs

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/// Borrowed metadata from a parsed GSYM header.
25///
26/// The fields are normalized across versions, so the same struct describes a v1
27/// and a v2 file.
28///
29/// Returned by [`Gsym::header`].
30#[derive(Clone, Copy, Debug, Eq, PartialEq)]
31#[non_exhaustive]
32pub struct Header<'data> {
33    /// Format version.
34    pub version: GsymVersion,
35    /// Byte order used for fixed-width integers.
36    pub endian: Endian,
37    /// Width in bytes of each address-table entry.
38    pub address_offset_size: u8,
39    /// Base address added to every address-table entry.
40    pub base_address: u64,
41    /// Number of functions in the file.
42    pub address_count: u32,
43    /// Opaque build identifier borrowed from the input.
44    pub build_id: &'data [u8],
45}
46
47/// Controls which optional records an allocating lookup inspects.
48///
49/// Every field defaults to `true`, which is what [`Gsym::lookup`] uses. Turning
50/// a field off skips that work entirely, so a name-only lookup costs less than
51/// a full one.
52///
53/// ```
54/// use gsym::LookupOptions;
55///
56/// let names_only = LookupOptions {
57///     line_information: false,
58///     inline_frames: false,
59///     call_sites: false,
60/// };
61/// assert!(LookupOptions::default().line_information);
62/// assert!(!names_only.line_information);
63/// ```
64#[derive(Clone, Copy, Debug, Eq, PartialEq)]
65pub struct LookupOptions {
66    /// Resolve source files and lines.
67    pub line_information: bool,
68    /// Resolve inline-call frames.
69    pub inline_frames: bool,
70    /// Resolve matching call-site patterns.
71    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/// Controls which optional records frame visitation inspects.
85///
86/// The same as [`LookupOptions`] without `call_sites`, which
87/// [`Gsym::for_each_frame`] does not report. Converting to [`LookupOptions`]
88/// leaves call sites disabled.
89#[derive(Clone, Copy, Debug, Eq, PartialEq)]
90pub struct FrameLookupOptions {
91    /// Resolve source files and lines.
92    pub line_information: bool,
93    /// Resolve inline-call frames.
94    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/// Reusable buffer for allocation-free address lookup.
117///
118/// Reusing one scratch across lookups avoids allocating per address. It carries
119/// no state between calls, so one per thread is enough.
120///
121/// Used by [`Gsym::lookup_with_options`] and [`Gsym::for_each_frame`].
122#[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    /// Creates scratch storage preallocated for `inline_depth` nested frames.
137    #[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/// Aggregate counts produced by full-file verification.
150///
151/// Returned by [`Gsym::verify`], which checks every function in the file and
152/// everything it references.
153#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
154#[non_exhaustive]
155pub struct VerifyReport {
156    /// Number of address-table entries verified.
157    pub functions: usize,
158    /// Number of file-table entries verified.
159    pub files: usize,
160    /// Number of strings checked.
161    pub strings: usize,
162    /// Total `FunctionInfo` section size in bytes.
163    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
174/// Parsed GSYM data backed by caller-selected byte storage.
175///
176/// `D` may be a borrowed slice or an owned type such as `Vec<u8>`,
177/// `Box<[u8]>`, or `Arc<[u8]>`, and none of them is copied. With the `mmap`
178/// feature, `MappedGsym` is this same type over a read-only memory map.
179///
180/// Construct one with [`Gsym::open`] for a path or [`Gsym::parse`] for bytes.
181/// Both reject an unsupported version, a truncated file, or a section outside
182/// the input. A malformed function record is reported when it is read, so
183/// [`Gsym::verify`] is the way to check a whole file up front.
184///
185/// Lookups take `&self` and keep no interior state, so a reader can be shared
186/// between threads whenever its storage is `Sync`.
187///
188/// # Borrowed and owned storage
189///
190/// ```
191/// use gsym::{AddressRange, Function, Gsym, GsymBuilder};
192///
193/// let mut builder = GsymBuilder::new();
194/// builder.add_function(Function::new(
195///     AddressRange::new(0x2000, 0x2010),
196///     b"borrowed",
197/// ))?;
198/// let bytes = builder.to_bytes()?;
199///
200/// let borrowed = Gsym::parse(bytes.as_slice())?;
201/// assert_eq!(borrowed.as_ref().as_ptr(), bytes.as_ptr());
202///
203/// let owned = Gsym::parse(bytes)?;
204/// assert_eq!(owned.lookup(0x2000)?.unwrap().frames()[0].name, b"borrowed");
205/// # Ok::<(), gsym::Error>(())
206/// ```
207pub 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    /// Opens a GSYM file as an owned, immutable byte snapshot.
228    ///
229    /// This is the safe filesystem entry point. Use
230    /// `MappedGsym::map` when demand paging is worth the file-stability
231    /// contract of a memory map.
232    ///
233    /// # Errors
234    ///
235    /// Returns a contextual I/O error when the file cannot be read, or a
236    /// format error when its GSYM metadata is invalid.
237    ///
238    /// ```no_run
239    /// use gsym::Gsym;
240    ///
241    /// let gsym = Gsym::open("app.gsym")?;
242    /// let symbol = gsym.lookup(0x401000)?;
243    /// # Ok::<(), gsym::Error>(())
244    /// ```
245    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    /// Parses and validates the top-level GSYM tables without copying them.
258    ///
259    /// # Errors
260    ///
261    /// Returns an error for an unsupported version, truncated input, invalid
262    /// table layout, or out-of-bounds section.
263    pub fn parse(data: D) -> Result<Self> {
264        let layout = layout::parse(data.as_ref())?;
265        Ok(Self { data, layout })
266    }
267
268    /// Returns the caller-provided byte storage.
269    #[must_use]
270    pub fn into_inner(self) -> D {
271        self.data
272    }
273
274    /// Returns decoded header metadata.
275    #[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    /// Returns the opaque build identifier, or an empty slice when absent.
291    #[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    /// Iterates all indexed functions in address-table order.
300    #[must_use]
301    pub const fn functions(&self) -> Functions<'_, D> {
302        Functions {
303            gsym: self,
304            next: 0,
305        }
306    }
307
308    /// Returns a borrowed function record by address-table index.
309    ///
310    /// # Errors
311    ///
312    /// Returns [`Error::FunctionIndexOutOfBounds`] when `index` does not exist,
313    /// or a format error when its address or function header is malformed.
314    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    /// Optionally returns a borrowed function record by address-table index.
323    ///
324    /// This is the non-erroring bounds-checking counterpart to [`Self::function`].
325    ///
326    /// # Errors
327    ///
328    /// Returns a format error if the indexed address or function header is
329    /// malformed. An out-of-bounds index returns `Ok(None)`.
330    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            endian: self.layout.endian,
340            string_offset_size: self.layout.string_offset_size,
341            string_table: self.layout.string_table.clone(),
342            file_table: self.layout.file_table.clone(),
343            file_count: self.layout.file_count,
344        }))
345    }
346
347    pub(in crate::reader) fn raw_function(&self, index: usize) -> Result<Option<RawFunction<'_>>> {
348        if index >= self.layout.address_count as usize {
349            return Ok(None);
350        }
351        let start = self.address(index)?;
352        self.raw_function_at(index, start).map(Some)
353    }
354
355    /// Reads the record at `index`, whose start address the caller already has.
356    ///
357    /// Lookup walks a run of functions that share a start address, so passing
358    /// it in saves re-decoding the same address-table entry per candidate.
359    ///
360    /// `index` must be below `address_count`.
361    #[inline]
362    pub(in crate::reader) fn raw_function_at(
363        &self,
364        index: usize,
365        start: u64,
366    ) -> Result<RawFunction<'_>> {
367        let offset = self.function_offset(index)?;
368        let section_end = self.layout.function_info.end;
369        if offset < self.layout.function_info.start || offset >= section_end {
370            return Err(Error::InvalidOffset {
371                offset: offset as u64,
372                input_len: self.data.as_ref().len(),
373            });
374        }
375        let data =
376            self.data
377                .as_ref()
378                .get(offset..section_end)
379                .ok_or_else(|| Error::InvalidOffset {
380                    offset: offset as u64,
381                    input_len: self.data.as_ref().len(),
382                })?;
383        let mut header = Cursor::new(data, self.layout.endian);
384        let size = header.read_u32()?;
385        let name_offset = header.read_uint(self.layout.string_offset_size)?;
386        if name_offset == 0 {
387            return Err(Error::ZeroNameOffset);
388        }
389        let end = start
390            .checked_add(u64::from(size))
391            .ok_or(Error::Overflow("function range"))?;
392        let raw = RawFunction {
393            range: AddressRange::new(start, end),
394            name: name_offset,
395            data,
396            records: data.get(header.position()..).ok_or(Error::InvalidFormat(
397                "function record header overruns its record",
398            ))?,
399        };
400        Ok(raw)
401    }
402
403    /// Resolves a string-table offset to borrowed bytes.
404    ///
405    /// # Errors
406    ///
407    /// Returns an error for an out-of-bounds offset or missing NUL terminator.
408    pub fn string(&self, offset: u64) -> Result<&[u8]> {
409        string_at(self.data.as_ref(), &self.layout.string_table, offset)
410    }
411
412    /// Resolves a file-table index to borrowed directory and basename bytes.
413    ///
414    /// # Errors
415    ///
416    /// Returns an error for an invalid index or malformed string reference.
417    pub fn file(&self, index: impl Into<FileIndex>) -> Result<(&[u8], &[u8])> {
418        file_at(
419            self.data.as_ref(),
420            self.layout.endian,
421            self.layout.string_offset_size,
422            &self.layout.file_table,
423            self.layout.file_count,
424            &self.layout.string_table,
425            index.into(),
426        )
427    }
428
429    /// Fully verifies all indexed functions and their referenced metadata.
430    ///
431    /// Checks that the address table is sorted, that every function record
432    /// decodes, and that the strings, files, line programs, and inline ranges
433    /// they reference are in bounds and well formed. Cost is proportional to the
434    /// file, so this belongs at load time for input of unknown provenance
435    /// rather than in front of each lookup.
436    ///
437    /// # Errors
438    ///
439    /// Returns the first structural or semantic validation error.
440    ///
441    /// ```
442    /// use gsym::{AddressRange, Function, Gsym, GsymBuilder};
443    ///
444    /// let mut builder = GsymBuilder::new();
445    /// builder.add_function(Function::new(
446    ///     AddressRange::new(0x1000, 0x1010),
447    ///     b"verified",
448    /// ))?;
449    /// let bytes = builder.to_bytes()?;
450    ///
451    /// let report = Gsym::parse(&bytes)?.verify()?;
452    /// assert_eq!(report.functions, 1);
453    /// # Ok::<(), gsym::Error>(())
454    /// ```
455    pub fn verify(&self) -> Result<VerifyReport> {
456        self.verify_with(|_, _| Ok(()))
457    }
458
459    pub(crate) fn decode_all_verified(&self) -> Result<(VerifyReport, Vec<crate::Function>)> {
460        let mut functions = Vec::with_capacity(self.layout.address_count as usize);
461        let report = self.verify_with(|reference, encoded| {
462            functions.push(owned::decode(reference, encoded)?);
463            Ok(())
464        })?;
465        Ok((report, functions))
466    }
467
468    fn verify_with(
469        &self,
470        mut visitor: impl FnMut(&FunctionRef<'_>, EncodedFunction) -> Result<()>,
471    ) -> Result<VerifyReport> {
472        if self
473            .data
474            .as_ref()
475            .get(self.layout.string_table.start)
476            .copied()
477            != Some(0)
478        {
479            return Err(Error::InvalidFormat(
480                "GSYM string table does not begin with an empty string",
481            ));
482        }
483        let mut previous = None;
484        for index in 0..self.layout.address_count as usize {
485            let address = self.address(index)?;
486            if previous.is_some_and(|value| address < value) {
487                return Err(Error::InvalidFormat("address table is not sorted"));
488            }
489            previous = Some(address);
490            let function = self.function(index)?;
491            let decoded = function.decode_encoded()?;
492            owned::validate(self, &decoded)?;
493            visitor(&function, decoded)?;
494        }
495        for index in 0..self.layout.file_count {
496            let _ = self.file(index)?;
497        }
498        Ok(VerifyReport {
499            functions: self.layout.address_count as usize,
500            files: self.layout.file_count as usize,
501            strings: self
502                .data
503                .as_ref()
504                .get(self.layout.string_table.clone())
505                .unwrap_or_default()
506                .iter()
507                .fold(0_usize, |total, byte| {
508                    total.saturating_add(usize::from(*byte == 0))
509                }),
510            function_info_bytes: self.layout.function_info.len(),
511        })
512    }
513
514    /// Reads one address-table entry.
515    ///
516    /// Single-entry reads use a cursor. Whole-table scans use typed slices in
517    /// [`Self::find_address_index`].
518    pub(super) fn address(&self, index: usize) -> Result<u64> {
519        let width = usize::from(self.layout.address_offset_size);
520        let offset = self
521            .layout
522            .address_offsets
523            .start
524            .checked_add(
525                index
526                    .checked_mul(width)
527                    .ok_or(Error::Overflow("address table index"))?,
528            )
529            .ok_or(Error::Overflow("address table offset"))?;
530        let mut cursor = Cursor::at(self.data.as_ref(), self.layout.endian, offset)?;
531        self.layout
532            .base_address
533            .checked_add(cursor.read_uint(self.layout.address_offset_size)?)
534            .ok_or(Error::Overflow("function address"))
535    }
536
537    fn function_offset(&self, index: usize) -> Result<usize> {
538        let width: u8 = match self.layout.version {
539            VersionLayout::V1 => 4,
540            VersionLayout::V2 => 8,
541        };
542        let offset = self
543            .layout
544            .address_info_offsets
545            .start
546            .checked_add(
547                index
548                    .checked_mul(usize::from(width))
549                    .ok_or(Error::Overflow("address-info table index"))?,
550            )
551            .ok_or(Error::Overflow("address-info table offset"))?;
552        let mut cursor = Cursor::at(self.data.as_ref(), self.layout.endian, offset)?;
553        let relative = cursor.read_uint(width)?;
554        let absolute = match self.layout.version {
555            VersionLayout::V1 => relative,
556            VersionLayout::V2 => relative
557                .checked_add(self.layout.function_info.start as u64)
558                .ok_or(Error::Overflow("FunctionInfo offset"))?,
559        };
560        usize::try_from(absolute).map_err(|_| Error::Overflow("FunctionInfo offset conversion"))
561    }
562
563    pub(super) fn find_address_index(&self, address: u64) -> Result<Option<usize>> {
564        if address < self.layout.base_address || self.layout.address_count == 0 {
565            return Ok(None);
566        }
567        let count = self.layout.address_count as usize;
568        // An entry that would overflow `base + entry` also exceeds
569        // `address - base`, so comparing relative addresses preserves order.
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 mut low = 0usize;
633    let mut high = entries.len().checked_div(N).unwrap_or(0);
634    while low < high {
635        let middle = low.saturating_add(high.saturating_sub(low) / 2);
636        let start = middle.saturating_mul(N);
637        // The caller supplies exactly `count * N` bytes, so a missing chunk is
638        // unreachable.
639        let value = entries
640            .get(start..)
641            .and_then(<[u8]>::first_chunk::<N>)
642            .map_or(u64::MAX, |entry| decode(*entry));
643        if value <= probe {
644            low = middle.saturating_add(1);
645        } else {
646            high = middle;
647        }
648    }
649    low
650}
651
652#[inline]
653fn typed_partition_point<T>(entries: &[u8], probe: u64, decode: impl Fn(&T) -> u64) -> Result<usize>
654where
655    [T]: FromBytes + KnownLayout + Immutable,
656{
657    let entries = <[T]>::ref_from_bytes(entries)
658        .map_err(|_| Error::InvalidFormat("address table has an invalid typed layout"))?;
659    Ok(entries.partition_point(|entry| decode(entry) <= probe))
660}
661
662impl<D: AsRef<[u8]>> AsRef<[u8]> for Gsym<D> {
663    fn as_ref(&self) -> &[u8] {
664        self.data.as_ref()
665    }
666}