Skip to main content

gsym/
transform.rs

1use std::fmt;
2
3use crate::{
4    Endian, Error, FileEntry, FileIndex, Function, Gsym, GsymBuilder, GsymVersion, InlineNode,
5    Result,
6};
7
8impl<D: AsRef<[u8]>> Gsym<D> {
9    /// Decodes every file and function into an owned semantic model.
10    ///
11    /// A record type this crate cannot represent is an error rather than a
12    /// silent drop.
13    ///
14    /// This decodes and validates the whole file, so it costs about as much as
15    /// [`Self::verify`].
16    ///
17    /// # Errors
18    ///
19    /// Returns the first structural, reference, or semantic decoding error.
20    pub fn decode_all(&self) -> Result<DecodedGsym> {
21        let (report, functions) = self.decode_all_verified()?;
22        let header = self.header();
23        let mut files = Vec::with_capacity(report.files.max(1));
24        if report.files == 0 {
25            files.push(FileEntry::default());
26        }
27        for index in 0..report.files {
28            let index = u32::try_from(index).map_err(|_| Error::Overflow("file index"))?;
29            let (directory, basename) = self.file(index)?;
30            files.push(FileEntry {
31                directory: directory.to_vec(),
32                basename: basename.to_vec(),
33            });
34        }
35        Ok(DecodedGsym {
36            source_version: header.version,
37            source_endian: header.endian,
38            base_address: header.base_address,
39            build_id: header.build_id.to_vec(),
40            files,
41            functions,
42        })
43    }
44
45    /// Re-encodes this file with a selected version or byte order.
46    ///
47    /// # Errors
48    ///
49    /// Returns an error if the input is malformed or the semantic data cannot
50    /// be represented by the requested output version.
51    pub fn transcode(&self, options: TranscodeOptions) -> Result<Vec<u8>> {
52        self.decode_all()?.transcode(options)
53    }
54}
55
56/// An owned, version-independent representation of a complete GSYM file.
57///
58/// Decode with [`Gsym::decode_all`](crate::Gsym::decode_all), edit the public
59/// semantic fields when needed, then use [`Self::into_builder`] or
60/// [`Self::transcode`] to move the model into a new encoding.
61///
62/// Decoding is all-or-nothing: a record type this crate cannot represent is an
63/// error rather than a silent drop. `source_version` and `source_endian` record
64/// what the input used, and [`TranscodeOptions`] overrides either one for the
65/// output.
66///
67/// File indices in the model refer to [`Self::files`], including the reserved
68/// empty entry at index zero. Re-encoding renumbers the table and keeps only
69/// the files the retained functions reference.
70#[derive(Eq, PartialEq)]
71pub struct DecodedGsym {
72    /// Version from which this model was decoded.
73    pub source_version: GsymVersion,
74    /// Byte order from which this model was decoded.
75    pub source_endian: Endian,
76    /// Image base address.
77    pub base_address: u64,
78    /// Opaque build identifier.
79    pub build_id: Vec<u8>,
80    /// Complete file table, including reserved index zero.
81    pub files: Vec<FileEntry>,
82    /// Fully decoded semantic functions.
83    pub functions: Vec<Function>,
84}
85
86impl fmt::Debug for DecodedGsym {
87    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
88        formatter
89            .debug_struct("DecodedGsym")
90            .field("source_version", &self.source_version)
91            .field("source_endian", &self.source_endian)
92            .field("base_address", &self.base_address)
93            .field("build_id_len", &self.build_id.len())
94            .field("file_count", &self.files.len())
95            .field("function_count", &self.functions.len())
96            .finish_non_exhaustive()
97    }
98}
99
100/// Output choices for semantic GSYM transcoding.
101///
102/// `Default` keeps both the version and the byte order of the input, which
103/// makes a transcode a pure re-encode. Setting a field converts that property.
104///
105/// ```
106/// use gsym::{Endian, GsymVersion, TranscodeOptions};
107///
108/// let keep_everything = TranscodeOptions::default();
109/// assert!(keep_everything.version.is_none());
110///
111/// let to_big_endian_v2 = TranscodeOptions {
112///     version: Some(GsymVersion::V2),
113///     endian: Some(Endian::Big),
114/// };
115/// # let _ = to_big_endian_v2;
116/// ```
117#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
118pub struct TranscodeOptions {
119    /// Preserve the input version when absent.
120    pub version: Option<GsymVersion>,
121    /// Preserve the input byte order when absent.
122    pub endian: Option<Endian>,
123}
124
125/// One independently readable shard of a segmented GSYM image.
126///
127/// Each segment is a complete GSYM file that [`Gsym::parse`](crate::Gsym::parse)
128/// accepts on its own, holding a contiguous span of the source file's functions
129/// and only the source files those functions reference. The address fields let
130/// a consumer pick the right shard for an address without opening it.
131///
132/// Produced by [`DecodedGsym::segments`].
133#[derive(Eq, PartialEq)]
134#[non_exhaustive]
135pub struct GsymSegment {
136    /// Lowest function start address in this segment.
137    pub first_address: u64,
138    /// Exclusive maximum function end address in this segment.
139    pub end_address: u64,
140    /// Number of top-level functions in this segment.
141    pub function_count: usize,
142    bytes: Box<[u8]>,
143}
144
145impl GsymSegment {
146    /// Returns the complete independently readable GSYM image.
147    #[must_use]
148    pub fn bytes(&self) -> &[u8] {
149        &self.bytes
150    }
151
152    /// Returns the owned GSYM image.
153    #[must_use]
154    pub fn into_bytes(self) -> Box<[u8]> {
155        self.bytes
156    }
157}
158
159impl fmt::Debug for GsymSegment {
160    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
161        formatter
162            .debug_struct("GsymSegment")
163            .field("first_address", &self.first_address)
164            .field("end_address", &self.end_address)
165            .field("function_count", &self.function_count)
166            .field("byte_len", &self.bytes.len())
167            .finish()
168    }
169}
170
171impl DecodedGsym {
172    /// Builds a deterministic writer model by cloning this decoded model.
173    ///
174    /// Use [`Self::into_builder`] when this model is no longer needed.
175    ///
176    /// # Errors
177    ///
178    /// Returns an error for invalid file references or model data.
179    pub fn to_builder(&self, options: TranscodeOptions) -> Result<GsymBuilder> {
180        self.builder_for_functions(self.functions.iter(), options)
181    }
182
183    /// Converts this decoded model into a deterministic writer without cloning
184    /// its file or function trees.
185    ///
186    /// # Errors
187    ///
188    /// Returns an error for invalid file references or model data.
189    pub fn into_builder(self, options: TranscodeOptions) -> Result<GsymBuilder> {
190        let Self {
191            source_version,
192            source_endian,
193            base_address,
194            build_id,
195            files,
196            functions,
197        } = self;
198        if files
199            .first()
200            .is_none_or(|file| *file != FileEntry::default())
201        {
202            return Err(Error::InvalidModel("file-table index zero must be empty"));
203        }
204        let mut used = vec![false; files.len()];
205        if let Some(zero) = used.first_mut() {
206            *zero = true;
207        }
208        for function in &functions {
209            mark_function_files(function, &mut used)?;
210        }
211        let mut builder = GsymBuilder::new()
212            .version(options.version.unwrap_or(source_version))
213            .endian(options.endian.unwrap_or(source_endian))
214            .base_address(base_address)
215            .build_id(build_id)
216            .repair_zero_sized_functions(false);
217        let mut remap = vec![FileIndex::ZERO; files.len()];
218        for (old, file) in files.into_iter().enumerate().skip(1) {
219            if used.get(old).copied().unwrap_or(false)
220                && let Some(slot) = remap.get_mut(old)
221            {
222                *slot = builder.add_file(file)?;
223            }
224        }
225        for mut function in functions {
226            remap_function_files(&mut function, &remap)?;
227            builder.add_function(function)?;
228        }
229        Ok(builder)
230    }
231
232    /// Encodes the complete model using the requested output settings.
233    ///
234    /// ```
235    /// use gsym::{
236    ///     AddressRange, Endian, Function, Gsym, GsymBuilder, GsymVersion,
237    ///     TranscodeOptions,
238    /// };
239    ///
240    /// let mut builder = GsymBuilder::new();
241    /// builder.add_function(Function::new(
242    ///     AddressRange::new(0x4000, 0x4010),
243    ///     b"transcoded",
244    /// ))?;
245    /// let source = builder.to_bytes()?;
246    /// let decoded = Gsym::parse(source)?.decode_all()?;
247    /// let output = decoded.transcode(TranscodeOptions {
248    ///     version: Some(GsymVersion::V2),
249    ///     endian: Some(Endian::Big),
250    /// })?;
251    ///
252    /// let reparsed = Gsym::parse(output)?;
253    /// assert_eq!(reparsed.header().version, GsymVersion::V2);
254    /// assert_eq!(reparsed.header().endian, Endian::Big);
255    /// # Ok::<(), gsym::Error>(())
256    /// ```
257    ///
258    /// # Errors
259    ///
260    /// Returns an error when the model cannot be represented by the requested
261    /// format version.
262    pub fn transcode(self, options: TranscodeOptions) -> Result<Vec<u8>> {
263        self.into_builder(options)?.to_bytes()
264    }
265
266    /// Splits the model into independently valid GSYM files near `target_size`.
267    ///
268    /// A single record larger than the target is emitted alone. Each shard
269    /// contains only source files referenced by its functions. Boundaries are
270    /// selected from exact encoded sizes, so all multi-function shards are at
271    /// most the requested target.
272    ///
273    /// Each shard covers a contiguous address span, so
274    /// [`GsymSegment::first_address`] and [`GsymSegment::end_address`] are
275    /// enough to route an address to a shard. Segmentation is considerably more
276    /// expensive than a single encode.
277    ///
278    /// # Errors
279    ///
280    /// Returns an error for a zero target, empty model, invalid references, or
281    /// an encoding failure.
282    pub fn segments(
283        &self,
284        target_size: usize,
285        options: TranscodeOptions,
286    ) -> Result<Vec<GsymSegment>> {
287        if target_size == 0 {
288            return Err(Error::InvalidModel("segment target size must not be zero"));
289        }
290        if self.functions.is_empty() {
291            return Err(Error::InvalidModel("at least one function is required"));
292        }
293        let mut functions = self.functions.iter().collect::<Vec<_>>();
294        functions.sort_by_key(|function| (function.range.start, function.range.end));
295        let mut segments = Vec::new();
296        let mut start = 0;
297        while start < functions.len() {
298            let minimum = start.saturating_add(1);
299            let mut low = minimum;
300            let mut high = functions.len();
301            let mut best = minimum;
302            let window = |end: usize| {
303                functions
304                    .get(start..end)
305                    .ok_or(Error::InvalidModel("segment partition is out of range"))
306            };
307            let mut best_bytes = self
308                .builder_for_functions(window(best)?.iter().copied(), options)?
309                .to_bytes()?;
310            while low <= high {
311                let middle = low.saturating_add(high.saturating_sub(low) / 2);
312                let bytes = self
313                    .builder_for_functions(window(middle)?.iter().copied(), options)?
314                    .to_bytes()?;
315                if bytes.len() <= target_size || middle == minimum {
316                    best = middle;
317                    best_bytes = bytes;
318                    low = middle.saturating_add(1);
319                } else {
320                    high = middle.saturating_sub(1);
321                }
322            }
323            let selected = window(best)?;
324            let first = selected
325                .first()
326                .ok_or(Error::InvalidModel("segment partition is empty"))?;
327            segments.push(GsymSegment {
328                first_address: first.range.start,
329                end_address: selected
330                    .iter()
331                    .map(|function| function.range.end)
332                    .max()
333                    .unwrap_or(first.range.end),
334                function_count: selected.len(),
335                bytes: best_bytes.into_boxed_slice(),
336            });
337            start = best;
338        }
339        Ok(segments)
340    }
341
342    fn builder_for_functions<'function>(
343        &self,
344        functions: impl Clone + IntoIterator<Item = &'function Function>,
345        options: TranscodeOptions,
346    ) -> Result<GsymBuilder> {
347        if self
348            .files
349            .first()
350            .is_none_or(|file| *file != FileEntry::default())
351        {
352            return Err(Error::InvalidModel("file-table index zero must be empty"));
353        }
354        let mut used = vec![false; self.files.len()];
355        if let Some(zero) = used.first_mut() {
356            *zero = true;
357        }
358        for function in functions.clone() {
359            mark_function_files(function, &mut used)?;
360        }
361        let mut builder = GsymBuilder::new()
362            .version(options.version.unwrap_or(self.source_version))
363            .endian(options.endian.unwrap_or(self.source_endian))
364            .base_address(self.base_address)
365            .build_id(self.build_id.clone())
366            .repair_zero_sized_functions(false);
367        let mut remap = vec![FileIndex::ZERO; self.files.len()];
368        for (old, file) in self.files.iter().enumerate().skip(1) {
369            if used.get(old).copied().unwrap_or(false)
370                && let Some(slot) = remap.get_mut(old)
371            {
372                *slot = builder.add_file(file.clone())?;
373            }
374        }
375        for function in functions {
376            let mut function = function.clone();
377            remap_function_files(&mut function, &remap)?;
378            builder.add_function(function)?;
379        }
380        Ok(builder)
381    }
382}
383
384fn mark_file(index: FileIndex, used: &mut [bool]) -> Result<()> {
385    let slot = used
386        .get_mut(index.get() as usize)
387        .ok_or(Error::InvalidModel("function references a missing file"))?;
388    *slot = true;
389    Ok(())
390}
391
392fn mark_inline_files(node: &InlineNode, used: &mut [bool]) -> Result<()> {
393    mark_file(node.call_file, used)?;
394    for child in &node.children {
395        mark_inline_files(child, used)?;
396    }
397    Ok(())
398}
399
400fn mark_function_files(function: &Function, used: &mut [bool]) -> Result<()> {
401    for line in &function.lines {
402        mark_file(line.file, used)?;
403    }
404    if let Some(inline) = &function.inline {
405        mark_inline_files(inline, used)?;
406    }
407    for merged in &function.merged {
408        mark_function_files(merged, used)?;
409    }
410    Ok(())
411}
412
413fn remap_file(index: &mut FileIndex, remap: &[FileIndex]) -> Result<()> {
414    *index = *remap
415        .get(index.get() as usize)
416        .ok_or(Error::InvalidModel("function references a missing file"))?;
417    Ok(())
418}
419
420fn remap_inline_files(node: &mut InlineNode, remap: &[FileIndex]) -> Result<()> {
421    remap_file(&mut node.call_file, remap)?;
422    for child in &mut node.children {
423        remap_inline_files(child, remap)?;
424    }
425    Ok(())
426}
427
428fn remap_function_files(function: &mut Function, remap: &[FileIndex]) -> Result<()> {
429    for line in &mut function.lines {
430        remap_file(&mut line.file, remap)?;
431    }
432    if let Some(inline) = &mut function.inline {
433        remap_inline_files(inline, remap)?;
434    }
435    for merged in &mut function.merged {
436        remap_function_files(merged, remap)?;
437    }
438    Ok(())
439}