1 /* 2 * Copyright (C) 2012 Google Inc. All rights reserved. 3 * 4 * Redistribution and use in source and binary forms, with or without 5 * modification, are permitted provided that the following conditions are 6 * met: 7 * 8 * * Redistributions of source code must retain the above copyright 9 * notice, this list of conditions and the following disclaimer. 10 * * Redistributions in binary form must reproduce the above 11 * copyright notice, this list of conditions and the following disclaimer 12 * in the documentation and/or other materials provided with the 13 * distribution. 14 * * Neither the name of Google Inc. nor the names of its 15 * contributors may be used to endorse or promote products derived from 16 * this software without specific prior written permission. 17 * 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 29 */ 30 31 /** 32 * @constructor 33 */ 34 function SourceMapV3() 35 { 36 /** @type {number} */ this.version; 37 /** @type {string|undefined} */ this.file; 38 /** @type {!Array.<string>} */ this.sources; 39 /** @type {!Array.<!SourceMapV3.Section>|undefined} */ this.sections; 40 /** @type {string} */ this.mappings; 41 /** @type {string|undefined} */ this.sourceRoot; 42 } 43 44 /** 45 * @constructor 46 */ 47 SourceMapV3.Section = function() 48 { 49 /** @type {!SourceMapV3} */ this.map; 50 /** @type {!SourceMapV3.Offset} */ this.offset; 51 } 52 53 /** 54 * @constructor 55 */ 56 SourceMapV3.Offset = function() 57 { 58 /** @type {number} */ this.line; 59 /** @type {number} */ this.column; 60 } 61 62 /** 63 * Implements Source Map V3 model. See http://code.google.com/p/closure-compiler/wiki/SourceMaps 64 * for format description. 65 * @constructor 66 * @param {string} sourceMappingURL 67 * @param {!SourceMapV3} payload 68 */ 69 WebInspector.SourceMap = function(sourceMappingURL, payload) 70 { 71 if (!WebInspector.SourceMap.prototype._base64Map) { 72 const base64Digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; 73 WebInspector.SourceMap.prototype._base64Map = {}; 74 for (var i = 0; i < base64Digits.length; ++i) 75 WebInspector.SourceMap.prototype._base64Map[base64Digits.charAt(i)] = i; 76 } 77 78 this._sourceMappingURL = sourceMappingURL; 79 this._reverseMappingsBySourceURL = {}; 80 this._mappings = []; 81 this._sources = {}; 82 this._sourceContentByURL = {}; 83 this._parseMappingPayload(payload); 84 } 85 86 /** 87 * @param {string} sourceMapURL 88 * @param {string} compiledURL 89 * @param {function(?WebInspector.SourceMap)} callback 90 * @this {WebInspector.SourceMap} 91 */ 92 WebInspector.SourceMap.load = function(sourceMapURL, compiledURL, callback) 93 { 94 var resourceTreeModel = WebInspector.resourceTreeModel; 95 if (resourceTreeModel.cachedResourcesLoaded()) 96 loadResource(); 97 else 98 resourceTreeModel.addEventListener(WebInspector.ResourceTreeModel.EventTypes.CachedResourcesLoaded, cachedResourcesLoaded); 99 100 function cachedResourcesLoaded() 101 { 102 resourceTreeModel.removeEventListener(WebInspector.ResourceTreeModel.EventTypes.CachedResourcesLoaded, cachedResourcesLoaded); 103 loadResource(); 104 } 105 106 function loadResource() 107 { 108 var headers = {}; 109 NetworkAgent.loadResourceForFrontend(resourceTreeModel.mainFrame.id, sourceMapURL, headers, contentLoaded); 110 } 111 112 /** 113 * @param {?Protocol.Error} error 114 * @param {number} statusCode 115 * @param {!NetworkAgent.Headers} headers 116 * @param {string} content 117 */ 118 function contentLoaded(error, statusCode, headers, content) 119 { 120 if (error || !content || statusCode >= 400) { 121 callback(null); 122 return; 123 } 124 125 if (content.slice(0, 3) === ")]}") 126 content = content.substring(content.indexOf('\n')); 127 try { 128 var payload = /** @type {!SourceMapV3} */ (JSON.parse(content)); 129 var baseURL = sourceMapURL.startsWith("data:") ? compiledURL : sourceMapURL; 130 callback(new WebInspector.SourceMap(baseURL, payload)); 131 } catch(e) { 132 console.error(e.message); 133 callback(null); 134 } 135 } 136 } 137 138 WebInspector.SourceMap.prototype = { 139 /** 140 * @return {string} 141 */ 142 url: function() 143 { 144 return this._sourceMappingURL; 145 }, 146 147 /** 148 * @return {!Array.<string>} 149 */ 150 sources: function() 151 { 152 return Object.keys(this._sources); 153 }, 154 155 /** 156 * @param {string} sourceURL 157 * @return {string|undefined} 158 */ 159 sourceContent: function(sourceURL) 160 { 161 return this._sourceContentByURL[sourceURL]; 162 }, 163 164 /** 165 * @param {string} sourceURL 166 * @param {!WebInspector.ResourceType} contentType 167 * @return {!WebInspector.ContentProvider} 168 */ 169 sourceContentProvider: function(sourceURL, contentType) 170 { 171 var sourceContent = this.sourceContent(sourceURL); 172 if (sourceContent) 173 return new WebInspector.StaticContentProvider(contentType, sourceContent); 174 return new WebInspector.CompilerSourceMappingContentProvider(sourceURL, contentType); 175 }, 176 177 /** 178 * @param {!SourceMapV3} mappingPayload 179 */ 180 _parseMappingPayload: function(mappingPayload) 181 { 182 if (mappingPayload.sections) 183 this._parseSections(mappingPayload.sections); 184 else 185 this._parseMap(mappingPayload, 0, 0); 186 }, 187 188 /** 189 * @param {!Array.<!SourceMapV3.Section>} sections 190 */ 191 _parseSections: function(sections) 192 { 193 for (var i = 0; i < sections.length; ++i) { 194 var section = sections[i]; 195 this._parseMap(section.map, section.offset.line, section.offset.column); 196 } 197 }, 198 199 /** 200 * @param {number} lineNumber in compiled resource 201 * @param {number} columnNumber in compiled resource 202 * @return {?Array.<number|string>} 203 */ 204 findEntry: function(lineNumber, columnNumber) 205 { 206 var first = 0; 207 var count = this._mappings.length; 208 while (count > 1) { 209 var step = count >> 1; 210 var middle = first + step; 211 var mapping = this._mappings[middle]; 212 if (lineNumber < mapping[0] || (lineNumber === mapping[0] && columnNumber < mapping[1])) 213 count = step; 214 else { 215 first = middle; 216 count -= step; 217 } 218 } 219 var entry = this._mappings[first]; 220 if (!first && entry && (lineNumber < entry[0] || (lineNumber === entry[0] && columnNumber < entry[1]))) 221 return null; 222 return entry; 223 }, 224 225 /** 226 * @param {string} sourceURL of the originating resource 227 * @param {number} lineNumber in the originating resource 228 * @param {number=} span 229 * @return {?Array.<*>} 230 */ 231 findEntryReversed: function(sourceURL, lineNumber, span) 232 { 233 var mappings = this._reverseMappingsBySourceURL[sourceURL]; 234 var maxLineNumber = typeof span === "number" ? Math.min(lineNumber + span + 1, mappings.length) : mappings.length; 235 for ( ; lineNumber < maxLineNumber; ++lineNumber) { 236 var mapping = mappings[lineNumber]; 237 if (mapping) 238 return mapping; 239 } 240 return null; 241 }, 242 243 /** 244 * @override 245 */ 246 _parseMap: function(map, lineNumber, columnNumber) 247 { 248 var sourceIndex = 0; 249 var sourceLineNumber = 0; 250 var sourceColumnNumber = 0; 251 var nameIndex = 0; 252 253 var sources = []; 254 var originalToCanonicalURLMap = {}; 255 for (var i = 0; i < map.sources.length; ++i) { 256 var originalSourceURL = map.sources[i]; 257 var sourceRoot = map.sourceRoot || ""; 258 if (sourceRoot && !sourceRoot.endsWith("/")) 259 sourceRoot += "/"; 260 var href = sourceRoot + originalSourceURL; 261 var url = WebInspector.ParsedURL.completeURL(this._sourceMappingURL, href) || href; 262 originalToCanonicalURLMap[originalSourceURL] = url; 263 sources.push(url); 264 this._sources[url] = true; 265 266 if (map.sourcesContent && map.sourcesContent[i]) 267 this._sourceContentByURL[url] = map.sourcesContent[i]; 268 } 269 270 var stringCharIterator = new WebInspector.SourceMap.StringCharIterator(map.mappings); 271 var sourceURL = sources[sourceIndex]; 272 273 while (true) { 274 if (stringCharIterator.peek() === ",") 275 stringCharIterator.next(); 276 else { 277 while (stringCharIterator.peek() === ";") { 278 lineNumber += 1; 279 columnNumber = 0; 280 stringCharIterator.next(); 281 } 282 if (!stringCharIterator.hasNext()) 283 break; 284 } 285 286 columnNumber += this._decodeVLQ(stringCharIterator); 287 if (!stringCharIterator.hasNext() || this._isSeparator(stringCharIterator.peek())) { 288 this._mappings.push([lineNumber, columnNumber]); 289 continue; 290 } 291 292 var sourceIndexDelta = this._decodeVLQ(stringCharIterator); 293 if (sourceIndexDelta) { 294 sourceIndex += sourceIndexDelta; 295 sourceURL = sources[sourceIndex]; 296 } 297 sourceLineNumber += this._decodeVLQ(stringCharIterator); 298 sourceColumnNumber += this._decodeVLQ(stringCharIterator); 299 if (!this._isSeparator(stringCharIterator.peek())) 300 nameIndex += this._decodeVLQ(stringCharIterator); 301 302 this._mappings.push([lineNumber, columnNumber, sourceURL, sourceLineNumber, sourceColumnNumber]); 303 } 304 305 for (var i = 0; i < this._mappings.length; ++i) { 306 var mapping = this._mappings[i]; 307 var url = mapping[2]; 308 if (!url) 309 continue; 310 if (!this._reverseMappingsBySourceURL[url]) 311 this._reverseMappingsBySourceURL[url] = []; 312 var reverseMappings = this._reverseMappingsBySourceURL[url]; 313 var sourceLine = mapping[3]; 314 if (!reverseMappings[sourceLine]) 315 reverseMappings[sourceLine] = [mapping[0], mapping[1]]; 316 } 317 }, 318 319 /** 320 * @param {string} char 321 * @return {boolean} 322 */ 323 _isSeparator: function(char) 324 { 325 return char === "," || char === ";"; 326 }, 327 328 /** 329 * @param {!WebInspector.SourceMap.StringCharIterator} stringCharIterator 330 * @return {number} 331 */ 332 _decodeVLQ: function(stringCharIterator) 333 { 334 // Read unsigned value. 335 var result = 0; 336 var shift = 0; 337 do { 338 var digit = this._base64Map[stringCharIterator.next()]; 339 result += (digit & this._VLQ_BASE_MASK) << shift; 340 shift += this._VLQ_BASE_SHIFT; 341 } while (digit & this._VLQ_CONTINUATION_MASK); 342 343 // Fix the sign. 344 var negative = result & 1; 345 result >>= 1; 346 return negative ? -result : result; 347 }, 348 349 _VLQ_BASE_SHIFT: 5, 350 _VLQ_BASE_MASK: (1 << 5) - 1, 351 _VLQ_CONTINUATION_MASK: 1 << 5 352 } 353 354 /** 355 * @constructor 356 * @param {string} string 357 */ 358 WebInspector.SourceMap.StringCharIterator = function(string) 359 { 360 this._string = string; 361 this._position = 0; 362 } 363 364 WebInspector.SourceMap.StringCharIterator.prototype = { 365 /** 366 * @return {string} 367 */ 368 next: function() 369 { 370 return this._string.charAt(this._position++); 371 }, 372 373 /** 374 * @return {string} 375 */ 376 peek: function() 377 { 378 return this._string.charAt(this._position); 379 }, 380 381 /** 382 * @return {boolean} 383 */ 384 hasNext: function() 385 { 386 return this._position < this._string.length; 387 } 388 } 389