summaryrefslogtreecommitdiffstats
path: root/devtools/client/shared/source-map-loader/utils
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-19 00:47:55 +0000
commit26a029d407be480d791972afb5975cf62c9360a6 (patch)
treef435a8308119effd964b339f76abb83a57c29483 /devtools/client/shared/source-map-loader/utils
parentInitial commit. (diff)
downloadfirefox-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/utils')
-rw-r--r--devtools/client/shared/source-map-loader/utils/assert.js13
-rw-r--r--devtools/client/shared/source-map-loader/utils/fetchSourceMap.js139
-rw-r--r--devtools/client/shared/source-map-loader/utils/getOriginalStackFrames.js38
-rw-r--r--devtools/client/shared/source-map-loader/utils/index.js103
-rw-r--r--devtools/client/shared/source-map-loader/utils/moz.build15
-rw-r--r--devtools/client/shared/source-map-loader/utils/network-request.js43
-rw-r--r--devtools/client/shared/source-map-loader/utils/sourceMapRequests.js96
-rw-r--r--devtools/client/shared/source-map-loader/utils/wasmRemap.js107
8 files changed, 554 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..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;