1use std::cmp::Ordering;
2
3extern crate serde;
4extern crate serde_json;
5
6use base64_vlq;
7
8static SOURCE_MAP_VERSION: u32 = 3;
9
10#[allow(non_snake_case)]
11#[derive(Deserialize, Debug)]
12struct SourceMap {
13 version: u32,
14 sources: Vec<String>,
15 names: Vec<String>,
16 sourceRoot: Option<String>,
17 mappings: String,
18 file: Option<String>
19
20 }
24
25#[derive(Clone, Eq, PartialEq, Debug)]
26pub struct CodePosition {
27 pub line: u32,
29 pub column: u32
31}
32
33#[derive(Clone, Eq, PartialEq, Debug)]
34pub struct Mapping {
35 pub generated: CodePosition,
37 pub original: CodePosition,
39 pub source: String,
41 pub name: String
43}
44
45#[derive(Debug)]
46pub struct Cache {
47 generated_mappings: Vec<Mapping>,
48 pub source_root: String
50}
51
52pub fn consume(source_map_json: &str) -> Result<Cache, String> {
82 let source_map: SourceMap = match serde_json::from_str(source_map_json) {
83 Ok(x) => x,
84 Err(err) => return Err(format!("{}", err))
85 };
86
87 parse_mappings(&source_map)
88}
89
90fn parse_mappings(source_map: &SourceMap) -> Result<Cache, String>{
91 if source_map.version != SOURCE_MAP_VERSION {
92 return Err("Only Source Map version 3 is implemented".into())
93 }
94
95 let sources_length = source_map.sources.len() as u32;
96 let names_length = source_map.names.len() as u32;
97
98 let mut generated_mappings: Vec<Mapping> = Vec::new();
99
100 let mut generated_line: u32 = 0;
101 let mut previous_original_line: u32 = 0;
102 let mut previous_original_column: u32 = 0;
103 let mut previous_source: u32 = 0;
104 let mut previous_name: u32 = 0;
105
106 for line in source_map.mappings.as_bytes().split(|&x| x == (';' as u8)) {
107 generated_line += 1;
108 let mut previous_generated_column: u32 = 0;
109
110 for segment in line.split(|&x| x == (',' as u8)) {
111 let segment_length = segment.len();
112 let mut fields: Vec<i32> = Vec::new();
113 let mut character_index = 0;
114 while character_index < segment_length {
115 match base64_vlq::decode(&segment[character_index..segment_length]) {
116 Some((value, field_length)) => {
117 fields.push(value);
118 character_index += field_length;
119 },
120 None => return Err("Invalid VLQ mapping field".into())
121 };
122 }
123
124 if fields.len() < 1 {
125 continue;
126 }
127
128 if fields.len() == 2 {
129 return Err("Found a source, but no line and column".into());
130 }
131
132 if fields.len() == 3 {
133 return Err("Found a source and line, but no column".into());
134 }
135
136 let mut mapping = Mapping {
137 generated: CodePosition {
138 line: generated_line,
139 column: ((previous_generated_column as i32) + fields[0]) as u32
140 },
141 original: CodePosition {
142 line: 0,
143 column: 0
144 },
145 source: "".into(),
146 name: "".into()
147 };
148
149 previous_generated_column = mapping.generated.column;
150
151 if fields.len() > 1 {
152 previous_source = ((previous_source as i32) + fields[1]) as u32;
154 if previous_source < sources_length {
155 mapping.source = source_map.sources[previous_source as usize].to_owned();
156 } else {
157 return Err(format!("Invalid source map: reference to source index {} when source list length is {}", previous_source, sources_length));
158 }
159
160 previous_original_line = ((previous_original_line as i32) + fields[2]) as u32;
162 mapping.original.line = previous_original_line.checked_add(1).ok_or("Line number overflowed")?;
164
165 previous_original_column = ((previous_original_column as i32) + fields[3]) as u32;
167 mapping.original.column = previous_original_column;
168
169 if fields.len() > 4 {
170 previous_name = ((previous_name as i32) + fields[4]) as u32;
172 if previous_name < names_length {
173 mapping.name = source_map.names[previous_name as usize].to_owned();
174 } else {
175 return Err(format!("Invalid source map: reference to name index {} when name list length is {}", previous_name, names_length));
176 }
177 }
178 }
179
180 generated_mappings.push(mapping);
181 }
182 }
183
184 if generated_mappings.len() < 1 {
185 return Err("Source Map contains no mappings".to_owned());
186 }
187
188 fn sort_key(mapping: &Mapping) -> (u32, u32) {
189 (mapping.generated.line, mapping.generated.column)
190 }
191 generated_mappings.sort_by(|a, b| sort_key(a).cmp(&sort_key(b)));
192
193 Ok(Cache {
194 generated_mappings: generated_mappings,
195 source_root: match &source_map.sourceRoot {
196 &Some(ref x) => x.to_owned(),
197 &None => "".into()
198 }
199 })
200}
201
202
203impl Cache {
204 pub fn mapping_for_generated_position(&self, line: u32, column: u32) -> Mapping {
232 let matcher = |mapping: &Mapping| -> Ordering {
233 (mapping.generated.line, mapping.generated.column).cmp(&(line, column))
234 };
235 let mappings = &self.generated_mappings;
236 match mappings.binary_search_by(matcher) {
237 Ok(index) => &self.generated_mappings[index],
238 Err(index) => &self.generated_mappings[if index >= mappings.len() { mappings.len() - 1 } else { index }]
239 }.clone()
240 }
241}
242
243macro_rules! assert_equal_mappings(
244 ($a:expr, $b:expr) => (
245 if $a != $b {
246 panic!(format!("\n\n{:?}\n\n!=\n\n{:?}\n\n", $a, $b));
247 }
248 );
249);
250
251#[test]
252fn test_source_map_issue_64() {
253 let cache = consume(r#"{
254 "version": 3,
255 "file": "foo.js",
256 "sourceRoot": "http://example.com/",
257 "sources": ["/a"],
258 "names": [],
259 "mappings": "AACA",
260 "sourcesContent": ["foo"]
261 }"#).unwrap();
262
263 let expected = Mapping {
264 generated: CodePosition { line: 1, column: 0 },
265 original: CodePosition { line: 2, column: 0 },
266 source: "/a".into(),
267 name: "".into()
268 };
269 let actual = cache.mapping_for_generated_position(1, 0);
270 assert_equal_mappings!(actual, expected);
271}
272
273#[test]
274fn test_source_map_issue_72_duplicate_sources() {
275 let cache = consume(r#"{
276 "version": 3,
277 "file": "foo.js",
278 "sources": ["source1.js", "source1.js", "source3.js"],
279 "names": [],
280 "mappings": ";EAAC;;IAEE;;MEEE",
281 "sourceRoot": "http://example.com"
282 }"#).unwrap();
283
284
285 {
286 let expected = Mapping {
287 generated: CodePosition { line: 2, column: 2 },
288 original: CodePosition { line: 1, column: 1 },
289 source: "source1.js".into(),
290 name: "".into()
291 };
292 let actual = cache.mapping_for_generated_position(2, 2);
293 assert_equal_mappings!(actual, expected);
294 }
295
296 {
297 let expected = Mapping {
298 generated: CodePosition { line: 4, column: 4 },
299 original: CodePosition { line: 3, column: 3 },
300 source: "source1.js".into(),
301 name: "".into()
302 };
303 let actual = cache.mapping_for_generated_position(4, 4);
304 assert_equal_mappings!(actual, expected);
305 }
306
307 {
308 let expected = Mapping {
309 generated: CodePosition { line: 6, column: 6 },
310 original: CodePosition { line: 5, column: 5 },
311 source: "source3.js".into(),
312 name: "".into()
313 };
314 let actual = cache.mapping_for_generated_position(6, 6);
315 assert_equal_mappings!(actual, expected);
316 }
317}
318
319#[test]
320fn test_source_map_issue_72_duplicate_names() {
321 let cache = consume(r#"{
322 "version": 3,
323 "file": "foo.js",
324 "sources": ["source.js"],
325 "names": ["name1", "name1", "name3"],
326 "mappings": ";EAACA;;IAEEA;;MAEEE",
327 "sourceRoot": "http://example.com"
328 }"#).unwrap();
329
330 {
331 let expected = Mapping {
332 generated: CodePosition { line: 2, column: 2 },
333 original: CodePosition { line: 1, column: 1 },
334 source: "source.js".into(),
335 name: "name1".into()
336 };
337 let actual = cache.mapping_for_generated_position(2, 2);
338 assert_equal_mappings!(actual, expected);
339 }
340
341 {
342 let expected = Mapping {
343 generated: CodePosition { line: 4, column: 4 },
344 original: CodePosition { line: 3, column: 3 },
345 source: "source.js".into(),
346 name: "name1".into()
347 };
348 let actual = cache.mapping_for_generated_position(4, 4);
349 assert_equal_mappings!(actual, expected);
350 }
351
352 {
353 let expected = Mapping {
354 generated: CodePosition { line: 6, column: 6 },
355 original: CodePosition { line: 5, column: 5 },
356 source: "source.js".into(),
357 name: "name3".into()
358 };
359 let actual = cache.mapping_for_generated_position(6, 6);
360 assert_equal_mappings!(actual, expected);
361 }
362}
363
364#[test]
365fn it_allows_omitting_source_root() {
366 let cache_result: Result<Cache, String> = consume(r#"{
367 "version": 3,
368 "file": "foo.js",
369 "sources": ["source.js"],
370 "names": ["name1", "name1", "name3"],
371 "mappings": ";EAACA;;IAEEA;;MAEEE"
372 }"#);
373 match cache_result {
374 Ok(_) => {},
375 Err(s) => panic!(format!("Error due to omitting: '{}'", s))
376 }
377}
378
379#[test]
380fn it_rejects_older_source_map_revisions() {
381 let cache_result = consume(r#"{
382 "version": 2,
383 "file": "",
384 "sources": ["source.js"],
385 "names": ["name1", "name1", "name3"],
386 "mappings": ";EAACA;;IAEEA;;MAEEE",
387 "sourceRoot": "http://example.com"
388 }"#);
389 match cache_result {
390 Ok(_) => panic!("Source Map revision < 3 should be rejected"),
391 Err(_) => {}
392 }
393}
394
395#[test]
396fn it_does_not_panic_due_to_malformed_source_maps() {
397 let cache_result = consume(r#"{
398 "version": 3,
399 "file": "",
400 "sources": [],
401 "names": [],
402 "mappings": ";EAACA;;IAEEA;;MAEEE"
403 }"#);
404 match cache_result {
405 Ok(_) => panic!("Invalid source maps should be rejected"),
406 Err(_) => {}
407 }
408}
409
410#[test]
411fn it_returns_error_when_there_are_no_mappings() {
412 let cache_result = consume(r#"{
413 "version": 3,
414 "file": "foo.js",
415 "sources": ["source.js"],
416 "names": ["name1", "name1", "name3"],
417 "mappings": ";;;"
418 }"#);
419 match cache_result {
420 Ok(_) => panic!("Source maps with no mappings should be rejected"),
421 Err(_) => {}
422 }
423}
424
425#[test]
426fn it_does_not_panic_when_querying_for_position_2() {
427 let cache = consume(r#"{
429 "version": 3,
430 "file": "foo.js",
431 "sources": ["source.js"],
432 "names": ["name1", "name1", "name3"],
433 "mappings": "Z",
434 "sourceRoot": "http://example.com"
435 }"#).unwrap();
436 cache.mapping_for_generated_position(2, 2);
437}
438
439#[test]
440fn it_does_not_panic_on_invalid_bit_shifts() {
441 match consume(r#"{
443 "version": 3,
444 "file": "foo.js",
445 "sources": ["source.js"],
446 "names": ["name1", "name1", "name3"],
447 "mappings": "00000001",
448 "sourceRoot": "http://example.com"
449 }"#) {
450 Err(s) => assert!(s == "Invalid VLQ mapping field"),
451 _ => panic!("Invalid source map should fail to consume")
452 };
453}
454
455#[test]
456fn it_does_not_panic_from_add_overflow() {
457 match consume(r#"{
459 "version": 3,
460 "file": "foo.js",
461 "sources": ["source.js"],
462 "names": ["name1", "name1", "name3"],
463 "mappings": "BBDDDDDDBBBBBBBc;*;ZZBBBBBBBBBBv",
464 "sourceRoot": "http://example.com"
465 }"#) {
466 Err(s) => assert!(s == "Line number overflowed"),
467 _ => panic!("Invalid source map should fail to consume")
468 };
469}