Skip to main content

gsym/
transform.rs

1use std::fmt;
2
3use crate::format::function::{check_inline_depth, check_merged_depth};
4use crate::{
5    Endian, Error, FileEntry, FileIndex, Function, FunctionSetPolicy, Gsym, GsymBuilder,
6    GsymVersion, InlineNode, Result,
7};
8
9impl<D: AsRef<[u8]>> Gsym<D> {
10    /// Decodes every file and function into an owned semantic model.
11    ///
12    /// A record type this crate cannot represent is rejected to prevent a
13    /// lossy transformation.
14    ///
15    /// # Errors
16    ///
17    /// Returns the first structural, reference, or semantic decoding error,
18    /// including a file table whose reserved entry zero is not empty.
19    pub fn decode_all(&self) -> Result<DecodedGsym> {
20        let (report, functions) = self.decode_all_verified()?;
21        let header = self.header();
22        let mut files = Vec::with_capacity(report.files.max(1));
23        if report.files == 0 {
24            files.push(FileEntry::default());
25        }
26        for index in 0..report.files {
27            let index = u32::try_from(index).map_err(|_| Error::Overflow("file index"))?;
28            let (directory, basename) = self.file(index)?;
29            files.push(FileEntry {
30                directory: directory.to_vec(),
31                basename: basename.to_vec(),
32            });
33        }
34        Ok(DecodedGsym {
35            source_version: header.version,
36            source_endian: header.endian,
37            base_address: header.base_address,
38            build_id: header.build_id.to_vec(),
39            files,
40            functions,
41        })
42    }
43
44    /// Re-encodes this file with a selected version or byte order.
45    ///
46    /// # Errors
47    ///
48    /// Returns an error if the input is malformed or the semantic data cannot
49    /// be represented by the requested output version.
50    pub fn transcode(&self, options: TranscodeOptions) -> Result<Vec<u8>> {
51        self.decode_all()?.transcode(options)
52    }
53}
54
55/// An owned, version-independent representation of a complete GSYM file.
56///
57/// Decode with [`Gsym::decode_all`](crate::Gsym::decode_all), edit the public
58/// semantic fields when needed, then use [`Self::into_builder`] or
59/// [`Self::transcode`] to move the model into a new encoding.
60///
61/// A `FunctionInfo` record of a type this crate does not model makes decoding
62/// fail rather than silently disappearing during re-encoding. `source_version`
63/// and `source_endian` record what the input used, and [`TranscodeOptions`]
64/// overrides either one for the output.
65///
66/// File indices in the model refer to [`Self::files`], including the reserved
67/// empty entry at index zero. Re-encoding renumbers the table and keeps only
68/// the files the retained functions reference.
69#[derive(Eq, PartialEq)]
70pub struct DecodedGsym {
71    /// Version from which this model was decoded.
72    pub source_version: GsymVersion,
73    /// Byte order from which this model was decoded.
74    pub source_endian: Endian,
75    /// Image base address.
76    pub base_address: u64,
77    /// Opaque build identifier.
78    pub build_id: Vec<u8>,
79    /// Complete file table, including reserved index zero.
80    pub files: Vec<FileEntry>,
81    /// Fully decoded semantic functions.
82    pub functions: Vec<Function>,
83}
84
85impl fmt::Debug for DecodedGsym {
86    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
87        formatter
88            .debug_struct("DecodedGsym")
89            .field("source_version", &self.source_version)
90            .field("source_endian", &self.source_endian)
91            .field("base_address", &self.base_address)
92            .field("build_id_len", &self.build_id.len())
93            .field("file_count", &self.files.len())
94            .field("function_count", &self.functions.len())
95            .finish_non_exhaustive()
96    }
97}
98
99/// Output choices for semantic GSYM transcoding.
100///
101/// `Default` keeps both the version and the byte order of the input, which
102/// makes a transcode a pure re-encode. Setting a field converts that property.
103///
104/// ```
105/// use gsym::{Endian, GsymVersion, TranscodeOptions};
106///
107/// let keep_everything = TranscodeOptions::default();
108/// assert!(keep_everything.version.is_none());
109///
110/// let to_big_endian_v2 = TranscodeOptions {
111///     version: Some(GsymVersion::V2),
112///     endian: Some(Endian::Big),
113/// };
114/// # let _ = to_big_endian_v2;
115/// ```
116#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)]
117pub struct TranscodeOptions {
118    /// Preserve the input version when absent.
119    pub version: Option<GsymVersion>,
120    /// Preserve the input byte order when absent.
121    pub endian: Option<Endian>,
122}
123
124/// One independently readable shard of a segmented GSYM image.
125///
126/// Each segment is a complete GSYM file that [`Gsym::parse`](crate::Gsym::parse)
127/// accepts on its own, holding a contiguous span of the source file's functions
128/// and only the source files those functions reference. The address fields let
129/// a consumer pick the right shard for an address without opening it.
130///
131/// Produced by [`DecodedGsym::segments`].
132#[derive(Eq, PartialEq)]
133#[non_exhaustive]
134pub struct GsymSegment {
135    /// Lowest function start address in this segment.
136    pub first_address: u64,
137    /// Exclusive end of this segment's span, equal to the next segment's
138    /// [`first_address`](Self::first_address).
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        let used = used_files(&files, &functions)?;
199        let mut builder = new_builder(
200            source_version,
201            source_endian,
202            base_address,
203            build_id,
204            options,
205        );
206        let mut remap = vec![FileIndex::ZERO; files.len()];
207        for (old, file) in files.into_iter().enumerate().skip(1) {
208            if used.get(old).copied().unwrap_or(false)
209                && let Some(slot) = remap.get_mut(old)
210            {
211                *slot = builder.add_file(file)?;
212            }
213        }
214        for mut function in functions {
215            remap_function_files(&mut function, &remap)?;
216            builder.add_function(function)?;
217        }
218        Ok(builder)
219    }
220
221    /// Encodes the complete model using the requested output settings.
222    ///
223    /// ```
224    /// use gsym::{
225    ///     AddressRange, Endian, Function, Gsym, GsymBuilder, GsymVersion,
226    ///     TranscodeOptions,
227    /// };
228    ///
229    /// let mut builder = GsymBuilder::new();
230    /// builder.add_function(Function::new(
231    ///     AddressRange::new(0x4000, 0x4010),
232    ///     b"transcoded",
233    /// ))?;
234    /// let source = builder.to_bytes()?;
235    /// let decoded = Gsym::parse(source)?.decode_all()?;
236    /// let output = decoded.transcode(TranscodeOptions {
237    ///     version: Some(GsymVersion::V2),
238    ///     endian: Some(Endian::Big),
239    /// })?;
240    ///
241    /// let reparsed = Gsym::parse(output)?;
242    /// assert_eq!(reparsed.header().version, GsymVersion::V2);
243    /// assert_eq!(reparsed.header().endian, Endian::Big);
244    /// # Ok::<(), gsym::Error>(())
245    /// ```
246    ///
247    /// # Errors
248    ///
249    /// Returns an error when the model cannot be represented by the requested
250    /// format version.
251    pub fn transcode(self, options: TranscodeOptions) -> Result<Vec<u8>> {
252        self.into_builder(options)?.to_bytes()
253    }
254
255    /// Splits the model into independently valid GSYM files near `target_size`.
256    ///
257    /// A single record larger than the target is emitted alone. Each shard
258    /// contains only source files referenced by its functions. Boundaries are
259    /// selected from exact encoded sizes, so all multi-function shards are at
260    /// most the requested target.
261    ///
262    /// Shards are ordered by address and their `[first_address, end_address)`
263    /// spans tile the covered range without a gap, so
264    /// [`GsymSegment::first_address`] and [`GsymSegment::end_address`] are
265    /// enough to route an address to a shard: the shard whose span holds an
266    /// address is the shard that resolves it, and an address below the first
267    /// shard resolves nowhere, exactly as in the unsplit file. Segmentation is
268    /// considerably more expensive than a single encode.
269    ///
270    /// # Errors
271    ///
272    /// Returns an error for a zero target, empty model, invalid references, or
273    /// an encoding failure.
274    pub fn segments(
275        &self,
276        target_size: usize,
277        options: TranscodeOptions,
278    ) -> Result<Vec<GsymSegment>> {
279        if target_size == 0 {
280            return Err(Error::InvalidModel("segment target size must not be zero"));
281        }
282        if self.functions.is_empty() {
283            return Err(Error::InvalidModel("at least one function is required"));
284        }
285        let mut functions = self.functions.iter().collect::<Vec<_>>();
286        functions.sort_by_key(|function| (function.range.start, function.range.end));
287        let mut segments = Vec::new();
288        let mut start = 0;
289        while start < functions.len() {
290            let minimum = start.saturating_add(1);
291            let mut best = minimum;
292            let window = |end: usize| {
293                functions
294                    .get(start..end)
295                    .ok_or(Error::InvalidModel("segment partition is out of range"))
296            };
297            let mut best_bytes = self
298                .builder_for_functions(window(best)?.iter().copied(), options)?
299                .to_bytes()?;
300            let mut ceiling = functions.len().saturating_add(1);
301            let mut span = 1_usize;
302            while best < functions.len() {
303                let candidate = minimum.saturating_add(span).min(functions.len());
304                if candidate <= best {
305                    break;
306                }
307                let bytes = self
308                    .builder_for_functions(window(candidate)?.iter().copied(), options)?
309                    .to_bytes()?;
310                if bytes.len() > target_size {
311                    ceiling = candidate;
312                    break;
313                }
314                best = candidate;
315                best_bytes = bytes;
316                span = span.saturating_mul(2);
317            }
318            let mut low = best.saturating_add(1);
319            let mut high = ceiling.saturating_sub(1);
320            while low <= high && high <= functions.len() {
321                let middle = low.saturating_add(high.saturating_sub(low) / 2);
322                let bytes = self
323                    .builder_for_functions(window(middle)?.iter().copied(), options)?
324                    .to_bytes()?;
325                if bytes.len() <= target_size {
326                    best = middle;
327                    best_bytes = bytes;
328                    low = middle.saturating_add(1);
329                } else {
330                    high = middle.saturating_sub(1);
331                }
332            }
333            let selected = window(best)?;
334            let first = selected
335                .first()
336                .ok_or(Error::InvalidModel("segment partition is empty"))?;
337            let last = selected
338                .last()
339                .ok_or(Error::InvalidModel("segment partition is empty"))?;
340            segments.push(GsymSegment {
341                first_address: first.range.start,
342                end_address: match functions.get(best) {
343                    Some(next) => next.range.start,
344                    None if last.range.is_empty() => u64::MAX,
345                    None => selected
346                        .iter()
347                        .map(|function| function.range.end)
348                        .max()
349                        .unwrap_or(first.range.end),
350                },
351                function_count: selected.len(),
352                bytes: best_bytes.into_boxed_slice(),
353            });
354            start = best;
355        }
356        Ok(segments)
357    }
358
359    fn builder_for_functions<'function>(
360        &self,
361        functions: impl Clone + IntoIterator<Item = &'function Function>,
362        options: TranscodeOptions,
363    ) -> Result<GsymBuilder> {
364        let used = used_files(&self.files, functions.clone())?;
365        let mut builder = new_builder(
366            self.source_version,
367            self.source_endian,
368            self.base_address,
369            self.build_id.clone(),
370            options,
371        );
372        let mut remap = vec![FileIndex::ZERO; self.files.len()];
373        for (old, file) in self.files.iter().enumerate().skip(1) {
374            if used.get(old).copied().unwrap_or(false)
375                && let Some(slot) = remap.get_mut(old)
376            {
377                *slot = builder.add_file(file.clone())?;
378            }
379        }
380        for function in functions {
381            let mut function = function.clone();
382            remap_function_files(&mut function, &remap)?;
383            builder.add_function(function)?;
384        }
385        Ok(builder)
386    }
387}
388
389fn used_files<'function>(
390    files: &[FileEntry],
391    functions: impl IntoIterator<Item = &'function Function>,
392) -> Result<Vec<bool>> {
393    if files
394        .first()
395        .is_none_or(|file| *file != FileEntry::default())
396    {
397        return Err(Error::InvalidModel("file-table index zero must be empty"));
398    }
399    let mut used = vec![false; files.len()];
400    if let Some(zero) = used.first_mut() {
401        *zero = true;
402    }
403    for function in functions {
404        mark_function_files(function, &mut used)?;
405    }
406    Ok(used)
407}
408
409fn new_builder(
410    source_version: GsymVersion,
411    source_endian: Endian,
412    base_address: u64,
413    build_id: Vec<u8>,
414    options: TranscodeOptions,
415) -> GsymBuilder {
416    GsymBuilder::new()
417        .version(options.version.unwrap_or(source_version))
418        .endian(options.endian.unwrap_or(source_endian))
419        .base_address(base_address)
420        .build_id(build_id)
421        .repair_zero_sized_functions(false)
422        .function_set(FunctionSetPolicy::Preserve)
423}
424
425fn mark_file(index: FileIndex, used: &mut [bool]) -> Result<()> {
426    let slot = used
427        .get_mut(index.get() as usize)
428        .ok_or(Error::InvalidModel("function references a missing file"))?;
429    *slot = true;
430    Ok(())
431}
432
433fn mark_inline_files(node: &InlineNode, used: &mut [bool], depth: usize) -> Result<()> {
434    check_inline_depth(depth)?;
435    mark_file(node.call_file, used)?;
436    for child in &node.children {
437        mark_inline_files(child, used, depth.saturating_add(1))?;
438    }
439    Ok(())
440}
441
442fn mark_function_files(function: &Function, used: &mut [bool]) -> Result<()> {
443    mark_function_files_at(function, used, 0)
444}
445
446fn mark_function_files_at(function: &Function, used: &mut [bool], depth: usize) -> Result<()> {
447    check_merged_depth(depth)?;
448    for line in &function.lines {
449        mark_file(line.file, used)?;
450    }
451    if let Some(inline) = &function.inline {
452        mark_inline_files(inline, used, 0)?;
453    }
454    for merged in &function.merged {
455        mark_function_files_at(merged, used, depth.saturating_add(1))?;
456    }
457    Ok(())
458}
459
460fn remap_file(index: &mut FileIndex, remap: &[FileIndex]) -> Result<()> {
461    *index = *remap
462        .get(index.get() as usize)
463        .ok_or(Error::InvalidModel("function references a missing file"))?;
464    Ok(())
465}
466
467fn remap_inline_files(node: &mut InlineNode, remap: &[FileIndex], depth: usize) -> Result<()> {
468    check_inline_depth(depth)?;
469    remap_file(&mut node.call_file, remap)?;
470    for child in &mut node.children {
471        remap_inline_files(child, remap, depth.saturating_add(1))?;
472    }
473    Ok(())
474}
475
476fn remap_function_files(function: &mut Function, remap: &[FileIndex]) -> Result<()> {
477    remap_function_files_at(function, remap, 0)
478}
479
480fn remap_function_files_at(
481    function: &mut Function,
482    remap: &[FileIndex],
483    depth: usize,
484) -> Result<()> {
485    check_merged_depth(depth)?;
486    for line in &mut function.lines {
487        remap_file(&mut line.file, remap)?;
488    }
489    if let Some(inline) = &mut function.inline {
490        remap_inline_files(inline, remap, 0)?;
491    }
492    for merged in &mut function.merged {
493        remap_function_files_at(merged, remap, depth.saturating_add(1))?;
494    }
495    Ok(())
496}