Skip to content

Commit eb0521d

Browse files
authored
Merge pull request #22 from getsentry/hermes-sourcemaps
feat: Support Hermes (react-native) SourceMaps
2 parents a086b92 + a63ff30 commit eb0521d

File tree

19 files changed

+297
-28
lines changed

19 files changed

+297
-28
lines changed

.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,3 @@
1+
.DS_Store
12
target
23
Cargo.lock

examples/read.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ fn load_from_reader<R: Read>(mut rdr: R) -> SourceMap {
1313
..Default::default()
1414
})
1515
.unwrap(),
16+
_ => panic!("unexpected sourcemap format"),
1617
}
1718
}
1819

examples/rewrite.rs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ fn load_from_reader<R: Read>(mut rdr: R) -> SourceMap {
3030
..Default::default()
3131
})
3232
.unwrap(),
33+
_ => panic!("unexpected sourcemap format"),
3334
}
3435
}
3536

examples/split_ram_bundle.rs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ source files and their sourcemaps.
1616
Both indexed and file RAM bundles are supported.
1717
";
1818

19-
fn main() -> Result<(), Box<std::error::Error>> {
19+
fn main() -> Result<(), Box<dyn std::error::Error>> {
2020
let args: Vec<_> = env::args().collect();
2121
if args.len() < 4 {
2222
println!("{}", USAGE);

src/decoder.rs

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ use serde_json;
66
use serde_json::Value;
77

88
use crate::errors::{Error, Result};
9+
use crate::hermes::decode_hermes;
910
use crate::jsontypes::RawSourceMap;
1011
use crate::types::{DecodedMap, RawToken, SourceMap, SourceMapIndex, SourceMapSection};
1112
use crate::vlq::parse_vlq_segment;
@@ -121,7 +122,7 @@ pub fn strip_junk_header(slice: &[u8]) -> io::Result<&[u8]> {
121122
Ok(&slice[slice.len()..])
122123
}
123124

124-
fn decode_regular(rsm: RawSourceMap) -> Result<SourceMap> {
125+
pub fn decode_regular(rsm: RawSourceMap) -> Result<SourceMap> {
125126
let mut dst_col;
126127
let mut src_id = 0;
127128
let mut src_line = 0;
@@ -267,6 +268,8 @@ fn decode_index(rsm: RawSourceMap) -> Result<SourceMapIndex> {
267268
fn decode_common(rsm: RawSourceMap) -> Result<DecodedMap> {
268269
Ok(if rsm.sections.is_some() {
269270
DecodedMap::Index(decode_index(rsm)?)
271+
} else if rsm.x_facebook_sources.is_some() {
272+
DecodedMap::Hermes(decode_hermes(rsm)?)
270273
} else {
271274
DecodedMap::Regular(decode_regular(rsm)?)
272275
})

src/encoder.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ impl Encodable for SourceMap {
9494
mappings: Some(serialize_mappings(self)),
9595
x_facebook_offsets: None,
9696
x_metro_module_paths: None,
97+
x_facebook_sources: None,
9798
}
9899
}
99100
}
@@ -124,6 +125,7 @@ impl Encodable for SourceMapIndex {
124125
mappings: None,
125126
x_facebook_offsets: None,
126127
x_metro_module_paths: None,
128+
x_facebook_sources: None,
127129
}
128130
}
129131
}
@@ -133,6 +135,7 @@ impl Encodable for DecodedMap {
133135
match *self {
134136
DecodedMap::Regular(ref sm) => sm.as_raw_sourcemap(),
135137
DecodedMap::Index(ref smi) => smi.as_raw_sourcemap(),
138+
DecodedMap::Hermes(ref smh) => smh.as_raw_sourcemap(),
136139
}
137140
}
138141
}

src/errors.rs

Lines changed: 4 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -31,12 +31,8 @@ pub enum Error {
3131
BadSourceReference(u32),
3232
/// a reference to a non existing name was encountered
3333
BadNameReference(u32),
34-
/// Indicates that an indexed sourcemap was encountered when
35-
/// a regular sourcemap was expected
36-
IndexedSourcemap,
37-
/// Indicates that an regular (non-indexed) sourcemap was when
38-
/// a sourcemap index was expected
39-
RegularSourcemap,
34+
/// Indicates that an incompatible sourcemap format was encountered
35+
IncompatibleSourceMap,
4036
/// Indicates an invalid data URL
4137
InvalidDataUrl,
4238
/// Flatten failed
@@ -97,8 +93,7 @@ impl error::Error for Error {
9793
BadSegmentSize(_) => "bad segment size",
9894
BadSourceReference(_) => "bad source reference",
9995
BadNameReference(_) => "bad name reference",
100-
IndexedSourcemap => "unexpected indexed sourcemap",
101-
RegularSourcemap => "unexpected sourcemap",
96+
IncompatibleSourceMap => "incompatible sourcemap",
10297
InvalidDataUrl => "invalid data URL",
10398
CannotFlatten(_) => "cannot flatten the given indexed sourcemap",
10499
InvalidRamBundleMagic => "invalid magic number for ram bundle",
@@ -133,11 +128,7 @@ impl fmt::Display for Error {
133128
BadSegmentSize(size) => write!(f, "got {} segments, expected 4 or 5", size),
134129
BadSourceReference(id) => write!(f, "bad reference to source #{}", id),
135130
BadNameReference(id) => write!(f, "bad reference to name #{}", id),
136-
IndexedSourcemap => write!(f, "encountered unexpected indexed sourcemap"),
137-
RegularSourcemap => write!(
138-
f,
139-
"encountered unexpected sourcemap where index was expected"
140-
),
131+
IncompatibleSourceMap => write!(f, "encountered incompatible sourcemap format"),
141132
InvalidDataUrl => write!(f, "the provided data URL is invalid"),
142133
CannotFlatten(ref msg) => write!(f, "cannot flatten the indexed sourcemap: {}", msg),
143134
InvalidRamBundleMagic => write!(f, "invalid magic number for ram bundle"),

src/hermes.rs

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

src/jsontypes.rs

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,18 @@ pub struct RawSection {
1515
pub map: Option<Box<RawSourceMap>>,
1616
}
1717

18+
#[derive(Serialize, Deserialize, Clone)]
19+
pub struct FacebookScopeMapping {
20+
pub names: Vec<String>,
21+
pub mappings: String,
22+
}
23+
24+
// Each element here is matching the `sources` of the outer SourceMap.
25+
// It has a list of metadata, the first one of which is a *function map*,
26+
// containing scope information as a nested source map.
27+
// See the decoder in `hermes.rs` for details.
28+
pub type FacebookSources = Option<Vec<Option<Vec<FacebookScopeMapping>>>>;
29+
1830
#[derive(Serialize, Deserialize)]
1931
pub struct RawSourceMap {
2032
pub version: Option<u32>,
@@ -35,6 +47,8 @@ pub struct RawSourceMap {
3547
pub x_facebook_offsets: Option<Vec<Option<u32>>>,
3648
#[serde(skip_serializing_if = "Option::is_none")]
3749
pub x_metro_module_paths: Option<Vec<String>>,
50+
#[serde(skip_serializing_if = "Option::is_none")]
51+
pub x_facebook_sources: FacebookSources,
3852
}
3953

4054
#[derive(Deserialize)]

src/lib.rs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -55,10 +55,11 @@ pub use crate::detector::{
5555
SourceMapRef,
5656
};
5757
pub use crate::errors::{Error, Result};
58+
pub use crate::hermes::SourceMapHermes;
5859
pub use crate::sourceview::SourceView;
5960
pub use crate::types::{
60-
DecodedMap, RawToken, RewriteOptions, SourceMap, SourceMapIndex, SourceMapSection,
61-
SourceMapSectionIter, Token, TokenIter,
61+
DecodedMap, IndexIter, NameIter, RawToken, RewriteOptions, SourceContentsIter, SourceIter,
62+
SourceMap, SourceMapIndex, SourceMapSection, SourceMapSectionIter, Token, TokenIter,
6263
};
6364
pub use crate::utils::make_relative_path;
6465

@@ -67,6 +68,7 @@ mod decoder;
6768
mod detector;
6869
mod encoder;
6970
mod errors;
71+
mod hermes;
7072
mod jsontypes;
7173
mod sourceview;
7274
mod types;

0 commit comments

Comments
 (0)