diff options
author | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
---|---|---|
committer | Daniel Baumann <daniel.baumann@progress-linux.org> | 2024-04-07 17:32:43 +0000 |
commit | 6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch) | |
tree | a68f146d7fa01f0134297619fbe7e33db084e0aa /devtools/client/shared/source-map-loader/utils | |
parent | Initial commit. (diff) | |
download | thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.tar.xz thunderbird-6bf0a5cb5034a7e684dcc3500e841785237ce2dd.zip |
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'devtools/client/shared/source-map-loader/utils')
8 files changed, 562 insertions, 0 deletions
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..8cf7ab31a9 --- /dev/null +++ b/devtools/client/shared/source-map-loader/utils/fetchSourceMap.js @@ -0,0 +1,137 @@ +/* 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 { 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 { sourceMapURL: resolvedString, baseURL }; +} + +async function _resolveAndFetch(generatedSource) { + // Fetch the sourcemap over the network and create it. + const { sourceMapURL, baseURL } = _resolveSourceMapURL(generatedSource); + + let fetched = await networkRequest(sourceMapURL, { + 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) { + 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 = _resolveAndFetch(generatedSource); + // 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 }; 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..49ceecd71a --- /dev/null +++ b/devtools/client/shared/source-map-loader/utils/sourceMapRequests.js @@ -0,0 +1,106 @@ +/* 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(); + const sources = []; + let ignoreListUrls = []; + + if (map.x_google_ignoreList?.length) { + ignoreListUrls = map.x_google_ignoreList.map( + sourceIndex => map.sources[sourceIndex] + ); + } + + for (const url of map.sources) { + const id = generatedToOriginalId(generatedId, url); + + urlsById.set(id, url); + sources.push({ id, url }); + } + return { map, urlsById, sources, ignoreListUrls }; + }) + ); +} + +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; |