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