js_source_mapper/
consume.rs

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  // We skip this. Keeping megabytes of data that we do not care about
21  // in memory seems reckless to caches.
22  //sourcesContent: Option<vec<String>>,
23}
24
25#[derive(Clone, Eq, PartialEq, Debug)]
26pub struct CodePosition {
27  /** Line number in a code file, starting from 1 */
28  pub line: u32,
29  /** Column number in a code file, starting from 0 */
30  pub column: u32
31}
32
33#[derive(Clone, Eq, PartialEq, Debug)]
34pub struct Mapping {
35  /** The position in the generated file */
36  pub generated: CodePosition,
37  /** The position in the corresponding original source file */
38  pub original: CodePosition,
39  /** The original source file */
40  pub source: String,
41  /** The original source name of the function/class, if applicable */
42  pub name: String
43}
44
45#[derive(Debug)]
46pub struct Cache {
47  generated_mappings: Vec<Mapping>,
48  /** The path prefix of mapping source paths */
49  pub source_root: String
50}
51
52/**
53 * consume parses a SourceMap into a cache that can be queried for mappings
54 *
55 * The only parameter is the raw source map as a JSON string.
56 * According to the [source map spec][source-map-spec], source maps have the following attributes:
57 *
58 *   - version: Which version of the source map spec this map is following.
59 *   - sources: An array of URLs to the original source files.
60 *   - names: An array of identifiers which can be referrenced by individual mappings.
61 *   - sourceRoot: Optional. The URL root from which all sources are relative.
62 *   - sourcesContent: Optional. An array of contents of the original source files.
63 *   - mappings: A string of base64 VLQs which contain the actual mappings.
64 *   - file: Optional. The generated file this source map is associated with.
65 *
66 * Here is an example source map:
67 *
68 * ```json
69 *     {
70 *       "version": 3,
71 *       "file": "out.js",
72 *       "sourceRoot" : "",
73 *       "sources": ["foo.js", "bar.js"],
74 *       "names": ["src", "maps", "are", "fun"],
75 *       "mappings": "AA,AB;;ABCDE;"
76 *     }
77 * ```
78 *
79 * [source-map-spec]: https://docs.google.com/document/d/1U1RGAehQwRypUTovF1KRlpiOFze0b-_2gc6fAH0KY0k/edit?pli=1#
80 */
81pub 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        // Original source.
153        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        // Original line.
161        previous_original_line = ((previous_original_line as i32) + fields[2]) as u32;
162        // Lines are stored 0-based
163        mapping.original.line = previous_original_line.checked_add(1).ok_or("Line number overflowed")?;
164
165        // Original column.
166        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          // Original name.
171          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  /**
205   * Returns the original source, line, column and name information for the generated
206   * source's line and column positions provided.
207   *
208   * # Arguments
209   *
210   * * line: The line number in the generated source.
211   * * column: The column number in the generated source.
212   *
213   * # Examples
214   *
215   * ```
216   * use js_source_mapper::consume;
217   *
218   * let cache = consume(r#"{ "version": 3, "file": "foo.js", "sources": ["source.js"], "names": ["name1", "name1", "name3"], "mappings": ";EAACA;;IAEEA;;MAEEE", "sourceRoot": "http://example.com" }"#).unwrap();
219   *
220   * println!("{:?}", cache.mapping_for_generated_position(2, 2));
221   * // => Mapping {
222   * //   generated: CodePosition { line: 2, column: 2 },
223   * //   original: CodePosition { line: 1, column: 1 },
224   * //   source: "source.js"
225   * //   name: "name1"
226   * // }
227   * ```
228   *
229   */
230
231  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  // Found with cargo-fuzz
428  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  // Found with cargo-fuzz
442  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  // Found with cargo-fuzz
458  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}