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 67static std::string SourceMapRunner = R"JS( 68let base64Map; 69 70const VLQ_BASE_SHIFT = 5; 71const VLQ_BASE_MASK = (1 << 5) - 1; 72const VLQ_CONTINUATION_MASK = 1 << 5; 73 74class StringCharIterator { 75 /** 76 * @constructor 77 * @param {string} string 78 */ 79 constructor(string) { 80 this._string = string; 81 this._position = 0; 82 } 83 84 /** 85 * @return {string} 86 */ 87 next() { 88 return this._string.charAt(this._position++); 89 } 90 91 /** 92 * @return {string} 93 */ 94 peek() { 95 return this._string.charAt(this._position); 96 } 97 98 /** 99 * @return {boolean} 100 */ 101 hasNext() { 102 return this._position < this._string.length; 103 } 104} 105 106/** 107 * Implements Source Map V3 model. 108 * See https://github.com/google/closure-compiler/wiki/Source-Maps 109 * for format description. 110 */ 111class SourceMap { 112 #payload; 113 #mappings = []; 114 #sources = {}; 115 #sourceContentByURL = {}; 116 117 /** 118 * @constructor 119 * @param {SourceMapV3} payload 120 */ 121 constructor(payload) { 122 if (!base64Map) { 123 const base64Digits = 124 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/'; 125 base64Map = {}; 126 for (let i = 0; i < base64Digits.length; ++i) 127 base64Map[base64Digits[i]] = i; 128 } 129 this.#payload = cloneSourceMapV3(payload); 130 this.#parseMappingPayload(); 131 } 132 133 /** 134 * @return {object} raw source map v3 payload. 135 */ 136 get payload() { 137 return cloneSourceMapV3(this.#payload); 138 } 139 140 #parseMappingPayload = () => { 141 if (this.#payload.sections) { 142 this.#parseSections(this.#payload.sections); 143 } else { 144 this.#parseMap(this.#payload, 0, 0); 145 } 146 this.#mappings.sort(compareSourceMapEntry); 147 }; 148 149 /** 150 * @param {Array.<SourceMapV3.Section>} sections 151 */ 152 #parseSections = (sections) => { 153 for (let i = 0; i < sections.length; ++i) { 154 const section = sections[i]; 155 this.#parseMap(section.map, section.offset.line, section.offset.column); 156 } 157 }; 158 159 /** 160 * @param {number} lineOffset 0-indexed line offset in compiled resource 161 * @param {number} columnOffset 0-indexed column offset in compiled resource 162 * @return {object} representing start of range if found, or empty object 163 */ 164 findEntry(lineOffset, columnOffset) { 165 let first = 0; 166 let count = this.#mappings.length; 167 while (count > 1) { 168 const step = count >> 1; 169 const middle = first + step; 170 const mapping = this.#mappings[middle]; 171 if (lineOffset < mapping[0] || 172 (lineOffset === mapping[0] && columnOffset < mapping[1])) { 173 count = step; 174 } else { 175 first = middle; 176 count -= step; 177 } 178 } 179 const entry = this.#mappings[first]; 180 if (!first && entry && (lineOffset < entry[0] || 181 (lineOffset === entry[0] && columnOffset < entry[1]))) { 182 return {}; 183 } else if (!entry) { 184 return {}; 185 } 186 return { 187 generatedLine: entry[0], 188 generatedColumn: entry[1], 189 originalSource: entry[2], 190 originalLine: entry[3], 191 originalColumn: entry[4], 192 name: entry[5], 193 }; 194 } 195 196 /** 197 * @param {number} lineNumber 1-indexed line number in compiled resource call site 198 * @param {number} columnNumber 1-indexed column number in compiled resource call site 199 * @return {object} representing origin call site if found, or empty object 200 */ 201 findOrigin(lineNumber, columnNumber) { 202 const range = this.findEntry(lineNumber - 1, columnNumber - 1); 203 if ( 204 range.originalSource === undefined || 205 range.originalLine === undefined || 206 range.originalColumn === undefined || 207 range.generatedLine === undefined || 208 range.generatedColumn === undefined 209 ) { 210 return {}; 211 } 212 const lineOffset = lineNumber - range.generatedLine; 213 const columnOffset = columnNumber - range.generatedColumn; 214 return { 215 name: range.name, 216 fileName: range.originalSource, 217 lineNumber: range.originalLine + lineOffset, 218 columnNumber: range.originalColumn + columnOffset, 219 }; 220 } 221 222 /** 223 * @override 224 */ 225 #parseMap(map, lineNumber, columnNumber) { 226 let sourceIndex = 0; 227 let sourceLineNumber = 0; 228 let sourceColumnNumber = 0; 229 let nameIndex = 0; 230 231 const sources = []; 232 const originalToCanonicalURLMap = {}; 233 for (let i = 0; i < map.sources.length; ++i) { 234 const url = map.sources[i]; 235 originalToCanonicalURLMap[url] = url; 236 sources.push(url); 237 this.#sources[url] = true; 238 239 if (map.sourcesContent && map.sourcesContent[i]) 240 this.#sourceContentByURL[url] = map.sourcesContent[i]; 241 } 242 243 const stringCharIterator = new StringCharIterator(map.mappings); 244 let sourceURL = sources[sourceIndex]; 245 while (true) { 246 if (stringCharIterator.peek() === ',') 247 stringCharIterator.next(); 248 else { 249 while (stringCharIterator.peek() === ';') { 250 lineNumber += 1; 251 columnNumber = 0; 252 stringCharIterator.next(); 253 } 254 if (!stringCharIterator.hasNext()) 255 break; 256 } 257 258 columnNumber += decodeVLQ(stringCharIterator); 259 if (isSeparator(stringCharIterator.peek())) { 260 this.#mappings.push([lineNumber, columnNumber]); 261 continue; 262 } 263 264 const sourceIndexDelta = decodeVLQ(stringCharIterator); 265 if (sourceIndexDelta) { 266 sourceIndex += sourceIndexDelta; 267 sourceURL = sources[sourceIndex]; 268 } 269 sourceLineNumber += decodeVLQ(stringCharIterator); 270 sourceColumnNumber += decodeVLQ(stringCharIterator); 271 272 let name; 273 if (!isSeparator(stringCharIterator.peek())) { 274 nameIndex += decodeVLQ(stringCharIterator); 275 name = map.names?.[nameIndex]; 276 } 277 278 this.#mappings.push([ 279 lineNumber, columnNumber, sourceURL, sourceLineNumber, 280 sourceColumnNumber, name],); 281 } 282 } 283} 284 285/** 286 * @param {string} char 287 * @return {boolean} 288 */ 289function isSeparator(char) { 290 return char === ',' || char === ';'; 291} 292 293/** 294 * @param {SourceMap.StringCharIterator} stringCharIterator 295 * @return {number} 296 */ 297function decodeVLQ(stringCharIterator) { 298 // Read unsigned value. 299 let result = 0; 300 let shift = 0; 301 let digit; 302 do { 303 digit = base64Map[stringCharIterator.next()]; 304 result += (digit & VLQ_BASE_MASK) << shift; 305 shift += VLQ_BASE_SHIFT; 306 } while (digit & VLQ_CONTINUATION_MASK); 307 308 // Fix the sign. 309 const negative = result & 1; 310 // Use unsigned right shift, so that the 32nd bit is properly shifted to the 311 // 31st, and the 32nd becomes unset. 312 result >>>= 1; 313 if (!negative) { 314 return result; 315 } 316 317 // We need to OR here to ensure the 32nd bit (the sign bit in an Int32) is 318 // always set for negative numbers. If `result` were 1, (meaning `negate` is 319 // true and all other bits were zeros), `result` would now be 0. But -0 320 // doesn't flip the 32nd bit as intended. All other numbers will successfully 321 // set the 32nd bit without issue, so doing this is a noop for them. 322 return -result | (1 << 31); 323} 324 325/** 326 * @param {SourceMapV3} payload 327 * @return {SourceMapV3} 328 */ 329function cloneSourceMapV3(payload) { 330 payload = { ...payload }; 331 for (const key in payload) { 332 if (Object.prototype.hasOwnProperty.call(payload, key) && 333 Array.isArray(payload[key])) { 334 payload[key] = payload[key].slice(); 335 } 336 } 337 return payload; 338} 339 340/** 341 * @param {Array} entry1 source map entry [lineNumber, columnNumber, sourceURL, 342 * sourceLineNumber, sourceColumnNumber] 343 * @param {Array} entry2 source map entry. 344 * @return {number} 345 */ 346function compareSourceMapEntry(entry1, entry2) { 347 const { 0: lineNumber1, 1: columnNumber1 } = entry1; 348 const { 0: lineNumber2, 1: columnNumber2 } = entry2; 349 if (lineNumber1 !== lineNumber2) { 350 return lineNumber1 - lineNumber2; 351 } 352 return columnNumber1 - columnNumber2; 353} 354 355result = function(error, trace, sourceMap) { 356 try { 357 const payload = JSON.parse(sourceMap) 358 const sm = new SourceMap(payload) 359 const preparedStack = trace.map((t, i) => { 360 const str = i !== 0 ? '\n at ' : ''; 361 const { 362 originalLine, 363 originalColumn, 364 originalSource, 365 } = sm.findEntry(t.getLineNumber() - 1, t.getColumnNumber() - 1); 366 if (originalSource && originalLine !== undefined && 367 originalColumn !== undefined) { 368 // A stack trace will often have several call sites in a row within the 369 // same file, cache the source map and file content accordingly: 370 let fileName = t.getFileName(); 371 if (fileName === undefined) { 372 fileName = t.getEvalOrigin(); 373 } 374 const fnName = t.getFunctionName() ?? t.getMethodName(); 375 const typeName = t.getTypeName(); 376 const namePrefix = typeName !== null && typeName !== 'global' ? `${typeName}.` : ''; 377 const originalName = `${namePrefix}${fnName || '<anonymous>'}`; 378 const hasName = !!originalName; 379 return `${str}${originalName}${hasName ? ' (' : ''}` + 380 `${originalSource}:${originalLine + 1}:` + 381 `${originalColumn + 1}${hasName ? ')' : ''}`; 382 } 383 return `${str}${t}` 384 }).join(''); 385 return `${error}\n at ${preparedStack}` 386 } catch(e) { 387 const originStack = trace.map((t, i) => { 388 const str = i !== 0 ? '\n at ' : ''; 389 return `${str}${t}` 390 }).join('') 391 return `${error}\n at ${originStack}` 392 } 393} 394)JS"; 395