• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1// This file is a modified version of:
2// https://cs.chromium.org/chromium/src/v8/tools/SourceMap.js?rcl=dd10454c1d
3// from the V8 codebase. Logic specific to WebInspector is removed and linting
4// is made to match the Node.js style guide.
5
6// Copyright 2013 the V8 project authors. All rights reserved.
7// Redistribution and use in source and binary forms, with or without
8// modification, are permitted provided that the following conditions are
9// met:
10//
11//     * Redistributions of source code must retain the above copyright
12//       notice, this list of conditions and the following disclaimer.
13//     * Redistributions in binary form must reproduce the above
14//       copyright notice, this list of conditions and the following
15//       disclaimer in the documentation and/or other materials provided
16//       with the distribution.
17//     * Neither the name of Google Inc. nor the names of its
18//       contributors may be used to endorse or promote products derived
19//       from this software without specific prior written permission.
20//
21// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
22// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
23// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
24// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
25// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
26// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
27// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
28// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
29// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
30// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
31// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
32
33// This is a copy from blink dev tools, see:
34// http://src.chromium.org/viewvc/blink/trunk/Source/devtools/front_end/SourceMap.js
35// revision: 153407
36
37/*
38 * Copyright (C) 2012 Google Inc. All rights reserved.
39 *
40 * Redistribution and use in source and binary forms, with or without
41 * modification, are permitted provided that the following conditions are
42 * met:
43 *
44 *     * Redistributions of source code must retain the above copyright
45 * notice, this list of conditions and the following disclaimer.
46 *     * Redistributions in binary form must reproduce the above
47 * copyright notice, this list of conditions and the following disclaimer
48 * in the documentation and/or other materials provided with the
49 * distribution.
50 *     * Neither the name of Google Inc. nor the names of its
51 * contributors may be used to endorse or promote products derived from
52 * this software without specific prior written permission.
53 *
54 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
55 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
56 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
57 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
58 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
59 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
60 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
61 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
62 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
63 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
64 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
65 */
66
67'use strict';
68
69const {
70  ArrayIsArray,
71  ArrayPrototypePush,
72  ArrayPrototypeSlice,
73  ArrayPrototypeSort,
74  ObjectPrototypeHasOwnProperty,
75  StringPrototypeCharAt,
76} = primordials;
77
78const { validateObject } = require('internal/validators');
79
80let base64Map;
81
82const VLQ_BASE_SHIFT = 5;
83const VLQ_BASE_MASK = (1 << 5) - 1;
84const VLQ_CONTINUATION_MASK = 1 << 5;
85
86class StringCharIterator {
87  /**
88   * @constructor
89   * @param {string} string
90   */
91  constructor(string) {
92    this._string = string;
93    this._position = 0;
94  }
95
96  /**
97   * @return {string}
98   */
99  next() {
100    return StringPrototypeCharAt(this._string, this._position++);
101  }
102
103  /**
104   * @return {string}
105   */
106  peek() {
107    return StringPrototypeCharAt(this._string, this._position);
108  }
109
110  /**
111   * @return {boolean}
112   */
113  hasNext() {
114    return this._position < this._string.length;
115  }
116}
117
118/**
119 * Implements Source Map V3 model.
120 * See https://github.com/google/closure-compiler/wiki/Source-Maps
121 * for format description.
122 */
123class SourceMap {
124  #payload;
125  #mappings = [];
126  #sources = {};
127  #sourceContentByURL = {};
128
129  /**
130   * @constructor
131   * @param {SourceMapV3} payload
132   */
133  constructor(payload) {
134    if (!base64Map) {
135      const base64Digits =
136             'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/';
137      base64Map = {};
138      for (let i = 0; i < base64Digits.length; ++i)
139        base64Map[base64Digits[i]] = i;
140    }
141    this.#payload = cloneSourceMapV3(payload);
142    this.#parseMappingPayload();
143  }
144
145  /**
146   * @return {object} raw source map v3 payload.
147   */
148  get payload() {
149    return cloneSourceMapV3(this.#payload);
150  }
151
152  #parseMappingPayload = () => {
153    if (this.#payload.sections) {
154      this.#parseSections(this.#payload.sections);
155    } else {
156      this.#parseMap(this.#payload, 0, 0);
157    }
158    ArrayPrototypeSort(this.#mappings, compareSourceMapEntry);
159  };
160
161  /**
162   * @param {Array.<SourceMapV3.Section>} sections
163   */
164  #parseSections = (sections) => {
165    for (let i = 0; i < sections.length; ++i) {
166      const section = sections[i];
167      this.#parseMap(section.map, section.offset.line, section.offset.column);
168    }
169  };
170
171  /**
172   * @param {number} lineOffset 0-indexed line offset in compiled resource
173   * @param {number} columnOffset 0-indexed column offset in compiled resource
174   * @return {object} representing start of range if found, or empty object
175   */
176  findEntry(lineOffset, columnOffset) {
177    let first = 0;
178    let count = this.#mappings.length;
179    while (count > 1) {
180      const step = count >> 1;
181      const middle = first + step;
182      const mapping = this.#mappings[middle];
183      if (lineOffset < mapping[0] ||
184          (lineOffset === mapping[0] && columnOffset < mapping[1])) {
185        count = step;
186      } else {
187        first = middle;
188        count -= step;
189      }
190    }
191    const entry = this.#mappings[first];
192    if (!first && entry && (lineOffset < entry[0] ||
193        (lineOffset === entry[0] && columnOffset < entry[1]))) {
194      return {};
195    } else if (!entry) {
196      return {};
197    }
198    return {
199      generatedLine: entry[0],
200      generatedColumn: entry[1],
201      originalSource: entry[2],
202      originalLine: entry[3],
203      originalColumn: entry[4],
204      name: entry[5],
205    };
206  }
207
208  /**
209   * @param {number} lineNumber 1-indexed line number in compiled resource call site
210   * @param {number} columnNumber 1-indexed column number in compiled resource call site
211   * @return {object} representing origin call site if found, or empty object
212   */
213  findOrigin(lineNumber, columnNumber) {
214    const range = this.findEntry(lineNumber - 1, columnNumber - 1);
215    if (
216      range.originalSource === undefined ||
217      range.originalLine === undefined ||
218      range.originalColumn === undefined ||
219      range.generatedLine === undefined ||
220      range.generatedColumn === undefined
221    ) {
222      return {};
223    }
224    const lineOffset = lineNumber - range.generatedLine;
225    const columnOffset = columnNumber - range.generatedColumn;
226    return {
227      name: range.name,
228      fileName: range.originalSource,
229      lineNumber: range.originalLine + lineOffset,
230      columnNumber: range.originalColumn + columnOffset,
231    };
232  }
233
234  /**
235   * @override
236   */
237  #parseMap(map, lineNumber, columnNumber) {
238    let sourceIndex = 0;
239    let sourceLineNumber = 0;
240    let sourceColumnNumber = 0;
241    let nameIndex = 0;
242
243    const sources = [];
244    const originalToCanonicalURLMap = {};
245    for (let i = 0; i < map.sources.length; ++i) {
246      const url = map.sources[i];
247      originalToCanonicalURLMap[url] = url;
248      ArrayPrototypePush(sources, url);
249      this.#sources[url] = true;
250
251      if (map.sourcesContent && map.sourcesContent[i])
252        this.#sourceContentByURL[url] = map.sourcesContent[i];
253    }
254
255    const stringCharIterator = new StringCharIterator(map.mappings);
256    let sourceURL = sources[sourceIndex];
257    while (true) {
258      if (stringCharIterator.peek() === ',')
259        stringCharIterator.next();
260      else {
261        while (stringCharIterator.peek() === ';') {
262          lineNumber += 1;
263          columnNumber = 0;
264          stringCharIterator.next();
265        }
266        if (!stringCharIterator.hasNext())
267          break;
268      }
269
270      columnNumber += decodeVLQ(stringCharIterator);
271      if (isSeparator(stringCharIterator.peek())) {
272        ArrayPrototypePush(this.#mappings, [lineNumber, columnNumber]);
273        continue;
274      }
275
276      const sourceIndexDelta = decodeVLQ(stringCharIterator);
277      if (sourceIndexDelta) {
278        sourceIndex += sourceIndexDelta;
279        sourceURL = sources[sourceIndex];
280      }
281      sourceLineNumber += decodeVLQ(stringCharIterator);
282      sourceColumnNumber += decodeVLQ(stringCharIterator);
283
284      let name;
285      if (!isSeparator(stringCharIterator.peek())) {
286        nameIndex += decodeVLQ(stringCharIterator);
287        name = map.names?.[nameIndex];
288      }
289
290      ArrayPrototypePush(
291        this.#mappings,
292        [lineNumber, columnNumber, sourceURL, sourceLineNumber,
293         sourceColumnNumber, name],
294      );
295    }
296  }
297}
298
299/**
300 * @param {string} char
301 * @return {boolean}
302 */
303function isSeparator(char) {
304  return char === ',' || char === ';';
305}
306
307/**
308 * @param {SourceMap.StringCharIterator} stringCharIterator
309 * @return {number}
310 */
311function decodeVLQ(stringCharIterator) {
312  // Read unsigned value.
313  let result = 0;
314  let shift = 0;
315  let digit;
316  do {
317    digit = base64Map[stringCharIterator.next()];
318    result += (digit & VLQ_BASE_MASK) << shift;
319    shift += VLQ_BASE_SHIFT;
320  } while (digit & VLQ_CONTINUATION_MASK);
321
322  // Fix the sign.
323  const negative = result & 1;
324  // Use unsigned right shift, so that the 32nd bit is properly shifted to the
325  // 31st, and the 32nd becomes unset.
326  result >>>= 1;
327  if (!negative) {
328    return result;
329  }
330
331  // We need to OR here to ensure the 32nd bit (the sign bit in an Int32) is
332  // always set for negative numbers. If `result` were 1, (meaning `negate` is
333  // true and all other bits were zeros), `result` would now be 0. But -0
334  // doesn't flip the 32nd bit as intended. All other numbers will successfully
335  // set the 32nd bit without issue, so doing this is a noop for them.
336  return -result | (1 << 31);
337}
338
339/**
340 * @param {SourceMapV3} payload
341 * @return {SourceMapV3}
342 */
343function cloneSourceMapV3(payload) {
344  validateObject(payload, 'payload');
345  payload = { ...payload };
346  for (const key in payload) {
347    if (ObjectPrototypeHasOwnProperty(payload, key) &&
348        ArrayIsArray(payload[key])) {
349      payload[key] = ArrayPrototypeSlice(payload[key]);
350    }
351  }
352  return payload;
353}
354
355/**
356 * @param {Array} entry1 source map entry [lineNumber, columnNumber, sourceURL,
357 *  sourceLineNumber, sourceColumnNumber]
358 * @param {Array} entry2 source map entry.
359 * @return {number}
360 */
361function compareSourceMapEntry(entry1, entry2) {
362  const { 0: lineNumber1, 1: columnNumber1 } = entry1;
363  const { 0: lineNumber2, 1: columnNumber2 } = entry2;
364  if (lineNumber1 !== lineNumber2) {
365    return lineNumber1 - lineNumber2;
366  }
367  return columnNumber1 - columnNumber2;
368}
369
370module.exports = {
371  SourceMap,
372};
373