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 NUL-terminated strings stored in the string table.
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            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    /// Reads the record at `index`, whose start address the caller already has.
352    ///
353    /// Lookup walks a run of functions that share a start address, so passing
354    /// it in saves re-decoding the same address-table entry per candidate.
355    ///
356    /// `index` must be below `address_count`.
357    #[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    /// Resolves a string-table offset to borrowed bytes.
400    ///
401    /// # Errors
402    ///
403    /// Returns an error for an out-of-bounds offset or missing NUL terminator.
404    pub fn string(&self, offset: u64) -> Result<&[u8]> {
405        string_at(self.data.as_ref(), &self.layout.string_table, offset)
406    }
407
408    /// Resolves a file-table index to borrowed directory and basename bytes.
409    ///
410    /// # Errors
411    ///
412    /// Returns an error for an invalid index or malformed string reference.
413    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    /// Fully verifies all indexed functions and their referenced metadata.
426    ///
427    /// Checks that the address table is sorted, that every function record
428    /// decodes, and that the strings, files, line programs, and inline ranges
429    /// they reference are in bounds and well formed. Cost is proportional to the
430    /// file, so this belongs at load time for an untrusted file, not in front of
431    /// each lookup.
432    ///
433    /// # Errors
434    ///
435    /// Returns the first structural or semantic validation error.
436    ///
437    /// ```
438    /// use gsym::{AddressRange, Function, Gsym, GsymBuilder};
439    ///
440    /// let mut builder = GsymBuilder::new();
441    /// builder.add_function(Function::new(
442    ///     AddressRange::new(0x1000, 0x1010),
443    ///     b"verified",
444    /// ))?;
445    /// let bytes = builder.to_bytes()?;
446    ///
447    /// let report = Gsym::parse(&bytes)?.verify()?;
448    /// assert_eq!(report.functions, 1);
449    /// # Ok::<(), gsym::Error>(())
450    /// ```
451    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    /// Reads one address-table entry.
517    ///
518    /// Single-entry reads use a cursor. Whole-table scans use typed slices in
519    /// [`Self::find_address_index`].
520    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}