sourcemap/
hermes.rs

1use crate::decoder::{decode, decode_regular, decode_slice};
2use crate::encoder::{encode, Encodable};
3use crate::errors::{Error, Result};
4use crate::jsontypes::{FacebookScopeMapping, FacebookSources, RawSourceMap};
5use crate::types::{DecodedMap, RewriteOptions, SourceMap};
6use crate::utils::greatest_lower_bound;
7use crate::vlq::parse_vlq_segment_into;
8use crate::Token;
9use std::io::{Read, Write};
10use std::ops::{Deref, DerefMut};
11
12/// These are starting locations of scopes.
13/// The `name_index` represents the index into the `HermesFunctionMap.names` vec,
14/// which represents the function names/scopes.
15#[derive(Debug, Clone, PartialEq)]
16pub struct HermesScopeOffset {
17    line: u32,
18    column: u32,
19    name_index: u32,
20}
21
22#[derive(Debug, Clone, PartialEq)]
23pub struct HermesFunctionMap {
24    names: Vec<String>,
25    mappings: Vec<HermesScopeOffset>,
26}
27
28/// Represents a `react-native`-style SourceMap, which has additional scope
29/// information embedded.
30#[derive(Debug, Clone, PartialEq)]
31pub struct SourceMapHermes {
32    pub(crate) sm: SourceMap,
33    // There should be one `HermesFunctionMap` per each `sources` entry in the main SourceMap.
34    function_maps: Vec<Option<HermesFunctionMap>>,
35    // XXX: right now, I am too lazy to actually serialize the above `function_maps`
36    // back into json types, so just keep the original json. Might be a bit inefficient, but meh.
37    raw_facebook_sources: FacebookSources,
38}
39
40impl Deref for SourceMapHermes {
41    type Target = SourceMap;
42
43    fn deref(&self) -> &Self::Target {
44        &self.sm
45    }
46}
47
48impl DerefMut for SourceMapHermes {
49    fn deref_mut(&mut self) -> &mut Self::Target {
50        &mut self.sm
51    }
52}
53
54impl Encodable for SourceMapHermes {
55    fn as_raw_sourcemap(&self) -> RawSourceMap {
56        // TODO: need to serialize the `HermesFunctionMap` mappings
57        let mut rsm = self.sm.as_raw_sourcemap();
58        rsm.x_facebook_sources
59            .clone_from(&self.raw_facebook_sources);
60        rsm
61    }
62}
63
64impl SourceMapHermes {
65    /// Creates a sourcemap from a reader over a JSON stream in UTF-8
66    /// format.
67    ///
68    /// See [`SourceMap::from_reader`](struct.SourceMap.html#method.from_reader)
69    pub fn from_reader<R: Read>(rdr: R) -> Result<Self> {
70        match decode(rdr)? {
71            DecodedMap::Hermes(sm) => Ok(sm),
72            _ => Err(Error::IncompatibleSourceMap),
73        }
74    }
75
76    /// Creates a sourcemap from a reader over a JSON byte slice in UTF-8
77    /// format.
78    ///
79    /// See [`SourceMap::from_slice`](struct.SourceMap.html#method.from_slice)
80    pub fn from_slice(slice: &[u8]) -> Result<Self> {
81        match decode_slice(slice)? {
82            DecodedMap::Hermes(sm) => Ok(sm),
83            _ => Err(Error::IncompatibleSourceMap),
84        }
85    }
86
87    /// Writes a sourcemap into a writer.
88    ///
89    /// See [`SourceMap::to_writer`](struct.SourceMap.html#method.to_writer)
90    pub fn to_writer<W: Write>(&self, w: W) -> Result<()> {
91        encode(self, w)
92    }
93
94    /// Given a bytecode offset, this will find the enclosing scopes function
95    /// name.
96    pub fn get_original_function_name(&self, bytecode_offset: u32) -> Option<&str> {
97        let token = self.sm.lookup_token(0, bytecode_offset)?;
98
99        self.get_scope_for_token(token)
100    }
101
102    /// Resolves the name of the enclosing function for the given [`Token`].
103    pub fn get_scope_for_token(&self, token: Token) -> Option<&str> {
104        let function_map = self
105            .function_maps
106            .get(token.get_src_id() as usize)?
107            .as_ref()?;
108
109        // Find the closest mapping, just like here:
110        // https://github.com/facebook/metro/blob/63b523eb20e7bdf62018aeaf195bb5a3a1a67f36/packages/metro-symbolicate/src/SourceMetadataMapConsumer.js#L204-L231
111        // Mappings use 1-based index for lines, and 0-based index for cols, as seen here:
112        // https://github.com/facebook/metro/blob/f2d80cebe66d3c64742f67259f41da26e83a0d8d/packages/metro/src/Server/symbolicate.js#L58-L60
113        let (_mapping_idx, mapping) = greatest_lower_bound(
114            &function_map.mappings,
115            &(token.get_src_line() + 1, token.get_src_col()),
116            |o| (o.line, o.column),
117        )?;
118        function_map
119            .names
120            .get(mapping.name_index as usize)
121            .map(|n| n.as_str())
122    }
123
124    /// This rewrites the sourcemap according to the provided rewrite
125    /// options.
126    ///
127    /// See [`SourceMap::rewrite`](struct.SourceMap.html#method.rewrite)
128    pub fn rewrite(self, options: &RewriteOptions<'_>) -> Result<Self> {
129        let Self {
130            sm,
131            mut function_maps,
132            mut raw_facebook_sources,
133        } = self;
134
135        let (sm, mapping) = sm.rewrite_with_mapping(options)?;
136
137        if function_maps.len() >= mapping.len() {
138            function_maps = mapping
139                .iter()
140                .map(|idx| function_maps[*idx as usize].take())
141                .collect();
142            raw_facebook_sources = raw_facebook_sources.map(|mut sources| {
143                mapping
144                    .into_iter()
145                    .map(|idx| sources[idx as usize].take())
146                    .collect()
147            });
148        }
149
150        Ok(Self {
151            sm,
152            function_maps,
153            raw_facebook_sources,
154        })
155    }
156}
157
158pub fn decode_hermes(mut rsm: RawSourceMap) -> Result<SourceMapHermes> {
159    let x_facebook_sources = rsm
160        .x_facebook_sources
161        .take()
162        .ok_or(Error::IncompatibleSourceMap)?;
163
164    // This is basically the logic from here:
165    // https://github.com/facebook/metro/blob/63b523eb20e7bdf62018aeaf195bb5a3a1a67f36/packages/metro-symbolicate/src/SourceMetadataMapConsumer.js#L182-L202
166
167    let mut nums = Vec::with_capacity(4);
168
169    let function_maps = x_facebook_sources
170        .iter()
171        .map(|v| {
172            let FacebookScopeMapping {
173                names,
174                mappings: raw_mappings,
175            } = v.as_ref()?.iter().next()?;
176
177            let mut mappings = vec![];
178            let mut line = 1;
179            let mut name_index = 0;
180
181            for line_mapping in raw_mappings.split(';') {
182                if line_mapping.is_empty() {
183                    continue;
184                }
185
186                let mut column = 0;
187
188                for mapping in line_mapping.split(',') {
189                    if mapping.is_empty() {
190                        continue;
191                    }
192
193                    nums.clear();
194                    parse_vlq_segment_into(mapping, &mut nums).ok()?;
195                    let mut nums = nums.iter().copied();
196
197                    column = (i64::from(column) + nums.next()?) as u32;
198                    name_index = (i64::from(name_index) + nums.next().unwrap_or(0)) as u32;
199                    line = (i64::from(line) + nums.next().unwrap_or(0)) as u32;
200                    mappings.push(HermesScopeOffset {
201                        column,
202                        line,
203                        name_index,
204                    });
205                }
206            }
207            Some(HermesFunctionMap {
208                names: names.clone(),
209                mappings,
210            })
211        })
212        .collect();
213
214    let sm = decode_regular(rsm)?;
215    Ok(SourceMapHermes {
216        sm,
217        function_maps,
218        raw_facebook_sources: Some(x_facebook_sources),
219    })
220}