symbolic_sourcemap/
lib.rs

1//! Provides sourcemap support.
2
3#![warn(missing_docs)]
4
5use std::borrow::Cow;
6use std::fmt;
7use std::ops::Deref;
8
9#[cfg(test)]
10use similar_asserts::assert_eq;
11
12/// An error returned when parsing source maps.
13#[derive(Debug)]
14pub struct ParseSourceMapError(sourcemap::Error);
15
16impl fmt::Display for ParseSourceMapError {
17    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
18        match self.0 {
19            sourcemap::Error::Io(..) => write!(f, "sourcemap parsing failed with io error"),
20            sourcemap::Error::Utf8(..) => write!(f, "sourcemap parsing failed due to bad utf-8"),
21            sourcemap::Error::BadJson(..) => write!(f, "invalid json data on sourcemap parsing"),
22            ref other => write!(f, "{}", other),
23        }
24    }
25}
26
27impl std::error::Error for ParseSourceMapError {
28    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
29        Some(match self.0 {
30            sourcemap::Error::Io(ref err) => err,
31            sourcemap::Error::Utf8(ref err) => err,
32            sourcemap::Error::BadJson(ref err) => err,
33            _ => return None,
34        })
35    }
36}
37
38impl From<sourcemap::Error> for ParseSourceMapError {
39    fn from(error: sourcemap::Error) -> ParseSourceMapError {
40        ParseSourceMapError(error)
41    }
42}
43
44/// Represents JS source code.
45pub struct SourceView<'a> {
46    sv: sourcemap::SourceView<'a>,
47}
48
49enum SourceMapType {
50    Regular(sourcemap::SourceMap),
51    Hermes(sourcemap::SourceMapHermes),
52}
53
54impl Deref for SourceMapType {
55    type Target = sourcemap::SourceMap;
56
57    fn deref(&self) -> &Self::Target {
58        match self {
59            SourceMapType::Regular(sm) => sm,
60            SourceMapType::Hermes(smh) => smh,
61        }
62    }
63}
64
65/// Represents a source map.
66pub struct SourceMapView {
67    sm: SourceMapType,
68}
69
70/// A matched token.
71#[derive(Debug, Default, PartialEq)]
72pub struct TokenMatch<'a> {
73    /// The line number in the original source file.
74    pub src_line: u32,
75    /// The column number in the original source file.
76    pub src_col: u32,
77    /// The column number in the minifid source file.
78    pub dst_line: u32,
79    /// The column number in the minified source file.
80    pub dst_col: u32,
81    /// The source ID of the token.
82    pub src_id: u32,
83    /// The token name, if present.
84    pub name: Option<&'a str>,
85    /// The source.
86    pub src: Option<&'a str>,
87    /// The name of the function containing the token.
88    pub function_name: Option<String>,
89}
90
91impl<'a> SourceView<'a> {
92    /// Creates a view from a string.
93    pub fn new(source: &'a str) -> Self {
94        SourceView {
95            sv: sourcemap::SourceView::new(source),
96        }
97    }
98
99    /// Creates a view from a string.
100    pub fn from_string(source: String) -> Self {
101        SourceView {
102            sv: sourcemap::SourceView::from_string(source),
103        }
104    }
105
106    /// Creates a soruce view from bytes ignoring utf-8 errors.
107    pub fn from_slice(source: &'a [u8]) -> Self {
108        match String::from_utf8_lossy(source) {
109            Cow::Owned(s) => SourceView::from_string(s),
110            Cow::Borrowed(s) => SourceView::new(s),
111        }
112    }
113
114    /// Returns the embedded source a string.
115    pub fn as_str(&self) -> &str {
116        self.sv.source()
117    }
118
119    /// Returns a specific line.
120    pub fn get_line(&self, idx: u32) -> Option<&str> {
121        self.sv.get_line(idx)
122    }
123
124    /// Returns the number of lines.
125    pub fn line_count(&self) -> usize {
126        self.sv.line_count()
127    }
128}
129
130impl SourceMapView {
131    /// Constructs a sourcemap from a slice.
132    ///
133    /// If the sourcemap is an index it is being flattened.  If flattening
134    /// is not possible then an error is raised.
135    pub fn from_json_slice(buffer: &[u8]) -> Result<Self, ParseSourceMapError> {
136        Ok(SourceMapView {
137            sm: match sourcemap::decode_slice(buffer)? {
138                sourcemap::DecodedMap::Regular(sm) => SourceMapType::Regular(sm),
139                sourcemap::DecodedMap::Index(smi) => SourceMapType::Regular(smi.flatten()?),
140                sourcemap::DecodedMap::Hermes(smh) => SourceMapType::Hermes(smh),
141            },
142        })
143    }
144
145    /// Looks up a token and returns it.
146    pub fn lookup_token(&self, line: u32, col: u32) -> Option<TokenMatch<'_>> {
147        self.sm
148            .lookup_token(line, col)
149            .map(|tok| self.make_token_match(tok))
150    }
151
152    /// Returns a token for a specific index.
153    pub fn get_token(&self, idx: u32) -> Option<TokenMatch<'_>> {
154        self.sm.get_token(idx).map(|tok| self.make_token_match(tok))
155    }
156
157    /// Returns the number of tokens.
158    pub fn get_token_count(&self) -> u32 {
159        self.sm.get_token_count()
160    }
161
162    /// Returns a source view for the given source.
163    pub fn get_source_view(&self, idx: u32) -> Option<&SourceView<'_>> {
164        self.sm
165            .get_source_view(idx)
166            .map(|s| unsafe { &*(s as *const _ as *const SourceView<'_>) })
167    }
168
169    /// Returns the source name for an index.
170    pub fn get_source_name(&self, idx: u32) -> Option<&str> {
171        self.sm.get_source(idx)
172    }
173
174    /// Returns the number of sources.
175    pub fn get_source_count(&self) -> u32 {
176        self.sm.get_source_count()
177    }
178
179    /// Looks up a token and the original function name.
180    ///
181    /// This is similar to `lookup_token` but if a minified function name and
182    /// the sourceview to the minified source is available this function will
183    /// also resolve the original function name.  This is used to fully
184    /// resolve tracebacks.
185    pub fn lookup_token_with_function_name<'a, 'b>(
186        &'a self,
187        line: u32,
188        col: u32,
189        minified_name: &str,
190        source: &SourceView<'b>,
191    ) -> Option<TokenMatch<'a>> {
192        match &self.sm {
193            // Instead of regular line/column pairs, Hermes uses bytecode offsets, which always
194            // have `line == 0`.
195            // However, a `SourceMapHermes` is defined by having `x_facebook_sources` scope
196            // information, which can actually be used without Hermes.
197            // So if our stack frame has `line > 0` (0-based), it is extremely likely we don’t run
198            // on hermes at all, in which case just fall back to regular sourcemap logic.
199            // Luckily, `metro` puts a prelude on line 0,
200            // so regular non-hermes user code should always have `line > 0`.
201            SourceMapType::Hermes(smh) if line == 0 => {
202                // we use `col + 1` here, since hermes uses bytecode offsets which are 0-based,
203                // and the upstream python code does a `- 1` here:
204                // https://github.com/getsentry/sentry/blob/fdabccac7576c80674c2fed556d4c5407657dc4c/src/sentry/lang/javascript/processor.py#L584-L586
205                smh.lookup_token(line, col + 1).map(|token| {
206                    let mut rv = self.make_token_match(token);
207                    rv.function_name = smh.get_original_function_name(col + 1).map(str::to_owned);
208                    rv
209                })
210            }
211            _ => self.sm.lookup_token(line, col).map(|token| {
212                let mut rv = self.make_token_match(token);
213                rv.function_name = source
214                    .sv
215                    .get_original_function_name(token, minified_name)
216                    .map(str::to_owned);
217                rv
218            }),
219        }
220    }
221
222    fn make_token_match<'a>(&'a self, tok: sourcemap::Token<'a>) -> TokenMatch<'a> {
223        TokenMatch {
224            src_line: tok.get_src_line(),
225            src_col: tok.get_src_col(),
226            dst_line: tok.get_dst_line(),
227            dst_col: tok.get_dst_col(),
228            src_id: tok.get_src_id(),
229            name: tok.get_name(),
230            src: tok.get_source(),
231            function_name: None,
232        }
233    }
234}
235
236#[test]
237fn test_react_native_hermes() {
238    let bytes = include_bytes!("../tests/fixtures/react-native-hermes.map");
239    let smv = SourceMapView::from_json_slice(bytes).unwrap();
240    let sv = SourceView::new("");
241
242    //    at foo (address at unknown:1:11939)
243    assert_eq!(
244        smv.lookup_token_with_function_name(0, 11939, "", &sv),
245        Some(TokenMatch {
246            src_line: 1,
247            src_col: 10,
248            dst_line: 0,
249            dst_col: 11939,
250            src_id: 5,
251            name: None,
252            src: Some("module.js"),
253            function_name: Some("foo".into())
254        })
255    );
256
257    //    at anonymous (address at unknown:1:11857)
258    assert_eq!(
259        smv.lookup_token_with_function_name(0, 11857, "", &sv),
260        Some(TokenMatch {
261            src_line: 2,
262            src_col: 0,
263            dst_line: 0,
264            dst_col: 11857,
265            src_id: 4,
266            name: None,
267            src: Some("input.js"),
268            function_name: Some("<global>".into())
269        })
270    );
271}
272
273#[test]
274fn test_react_native_metro() {
275    let source = include_str!("../tests/fixtures/react-native-metro.js");
276    let bytes = include_bytes!("../tests/fixtures/react-native-metro.js.map");
277    let smv = SourceMapView::from_json_slice(bytes).unwrap();
278    let sv = SourceView::new(source);
279
280    //    e.foo (react-native-metro.js:7:101)
281    assert_eq!(
282        smv.lookup_token_with_function_name(6, 100, "e.foo", &sv),
283        Some(TokenMatch {
284            src_line: 1,
285            src_col: 10,
286            dst_line: 6,
287            dst_col: 100,
288            src_id: 6,
289            name: None,
290            src: Some("module.js"),
291            function_name: None,
292        })
293    );
294
295    //    at react-native-metro.js:6:44
296    assert_eq!(
297        smv.lookup_token_with_function_name(5, 43, "", &sv),
298        Some(TokenMatch {
299            src_line: 2,
300            src_col: 0,
301            dst_line: 5,
302            dst_col: 39,
303            src_id: 5,
304            name: Some("foo"),
305            src: Some("input.js"),
306            function_name: None,
307        })
308    );
309
310    // in case we have a `metro` bundle, but a `hermes` bytecode offset (something out of range),
311    // we can’t resolve this.
312    assert_eq!(smv.lookup_token_with_function_name(0, 11857, "", &sv), None);
313}