Skip to main content

gsym/
builder.rs

1use std::fmt;
2use std::hash::{BuildHasher, RandomState};
3use std::io::Write;
4
5use hashbrown::HashTable;
6
7use crate::model::{AddressRange, FileEntry, FileIndex, Function};
8use crate::validation::validate_for_builder;
9use crate::writer::WriterOptions;
10use crate::{Error, GsymVersion, Result};
11
12/// Finalization policy used by [`GsymBuilder`].
13///
14/// [`Default`] enables `repair_zero_sized_functions` and disables
15/// `merge_equal_address_functions`, matching what `llvm-gsymutil` does without
16/// extra flags. The [`GsymBuilder`] setters change the same fields one at a
17/// time.
18#[derive(Clone, Eq, PartialEq)]
19pub struct BuilderOptions {
20    /// Wire-format settings used by the writer.
21    pub writer: WriterOptions,
22    /// Executable virtual-address ranges, used to reject stale DWARF and to
23    /// repair zero-sized symbol-table functions.
24    pub executable_ranges: Box<[AddressRange]>,
25    /// Extend the final zero-sized function to its containing executable range.
26    pub repair_zero_sized_functions: bool,
27    /// Preserve equal-range aliases as `MergedFunctionsInfo`. This mirrors
28    /// llvm-gsymutil's explicit merged-functions mode and is off by default.
29    pub merge_equal_address_functions: bool,
30}
31
32impl fmt::Debug for BuilderOptions {
33    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
34        formatter
35            .debug_struct("BuilderOptions")
36            .field("writer", &self.writer)
37            .field("executable_range_count", &self.executable_ranges.len())
38            .field(
39                "repair_zero_sized_functions",
40                &self.repair_zero_sized_functions,
41            )
42            .field(
43                "merge_equal_address_functions",
44                &self.merge_equal_address_functions,
45            )
46            .finish()
47    }
48}
49
50impl Default for BuilderOptions {
51    fn default() -> Self {
52        Self {
53            writer: WriterOptions::default(),
54            executable_ranges: Box::default(),
55            repair_zero_sized_functions: true,
56            merge_equal_address_functions: false,
57        }
58    }
59}
60
61/// Version-independent, deterministic GSYM construction API.
62///
63/// Add [`FileEntry`] values and [`Function`] records, then encode with
64/// [`Self::to_bytes`] or [`Self::write_to`]. The same inputs and options always
65/// produce the same bytes, so output can be compared or content-addressed.
66///
67/// Two things to know while building:
68///
69/// - [`Self::add_file`] interns entries. Adding the same directory and basename
70///   twice returns the same [`FileIndex`], and index zero is permanently the
71///   empty entry.
72/// - [`Self::add_function`] validates as it goes and rejects an empty name, a
73///   reversed range, or a line row outside the function. Cross-record checks
74///   that need the whole model, such as file references, run at encode time.
75///
76/// Functions may be added in any order. Setters take and return `self`, so they
77/// chain, while `add_file` and `add_function` take `&mut self`.
78///
79/// # Example
80///
81/// ```
82/// use gsym::{AddressRange, Function, Gsym, GsymBuilder};
83///
84/// let mut builder = GsymBuilder::new().base_address(0x1000);
85/// builder.add_function(Function::new(
86///     AddressRange::new(0x1010, 0x1020),
87///     b"example",
88/// ))?;
89///
90/// let bytes = builder.to_bytes()?;
91/// let gsym = Gsym::parse(bytes)?;
92/// assert_eq!(gsym.lookup(0x1014)?.unwrap().frames()[0].name, b"example");
93/// # Ok::<(), gsym::Error>(())
94/// ```
95pub struct GsymBuilder {
96    options: BuilderOptions,
97    files: Vec<FileEntry>,
98    file_index: HashTable<FileSlot>,
99    hasher: RandomState,
100    functions: Vec<Function>,
101}
102
103#[derive(Clone, Copy, Debug)]
104struct FileSlot {
105    index: u32,
106    hash: u64,
107}
108
109fn interned_file(files: &[FileEntry], index: u32) -> Option<&FileEntry> {
110    files.get(usize::try_from(index).ok()?)
111}
112
113impl fmt::Debug for GsymBuilder {
114    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
115        formatter
116            .debug_struct("GsymBuilder")
117            .field("options", &self.options)
118            .field("file_count", &self.files.len())
119            .field("function_count", &self.functions.len())
120            .finish_non_exhaustive()
121    }
122}
123
124impl Default for GsymBuilder {
125    fn default() -> Self {
126        Self::new()
127    }
128}
129
130impl GsymBuilder {
131    /// Creates an empty builder using default options.
132    #[must_use]
133    pub fn new() -> Self {
134        let hasher = RandomState::new();
135        let empty = FileEntry::default();
136        let slot = FileSlot {
137            index: 0,
138            hash: hasher.hash_one(&empty),
139        };
140        let mut file_index = HashTable::new();
141        file_index.insert_unique(slot.hash, slot, |slot| slot.hash);
142        Self {
143            options: BuilderOptions::default(),
144            files: vec![empty],
145            file_index,
146            hasher,
147            functions: Vec::new(),
148        }
149    }
150
151    /// Creates an empty builder using `options`.
152    #[must_use]
153    pub fn with_options(options: BuilderOptions) -> Self {
154        let mut builder = Self::new();
155        builder.options = options;
156        builder
157    }
158
159    /// Returns the active builder options.
160    #[must_use]
161    pub const fn options(&self) -> &BuilderOptions {
162        &self.options
163    }
164
165    /// Selects the output GSYM version.
166    ///
167    /// Defaults to [`GsymVersion::V1`], which current tooling reads. Selecting
168    /// [`GsymVersion::V2`] lifts v1's 4 GiB offset limits and 20-byte build-ID
169    /// limit but needs LLVM 23 or newer on the reading side. Encoding reports
170    /// v1 limit errors and does not change versions automatically.
171    #[must_use]
172    pub const fn version(mut self, version: GsymVersion) -> Self {
173        self.options.writer.version = version;
174        self
175    }
176
177    /// Selects the output byte order.
178    #[must_use]
179    pub const fn endian(mut self, endian: crate::Endian) -> Self {
180        self.options.writer.endian = endian;
181        self
182    }
183
184    /// Sets the image base address.
185    ///
186    /// Leave it unset to use the lowest function address, which is what a
187    /// standalone file wants. Set it to the base address of the image the data
188    /// came from, so lookups can use that image's virtual addresses.
189    #[must_use]
190    pub const fn base_address(mut self, address: u64) -> Self {
191        self.options.writer.base_address = Some(address);
192        self
193    }
194
195    /// Sets the opaque build identifier stored in the GSYM header.
196    #[must_use]
197    pub fn build_id(mut self, build_id: impl Into<Vec<u8>>) -> Self {
198        self.options.writer.build_id = build_id.into();
199        self
200    }
201
202    /// Enables or disables final zero-sized-function repair.
203    #[must_use]
204    pub const fn repair_zero_sized_functions(mut self, enabled: bool) -> Self {
205        self.options.repair_zero_sized_functions = enabled;
206        self
207    }
208
209    /// Enables or disables merged records for equal-address functions.
210    #[must_use]
211    pub const fn merge_equal_address_functions(mut self, enabled: bool) -> Self {
212        self.options.merge_equal_address_functions = enabled;
213        self
214    }
215
216    /// Replaces the executable ranges used for liveness and size repair.
217    #[must_use]
218    pub fn executable_ranges(mut self, ranges: impl IntoIterator<Item = AddressRange>) -> Self {
219        self.options.executable_ranges = ranges.into_iter().collect::<Vec<_>>().into_boxed_slice();
220        self
221    }
222
223    /// Intern a source file and return its stable one-based index. Index zero
224    /// is permanently reserved for the empty file.
225    ///
226    /// Repeated calls with an equal entry return the same index without adding
227    /// a row, so callers can intern per line row instead of maintaining their
228    /// own map.
229    ///
230    /// # Errors
231    ///
232    /// Returns an error if the table exceeds the GSYM `u32` index space.
233    ///
234    /// ```
235    /// use gsym::{FileEntry, GsymBuilder};
236    ///
237    /// let mut builder = GsymBuilder::new();
238    /// let first = builder.add_file(FileEntry::new(b"/src", b"main.rs"))?;
239    /// let again = builder.add_file(FileEntry::new(b"/src", b"main.rs"))?;
240    /// assert_eq!(first, again);
241    /// assert_eq!(builder.files().len(), 2); // reserved entry plus main.rs
242    /// # Ok::<(), gsym::Error>(())
243    /// ```
244    pub fn add_file(&mut self, file: FileEntry) -> Result<FileIndex> {
245        let hash = self.hasher.hash_one(&file);
246        if let Some(slot) = self.file_index.find(hash, |slot| {
247            interned_file(&self.files, slot.index) == Some(&file)
248        }) {
249            return Ok(FileIndex::new(slot.index));
250        }
251        let index = u32::try_from(self.files.len()).map_err(|_| Error::Limit {
252            context: "file table",
253            value: self.files.len() as u64,
254            limit: u64::from(u32::MAX),
255        })?;
256        self.files.push(file);
257        self.file_index
258            .insert_unique(hash, FileSlot { index, hash }, |slot| slot.hash);
259        Ok(FileIndex::new(index))
260    }
261
262    /// Adds a validated function record.
263    ///
264    /// Insertion order does not matter. Line rows must already be sorted within
265    /// the function, and inline ranges must nest inside their parent.
266    ///
267    /// # Errors
268    ///
269    /// Returns an error for an empty name, invalid range, oversized function,
270    /// or line outside the function range.
271    pub fn add_function(&mut self, function: Function) -> Result<()> {
272        validate_for_builder(&function)?;
273        self.functions.push(function);
274        Ok(())
275    }
276
277    /// Returns the interned file table, including reserved index zero.
278    #[must_use]
279    pub fn files(&self) -> &[FileEntry] {
280        &self.files
281    }
282
283    /// Returns functions in insertion order before writer finalization.
284    #[must_use]
285    pub fn functions(&self) -> &[Function] {
286        &self.functions
287    }
288
289    /// Encodes this builder into `output`.
290    ///
291    /// # Errors
292    ///
293    /// Returns an error when the model cannot be represented by the selected
294    /// GSYM version or when writing fails.
295    pub fn write_to(self, output: impl Write) -> Result<()> {
296        crate::writer::write_builder(self, output)
297    }
298
299    /// Encodes this builder into a byte vector.
300    ///
301    /// # Errors
302    ///
303    /// Returns an error when the model cannot be represented by the selected
304    /// GSYM version.
305    pub fn to_bytes(self) -> Result<Vec<u8>> {
306        crate::writer::encode_builder_to_bytes(self)
307    }
308
309    pub(crate) fn into_parts(self) -> (BuilderOptions, Vec<FileEntry>, Vec<Function>) {
310        (self.options, self.files, self.functions)
311    }
312}