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