1/*
2 * Copyright (C) 2013 Apple 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
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26WebInspector.SourceMap = function(sourceMappingURL, payload, originalSourceCode)
27{
28    if (!WebInspector.SourceMap.prototype._base64Map) {
29        const base64Digits = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
30        WebInspector.SourceMap.prototype._base64Map = {};
31        for (var i = 0; i < base64Digits.length; ++i)
32            WebInspector.SourceMap.prototype._base64Map[base64Digits.charAt(i)] = i;
33    }
34
35    this._originalSourceCode = originalSourceCode || null;
36    this._sourceMapResources = {};
37    this._sourceMapResourcesList = [];
38
39    this._sourceMappingURL = sourceMappingURL;
40    this._reverseMappingsBySourceURL = {};
41    this._mappings = [];
42    this._sources = {};
43    this._sourceRoot = null;
44    this._sourceContentByURL = {};
45    this._parseMappingPayload(payload);
46}
47
48WebInspector.SourceMap.prototype = {
49
50    get originalSourceCode()
51    {
52        return this._originalSourceCode;
53    },
54
55    get sourceMappingBasePathURLComponents()
56    {
57        if (this._sourceMappingURLBasePathComponents)
58            return this._sourceMappingURLBasePathComponents;
59
60        if (this._sourceRoot) {
61            var baseURLPath = absoluteURL(this._sourceRoot, this._sourceMappingURL);
62            console.assert(baseURLPath);
63            if (baseURLPath) {
64                var urlComponents = parseURL(baseURLPath);
65                if (!/\/$/.test(urlComponents.path))
66                    urlComponents.path += "/";
67                this._sourceMappingURLBasePathComponents = urlComponents;
68                return this._sourceMappingURLBasePathComponents;
69            }
70        }
71
72        var urlComponents = parseURL(this._sourceMappingURL);
73        urlComponents.path = urlComponents.path.substr(0, urlComponents.path.lastIndexOf(urlComponents.lastPathComponent));
74        urlComponents.lastPathComponent = null;
75        this._sourceMappingURLBasePathComponents = urlComponents;
76        return this._sourceMappingURLBasePathComponents;
77    },
78
79    get resources()
80    {
81        return this._sourceMapResourcesList;
82    },
83
84    addResource: function(resource)
85    {
86        console.assert(!(resource.url in this._sourceMapResources));
87        this._sourceMapResources[resource.url] = resource;
88        this._sourceMapResourcesList.push(resource);
89    },
90
91    resourceForURL: function(url)
92    {
93        return this._sourceMapResources[url];
94    },
95
96    sources: function()
97    {
98        return Object.keys(this._sources);
99    },
100
101    sourceContent: function(sourceURL)
102    {
103        return this._sourceContentByURL[sourceURL];
104    },
105
106    _parseMappingPayload: function(mappingPayload)
107    {
108        if (mappingPayload.sections)
109            this._parseSections(mappingPayload.sections);
110        else
111            this._parseMap(mappingPayload, 0, 0);
112    },
113
114    _parseSections: function(sections)
115    {
116        for (var i = 0; i < sections.length; ++i) {
117            var section = sections[i];
118            this._parseMap(section.map, section.offset.line, section.offset.column);
119        }
120    },
121
122    findEntry: function(lineNumber, columnNumber)
123    {
124        var first = 0;
125        var count = this._mappings.length;
126        while (count > 1) {
127            var step = count >> 1;
128            var middle = first + step;
129            var mapping = this._mappings[middle];
130            if (lineNumber < mapping[0] || (lineNumber === mapping[0] && columnNumber < mapping[1]))
131                count = step;
132            else {
133                first = middle;
134                count -= step;
135            }
136        }
137        var entry = this._mappings[first];
138        if (!first && entry && (lineNumber < entry[0] || (lineNumber === entry[0] && columnNumber < entry[1])))
139            return null;
140        return entry;
141    },
142
143    findEntryReversed: function(sourceURL, lineNumber)
144    {
145        var mappings = this._reverseMappingsBySourceURL[sourceURL];
146        for ( ; lineNumber < mappings.length; ++lineNumber) {
147            var mapping = mappings[lineNumber];
148            if (mapping)
149                return mapping;
150        }
151        return this._mappings[0];
152    },
153
154    _parseMap: function(map, lineNumber, columnNumber)
155    {
156        var sourceIndex = 0;
157        var sourceLineNumber = 0;
158        var sourceColumnNumber = 0;
159        var nameIndex = 0;
160
161        var sources = [];
162        var originalToCanonicalURLMap = {};
163        for (var i = 0; i < map.sources.length; ++i) {
164            var originalSourceURL = map.sources[i];
165            var href = originalSourceURL;
166            if (map.sourceRoot && href.charAt(0) !== "/")
167                href = map.sourceRoot.replace(/\/+$/, "") + "/" + href;
168            var url = absoluteURL(href, this._sourceMappingURL) || href;
169            originalToCanonicalURLMap[originalSourceURL] = url;
170            sources.push(url);
171            this._sources[url] = true;
172
173            if (map.sourcesContent && map.sourcesContent[i])
174                this._sourceContentByURL[url] = map.sourcesContent[i];
175        }
176
177        this._sourceRoot = map.sourceRoot || null;
178
179        var stringCharIterator = new WebInspector.SourceMap.StringCharIterator(map.mappings);
180        var sourceURL = sources[sourceIndex];
181
182        while (true) {
183            if (stringCharIterator.peek() === ",")
184                stringCharIterator.next();
185            else {
186                while (stringCharIterator.peek() === ";") {
187                    lineNumber += 1;
188                    columnNumber = 0;
189                    stringCharIterator.next();
190                }
191                if (!stringCharIterator.hasNext())
192                    break;
193            }
194
195            columnNumber += this._decodeVLQ(stringCharIterator);
196            if (this._isSeparator(stringCharIterator.peek())) {
197                this._mappings.push([lineNumber, columnNumber]);
198                continue;
199            }
200
201            var sourceIndexDelta = this._decodeVLQ(stringCharIterator);
202            if (sourceIndexDelta) {
203                sourceIndex += sourceIndexDelta;
204                sourceURL = sources[sourceIndex];
205            }
206            sourceLineNumber += this._decodeVLQ(stringCharIterator);
207            sourceColumnNumber += this._decodeVLQ(stringCharIterator);
208            if (!this._isSeparator(stringCharIterator.peek()))
209                nameIndex += this._decodeVLQ(stringCharIterator);
210
211            this._mappings.push([lineNumber, columnNumber, sourceURL, sourceLineNumber, sourceColumnNumber]);
212        }
213
214        for (var i = 0; i < this._mappings.length; ++i) {
215            var mapping = this._mappings[i];
216            var url = mapping[2];
217            if (!url)
218                continue;
219            if (!this._reverseMappingsBySourceURL[url])
220                this._reverseMappingsBySourceURL[url] = [];
221            var reverseMappings = this._reverseMappingsBySourceURL[url];
222            var sourceLine = mapping[3];
223            if (!reverseMappings[sourceLine])
224                reverseMappings[sourceLine] = [mapping[0], mapping[1]];
225        }
226    },
227
228    _isSeparator: function(char)
229    {
230        return char === "," || char === ";";
231    },
232
233    _decodeVLQ: function(stringCharIterator)
234    {
235        // Read unsigned value.
236        var result = 0;
237        var shift = 0;
238        do {
239            var digit = this._base64Map[stringCharIterator.next()];
240            result += (digit & this._VLQ_BASE_MASK) << shift;
241            shift += this._VLQ_BASE_SHIFT;
242        } while (digit & this._VLQ_CONTINUATION_MASK);
243
244        // Fix the sign.
245        var negative = result & 1;
246        result >>= 1;
247        return negative ? -result : result;
248    },
249
250    _VLQ_BASE_SHIFT: 5,
251    _VLQ_BASE_MASK: (1 << 5) - 1,
252    _VLQ_CONTINUATION_MASK: 1 << 5
253}
254
255WebInspector.SourceMap.StringCharIterator = function(string)
256{
257    this._string = string;
258    this._position = 0;
259}
260
261WebInspector.SourceMap.StringCharIterator.prototype = {
262    next: function()
263    {
264        return this._string.charAt(this._position++);
265    },
266
267    peek: function()
268    {
269        return this._string.charAt(this._position);
270    },
271
272    hasNext: function()
273    {
274        return this._position < this._string.length;
275    }
276}
277