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}