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}