diff options
Diffstat (limited to 'devtools/client/shared/source-utils.js')
-rw-r--r-- | devtools/client/shared/source-utils.js | 359 |
1 files changed, 359 insertions, 0 deletions
diff --git a/devtools/client/shared/source-utils.js b/devtools/client/shared/source-utils.js new file mode 100644 index 0000000000..c060f7880f --- /dev/null +++ b/devtools/client/shared/source-utils.js @@ -0,0 +1,359 @@ +/* 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 { LocalizationHelper } = require("devtools/shared/l10n"); + +const l10n = new LocalizationHelper( + "devtools/client/locales/components.properties" +); +const UNKNOWN_SOURCE_STRING = l10n.getStr("frame.unknownSource"); + +// Character codes used in various parsing helper functions. +const CHAR_CODE_A = "a".charCodeAt(0); +const CHAR_CODE_B = "b".charCodeAt(0); +const CHAR_CODE_C = "c".charCodeAt(0); +const CHAR_CODE_D = "d".charCodeAt(0); +const CHAR_CODE_E = "e".charCodeAt(0); +const CHAR_CODE_F = "f".charCodeAt(0); +const CHAR_CODE_H = "h".charCodeAt(0); +const CHAR_CODE_I = "i".charCodeAt(0); +const CHAR_CODE_J = "j".charCodeAt(0); +const CHAR_CODE_L = "l".charCodeAt(0); +const CHAR_CODE_M = "m".charCodeAt(0); +const CHAR_CODE_N = "n".charCodeAt(0); +const CHAR_CODE_O = "o".charCodeAt(0); +const CHAR_CODE_P = "p".charCodeAt(0); +const CHAR_CODE_R = "r".charCodeAt(0); +const CHAR_CODE_S = "s".charCodeAt(0); +const CHAR_CODE_T = "t".charCodeAt(0); +const CHAR_CODE_U = "u".charCodeAt(0); +const CHAR_CODE_W = "w".charCodeAt(0); +const CHAR_CODE_COLON = ":".charCodeAt(0); +const CHAR_CODE_DASH = "-".charCodeAt(0); +const CHAR_CODE_L_SQUARE_BRACKET = "[".charCodeAt(0); +const CHAR_CODE_SLASH = "/".charCodeAt(0); + +// The cache used in the `parseURL` function. +const gURLStore = new Map(); +// The cache used in the `getSourceNames` function. +const gSourceNamesStore = new Map(); + +/** + * Takes a string and returns an object containing all the properties + * available on an URL instance, with additional properties (fileName), + * Leverages caching. + * + * @param {String} location + * @return {Object?} An object containing most properties available + * in https://developer.mozilla.org/en-US/docs/Web/API/URL + */ + +function parseURL(location) { + let url = gURLStore.get(location); + + if (url !== void 0) { + return url; + } + + try { + url = new URL(location); + // The callers were generally written to expect a URL from + // sdk/url, which is subtly different. So, work around some + // important differences here. + url = { + href: url.href, + protocol: url.protocol, + host: url.host, + hostname: url.hostname, + port: url.port || null, + pathname: url.pathname, + search: url.search, + hash: url.hash, + username: url.username, + password: url.password, + origin: url.origin, + }; + + // Definitions: + // Example: https://foo.com:8888/file.js + // `hostname`: "foo.com" + // `host`: "foo.com:8888" + const isChrome = isChromeScheme(location); + + url.fileName = url.pathname + ? url.pathname.slice(url.pathname.lastIndexOf("/") + 1) || "/" + : "/"; + + if (isChrome) { + url.hostname = null; + url.host = null; + } + + gURLStore.set(location, url); + return url; + } catch (e) { + gURLStore.set(location, null); + return null; + } +} + +/** + * Parse a source into a short and long name as well as a host name. + * + * @param {String} source + * The source to parse. Can be a URI or names like "(eval)" or + * "self-hosted". + * @return {Object} + * An object with the following properties: + * - {String} short: A short name for the source. + * - "http://page.com/test.js#go?q=query" -> "test.js" + * - {String} long: The full, long name for the source, with + hash/query stripped. + * - "http://page.com/test.js#go?q=query" -> "http://page.com/test.js" + * - {String?} host: If available, the host name for the source. + * - "http://page.com/test.js#go?q=query" -> "page.com" + */ +function getSourceNames(source) { + const data = gSourceNamesStore.get(source); + + if (data) { + return data; + } + + let short, long, host; + const sourceStr = source ? String(source) : ""; + + // If `data:...` uri + if (isDataScheme(sourceStr)) { + const commaIndex = sourceStr.indexOf(","); + if (commaIndex > -1) { + // The `short` name for a data URI becomes `data:` followed by the actual + // encoded content, omitting the MIME type, and charset. + short = `data:${sourceStr.substring(commaIndex + 1)}`.slice(0, 100); + const result = { short, long: sourceStr }; + gSourceNamesStore.set(source, result); + return result; + } + } + + const parsedUrl = parseURL(sourceStr); + + if (!parsedUrl) { + // Malformed URI. + long = sourceStr; + short = sourceStr.slice(0, 100); + } else { + host = parsedUrl.host; + + long = parsedUrl.href; + if (parsedUrl.hash) { + long = long.replace(parsedUrl.hash, ""); + } + if (parsedUrl.search) { + long = long.replace(parsedUrl.search, ""); + } + + short = parsedUrl.fileName; + // If `short` is just a slash, and we actually have a path, + // strip the slash and parse again to get a more useful short name. + // e.g. "http://foo.com/bar/" -> "bar", rather than "/" + if (short === "/" && parsedUrl.pathname !== "/") { + short = parseURL(long.replace(/\/$/, "")).fileName; + } + } + + if (!short) { + if (!long) { + long = UNKNOWN_SOURCE_STRING; + } + short = long.slice(0, 100); + } + + const result = { short, long, host }; + gSourceNamesStore.set(source, result); + return result; +} + +// For the functions below, we assume that we will never access the location +// argument out of bounds, which is indeed the vast majority of cases. +// +// They are written this way because they are hot. Each frame is checked for +// being content or chrome when processing the profile. + +function isColonSlashSlash(location, i = 0) { + return ( + location.charCodeAt(++i) === CHAR_CODE_COLON && + location.charCodeAt(++i) === CHAR_CODE_SLASH && + location.charCodeAt(++i) === CHAR_CODE_SLASH + ); +} + +function isDataScheme(location, i = 0) { + return ( + location.charCodeAt(i) === CHAR_CODE_D && + location.charCodeAt(++i) === CHAR_CODE_A && + location.charCodeAt(++i) === CHAR_CODE_T && + location.charCodeAt(++i) === CHAR_CODE_A && + location.charCodeAt(++i) === CHAR_CODE_COLON + ); +} + +function isContentScheme(location, i = 0) { + const firstChar = location.charCodeAt(i); + + switch (firstChar) { + // "http://" or "https://" + case CHAR_CODE_H: + if ( + location.charCodeAt(++i) === CHAR_CODE_T && + location.charCodeAt(++i) === CHAR_CODE_T && + location.charCodeAt(++i) === CHAR_CODE_P + ) { + if (location.charCodeAt(i + 1) === CHAR_CODE_S) { + ++i; + } + return isColonSlashSlash(location, i); + } + return false; + + // "file://" + case CHAR_CODE_F: + if ( + location.charCodeAt(++i) === CHAR_CODE_I && + location.charCodeAt(++i) === CHAR_CODE_L && + location.charCodeAt(++i) === CHAR_CODE_E + ) { + return isColonSlashSlash(location, i); + } + return false; + + // "app://" + case CHAR_CODE_A: + if ( + location.charCodeAt(++i) == CHAR_CODE_P && + location.charCodeAt(++i) == CHAR_CODE_P + ) { + return isColonSlashSlash(location, i); + } + return false; + + // "blob:" + case CHAR_CODE_B: + if ( + location.charCodeAt(++i) == CHAR_CODE_L && + location.charCodeAt(++i) == CHAR_CODE_O && + location.charCodeAt(++i) == CHAR_CODE_B && + location.charCodeAt(++i) == CHAR_CODE_COLON + ) { + return isContentScheme(location, i + 1); + } + return false; + + default: + return false; + } +} + +function isChromeString(location, i = 0) { + if ( + location.charCodeAt(i) === CHAR_CODE_C && + location.charCodeAt(++i) === CHAR_CODE_H && + location.charCodeAt(++i) === CHAR_CODE_R && + location.charCodeAt(++i) === CHAR_CODE_O && + location.charCodeAt(++i) === CHAR_CODE_M && + location.charCodeAt(++i) === CHAR_CODE_E + ) { + return isColonSlashSlash(location, i); + } + return false; +} + +function isResourceString(location, i = 0) { + if ( + location.charCodeAt(i) === CHAR_CODE_R && + location.charCodeAt(++i) === CHAR_CODE_E && + location.charCodeAt(++i) === CHAR_CODE_S && + location.charCodeAt(++i) === CHAR_CODE_O && + location.charCodeAt(++i) === CHAR_CODE_U && + location.charCodeAt(++i) === CHAR_CODE_R && + location.charCodeAt(++i) === CHAR_CODE_C && + location.charCodeAt(++i) === CHAR_CODE_E + ) { + return isColonSlashSlash(location, i); + } + return false; +} + +function isJarFileString(location, i = 0) { + if ( + location.charCodeAt(i) === CHAR_CODE_J && + location.charCodeAt(++i) === CHAR_CODE_A && + location.charCodeAt(++i) === CHAR_CODE_R && + location.charCodeAt(++i) === CHAR_CODE_COLON && + location.charCodeAt(++i) === CHAR_CODE_F && + location.charCodeAt(++i) === CHAR_CODE_I && + location.charCodeAt(++i) === CHAR_CODE_L && + location.charCodeAt(++i) === CHAR_CODE_E + ) { + return isColonSlashSlash(location, i); + } + return false; +} + +function isChromeScheme(location, i = 0) { + return ( + isChromeString(location, i) || + isResourceString(location, i) || + isJarFileString(location, i) + ); +} + +function isWASM(location, i = 0) { + return ( + // "wasm-function[" + location.charCodeAt(i) === CHAR_CODE_W && + location.charCodeAt(++i) === CHAR_CODE_A && + location.charCodeAt(++i) === CHAR_CODE_S && + location.charCodeAt(++i) === CHAR_CODE_M && + location.charCodeAt(++i) === CHAR_CODE_DASH && + location.charCodeAt(++i) === CHAR_CODE_F && + location.charCodeAt(++i) === CHAR_CODE_U && + location.charCodeAt(++i) === CHAR_CODE_N && + location.charCodeAt(++i) === CHAR_CODE_C && + location.charCodeAt(++i) === CHAR_CODE_T && + location.charCodeAt(++i) === CHAR_CODE_I && + location.charCodeAt(++i) === CHAR_CODE_O && + location.charCodeAt(++i) === CHAR_CODE_N && + location.charCodeAt(++i) === CHAR_CODE_L_SQUARE_BRACKET + ); +} + +/** + * A utility method to get the file name from a sourcemapped location + * The sourcemap location can be in any form. This method returns a + * formatted file name for different cases like Windows or OSX. + * @param source + * @returns String + */ +function getSourceMappedFile(source) { + // If sourcemapped source is a OSX path, return + // the characters after last "/". + // If sourcemapped source is a Windowss path, return + // the characters after last "\\". + if (source.lastIndexOf("/") >= 0) { + source = source.slice(source.lastIndexOf("/") + 1); + } else if (source.lastIndexOf("\\") >= 0) { + source = source.slice(source.lastIndexOf("\\") + 1); + } + return source; +} + +exports.parseURL = parseURL; +exports.getSourceNames = getSourceNames; +exports.isChromeScheme = isChromeScheme; +exports.isContentScheme = isContentScheme; +exports.isWASM = isWASM; +exports.isDataScheme = isDataScheme; +exports.getSourceMappedFile = getSourceMappedFile; |