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 * Implements Source Map V3 model. See http://code.google.com/p/closure-compiler/wiki/SourceMaps
33 * for format description.
34 * @constructor
35 * @param {string} sourceMappingURL
36 * @param {SourceMapV3} payload
37 */
38WebInspector.SourceMap = function(sourceMappingURL, payload)
39{
40    if (!WebInspector.SourceMap.prototype._base64Map) {
41        const base64Digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
42        WebInspector.SourceMap.prototype._base64Map = {};
43        for (var i = 0; i < base64Digits.length; ++i)
44            WebInspector.SourceMap.prototype._base64Map[base64Digits.charAt(i)] = i;
45    }
46
47    this._sourceMappingURL = sourceMappingURL;
48    this._reverseMappingsBySourceURL = {};
49    this._mappings = [];
50    this._sources = {};
51    this._sourceContentByURL = {};
52    this._parseMappingPayload(payload);
53}
54
55/**
56 * @param {string} sourceMapURL
57 * @param {string} compiledURL
58 * @return {WebInspector.SourceMap}
59 */
60WebInspector.SourceMap.load = function(sourceMapURL, compiledURL)
61{
62    try {
63        // FIXME: make sendRequest async.
64        var response = InspectorFrontendHost.loadResourceSynchronously(sourceMapURL);
65        if (!response)
66            return null;
67        if (response.slice(0, 3) === ")]}")
68            response = response.substring(response.indexOf('\n'));
69        var payload = /** @type {SourceMapV3} */ (JSON.parse(response));
70        var baseURL = sourceMapURL.startsWith("data:") ? compiledURL : sourceMapURL;
71        return new WebInspector.SourceMap(baseURL, payload);
72    } catch(e) {
73        console.error(e.message);
74        return null;
75    }
76}
77
78WebInspector.SourceMap.prototype = {
79    /**
80     * @return {Array.<string>}
81     */
82    sources: function()
83    {
84        return Object.keys(this._sources);
85    },
86
87    /**
88     * @param {string} sourceURL
89     * @return {string|undefined}
90     */
91    sourceContent: function(sourceURL)
92    {
93        return this._sourceContentByURL[sourceURL];
94    },
95
96    /**
97     * @param {SourceMapV3} mappingPayload
98     */
99    _parseMappingPayload: function(mappingPayload)
100    {
101        if (mappingPayload.sections)
102            this._parseSections(mappingPayload.sections);
103        else
104            this._parseMap(mappingPayload, 0, 0);
105    },
106
107    /**
108     * @param {Array.<SourceMapV3.Section>} sections
109     */
110    _parseSections: function(sections)
111    {
112        for (var i = 0; i < sections.length; ++i) {
113            var section = sections[i];
114            this._parseMap(section.map, section.offset.line, section.offset.column);
115        }
116    },
117
118    /**
119     * @param {number} lineNumber in compiled resource
120     * @param {number} columnNumber in compiled resource
121     * @return {?Array}
122     */
123    findEntry: function(lineNumber, columnNumber)
124    {
125        var first = 0;
126        var count = this._mappings.length;
127        while (count > 1) {
128          var step = count >> 1;
129          var middle = first + step;
130          var mapping = this._mappings[middle];
131          if (lineNumber < mapping[0] || (lineNumber === mapping[0] && columnNumber < mapping[1]))
132              count = step;
133          else {
134              first = middle;
135              count -= step;
136          }
137        }
138        var entry = this._mappings[first];
139        if (!first && entry && (lineNumber < entry[0] || (lineNumber === entry[0] && columnNumber < entry[1])))
140            return null;
141        return entry;
142    },
143
144    /**
145     * @param {string} sourceURL of the originating resource
146     * @param {number} lineNumber in the originating resource
147     * @return {Array}
148     */
149    findEntryReversed: function(sourceURL, lineNumber)
150    {
151        var mappings = this._reverseMappingsBySourceURL[sourceURL];
152        for ( ; lineNumber < mappings.length; ++lineNumber) {
153            var mapping = mappings[lineNumber];
154            if (mapping)
155                return mapping;
156        }
157        return this._mappings[0];
158    },
159
160    /**
161     * @override
162     */
163    _parseMap: function(map, lineNumber, columnNumber)
164    {
165        var sourceIndex = 0;
166        var sourceLineNumber = 0;
167        var sourceColumnNumber = 0;
168        var nameIndex = 0;
169
170        var sources = [];
171        var originalToCanonicalURLMap = {};
172        for (var i = 0; i < map.sources.length; ++i) {
173            var originalSourceURL = map.sources[i];
174            var href = (map.sourceRoot ? map.sourceRoot + "/" : "") + originalSourceURL;
175            var url = WebInspector.ParsedURL.completeURL(this._sourceMappingURL, href) || href;
176            originalToCanonicalURLMap[originalSourceURL] = url;
177            sources.push(url);
178            this._sources[url] = true;
179
180            if (map.sourcesContent && map.sourcesContent[i])
181                this._sourceContentByURL[url] = map.sourcesContent[i];
182        }
183
184        var stringCharIterator = new WebInspector.SourceMap.StringCharIterator(map.mappings);
185        var sourceURL = sources[sourceIndex];
186
187        while (true) {
188            if (stringCharIterator.peek() === ",")
189                stringCharIterator.next();
190            else {
191                while (stringCharIterator.peek() === ";") {
192                    lineNumber += 1;
193                    columnNumber = 0;
194                    stringCharIterator.next();
195                }
196                if (!stringCharIterator.hasNext())
197                    break;
198            }
199
200            columnNumber += this._decodeVLQ(stringCharIterator);
201            if (this._isSeparator(stringCharIterator.peek())) {
202                this._mappings.push([lineNumber, columnNumber]);
203                continue;
204            }
205
206            var sourceIndexDelta = this._decodeVLQ(stringCharIterator);
207            if (sourceIndexDelta) {
208                sourceIndex += sourceIndexDelta;
209                sourceURL = sources[sourceIndex];
210            }
211            sourceLineNumber += this._decodeVLQ(stringCharIterator);
212            sourceColumnNumber += this._decodeVLQ(stringCharIterator);
213            if (!this._isSeparator(stringCharIterator.peek()))
214                nameIndex += this._decodeVLQ(stringCharIterator);
215
216            this._mappings.push([lineNumber, columnNumber, sourceURL, sourceLineNumber, sourceColumnNumber]);
217        }
218
219        for (var i = 0; i < this._mappings.length; ++i) {
220            var mapping = this._mappings[i];
221            var url = mapping[2];
222            if (!url)
223                continue;
224            if (!this._reverseMappingsBySourceURL[url])
225                this._reverseMappingsBySourceURL[url] = [];
226            var reverseMappings = this._reverseMappingsBySourceURL[url];
227            var sourceLine = mapping[3];
228            if (!reverseMappings[sourceLine])
229                reverseMappings[sourceLine] = [mapping[0], mapping[1]];
230        }
231    },
232
233    /**
234     * @param {string} char
235     * @return {boolean}
236     */
237    _isSeparator: function(char)
238    {
239        return char === "," || char === ";";
240    },
241
242    /**
243     * @param {WebInspector.SourceMap.StringCharIterator} stringCharIterator
244     * @return {number}
245     */
246    _decodeVLQ: function(stringCharIterator)
247    {
248        // Read unsigned value.
249        var result = 0;
250        var shift = 0;
251        do {
252            var digit = this._base64Map[stringCharIterator.next()];
253            result += (digit & this._VLQ_BASE_MASK) << shift;
254            shift += this._VLQ_BASE_SHIFT;
255        } while (digit & this._VLQ_CONTINUATION_MASK);
256
257        // Fix the sign.
258        var negative = result & 1;
259        result >>= 1;
260        return negative ? -result : result;
261    },
262
263    _VLQ_BASE_SHIFT: 5,
264    _VLQ_BASE_MASK: (1 << 5) - 1,
265    _VLQ_CONTINUATION_MASK: 1 << 5
266}
267
268/**
269 * @constructor
270 * @param {string} string
271 */
272WebInspector.SourceMap.StringCharIterator = function(string)
273{
274    this._string = string;
275    this._position = 0;
276}
277
278WebInspector.SourceMap.StringCharIterator.prototype = {
279    /**
280     * @return {string}
281     */
282    next: function()
283    {
284        return this._string.charAt(this._position++);
285    },
286
287    /**
288     * @return {string}
289     */
290    peek: function()
291    {
292        return this._string.charAt(this._position);
293    },
294
295    /**
296     * @return {boolean}
297     */
298    hasNext: function()
299    {
300        return this._position < this._string.length;
301    }
302}
303