diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-19 00:47:55 +0000 |
commit | 26a029d407be480d791972afb5975cf62c9360a6 (patch) | |
tree | f435a8308119effd964b339f76abb83a57c29483 /devtools/client/shared/source-map-loader | |
parent | Initial commit. (diff) | |
download | firefox-26a029d407be480d791972afb5975cf62c9360a6.tar.xz firefox-26a029d407be480d791972afb5975cf62c9360a6.zip |
Adding upstream version 124.0.1.upstream/124.0.1
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/shared/source-map-loader')
41 files changed, 2763 insertions, 0 deletions
diff --git a/devtools/client/shared/source-map-loader/index.js b/devtools/client/shared/source-map-loader/index.js new file mode 100644 index 0000000000..ccdb86520f --- /dev/null +++ b/devtools/client/shared/source-map-loader/index.js @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +"use strict"; + +const { + WorkerDispatcher, +} = require("resource://devtools/client/shared/worker-utils.js"); +const EventEmitter = require("resource://devtools/shared/event-emitter.js"); +const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); + +const L10N = new LocalizationHelper( + "devtools/client/locales/toolbox.properties" +); + +const SOURCE_MAP_WORKER_URL = + "resource://devtools/client/shared/source-map-loader/worker.js"; + +const { + originalToGeneratedId, + generatedToOriginalId, + isGeneratedId, + isOriginalId, +} = require("resource://devtools/client/shared/source-map-loader/utils/index.js"); + +class SourceMapLoader extends WorkerDispatcher { + constructor(targetCommand) { + super(SOURCE_MAP_WORKER_URL); + this.#targetCommand = targetCommand; + } + + #setSourceMapForGeneratedSources = this.task( + "setSourceMapForGeneratedSources" + ); + #getOriginalURLs = this.task("getOriginalURLs"); + #getOriginalSourceText = this.task("getOriginalSourceText"); + #targetCommand = null; + + async getOriginalURLs(urlInfo) { + try { + return await this.#getOriginalURLs(urlInfo); + } catch (error) { + // Note that tests may not pass the targetCommand + if (this.#targetCommand) { + // Catch all errors and log them to the Web Console for users to see. + const message = L10N.getFormatStr( + "toolbox.sourceMapFailure", + error, + urlInfo.url, + urlInfo.sourceMapURL + ); + this.#targetCommand.targetFront.logWarningInPage(message, "source map"); + } + + // And re-throw the error so that the debugger can also interpret the error + throw error; + } + } + + hasOriginalURL = this.task("hasOriginalURL"); + getOriginalRanges = this.task("getOriginalRanges"); + + getGeneratedRanges = this.task("getGeneratedRanges", { + queue: true, + }); + getGeneratedLocation = this.task("getGeneratedLocation", { + queue: true, + }); + getOriginalLocation = this.task("getOriginalLocation", { + queue: true, + }); + + getOriginalLocations = this.task("getOriginalLocations"); + getGeneratedRangesForOriginal = this.task("getGeneratedRangesForOriginal"); + getFileGeneratedRange = this.task("getFileGeneratedRange"); + loadSourceMap = this.task("loadSourceMap"); + + async getOriginalSourceText(originalSourceId) { + try { + return await this.#getOriginalSourceText(originalSourceId); + } catch (error) { + const message = L10N.getFormatStr( + "toolbox.sourceMapSourceFailure", + error.message, + error.metadata ? error.metadata.url : "<unknown>" + ); + + // Note that tests may not pass the targetCommand + if (this.#targetCommand) { + // Catch all errors and log them to the Web Console for users to see. + this.#targetCommand.targetFront.logWarningInPage(message, "source map"); + } + + // Also replace the result with the error text. + // Note that this result has to have the same form + // as whatever the upstream getOriginalSourceText + // returns. + return { + text: message, + contentType: "text/plain", + }; + } + } + + clearSourceMaps = this.task("clearSourceMaps"); + getOriginalStackFrames = this.task("getOriginalStackFrames"); + + async setSourceMapForGeneratedSources(generatedIds, sourceMap) { + const rv = await this.#setSourceMapForGeneratedSources( + generatedIds, + sourceMap + ); + + // Notify and ensure waiting for the SourceMapURLService to process the source map before resolving. + // Otherwise tests start failing because of pending request made by this component. + await this.emitAsync("source-map-created", generatedIds); + + return rv; + } + + destroy() { + // Request to stop the underlying DOM Worker + this.stop(); + // Unregister all listeners + this.clearEvents(); + // SourceMapLoader may be leaked and so it is important to clear most outer references + this.#targetCommand = null; + } +} +EventEmitter.decorate(SourceMapLoader.prototype); + +module.exports = { + SourceMapLoader, + originalToGeneratedId, + generatedToOriginalId, + isGeneratedId, + isOriginalId, +}; diff --git a/devtools/client/shared/source-map-loader/moz.build b/devtools/client/shared/source-map-loader/moz.build new file mode 100644 index 0000000000..c9b399c4cb --- /dev/null +++ b/devtools/client/shared/source-map-loader/moz.build @@ -0,0 +1,18 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DIRS += [ + "utils", + "wasm-dwarf", +] + +DevToolsModules( + "index.js", + "source-map.js", + "worker.js", +) + +BROWSER_CHROME_MANIFESTS += ["test/browser/browser.toml"] diff --git a/devtools/client/shared/source-map-loader/source-map.js b/devtools/client/shared/source-map-loader/source-map.js new file mode 100644 index 0000000000..f493271236 --- /dev/null +++ b/devtools/client/shared/source-map-loader/source-map.js @@ -0,0 +1,646 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +"use strict"; + +/** + * Source Map Worker + * @module utils/source-map-worker + */ + +const { + SourceMapConsumer, +} = require("resource://devtools/client/shared/vendor/source-map/source-map.js"); + +// Initialize the source-map library right away so that all other code can use it. +SourceMapConsumer.initialize({ + "lib/mappings.wasm": + "resource://devtools/client/shared/vendor/source-map/lib/mappings.wasm", +}); + +const { + networkRequest, +} = require("resource://devtools/client/shared/source-map-loader/utils/network-request.js"); +const assert = require("resource://devtools/client/shared/source-map-loader/utils/assert.js"); +const { + fetchSourceMap, + resolveSourceMapURL, + hasOriginalURL, + clearOriginalURLs, +} = require("resource://devtools/client/shared/source-map-loader/utils/fetchSourceMap.js"); +const { + getSourceMap, + getSourceMapWithMetadata, + setSourceMap, + clearSourceMaps: clearSourceMapsRequests, +} = require("resource://devtools/client/shared/source-map-loader/utils/sourceMapRequests.js"); +const { + originalToGeneratedId, + generatedToOriginalId, + isGeneratedId, + isOriginalId, + getContentType, +} = require("resource://devtools/client/shared/source-map-loader/utils/index.js"); +const { + clearWasmXScopes, +} = require("resource://devtools/client/shared/source-map-loader/wasm-dwarf/wasmXScopes.js"); + +/** + * Create "original source info" objects being handed over to the main thread + * to describe original sources referenced in a source map + */ +function mapToOriginalSourceInfos(generatedId, urls) { + return urls.map(url => { + return { + id: generatedToOriginalId(generatedId, url), + url, + }; + }); +} + +/** + * Load the source map and retrieved infos about all the original sources + * referenced in that source map. + * + * @param {Object} generatedSource + * Source object for a bundle referencing a source map + * @return {Array<Object>|null} + * List of object with id and url attributes describing the original sources. + */ +async function getOriginalURLs(generatedSource) { + const { resolvedSourceMapURL, baseURL } = + resolveSourceMapURL(generatedSource); + const map = await fetchSourceMap( + generatedSource, + resolvedSourceMapURL, + baseURL + ); + return map ? mapToOriginalSourceInfos(generatedSource.id, map.sources) : null; +} + +/** + * Load the source map for a given bundle and return information + * about the related original sources and the source map itself. + * + * @param {Object} generatedSource + * Source object for the bundle. + * @return {Object} + * - {Array<Object>} sources + * Object with id and url attributes, refering to the related original sources + * referenced in the source map. + * - [String} resolvedSourceMapURL + * Absolute URL for the source map file. + * - {Array<String>} ignoreListUrls + * List of URLs of sources, designated by the source map, to be ignored in the debugger. + * - {String} exception + * In case of error, a string describing the situation. + */ +async function loadSourceMap(generatedSource) { + const { resolvedSourceMapURL, baseURL } = + resolveSourceMapURL(generatedSource); + try { + const map = await fetchSourceMap( + generatedSource, + resolvedSourceMapURL, + baseURL + ); + if (!map.sources.length) { + throw new Error("No sources are declared in this source map."); + } + let ignoreListUrls = []; + if (map.x_google_ignoreList?.length) { + ignoreListUrls = map.x_google_ignoreList.map( + sourceIndex => map.sources[sourceIndex] + ); + } + return { + sources: mapToOriginalSourceInfos(generatedSource.id, map.sources), + resolvedSourceMapURL, + ignoreListUrls, + }; + } catch (e) { + return { + sources: [], + resolvedSourceMapURL, + ignoreListUrls: [], + exception: e.message, + }; + } +} + +const COMPUTED_SPANS = new WeakSet(); + +const SOURCE_MAPPINGS = new WeakMap(); +async function getOriginalRanges(sourceId) { + if (!isOriginalId(sourceId)) { + return []; + } + + const generatedSourceId = originalToGeneratedId(sourceId); + const data = await getSourceMapWithMetadata(generatedSourceId); + if (!data) { + return []; + } + const { map } = data; + const url = data.urlsById.get(sourceId); + + let mappings = SOURCE_MAPPINGS.get(map); + if (!mappings) { + mappings = new Map(); + SOURCE_MAPPINGS.set(map, mappings); + } + + let fileMappings = mappings.get(url); + if (!fileMappings) { + fileMappings = []; + mappings.set(url, fileMappings); + + const originalMappings = fileMappings; + map.eachMapping( + mapping => { + if (mapping.source !== url) { + return; + } + + const last = originalMappings[originalMappings.length - 1]; + + if (last && last.line === mapping.originalLine) { + if (last.columnStart < mapping.originalColumn) { + last.columnEnd = mapping.originalColumn; + } else { + // Skip this duplicate original location, + return; + } + } + + originalMappings.push({ + line: mapping.originalLine, + columnStart: mapping.originalColumn, + columnEnd: Infinity, + }); + }, + null, + SourceMapConsumer.ORIGINAL_ORDER + ); + } + + return fileMappings; +} + +/** + * Given an original location, find the ranges on the generated file that + * are mapped from the original range containing the location. + */ +async function getGeneratedRanges(location) { + if (!isOriginalId(location.sourceId)) { + return []; + } + + const generatedSourceId = originalToGeneratedId(location.sourceId); + const data = await getSourceMapWithMetadata(generatedSourceId); + if (!data) { + return []; + } + const { urlsById, map } = data; + + if (!COMPUTED_SPANS.has(map)) { + COMPUTED_SPANS.add(map); + map.computeColumnSpans(); + } + + // We want to use 'allGeneratedPositionsFor' to get the _first_ generated + // location, but it hard-codes SourceMapConsumer.LEAST_UPPER_BOUND as the + // bias, making it search in the wrong direction for this usecase. + // To work around this, we use 'generatedPositionFor' and then look up the + // exact original location, making any bias value unnecessary, and then + // use that location for the call to 'allGeneratedPositionsFor'. + const genPos = map.generatedPositionFor({ + source: urlsById.get(location.sourceId), + line: location.line, + column: location.column == null ? 0 : location.column, + bias: SourceMapConsumer.GREATEST_LOWER_BOUND, + }); + if (genPos.line === null) { + return []; + } + + const positions = map.allGeneratedPositionsFor( + map.originalPositionFor({ + line: genPos.line, + column: genPos.column, + }) + ); + + return positions + .map(mapping => ({ + line: mapping.line, + columnStart: mapping.column, + columnEnd: mapping.lastColumn, + })) + .sort((a, b) => { + const line = a.line - b.line; + return line === 0 ? a.column - b.column : line; + }); +} + +async function getGeneratedLocation(location) { + if (!isOriginalId(location.sourceId)) { + return null; + } + + const generatedSourceId = originalToGeneratedId(location.sourceId); + const data = await getSourceMapWithMetadata(generatedSourceId); + if (!data) { + return null; + } + const { urlsById, map } = data; + + const positions = map.allGeneratedPositionsFor({ + source: urlsById.get(location.sourceId), + line: location.line, + column: location.column == null ? 0 : location.column, + }); + + // Prior to source-map 0.7, the source-map module returned the earliest + // generated location in the file when there were multiple generated + // locations. The current comparison fn in 0.7 does not appear to take + // generated location into account properly. + let match; + for (const pos of positions) { + if (!match || pos.line < match.line || pos.column < match.column) { + match = pos; + } + } + + if (!match) { + match = map.generatedPositionFor({ + source: urlsById.get(location.sourceId), + line: location.line, + column: location.column == null ? 0 : location.column, + bias: SourceMapConsumer.LEAST_UPPER_BOUND, + }); + } + + return { + sourceId: generatedSourceId, + line: match.line, + column: match.column, + }; +} + +/** + * Map the breakable positions (line and columns) from generated to original locations. + * + * @param {Object} breakpointPositions + * List of columns per line refering to the breakable columns per line + * for a given source: + * { + * 1: [2, 6], // On line 1, column 2 and 6 are breakable. + * ... + * } + * @param {string} sourceId + * The ID for the generated source. + */ +async function getOriginalLocations(breakpointPositions, sourceId) { + const map = await getSourceMap(sourceId); + if (!map) { + return null; + } + for (const line in breakpointPositions) { + const breakableColumnsPerLine = breakpointPositions[line]; + for (let i = 0; i < breakableColumnsPerLine.length; i++) { + const column = breakableColumnsPerLine[i]; + const mappedLocation = getOriginalLocationSync(map, { + sourceId, + line: parseInt(line, 10), + column, + }); + if (mappedLocation) { + // As we replace the `column` with the mappedLocation, + // also transfer the generated column so that we can compute both original and generated locations + // in the main thread. + mappedLocation.generatedColumn = column; + breakableColumnsPerLine[i] = mappedLocation; + } + } + } + return breakpointPositions; +} + +/** + * Query the source map for a mapping from bundle location to original location. + * + * @param {SourceMapConsumer} map + * The source map for the bundle source. + * @param {Object} location + * A location within a bundle to map to an original location. + * @param {Object} options + * @param {Boolean} options.looseSearch + * Optional, if true, will do a loose search on first column and next lines + * until a mapping is found. + * @return {location} + * The mapped location in the original source. + */ +function getOriginalLocationSync(map, location, { looseSearch = false } = {}) { + // First check for an exact match + let match = map.originalPositionFor({ + line: location.line, + column: location.column == null ? 0 : location.column, + }); + + // Then check for a loose match by sliding to first column and next lines + if (match.sourceUrl == null && looseSearch) { + let line = location.line; + // if a non-0 column was passed, we want to do the search from the beginning of the line, + // otherwise, we can start looking into next lines + let firstLineChecked = (location.column || 0) !== 0; + + // Avoid looping through the whole file and limit the sliding search to the next 10 lines. + while (match.sourceUrl === null && line < location.line + 10) { + if (firstLineChecked) { + line++; + } else { + firstLineChecked = true; + } + match = map.originalPositionFor({ + line, + column: 0, + bias: SourceMapConsumer.LEAST_UPPER_BOUND, + }); + } + } + + const { source: sourceUrl, line, column } = match; + + if (sourceUrl == null) { + // No url means the location didn't map. + return null; + } + + return { + sourceId: generatedToOriginalId(location.sourceId, sourceUrl), + sourceUrl, + line, + column, + }; +} + +/** + * Map a bundle location to an original one. + * + * @param {Object} location + * Bundle location + * @param {Object} options + * See getORiginalLocationSync. + * @return {Object} + * Original location + */ +async function getOriginalLocation(location, options) { + if (!isGeneratedId(location.sourceId)) { + return null; + } + + const map = await getSourceMap(location.sourceId); + if (!map) { + return null; + } + + return getOriginalLocationSync(map, location, options); +} + +async function getOriginalSourceText(originalSourceId) { + assert(isOriginalId(originalSourceId), "Source is not an original source"); + + const generatedSourceId = originalToGeneratedId(originalSourceId); + const data = await getSourceMapWithMetadata(generatedSourceId); + if (!data) { + return null; + } + const { urlsById, map } = data; + + const url = urlsById.get(originalSourceId); + let text = map.sourceContentFor(url, true); + if (!text) { + try { + const response = await networkRequest(url, { + sourceMapBaseURL: map.sourceMapBaseURL, + loadFromCache: false, + allowsRedirects: false, + }); + text = response.content; + } catch (err) { + // Workers exceptions are processed by worker-utils module and + // only metadata attribute is transferred between threads. + // Notify the main thread about which url failed loading. + err.metadata = { + url, + }; + throw err; + } + } + + return { + text, + contentType: getContentType(url || ""), + }; +} + +/** + * Find the set of ranges on the generated file that map from the original + * file's locations. + * + * @param sourceId - The original ID of the file we are processing. + * @param url - The original URL of the file we are processing. + * @param mergeUnmappedRegions - If unmapped regions are encountered between + * two mappings for the given original file, allow the two mappings to be + * merged anyway. This is useful if you are more interested in the general + * contiguous ranges associated with a file, rather than the specifics of + * the ranges provided by the sourcemap. + */ +const GENERATED_MAPPINGS = new WeakMap(); +async function getGeneratedRangesForOriginal( + sourceId, + mergeUnmappedRegions = false +) { + assert(isOriginalId(sourceId), "Source is not an original source"); + + const data = await getSourceMapWithMetadata(originalToGeneratedId(sourceId)); + // NOTE: this is only needed for Flow + if (!data) { + return []; + } + const { urlsById, map } = data; + const url = urlsById.get(sourceId); + + if (!COMPUTED_SPANS.has(map)) { + COMPUTED_SPANS.add(map); + map.computeColumnSpans(); + } + + if (!GENERATED_MAPPINGS.has(map)) { + GENERATED_MAPPINGS.set(map, new Map()); + } + + const generatedRangesMap = GENERATED_MAPPINGS.get(map); + if (!generatedRangesMap) { + return []; + } + + if (generatedRangesMap.has(sourceId)) { + // NOTE we need to coerce the result to an array for Flow + return generatedRangesMap.get(sourceId) || []; + } + + // Gather groups of mappings on the generated file, with new groups created + // if we cross a mapping for a different file. + let currentGroup = []; + const originalGroups = [currentGroup]; + map.eachMapping( + mapping => { + if (mapping.source === url) { + currentGroup.push({ + start: { + line: mapping.generatedLine, + column: mapping.generatedColumn, + }, + end: { + line: mapping.generatedLine, + // The lastGeneratedColumn value is an inclusive value so we add + // one to it to get the exclusive end position. + column: mapping.lastGeneratedColumn + 1, + }, + }); + } else if (typeof mapping.source === "string" && currentGroup.length) { + // If there is a URL, but it is for a _different_ file, we create a + // new group of mappings so that we can tell + currentGroup = []; + originalGroups.push(currentGroup); + } + }, + null, + SourceMapConsumer.GENERATED_ORDER + ); + + const generatedMappingsForOriginal = []; + if (mergeUnmappedRegions) { + // If we don't care about excluding unmapped regions, then we just need to + // create a range that is the fully encompasses each group, ignoring the + // empty space between each individual range. + for (const group of originalGroups) { + if (group.length) { + generatedMappingsForOriginal.push({ + start: group[0].start, + end: group[group.length - 1].end, + }); + } + } + } else { + let lastEntry; + for (const group of originalGroups) { + lastEntry = null; + for (const { start, end } of group) { + const lastEnd = lastEntry + ? wrappedMappingPosition(lastEntry.end) + : null; + + // If this entry comes immediately after the previous one, extend the + // range of the previous entry instead of adding a new one. + if ( + lastEntry && + lastEnd && + lastEnd.line === start.line && + lastEnd.column === start.column + ) { + lastEntry.end = end; + } else { + const newEntry = { start, end }; + generatedMappingsForOriginal.push(newEntry); + lastEntry = newEntry; + } + } + } + } + + generatedRangesMap.set(sourceId, generatedMappingsForOriginal); + return generatedMappingsForOriginal; +} + +function wrappedMappingPosition(pos) { + if (pos.column !== Infinity) { + return pos; + } + + // If the end of the entry consumes the whole line, treat it as wrapping to + // the next line. + return { + line: pos.line + 1, + column: 0, + }; +} + +async function getFileGeneratedRange(originalSourceId) { + assert(isOriginalId(originalSourceId), "Source is not an original source"); + + const data = await getSourceMapWithMetadata( + originalToGeneratedId(originalSourceId) + ); + if (!data) { + return null; + } + const { urlsById, map } = data; + + const start = map.generatedPositionFor({ + source: urlsById.get(originalSourceId), + line: 1, + column: 0, + bias: SourceMapConsumer.LEAST_UPPER_BOUND, + }); + + const end = map.generatedPositionFor({ + source: urlsById.get(originalSourceId), + line: Number.MAX_SAFE_INTEGER, + column: Number.MAX_SAFE_INTEGER, + bias: SourceMapConsumer.GREATEST_LOWER_BOUND, + }); + + return { + start, + end, + }; +} + +/** + * Set the sourceMap for multiple passed source ids. + * + * @param {Array<string>} generatedSourceIds + * @param {Object} map: An actual sourcemap (as generated with SourceMapGenerator#toJSON) + */ +function setSourceMapForGeneratedSources(generatedSourceIds, map) { + const sourceMapConsumer = new SourceMapConsumer(map); + for (const generatedId of generatedSourceIds) { + setSourceMap(generatedId, Promise.resolve(sourceMapConsumer)); + } +} + +function clearSourceMaps() { + clearSourceMapsRequests(); + clearWasmXScopes(); + clearOriginalURLs(); +} + +module.exports = { + getOriginalURLs, + loadSourceMap, + hasOriginalURL, + getOriginalRanges, + getGeneratedRanges, + getGeneratedLocation, + getOriginalLocation, + getOriginalLocations, + getOriginalSourceText, + getGeneratedRangesForOriginal, + getFileGeneratedRange, + setSourceMapForGeneratedSources, + clearSourceMaps, +}; diff --git a/devtools/client/shared/source-map-loader/test/browser/browser.toml b/devtools/client/shared/source-map-loader/test/browser/browser.toml new file mode 100644 index 0000000000..5a602e9eeb --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/browser.toml @@ -0,0 +1,21 @@ +[DEFAULT] +tags = "devtools" +subsuite = "devtools" +support-files = [ + "head.js", + "fixtures/*", + "!/devtools/client/shared/test/shared-head.js", + "!/devtools/client/shared/test/telemetry-test-helpers.js", +] + +["browser_getContentType.js"] + +["browser_locations.js"] + +["browser_source-map.js"] + +["browser_wasm-source-map.js"] +skip-if = [ + "http3", # Bug 1829298 + "http2", +] diff --git a/devtools/client/shared/source-map-loader/test/browser/browser_getContentType.js b/devtools/client/shared/source-map-loader/test/browser/browser_getContentType.js new file mode 100644 index 0000000000..5ac402951c --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/browser_getContentType.js @@ -0,0 +1,32 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Cover the automatic mapping of content type based on file extension + +const { + getContentType, + contentMapForTesting, +} = require("resource://devtools/client/shared/source-map-loader/utils/index.js"); + +add_task(async () => { + for (const ext in contentMapForTesting) { + is( + getContentType(`whatever.${ext}`), + contentMapForTesting[ext], + `${ext} file extension is correctly mapping the expected content type` + ); + } + is( + getContentType(`whateverjs`), + "text/plain", + `A valid extension in file name doesn't cause a special content type mapping` + ); + + is( + getContentType("whatever.platypus"), + "text/plain", + "Test unknown extension defaults to text plain" + ); +}); diff --git a/devtools/client/shared/source-map-loader/test/browser/browser_locations.js b/devtools/client/shared/source-map-loader/test/browser/browser_locations.js new file mode 100644 index 0000000000..4d43df5902 --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/browser_locations.js @@ -0,0 +1,141 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Covert getOriginalLocation and getGeneratedLocation functions. + +add_task(async function testGetOriginalLocation() { + await fetchFixtureSourceMap("bundle"); + + const generatedLocation = { + sourceId: "bundle.js", + line: 49, + }; + + const originalLocation = await gSourceMapLoader.getOriginalLocation( + generatedLocation + ); + Assert.deepEqual( + originalLocation, + { + column: 0, + line: 3, + sourceId: "bundle.js/originalSource-fe2c41d3535b76c158e39ba4f3ff826a", + sourceUrl: "webpack:///entry.js", + }, + "Mapped a generated location" + ); + + const originalLocation2 = await gSourceMapLoader.getOriginalLocation( + originalLocation + ); + Assert.deepEqual(originalLocation2, null, "No mapped location"); + + gSourceMapLoader.clearSourceMaps(); + const originalLocation3 = await gSourceMapLoader.getOriginalLocation( + generatedLocation + ); + Assert.deepEqual( + originalLocation3, + null, + "after clearing the source maps, the same generated location no longer maps" + ); +}); + +add_task(async function testGetGeneratedLocation() { + await fetchFixtureSourceMap("bundle"); + + const originalLocation = { + column: 0, + line: 3, + sourceId: "bundle.js/originalSource-fe2c41d3535b76c158e39ba4f3ff826a", + }; + + const source = { + url: "webpack:///entry.js", + id: "bundle.js/originalSource-fe2c41d3535b76c158e39ba4f3ff826a", + }; + + const generatedLocation = await gSourceMapLoader.getGeneratedLocation( + originalLocation, + source + ); + Assert.deepEqual( + generatedLocation, + { + sourceId: "bundle.js", + line: 49, + column: 0, + }, + "Map an original location" + ); + + { + gSourceMapLoader.clearSourceMaps(); + + const secondGeneratedLocation = await gSourceMapLoader.getGeneratedLocation( + originalLocation, + source + ); + Assert.deepEqual( + secondGeneratedLocation, + null, + "after clearing source maps, the same location no longer maps to an original location" + ); + } + + { + // we expect symmetric mappings, which means that if + // we map a generated location to an original location, + // and then map it back, we should get the original generated value. + // e.g. G[8, 0] -> O[5, 4] -> G[8, 0] + await fetchFixtureSourceMap("if.out"); + + const genLoc1 = { + sourceId: "if.out.js", + column: 0, + line: 8, + }; + + const ifSource = { + url: "if.js", + id: "if.out.js/originalSource-5ad3141023dae912c5f8833c7e03beeb", + }; + + const oLoc = await gSourceMapLoader.getOriginalLocation(genLoc1); + const genLoc2 = await gSourceMapLoader.getGeneratedLocation(oLoc, ifSource); + + Assert.deepEqual(genLoc2, genLoc1, "location mapping is symmetric"); + } + + { + // we expect that an undefined column will be handled like a + // location w/ column 0. e.g. G[8, u] -> O[5, 4] -> G[8, 0] + await fetchFixtureSourceMap("if.out"); + + const genLoc1 = { + sourceId: "if.out.js", + column: undefined, + line: 8, + }; + + const ifSource = { + url: "if.js", + id: "if.out.js/originalSource-5ad3141023dae912c5f8833c7e03beeb", + }; + + const oLoc = await gSourceMapLoader.getOriginalLocation(genLoc1); + const genLoc2 = await gSourceMapLoader.getGeneratedLocation(oLoc, ifSource); + + Assert.deepEqual( + genLoc2, + { + sourceId: "if.out.js", + column: 0, + line: 8, + }, + "undefined column is handled like 0 column" + ); + } +}); diff --git a/devtools/client/shared/source-map-loader/test/browser/browser_source-map.js b/devtools/client/shared/source-map-loader/test/browser/browser_source-map.js new file mode 100644 index 0000000000..34d4fedf27 --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/browser_source-map.js @@ -0,0 +1,163 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Cover the high level API of these modules: +// getOriginalURLs getGeneratedRangesForOriginal functions + +async function assertFixtureOriginalURLs( + fixtureName, + expectedUrls, + testMessage +) { + const originalSources = await fetchFixtureSourceMap(fixtureName); + const urls = originalSources.map(s => s.url); + Assert.deepEqual(urls, expectedUrls, testMessage); +} + +add_task(async function testGetOriginalURLs() { + await assertFixtureOriginalURLs( + "absolute", + ["https://example.com/cheese/heart.js"], + "Test absolute URL" + ); + + await assertFixtureOriginalURLs( + "bundle", + [ + "webpack:///webpack/bootstrap%204ef8c7ec7c1df790781e", + "webpack:///entry.js", + "webpack:///times2.js", + "webpack:///output.js", + "webpack:///opts.js", + ], + "Test source with a url" + ); + + await assertFixtureOriginalURLs( + "empty", + [`${URL_ROOT_SSL}fixtures/heart.js`], + "Test empty sourceRoot resolution" + ); + + await assertFixtureOriginalURLs( + "noroot", + [`${URL_ROOT_SSL}fixtures/heart.js`], + "Test Non-existing sourceRoot resolution" + ); + + await assertFixtureOriginalURLs( + "noroot2", + [`${URL_ROOT_SSL}fixtures/heart.js`], + "Test Non-existing sourceRoot resolution with relative URLs" + ); +}); + +add_task(async function testGetGeneratedRangesForOriginal() { + const originals = await fetchFixtureSourceMap("intermingled-sources"); + + const ranges = await gSourceMapLoader.getGeneratedRangesForOriginal( + originals[0].id + ); + + Assert.deepEqual( + ranges, + [ + { + start: { + line: 4, + column: 69, + }, + end: { + line: 9, + column: Infinity, + }, + }, + { + start: { + line: 11, + column: 0, + }, + end: { + line: 17, + column: 3, + }, + }, + { + start: { + line: 19, + column: 18, + }, + end: { + line: 19, + column: 22, + }, + }, + { + start: { + line: 26, + column: 0, + }, + end: { + line: 26, + column: Infinity, + }, + }, + { + start: { + line: 28, + column: 0, + }, + end: { + line: 28, + column: Infinity, + }, + }, + ], + "Test the overall generated ranges on the source" + ); + + { + // Note that we have to clear the source map in order to get the merged ranges, + // otherwise we are still fetching the previous unmerged ones! + const secondOriginals = await fetchFixtureSourceMap("intermingled-sources"); + const mergedRanges = await gSourceMapLoader.getGeneratedRangesForOriginal( + secondOriginals[0].id, + true + ); + + Assert.deepEqual( + mergedRanges, + [ + { + start: { + line: 4, + column: 69, + }, + end: { + line: 28, + column: Infinity, + }, + }, + ], + "Test the merged generated ranges on the source" + ); + } +}); + +add_task(async function testBaseURLErrorHandling() { + const source = { + id: "missingmap.js", + sourceMapURL: "missingmap.js.map", + // Notice the duplicated ":" which cause the error here + sourceMapBaseURL: "http:://example.com/", + }; + + try { + await gSourceMapLoader.getOriginalURLs(source); + ok(false, "Should throw"); + } catch (e) { + is(e.message, "URL constructor: http:://example.com/ is not a valid URL."); + } +}); diff --git a/devtools/client/shared/source-map-loader/test/browser/browser_wasm-source-map.js b/devtools/client/shared/source-map-loader/test/browser/browser_wasm-source-map.js new file mode 100644 index 0000000000..0f69d7b038 --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/browser_wasm-source-map.js @@ -0,0 +1,126 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// Test source mappings WASM sources. +// This test is quite general and test various functions. + +const { + WasmRemap, +} = require("resource://devtools/client/shared/source-map-loader/utils/wasmRemap.js"); +const { + SourceMapConsumer, +} = require("resource://devtools/client/shared/vendor/source-map/source-map.js"); + +SourceMapConsumer.initialize({ + "lib/mappings.wasm": + "resource://devtools/client/shared/vendor/source-map/lib/mappings.wasm", +}); + +add_task(async function smokeTest() { + const testMap1 = { + version: 3, + file: "min.js", + names: [], + sources: ["one.js", "two.js"], + sourceRoot: "/the/root", + mappings: "CAAC,IAAM,SACU,GAAC", + }; + const testMap1Entries = [ + { offset: 1, line: 1, column: 1 }, + { offset: 5, line: 1, column: 7 }, + { offset: 14, line: 2, column: 17 }, + { offset: 17, line: 2, column: 18 }, + ]; + + const map1 = await new SourceMapConsumer(testMap1); + + const remap1 = new WasmRemap(map1); + + is(remap1.file, "min.js"); + is(remap1.hasContentsOfAllSources(), false); + is(remap1.sources.length, 2); + is(remap1.sources[0], "/the/root/one.js"); + is(remap1.sources[1], "/the/root/two.js"); + + const expectedEntries = testMap1Entries.slice(0); + remap1.eachMapping(function (entry) { + const expected = expectedEntries.shift(); + is(entry.generatedLine, expected.offset); + is(entry.generatedColumn, 0); + is(entry.originalLine, expected.line); + is(entry.originalColumn, expected.column); + is(entry.name, null); + }); + + const pos1 = remap1.originalPositionFor({ line: 5, column: 0 }); + is(pos1.line, 1); + is(pos1.column, 7); + is(pos1.source, "/the/root/one.js"); + + const pos2 = remap1.generatedPositionFor({ + source: "/the/root/one.js", + line: 2, + column: 18, + }); + is(pos2.line, 17); + is(pos2.column, 0); + is(pos2.lastColumn, undefined); + + remap1.computeColumnSpans(); + const pos3 = remap1.allGeneratedPositionsFor({ + source: "/the/root/one.js", + line: 2, + column: 17, + }); + is(pos3.length, 1); + is(pos3[0].line, 14); + is(pos3[0].column, 0); + is(pos3[0].lastColumn, 0); + + map1.destroy(); +}); + +add_task(async function contentPresents() { + const testMap2 = { + version: 3, + file: "none.js", + names: [], + sources: ["zero.js"], + mappings: "", + sourcesContent: ["//test"], + }; + + const map2 = await new SourceMapConsumer(testMap2); + const remap2 = new WasmRemap(map2); + is(remap2.file, "none.js"); + ok(remap2.hasContentsOfAllSources()); + is(remap2.sourceContentFor("zero.js"), "//test"); + + map2.destroy(); +}); + +add_task(async function readAndTransposeWasmMap() { + const source = { + id: "wasm.js", + sourceMapBaseURL: "wasm:http://example.com/whatever/:min.js", + sourceMapURL: `${URL_ROOT}fixtures/wasm.js.map`, + isWasm: true, + }; + + const urls = await gSourceMapLoader.getOriginalURLs(source); + Assert.deepEqual(urls, [ + { + id: "wasm.js/originalSource-63954a1c231200652c0d99c6a69cd178", + url: `${URL_ROOT}fixtures/one.js`, + }, + ]); + + const { line, column } = await gSourceMapLoader.getOriginalLocation({ + sourceId: source.id, + line: 5, + }); + is(line, 1); + is(column, 7); +}); diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/absolute.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/absolute.js new file mode 100644 index 0000000000..adcc337eca --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/absolute.js @@ -0,0 +1,2 @@ +/* Doesn't really matter what is in here. */ +// # sourceMappingURL=absolute.js.map diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/absolute.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/absolute.js.map new file mode 100644 index 0000000000..0597515870 --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/absolute.js.map @@ -0,0 +1,10 @@ +{ + "version": 3, + "sources": [ + "heart.js" + ], + "names": [], + "mappings": ";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA,QAAO,SAAS;AAChB;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;;;;;ACfA;AACA;AACA;;;;;;;ACFA;AACA;AACA;;AAEA,mBAAkB;;;;;;;ACJlB;AACA;AACA", + "file": "absolute.js", + "sourceRoot": "https://example.com/cheese/" +} diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js new file mode 100644 index 0000000000..9ee477b856 --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js @@ -0,0 +1,94 @@ +/** ****/ (function(modules) { // webpackBootstrap +/** ****/ // The module cache +/** ****/ var installedModules = {}; +/** ****/ +/** ****/ // The require function +/** ****/ function __webpack_require__(moduleId) { +/** ****/ +/** ****/ // Check if module is in cache +/** ****/ if (installedModules[moduleId]) +/** ****/ {return installedModules[moduleId].exports;} +/** ****/ +/** ****/ // Create a new module (and put it into the cache) +/** ****/ var module = installedModules[moduleId] = { +/** ****/ exports: {}, +/** ****/ id: moduleId, +/** ****/ loaded: false +/** ****/ }; +/** ****/ +/** ****/ // Execute the module function +/** ****/ modules[moduleId].call(module.exports, module, module.exports, __webpack_require__); +/** ****/ +/** ****/ // Flag the module as loaded +/** ****/ module.loaded = true; +/** ****/ +/** ****/ // Return the exports of the module +/** ****/ return module.exports; +/** ****/ } +/** ****/ +/** ****/ +/** ****/ // expose the modules object (__webpack_modules__) +/** ****/ __webpack_require__.m = modules; +/** ****/ +/** ****/ // expose the module cache +/** ****/ __webpack_require__.c = installedModules; +/** ****/ +/** ****/ // __webpack_public_path__ +/** ****/ __webpack_require__.p = ""; +/** ****/ +/** ****/ // Load entry module and return exports +/** ****/ return __webpack_require__(0); +/** ****/ })([ +/* 0 */ +/** */ function(module, exports, __webpack_require__) { + + const times2 = __webpack_require__(1); + const { output } = __webpack_require__(2); + const opts = __webpack_require__(3); + + output(times2(1)); + output(times2(2)); + + if (opts.extra) { + output(times2(3)); +} + + window.keepMeAlive = function() { + // This function exists to make sure this script is never garbage + // collected. It is also callable from tests. + return times2(4); +}; + +/** * +/ }, +/* 1 */ +/** */ function(module, exports) { + + module.exports = function(x) { + return x * 2; +}; + +/** * +/ }, +/* 2 */ +/** */ function(module, exports) { + + function output(str) { + console.log(str); +} + + module.exports = { output }; + +/** * +/ }, +/* 3 */ +/** */ function(module, exports) { + + module.exports = { + extra: true +}; + +/** * +/ } +/** ****/ ]); +// # sourceMappingURL=bundle.js.map diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js.map new file mode 100644 index 0000000000..f53036cb8e --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/bundle.js.map @@ -0,0 +1,21 @@ +{ + "version": 3, + "sources": [ + "webpack:///webpack/bootstrap 4ef8c7ec7c1df790781e", + "webpack:///./entry.js", + "webpack:///./times2.js", + "webpack:///./output.js", + "webpack:///./opts.js" + ], + "names": [], + "mappings": ";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA,QAAO,SAAS;AAChB;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;;;;;ACfA;AACA;AACA;;;;;;;ACFA;AACA;AACA;;AAEA,mBAAkB;;;;;;;ACJlB;AACA;AACA", + "file": "bundle.js", + "sourcesContent": [ + " \t// The module cache\n \tvar installedModules = {};\n\n \t// The require function\n \tfunction __webpack_require__(moduleId) {\n\n \t\t// Check if module is in cache\n \t\tif(installedModules[moduleId])\n \t\t\treturn installedModules[moduleId].exports;\n\n \t\t// Create a new module (and put it into the cache)\n \t\tvar module = installedModules[moduleId] = {\n \t\t\texports: {},\n \t\t\tid: moduleId,\n \t\t\tloaded: false\n \t\t};\n\n \t\t// Execute the module function\n \t\tmodules[moduleId].call(module.exports, module, module.exports, __webpack_require__);\n\n \t\t// Flag the module as loaded\n \t\tmodule.loaded = true;\n\n \t\t// Return the exports of the module\n \t\treturn module.exports;\n \t}\n\n\n \t// expose the modules object (__webpack_modules__)\n \t__webpack_require__.m = modules;\n\n \t// expose the module cache\n \t__webpack_require__.c = installedModules;\n\n \t// __webpack_public_path__\n \t__webpack_require__.p = \"\";\n\n \t// Load entry module and return exports\n \treturn __webpack_require__(0);\n\n\n\n// WEBPACK FOOTER //\n// webpack/bootstrap 4ef8c7ec7c1df790781e", + "const times2 = require(\"./times2\");\nconst { output } = require(\"./output\");\nconst opts = require(\"./opts\");\n\noutput(times2(1));\noutput(times2(2));\n\nif(opts.extra) {\n output(times2(3));\n}\n\nwindow.keepMeAlive = function() {\n // This function exists to make sure this script is never garbage\n // collected. It is also callable from tests.\n return times2(4);\n}\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./entry.js\n// module id = 0\n// module chunks = 0", + "module.exports = function(x) {\n return x * 2;\n}\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./times2.js\n// module id = 1\n// module chunks = 0", + "function output(str) {\n console.log(str);\n}\n\nmodule.exports = { output };\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./output.js\n// module id = 2\n// module chunks = 0", + "module.exports = {\n extra: true\n};\n\n\n\n//////////////////\n// WEBPACK FOOTER\n// ./opts.js\n// module id = 3\n// module chunks = 0" + ], + "sourceRoot": "" +} diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/empty.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/empty.js new file mode 100644 index 0000000000..72c472a745 --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/empty.js @@ -0,0 +1,2 @@ +/* Doesn't really matter what is in here. */ +// # sourceMappingURL=empty.js.map diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/empty.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/empty.js.map new file mode 100644 index 0000000000..9ebfec598d --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/empty.js.map @@ -0,0 +1,10 @@ +{ + "version": 3, + "sources": [ + "heart.js" + ], + "names": [], + "mappings": ";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA,QAAO,SAAS;AAChB;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;;;;;ACfA;AACA;AACA;;;;;;;ACFA;AACA;AACA;;AAEA,mBAAkB;;;;;;;ACJlB;AACA;AACA", + "file": "empty.js", + "sourceRoot": "" +} diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/if.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/if.js new file mode 100644 index 0000000000..4ee69b19ef --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/if.js @@ -0,0 +1,12 @@ +function componentWillReceiveProps(nextProps) { + console.log("start"); + const { selectedSource } = nextProps; + + if ( + nextProps.startPanelSize !== this.props.startPanelSize || + nextProps.endPanelSize !== this.props.endPanelSize + ) { + this.state.editor.codeMirror.setSize(); + } + console.log("done"); +} diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/if.out.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/if.out.js new file mode 100644 index 0000000000..9bb0752a92 --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/if.out.js @@ -0,0 +1,16 @@ +"use strict"; + +function componentWillReceiveProps(nextProps) { + console.log("start"); + var selectedSource = nextProps.selectedSource; + + if ( + nextProps.startPanelSize !== this.props.startPanelSize || + nextProps.endPanelSize !== this.props.endPanelSize + ) { + this.state.editor.codeMirror.setSize(); + } + console.log("done"); +} + +//# sourceMappingURL=if.out.js.map diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/if.out.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/if.out.js.map new file mode 100644 index 0000000000..27feb5278f --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/if.out.js.map @@ -0,0 +1,7 @@ +{ + "version":3, + "sources":["if.js"], + "names":[], + "mappings":";;AAAA,SAAS,yBAAT,CAAmC,SAAnC,EAA8C;AAC7C,UAAQ,GAAR,CAAY,OAAZ;AAD6C,MAElC,cAFkC,GAEf,SAFe,CAElC,cAFkC;;;AAI1C,MACE,UAAU,cAAV,KAA6B,KAAK,KAAL,CAAW,cAAxC,IACA,UAAU,YAAV,KAA2B,KAAK,KAAL,CAAW,YAFxC,EAGE;AACA,SAAK,KAAL,CAAW,MAAX,CAAkB,UAAlB,CAA6B,OAA7B;AACD;AACJ,UAAQ,GAAR,CAAY,MAAZ;AACA","file":"if.out.js", + "sourcesContent":["function componentWillReceiveProps(nextProps) {\n\tconsole.log('start');\n const { selectedSource } = nextProps;\n\n if (\n nextProps.startPanelSize !== this.props.startPanelSize ||\n nextProps.endPanelSize !== this.props.endPanelSize\n ) {\n this.state.editor.codeMirror.setSize();\n }\n\tconsole.log('done');\n}\n"] +} diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/intermingled-sources.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/intermingled-sources.js new file mode 100644 index 0000000000..c3d9a150bd --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/intermingled-sources.js @@ -0,0 +1,62 @@ +"use strict"; + +var decl = (function() { + var _ref = _asyncToGenerator( + /*#__PURE__*/ regeneratorRuntime.mark(function _callee() { + return regeneratorRuntime.wrap( + function _callee$(_context) { + while (1) { + switch ((_context.prev = _context.next)) { + case 0: + console.log("2"); + + case 1: + case "end": + return _context.stop(); + } + } + }, + _callee, + this + ); + }) + ); + + return function decl() { + return _ref.apply(this, arguments); + }; +})(); + +function _asyncToGenerator(fn) { + return function() { + var gen = fn.apply(this, arguments); + return new Promise(function(resolve, reject) { + function step(key, arg) { + try { + var info = gen[key](arg); + var value = info.value; + } catch (error) { + reject(error); + return; + } + if (info.done) { + resolve(value); + } else { + return Promise.resolve(value).then( + function(value) { + step("next", value); + }, + function(err) { + step("throw", err); + } + ); + } + } + return step("next"); + }); + }; +} + +console.log("1"); + +console.log("3"); diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/intermingled-sources.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/intermingled-sources.js.map new file mode 100644 index 0000000000..2876b9900b --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/intermingled-sources.js.map @@ -0,0 +1,8 @@ +{ + "version":3, + "file":"output.js", + "mappings":";;;qEAEA;AAAA;AAAA;AAAA;AAAA;AACE,oBAAQ,GAAR,CAAY,GAAZ;;AADF;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,G;;kBAAe,I;;;;;;;AAFf,QAAQ,GAAR,CAAY,GAAZ;;AAMA,QAAQ,GAAR,CAAY,GAAZ", + "sources":["input.js"], + "sourcesContent":["console.log(\"1\")\n\nasync function decl(){\n console.log(\"2\");\n}\n\nconsole.log(\"3\");"], + "names":[] +}
\ No newline at end of file diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/missingmap.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/missingmap.js new file mode 100644 index 0000000000..5a162652a7 --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/missingmap.js @@ -0,0 +1,2 @@ +// Source where the map is missing. +// # sourceMappingURL=missingmap.js.map diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot.js new file mode 100644 index 0000000000..33d15cf494 --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot.js @@ -0,0 +1,2 @@ +/* Doesn't really matter what is in here. */ +// # sourceMappingURL=noroot.js.map diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot.js.map new file mode 100644 index 0000000000..dfeee2d765 --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot.js.map @@ -0,0 +1,9 @@ +{ + "version": 3, + "sources": [ + "heart.js" + ], + "names": [], + "mappings": ";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA,QAAO,SAAS;AAChB;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;;;;;ACfA;AACA;AACA;;;;;;;ACFA;AACA;AACA;;AAEA,mBAAkB;;;;;;;ACJlB;AACA;AACA", + "file": "noroot.js" +} diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot2.js b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot2.js new file mode 100644 index 0000000000..35aa77b92c --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot2.js @@ -0,0 +1,2 @@ +/* Doesn't really matter what is in here. */ +// # sourceMappingURL=noroot2.js.map diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot2.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot2.js.map new file mode 100644 index 0000000000..1d22100178 --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/noroot2.js.map @@ -0,0 +1,10 @@ +{ + "version": 3, + "sources": [ + "heart.js" + ], + "names": [], + "mappings": ";AAAA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA,uBAAe;AACf;AACA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;AACA;;;AAGA;AACA;;AAEA;AACA;;AAEA;AACA;;AAEA;AACA;;;;;;;ACtCA;AACA,QAAO,SAAS;AAChB;;AAEA;AACA;;AAEA;AACA;AACA;;AAEA;AACA;AACA;AACA;AACA;;;;;;;ACfA;AACA;AACA;;;;;;;ACFA;AACA;AACA;;AAEA,mBAAkB;;;;;;;ACJlB;AACA;AACA", + "file": "noroot.js", + "sourceContents": ["this is the full text"] +} diff --git a/devtools/client/shared/source-map-loader/test/browser/fixtures/wasm.js.map b/devtools/client/shared/source-map-loader/test/browser/fixtures/wasm.js.map new file mode 100644 index 0000000000..828f6a9aa4 --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/fixtures/wasm.js.map @@ -0,0 +1,7 @@ +{ + "version": 3, + "file": "wasm.js", + "names": [], + "sources": ["one.js"], + "mappings": "CAAC,IAAM" +} diff --git a/devtools/client/shared/source-map-loader/test/browser/head.js b/devtools/client/shared/source-map-loader/test/browser/head.js new file mode 100644 index 0000000000..0f54e5a5f5 --- /dev/null +++ b/devtools/client/shared/source-map-loader/test/browser/head.js @@ -0,0 +1,27 @@ +/* Any copyright is dedicated to the Public Domain. + http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +Services.scriptloader.loadSubScript( + "chrome://mochitests/content/browser/devtools/client/shared/test/shared-head.js", + this +); + +const { + SourceMapLoader, +} = require("resource://devtools/client/shared/source-map-loader/index.js"); + +const gSourceMapLoader = new SourceMapLoader(); + +function fetchFixtureSourceMap(name) { + gSourceMapLoader.clearSourceMaps(); + + const source = { + id: `${name}.js`, + sourceMapURL: `${name}.js.map`, + sourceMapBaseURL: `${URL_ROOT_SSL}fixtures/${name}.js`, + }; + + return gSourceMapLoader.getOriginalURLs(source); +} diff --git a/devtools/client/shared/source-map-loader/utils/assert.js b/devtools/client/shared/source-map-loader/utils/assert.js new file mode 100644 index 0000000000..e784100796 --- /dev/null +++ b/devtools/client/shared/source-map-loader/utils/assert.js @@ -0,0 +1,13 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +"use strict"; + +function assert(condition, message) { + if (!condition) { + throw new Error(`Assertion failure: ${message}`); + } +} + +module.exports = assert; diff --git a/devtools/client/shared/source-map-loader/utils/fetchSourceMap.js b/devtools/client/shared/source-map-loader/utils/fetchSourceMap.js new file mode 100644 index 0000000000..0fd8b15c25 --- /dev/null +++ b/devtools/client/shared/source-map-loader/utils/fetchSourceMap.js @@ -0,0 +1,139 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +"use strict"; + +const { + networkRequest, +} = require("resource://devtools/client/shared/source-map-loader/utils/network-request"); + +const { + SourceMapConsumer, +} = require("resource://devtools/client/shared/vendor/source-map/source-map.js"); +const { + getSourceMap, + setSourceMap, +} = require("resource://devtools/client/shared/source-map-loader/utils/sourceMapRequests"); +const { + WasmRemap, +} = require("resource://devtools/client/shared/source-map-loader/utils/wasmRemap"); +const { + convertToJSON, +} = require("resource://devtools/client/shared/source-map-loader/wasm-dwarf/convertToJSON"); + +// URLs which have been seen in a completed source map request. +const originalURLs = new Set(); + +function clearOriginalURLs() { + originalURLs.clear(); +} + +function hasOriginalURL(url) { + return originalURLs.has(url); +} + +function resolveSourceMapURL(source) { + let { sourceMapBaseURL, sourceMapURL } = source; + sourceMapBaseURL = sourceMapBaseURL || ""; + sourceMapURL = sourceMapURL || ""; + + if (!sourceMapBaseURL) { + // If the source doesn't have a URL, don't resolve anything. + return { resolvedSourceMapURL: sourceMapURL, baseURL: sourceMapURL }; + } + + let resolvedString; + let baseURL; + + // When the sourceMap is a data: URL, fall back to using the source's URL, + // if possible. We don't use `new URL` here because it will be _very_ slow + // for large inlined source-maps, and we don't actually need to parse them. + if (sourceMapURL.startsWith("data:")) { + resolvedString = sourceMapURL; + baseURL = sourceMapBaseURL; + } else { + resolvedString = new URL( + sourceMapURL, + // If the URL is a data: URL, the sourceMapURL needs to be absolute, so + // we might as well pass `undefined` to avoid parsing a potentially + // very large data: URL for no reason. + sourceMapBaseURL.startsWith("data:") ? undefined : sourceMapBaseURL + ).toString(); + baseURL = resolvedString; + } + + return { resolvedSourceMapURL: resolvedString, baseURL }; +} + +async function _fetch(generatedSource, resolvedSourceMapURL, baseURL) { + let fetched = await networkRequest(resolvedSourceMapURL, { + loadFromCache: false, + // Blocking redirects on the sourceMappingUrl as its not easy to verify if the + // redirect protocol matches the supported ones. + allowRedirects: false, + sourceMapBaseURL: generatedSource.sourceMapBaseURL, + }); + + if (fetched.isDwarf) { + fetched = { content: await convertToJSON(fetched.content) }; + } + + // Create the source map and fix it up. + let map = await new SourceMapConsumer(fetched.content, baseURL); + + if (generatedSource.isWasm) { + map = new WasmRemap(map); + // Check if experimental scope info exists. + if (fetched.content.includes("x-scopes")) { + const parsedJSON = JSON.parse(fetched.content); + map.xScopes = parsedJSON["x-scopes"]; + } + } + + // Extend the default map object with sourceMapBaseURL, used to check further + // network requests made for this sourcemap. + map.sourceMapBaseURL = generatedSource.sourceMapBaseURL; + + if (map && map.sources) { + map.sources.forEach(url => originalURLs.add(url)); + } + + return map; +} + +function fetchSourceMap(generatedSource, resolvedSourceMapURL, baseURL) { + const existingRequest = getSourceMap(generatedSource.id); + + // If it has already been requested, return the request. Make sure + // to do this even if sourcemapping is turned off, because + // pretty-printing uses sourcemaps. + // + // An important behavior here is that if it's in the middle of + // requesting it, all subsequent calls will block on the initial + // request. + if (existingRequest) { + return existingRequest; + } + + if (!generatedSource.sourceMapURL) { + return null; + } + + // Fire off the request, set it in the cache, and return it. + const req = _fetch(generatedSource, resolvedSourceMapURL, baseURL); + // Make sure the cached promise does not reject, because we only + // want to report the error once. + setSourceMap( + generatedSource.id, + req.catch(() => null) + ); + return req; +} + +module.exports = { + fetchSourceMap, + hasOriginalURL, + clearOriginalURLs, + resolveSourceMapURL, +}; diff --git a/devtools/client/shared/source-map-loader/utils/getOriginalStackFrames.js b/devtools/client/shared/source-map-loader/utils/getOriginalStackFrames.js new file mode 100644 index 0000000000..c1ffee10e4 --- /dev/null +++ b/devtools/client/shared/source-map-loader/utils/getOriginalStackFrames.js @@ -0,0 +1,38 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +"use strict"; + +const { + getWasmXScopes, +} = require("resource://devtools/client/shared/source-map-loader/wasm-dwarf/wasmXScopes"); +const { + getSourceMap, +} = require("resource://devtools/client/shared/source-map-loader/utils/sourceMapRequests"); +const { + generatedToOriginalId, +} = require("resource://devtools/client/shared/source-map-loader/utils/index"); + +// Returns expanded stack frames details based on the generated location. +// The function return null if not information was found. +async function getOriginalStackFrames(generatedLocation) { + const wasmXScopes = await getWasmXScopes(generatedLocation.sourceId, { + getSourceMap, + generatedToOriginalId, + }); + if (!wasmXScopes) { + return null; + } + + const scopes = wasmXScopes.search(generatedLocation); + if (scopes.length === 0) { + console.warn("Something wrong with debug data: none original frames found"); + return null; + } + return scopes; +} + +module.exports = { + getOriginalStackFrames, +}; diff --git a/devtools/client/shared/source-map-loader/utils/index.js b/devtools/client/shared/source-map-loader/utils/index.js new file mode 100644 index 0000000000..1bcfcfac7d --- /dev/null +++ b/devtools/client/shared/source-map-loader/utils/index.js @@ -0,0 +1,103 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +"use strict"; + +const md5 = require("resource://devtools/client/shared/vendor/md5.js"); + +function originalToGeneratedId(sourceId) { + if (isGeneratedId(sourceId)) { + return sourceId; + } + + const lastIndex = sourceId.lastIndexOf("/originalSource"); + + return lastIndex !== -1 ? sourceId.slice(0, lastIndex) : ""; +} + +const getMd5 = memoize(url => md5(url)); + +function generatedToOriginalId(generatedId, url) { + return `${generatedId}/originalSource-${getMd5(url)}`; +} + +function isOriginalId(id) { + return id.includes("/originalSource"); +} + +function isGeneratedId(id) { + return !isOriginalId(id); +} + +/** + * Trims the query part or reference identifier of a URL string, if necessary. + */ +function trimUrlQuery(url) { + const length = url.length; + + for (let i = 0; i < length; ++i) { + if (url[i] === "?" || url[i] === "&" || url[i] === "#") { + return url.slice(0, i); + } + } + + return url; +} + +// Map suffix to content type. +const contentMap = { + js: "text/javascript", + jsm: "text/javascript", + mjs: "text/javascript", + ts: "text/typescript", + tsx: "text/typescript-jsx", + jsx: "text/jsx", + vue: "text/vue", + coffee: "text/coffeescript", + elm: "text/elm", + cljc: "text/x-clojure", + cljs: "text/x-clojurescript", +}; + +/** + * Returns the content type for the specified URL. If no specific + * content type can be determined, "text/plain" is returned. + * + * @return String + * The content type. + */ +function getContentType(url) { + url = trimUrlQuery(url); + const dot = url.lastIndexOf("."); + if (dot >= 0) { + const name = url.substring(dot + 1); + if (name in contentMap) { + return contentMap[name]; + } + } + return "text/plain"; +} + +function memoize(func) { + const map = new Map(); + + return arg => { + if (map.has(arg)) { + return map.get(arg); + } + + const result = func(arg); + map.set(arg, result); + return result; + }; +} + +module.exports = { + originalToGeneratedId, + generatedToOriginalId, + isOriginalId, + isGeneratedId, + getContentType, + contentMapForTesting: contentMap, +}; diff --git a/devtools/client/shared/source-map-loader/utils/moz.build b/devtools/client/shared/source-map-loader/utils/moz.build new file mode 100644 index 0000000000..ffa77d72b7 --- /dev/null +++ b/devtools/client/shared/source-map-loader/utils/moz.build @@ -0,0 +1,15 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "assert.js", + "fetchSourceMap.js", + "getOriginalStackFrames.js", + "index.js", + "network-request.js", + "sourceMapRequests.js", + "wasmRemap.js", +) diff --git a/devtools/client/shared/source-map-loader/utils/network-request.js b/devtools/client/shared/source-map-loader/utils/network-request.js new file mode 100644 index 0000000000..f331dc446e --- /dev/null +++ b/devtools/client/shared/source-map-loader/utils/network-request.js @@ -0,0 +1,43 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +"use strict"; + +async function networkRequest(url, opts) { + const supportedProtocols = ["http:", "https:", "data:"]; + + // Add file, chrome or moz-extension if the initial source was served by the + // same protocol. + const ADDITIONAL_PROTOCOLS = ["chrome:", "file:", "moz-extension:"]; + for (const protocol of ADDITIONAL_PROTOCOLS) { + if (opts.sourceMapBaseURL?.startsWith(protocol)) { + supportedProtocols.push(protocol); + } + } + + if (supportedProtocols.every(protocol => !url.startsWith(protocol))) { + throw new Error(`unsupported protocol for sourcemap request ${url}`); + } + + const response = await fetch(url, { + cache: opts.loadFromCache ? "default" : "no-cache", + redirect: opts.allowRedirects ? "follow" : "error", + }); + + if (response.ok) { + if (response.headers.get("Content-Type") === "application/wasm") { + const buffer = await response.arrayBuffer(); + return { + content: buffer, + isDwarf: true, + }; + } + const text = await response.text(); + return { content: text }; + } + + throw new Error(`request failed with status ${response.status}`); +} + +module.exports = { networkRequest }; diff --git a/devtools/client/shared/source-map-loader/utils/sourceMapRequests.js b/devtools/client/shared/source-map-loader/utils/sourceMapRequests.js new file mode 100644 index 0000000000..95f0420cba --- /dev/null +++ b/devtools/client/shared/source-map-loader/utils/sourceMapRequests.js @@ -0,0 +1,96 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +"use strict"; + +const { + generatedToOriginalId, +} = require("resource://devtools/client/shared/source-map-loader/utils/index.js"); + +const sourceMapRequests = new Map(); + +function clearSourceMaps() { + for (const [, metadataPromise] of sourceMapRequests) { + // The source-map module leaks memory unless `.destroy` is called on + // the consumer instances when they are no longer being used. + metadataPromise.then( + metadata => { + if (metadata) { + metadata.map.destroy(); + } + }, + // We don't want this to cause any unhandled rejection errors. + () => {} + ); + } + + sourceMapRequests.clear(); +} + +/** + * For a given generated source, retrieve an object with many attributes: + * @param {String} generatedSourceId + * The id of the generated source + * + * @return {Object} Meta data object with many attributes + * - map: The SourceMapConsumer or WasmRemap instance + * - urlsById Map of Original Source ID (string) to Source URL (string) + * - sources: Array of object with the two following attributes: + * - id: Original Source ID (string) + * - url: Original Source URL (string) + */ +function getSourceMapWithMetadata(generatedSourceId) { + return sourceMapRequests.get(generatedSourceId); +} + +/** + * Retrieve the SourceMapConsumer or WasmRemap instance for a given generated source. + * + * @param {String} generatedSourceId + * The id of the generated source + * + * @return null | Promise<SourceMapConsumer | WasmRemap> + */ +function getSourceMap(generatedSourceId) { + const request = getSourceMapWithMetadata(generatedSourceId); + if (!request) { + return null; + } + + return request.then(result => result?.map); +} + +/** + * Record the SourceMapConsumer or WasmRemap instance for a given generated source. + * + * @param {String} generatedId + * The generated source ID. + * @param {Promise<SourceMapConsumer or WasmRemap>} request + * A promise which should resolve to either a SourceMapConsume or WasmRemap instance. + */ +function setSourceMap(generatedId, request) { + sourceMapRequests.set( + generatedId, + request.then(map => { + if (!map || !map.sources) { + return null; + } + + const urlsById = new Map(); + + for (const url of map.sources) { + const id = generatedToOriginalId(generatedId, url); + urlsById.set(id, url); + } + return { map, urlsById }; + }) + ); +} + +module.exports = { + clearSourceMaps, + getSourceMapWithMetadata, + getSourceMap, + setSourceMap, +}; diff --git a/devtools/client/shared/source-map-loader/utils/wasmRemap.js b/devtools/client/shared/source-map-loader/utils/wasmRemap.js new file mode 100644 index 0000000000..6b831c2919 --- /dev/null +++ b/devtools/client/shared/source-map-loader/utils/wasmRemap.js @@ -0,0 +1,107 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +"use strict"; + +/** + * SourceMapConsumer for WebAssembly source maps. It transposes columns with + * lines, which allows mapping data to be used with SpiderMonkey Debugger API. + */ +class WasmRemap { + /** + * @param map SourceMapConsumer + */ + constructor(map) { + this._map = map; + this.version = map.version; + this.file = map.file; + this._computeColumnSpans = false; + } + + get sources() { + return this._map.sources; + } + + get sourceRoot() { + return this._map.sourceRoot; + } + + get names() { + return this._map.names; + } + + get sourcesContent() { + return this._map.sourcesContent; + } + + get mappings() { + throw new Error("not supported"); + } + + computeColumnSpans() { + this._computeColumnSpans = true; + } + + originalPositionFor(generatedPosition) { + const result = this._map.originalPositionFor({ + line: 1, + column: generatedPosition.line, + bias: generatedPosition.bias, + }); + return result; + } + + _remapGeneratedPosition(position) { + const generatedPosition = { + line: position.column, + column: 0, + }; + if (this._computeColumnSpans) { + generatedPosition.lastColumn = 0; + } + return generatedPosition; + } + + generatedPositionFor(originalPosition) { + const position = this._map.generatedPositionFor(originalPosition); + return this._remapGeneratedPosition(position); + } + + allGeneratedPositionsFor(originalPosition) { + const positions = this._map.allGeneratedPositionsFor(originalPosition); + return positions.map(position => { + return this._remapGeneratedPosition(position); + }); + } + + hasContentsOfAllSources() { + return this._map.hasContentsOfAllSources(); + } + + sourceContentFor(source, returnNullOnMissing) { + return this._map.sourceContentFor(source, returnNullOnMissing); + } + + eachMapping(callback, context, order) { + this._map.eachMapping( + entry => { + const { source, generatedColumn, originalLine, originalColumn, name } = + entry; + callback({ + source, + generatedLine: generatedColumn, + generatedColumn: 0, + lastGeneratedColumn: 0, + originalLine, + originalColumn, + name, + }); + }, + context, + order + ); + } +} + +exports.WasmRemap = WasmRemap; diff --git a/devtools/client/shared/source-map-loader/wasm-dwarf/convertToJSON.js b/devtools/client/shared/source-map-loader/wasm-dwarf/convertToJSON.js new file mode 100644 index 0000000000..9faae5c901 --- /dev/null +++ b/devtools/client/shared/source-map-loader/wasm-dwarf/convertToJSON.js @@ -0,0 +1,66 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +/* eslint camelcase: 0*/ + +"use strict"; + +const { + getDwarfToWasmData, +} = require("resource://devtools/client/shared/source-map-loader/wasm-dwarf/wasmAsset.js"); + +let cachedWasmModule; +let utf8Decoder; + +function convertDwarf(wasm, instance) { + const { memory, alloc_mem, free_mem, convert_dwarf } = instance.exports; + const wasmPtr = alloc_mem(wasm.byteLength); + new Uint8Array(memory.buffer, wasmPtr, wasm.byteLength).set( + new Uint8Array(wasm) + ); + const resultPtr = alloc_mem(12); + const enableXScopes = true; + const success = convert_dwarf( + wasmPtr, + wasm.byteLength, + resultPtr, + resultPtr + 4, + enableXScopes + ); + free_mem(wasmPtr); + const resultView = new DataView(memory.buffer, resultPtr, 12); + const outputPtr = resultView.getUint32(0, true), + outputLen = resultView.getUint32(4, true); + free_mem(resultPtr); + if (!success) { + throw new Error("Unable to convert from DWARF sections"); + } + if (!utf8Decoder) { + utf8Decoder = new TextDecoder("utf-8"); + } + const output = utf8Decoder.decode( + new Uint8Array(memory.buffer, outputPtr, outputLen) + ); + free_mem(outputPtr); + return output; +} + +async function convertToJSON(buffer) { + // Note: We don't 'await' here because it could mean that multiple + // calls to 'convertToJSON' could cause multiple fetches to be started. + cachedWasmModule = cachedWasmModule || loadConverterModule(); + + return convertDwarf(buffer, await cachedWasmModule); +} + +async function loadConverterModule() { + const wasm = await getDwarfToWasmData(); + const imports = {}; + const { instance } = await WebAssembly.instantiate(wasm, imports); + return instance; +} + +module.exports = { + convertToJSON, +}; diff --git a/devtools/client/shared/source-map-loader/wasm-dwarf/dwarf_to_json.wasm b/devtools/client/shared/source-map-loader/wasm-dwarf/dwarf_to_json.wasm Binary files differnew file mode 100644 index 0000000000..c58b189035 --- /dev/null +++ b/devtools/client/shared/source-map-loader/wasm-dwarf/dwarf_to_json.wasm diff --git a/devtools/client/shared/source-map-loader/wasm-dwarf/moz.build b/devtools/client/shared/source-map-loader/wasm-dwarf/moz.build new file mode 100644 index 0000000000..8bc40676fa --- /dev/null +++ b/devtools/client/shared/source-map-loader/wasm-dwarf/moz.build @@ -0,0 +1,12 @@ +# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- +# vim: set filetype=python: +# This Source Code Form is subject to the terms of the Mozilla Public +# License, v. 2.0. If a copy of the MPL was not distributed with this +# file, You can obtain one at http://mozilla.org/MPL/2.0/. + +DevToolsModules( + "convertToJSON.js", + "wasmAsset.js", + "wasmDwarfExpressions.js", + "wasmXScopes.js", +) diff --git a/devtools/client/shared/source-map-loader/wasm-dwarf/wasmAsset.js b/devtools/client/shared/source-map-loader/wasm-dwarf/wasmAsset.js new file mode 100644 index 0000000000..b4de822ffd --- /dev/null +++ b/devtools/client/shared/source-map-loader/wasm-dwarf/wasmAsset.js @@ -0,0 +1,17 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +"use strict"; + +async function getDwarfToWasmData(name) { + const response = await fetch( + "resource://devtools/client/shared/source-map-loader/wasm-dwarf/dwarf_to_json.wasm" + ); + + return response.arrayBuffer(); +} + +module.exports = { + getDwarfToWasmData, +}; diff --git a/devtools/client/shared/source-map-loader/wasm-dwarf/wasmDwarfExpressions.js b/devtools/client/shared/source-map-loader/wasm-dwarf/wasmDwarfExpressions.js new file mode 100644 index 0000000000..c2f124e99f --- /dev/null +++ b/devtools/client/shared/source-map-loader/wasm-dwarf/wasmDwarfExpressions.js @@ -0,0 +1,260 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +/* eslint camelcase: 0*/ +/* eslint-disable no-inline-comments */ + +"use strict"; + +class Value { + val; + + constructor(val) { + this.val = val; + } + toString() { + return `${this.val}`; + } +} + +const Int32Formatter = { + fromAddr(addr) { + return `(new DataView(memory0.buffer).getInt32(${addr}, true))`; + }, + fromValue(value) { + return `${value.val}`; + }, +}; + +const Uint32Formatter = { + fromAddr(addr) { + return `(new DataView(memory0.buffer).getUint32(${addr}, true))`; + }, + fromValue(value) { + return `(${value.val} >>> 0)`; + }, +}; + +function createPieceFormatter(bytes) { + let getter; + switch (bytes) { + case 0: + case 1: + getter = "getUint8"; + break; + case 2: + getter = "getUint16"; + break; + case 3: + case 4: + default: + // FIXME 64-bit + getter = "getUint32"; + break; + } + const mask = (1 << (8 * bytes)) - 1; + return { + fromAddr(addr) { + return `(new DataView(memory0.buffer).${getter}(${addr}, true))`; + }, + fromValue(value) { + return `((${value.val} & ${mask}) >>> 0)`; + }, + }; +} + +// eslint-disable-next-line complexity +function toJS(buf, typeFormatter, frame_base = "fp()") { + const readU8 = function () { + return buf[i++]; + }; + const readS8 = function () { + return (readU8() << 24) >> 24; + }; + const readU16 = function () { + const w = buf[i] | (buf[i + 1] << 8); + i += 2; + return w; + }; + const readS16 = function () { + return (readU16() << 16) >> 16; + }; + const readS32 = function () { + const w = + buf[i] | (buf[i + 1] << 8) | (buf[i + 2] << 16) | (buf[i + 3] << 24); + i += 4; + return w; + }; + const readU32 = function () { + return readS32() >>> 0; + }; + const readU = function () { + let n = 0, + shift = 0, + b; + while ((b = readU8()) & 0x80) { + n |= (b & 0x7f) << shift; + shift += 7; + } + return n | (b << shift); + }; + const readS = function () { + let n = 0, + shift = 0, + b; + while ((b = readU8()) & 0x80) { + n |= (b & 0x7f) << shift; + shift += 7; + } + n |= b << shift; + shift += 7; + return shift > 32 ? (n << (32 - shift)) >> (32 - shift) : n; + }; + const popValue = function (formatter) { + const loc = stack.pop(); + if (loc instanceof Value) { + return formatter.fromValue(loc); + } + return formatter.fromAddr(loc); + }; + let i = 0, + a, + b; + const stack = [frame_base]; + while (i < buf.length) { + const code = buf[i++]; + switch (code) { + case 0x03 /* DW_OP_addr */: + stack.push(Uint32Formatter.fromAddr(readU32())); + break; + case 0x08 /* DW_OP_const1u 0x08 1 1-byte constant */: + stack.push(readU8()); + break; + case 0x09 /* DW_OP_const1s 0x09 1 1-byte constant */: + stack.push(readS8()); + break; + case 0x0a /* DW_OP_const2u 0x0a 1 2-byte constant */: + stack.push(readU16()); + break; + case 0x0b /* DW_OP_const2s 0x0b 1 2-byte constant */: + stack.push(readS16()); + break; + case 0x0c /* DW_OP_const2u 0x0a 1 2-byte constant */: + stack.push(readU32()); + break; + case 0x0d /* DW_OP_const2s 0x0b 1 2-byte constant */: + stack.push(readS32()); + break; + case 0x10 /* DW_OP_constu 0x10 1 ULEB128 constant */: + stack.push(readU()); + break; + case 0x11 /* DW_OP_const2s 0x0b 1 2-byte constant */: + stack.push(readS()); + break; + + case 0x1c /* DW_OP_minus */: + b = stack.pop(); + a = stack.pop(); + stack.push(`${a} - ${b}`); + break; + + case 0x22 /* DW_OP_plus */: + b = stack.pop(); + a = stack.pop(); + stack.push(`${a} + ${b}`); + break; + + case 0x23 /* DW_OP_plus_uconst */: + b = readU(); + a = stack.pop(); + stack.push(`${a} + ${b}`); + break; + + case 0x30 /* DW_OP_lit0 */: + case 0x31: + case 0x32: + case 0x33: + case 0x34: + case 0x35: + case 0x36: + case 0x37: + case 0x38: + case 0x39: + case 0x3a: + case 0x3b: + case 0x3c: + case 0x3d: + case 0x3e: + case 0x3f: + case 0x40: + case 0x41: + case 0x42: + case 0x43: + case 0x44: + case 0x45: + case 0x46: + case 0x47: + case 0x48: + case 0x49: + case 0x4a: + case 0x4b: + case 0x4c: + case 0x4d: + case 0x4e: + case 0x4f: + stack.push(`${code - 0x30}`); + break; + + case 0x93 /* DW_OP_piece */: { + a = readU(); + const formatter = createPieceFormatter(a); + stack.push(popValue(formatter)); + break; + } + + case 0x9f /* DW_OP_stack_value */: + stack.push(new Value(stack.pop())); + break; + + case 0xf6 /* WASM ext (old, FIXME phase out) */: + case 0xed /* WASM ext */: + b = readU(); + a = readS(); + switch (b) { + case 0: + stack.push(`var${a}`); + break; + case 1: + stack.push(`global${a}`); + break; + default: + stack.push(`ti${b}(${a})`); + break; + } + break; + + default: + // Unknown encoding, baling out + return null; + } + } + // FIXME use real DWARF type information + return popValue(typeFormatter); +} + +function decodeExpr(expr) { + if (expr.includes("//")) { + expr = expr.slice(0, expr.indexOf("//")).trim(); + } + const code = new Uint8Array(expr.length >> 1); + for (let i = 0; i < code.length; i++) { + code[i] = parseInt(expr.substr(i << 1, 2), 16); + } + const typeFormatter = Int32Formatter; + return toJS(code, typeFormatter) || `dwarf("${expr}")`; +} + +module.exports = { + decodeExpr, +}; diff --git a/devtools/client/shared/source-map-loader/wasm-dwarf/wasmXScopes.js b/devtools/client/shared/source-map-loader/wasm-dwarf/wasmXScopes.js new file mode 100644 index 0000000000..82faa51dbe --- /dev/null +++ b/devtools/client/shared/source-map-loader/wasm-dwarf/wasmXScopes.js @@ -0,0 +1,215 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +/* eslint camelcase: 0*/ + +"use strict"; + +const { + decodeExpr, +} = require("resource://devtools/client/shared/source-map-loader/wasm-dwarf/wasmDwarfExpressions"); + +const xScopes = new Map(); + +function indexLinkingNames(items) { + const result = new Map(); + let queue = [...items]; + while (queue.length) { + const item = queue.shift(); + if ("uid" in item) { + result.set(item.uid, item); + } else if ("linkage_name" in item) { + // TODO the linkage_name string value is used for compatibility + // with old format. Remove in favour of the uid referencing. + result.set(item.linkage_name, item); + } + if ("children" in item) { + queue = [...queue, ...item.children]; + } + } + return result; +} + +function getIndexedItem(index, key) { + if (typeof key === "object" && key != null) { + return index.get(key.uid); + } + if (typeof key === "string") { + return index.get(key); + } + return null; +} + +async function getXScopes(sourceId, getSourceMap) { + if (xScopes.has(sourceId)) { + return xScopes.get(sourceId); + } + const map = await getSourceMap(sourceId); + if (!map || !map.xScopes) { + xScopes.set(sourceId, null); + return null; + } + const { code_section_offset, debug_info } = map.xScopes; + const xScope = { + code_section_offset, + debug_info, + idIndex: indexLinkingNames(debug_info), + sources: map.sources, + }; + xScopes.set(sourceId, xScope); + return xScope; +} + +function isInRange(item, pc) { + if ("ranges" in item) { + return item.ranges.some(r => r[0] <= pc && pc < r[1]); + } + if ("high_pc" in item) { + return item.low_pc <= pc && pc < item.high_pc; + } + return false; +} + +function decodeExprAt(expr, pc) { + if (typeof expr === "string") { + return decodeExpr(expr); + } + const foundAt = expr.find(i => i.range[0] <= pc && pc < i.range[1]); + return foundAt ? decodeExpr(foundAt.expr) : null; +} + +function getVariables(scope, pc) { + const vars = scope.children + ? scope.children.reduce((result, item) => { + switch (item.tag) { + case "variable": + case "formal_parameter": + result.push({ + name: item.name || "", + expr: item.location ? decodeExprAt(item.location, pc) : null, + }); + break; + case "lexical_block": + // FIXME build scope blocks (instead of combining) + const tmp = getVariables(item, pc); + result = [...tmp.vars, ...result]; + break; + } + return result; + }, []) + : []; + const frameBase = scope.frame_base ? decodeExpr(scope.frame_base) : null; + return { + vars, + frameBase, + }; +} + +function filterScopes(items, pc, lastItem, index) { + if (!items) { + return []; + } + return items.reduce((result, item) => { + switch (item.tag) { + case "compile_unit": + if (isInRange(item, pc)) { + result = [ + ...result, + ...filterScopes(item.children, pc, lastItem, index), + ]; + } + break; + case "namespace": + case "structure_type": + case "union_type": + result = [ + ...result, + ...filterScopes(item.children, pc, lastItem, index), + ]; + break; + case "subprogram": + if (isInRange(item, pc)) { + const s = { + id: item.linkage_name, + name: item.name, + variables: getVariables(item, pc), + }; + result = [...result, s, ...filterScopes(item.children, pc, s, index)]; + } + break; + case "inlined_subroutine": + if (isInRange(item, pc)) { + const linkedItem = getIndexedItem(index, item.abstract_origin); + const s = { + id: item.abstract_origin, + name: linkedItem ? linkedItem.name : void 0, + variables: getVariables(item, pc), + }; + if (lastItem) { + lastItem.file = item.call_file; + lastItem.line = item.call_line; + } + result = [...result, s, ...filterScopes(item.children, pc, s, index)]; + } + break; + } + return result; + }, []); +} + +class XScope { + xScope; + sourceMapContext; + + constructor(xScopeData, sourceMapContext) { + this.xScope = xScopeData; + this.sourceMapContext = sourceMapContext; + } + + search(generatedLocation) { + const { code_section_offset, debug_info, sources, idIndex } = this.xScope; + const pc = generatedLocation.line - (code_section_offset || 0); + const scopes = filterScopes(debug_info, pc, null, idIndex); + scopes.reverse(); + + return scopes.map(i => { + if (!("file" in i)) { + return { + displayName: i.name || "", + variables: i.variables, + }; + } + const sourceId = this.sourceMapContext.generatedToOriginalId( + generatedLocation.sourceId, + sources[i.file || 0] + ); + return { + displayName: i.name || "", + variables: i.variables, + location: { + line: i.line || 0, + sourceId, + }, + }; + }); + } +} + +async function getWasmXScopes(sourceId, sourceMapContext) { + const { getSourceMap } = sourceMapContext; + const xScopeData = await getXScopes(sourceId, getSourceMap); + if (!xScopeData) { + return null; + } + return new XScope(xScopeData, sourceMapContext); +} + +function clearWasmXScopes() { + xScopes.clear(); +} + +module.exports = { + getWasmXScopes, + clearWasmXScopes, +}; diff --git a/devtools/client/shared/source-map-loader/worker.js b/devtools/client/shared/source-map-loader/worker.js new file mode 100644 index 0000000000..d730359fba --- /dev/null +++ b/devtools/client/shared/source-map-loader/worker.js @@ -0,0 +1,50 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at <http://mozilla.org/MPL/2.0/>. */ + +"use strict"; + +importScripts("resource://gre/modules/workers/require.js"); + +const { + getOriginalURLs, + hasOriginalURL, + getOriginalRanges, + getGeneratedRanges, + getGeneratedLocation, + getOriginalLocation, + getOriginalLocations, + getOriginalSourceText, + getGeneratedRangesForOriginal, + getFileGeneratedRange, + loadSourceMap, + clearSourceMaps, + setSourceMapForGeneratedSources, +} = require("resource://devtools/client/shared/source-map-loader/source-map.js"); + +const { + getOriginalStackFrames, +} = require("resource://devtools/client/shared/source-map-loader/utils/getOriginalStackFrames.js"); + +const { + workerHandler, +} = require("resource://devtools/client/shared/worker-utils.js"); + +// The interface is implemented in source-map to be +// easier to unit test. +self.onmessage = workerHandler({ + getOriginalURLs, + hasOriginalURL, + getOriginalRanges, + getGeneratedRanges, + getGeneratedLocation, + getOriginalLocation, + getOriginalLocations, + getOriginalSourceText, + getOriginalStackFrames, + getGeneratedRangesForOriginal, + getFileGeneratedRange, + loadSourceMap, + setSourceMapForGeneratedSources, + clearSourceMaps, +}); |