source-utils.js (10914B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 "use strict"; 5 6 const { LocalizationHelper } = require("resource://devtools/shared/l10n.js"); 7 8 const l10n = new LocalizationHelper( 9 "devtools/client/locales/components.properties" 10 ); 11 const UNKNOWN_SOURCE_STRING = l10n.getStr("frame.unknownSource"); 12 13 // Character codes used in various parsing helper functions. 14 const CHAR_CODE_A = "a".charCodeAt(0); 15 const CHAR_CODE_B = "b".charCodeAt(0); 16 const CHAR_CODE_C = "c".charCodeAt(0); 17 const CHAR_CODE_D = "d".charCodeAt(0); 18 const CHAR_CODE_E = "e".charCodeAt(0); 19 const CHAR_CODE_F = "f".charCodeAt(0); 20 const CHAR_CODE_H = "h".charCodeAt(0); 21 const CHAR_CODE_I = "i".charCodeAt(0); 22 const CHAR_CODE_J = "j".charCodeAt(0); 23 const CHAR_CODE_L = "l".charCodeAt(0); 24 const CHAR_CODE_M = "m".charCodeAt(0); 25 const CHAR_CODE_N = "n".charCodeAt(0); 26 const CHAR_CODE_O = "o".charCodeAt(0); 27 const CHAR_CODE_P = "p".charCodeAt(0); 28 const CHAR_CODE_R = "r".charCodeAt(0); 29 const CHAR_CODE_S = "s".charCodeAt(0); 30 const CHAR_CODE_T = "t".charCodeAt(0); 31 const CHAR_CODE_U = "u".charCodeAt(0); 32 const CHAR_CODE_W = "w".charCodeAt(0); 33 const CHAR_CODE_COLON = ":".charCodeAt(0); 34 const CHAR_CODE_DASH = "-".charCodeAt(0); 35 const CHAR_CODE_L_SQUARE_BRACKET = "[".charCodeAt(0); 36 const CHAR_CODE_SLASH = "/".charCodeAt(0); 37 38 // The cache used in the `parseURL` function. 39 const gURLStore = new Map(); 40 // The cache used in the `getSourceNames` function. 41 const gSourceNamesStore = new Map(); 42 43 /** 44 * Takes a string and returns an object containing all the properties 45 * available on an URL instance, with additional properties (fileName), 46 * Leverages caching. 47 * 48 * @param {string} location 49 * @return {object?} An object containing most properties available 50 * in https://developer.mozilla.org/en-US/docs/Web/API/URL 51 */ 52 53 function parseURL(location) { 54 let url = gURLStore.get(location); 55 56 if (url !== void 0) { 57 return url; 58 } 59 60 try { 61 url = new URL(location); 62 // The callers were generally written to expect a URL from 63 // sdk/url, which is subtly different. So, work around some 64 // important differences here. 65 url = { 66 href: url.href, 67 protocol: url.protocol, 68 host: url.host, 69 hostname: url.hostname, 70 port: url.port || null, 71 pathname: url.pathname, 72 search: url.search, 73 hash: url.hash, 74 username: url.username, 75 password: url.password, 76 origin: url.origin, 77 }; 78 79 // Definitions: 80 // Example: https://foo.com:8888/file.js 81 // `hostname`: "foo.com" 82 // `host`: "foo.com:8888" 83 const isChrome = isChromeScheme(location); 84 85 url.fileName = url.pathname 86 ? url.pathname.slice(url.pathname.lastIndexOf("/") + 1) || "/" 87 : "/"; 88 89 if (isChrome) { 90 url.hostname = null; 91 url.host = null; 92 } 93 94 gURLStore.set(location, url); 95 return url; 96 } catch (e) { 97 gURLStore.set(location, null); 98 return null; 99 } 100 } 101 102 /** 103 * Parse a source into a short and long name as well as a host name. 104 * 105 * @param {string} source 106 * The source to parse. Can be a URI or names like "(eval)" or 107 * "self-hosted". 108 * @return {object} 109 * An object with the following properties: 110 * - {String} short: A short name for the source. 111 * - "http://page.com/test.js#go?q=query" -> "test.js" 112 * - {String} long: The full, long name for the source, with 113 hash/query stripped. 114 * - "http://page.com/test.js#go?q=query" -> "http://page.com/test.js" 115 * - {String?} host: If available, the host name for the source. 116 * - "http://page.com/test.js#go?q=query" -> "page.com" 117 */ 118 function getSourceNames(source) { 119 const data = gSourceNamesStore.get(source); 120 121 if (data) { 122 return data; 123 } 124 125 let short, long, host; 126 const sourceStr = source ? String(source) : ""; 127 128 // If `data:...` uri 129 if (isDataScheme(sourceStr)) { 130 const commaIndex = sourceStr.indexOf(","); 131 if (commaIndex > -1) { 132 // The `short` name for a data URI becomes `data:` followed by the actual 133 // encoded content, omitting the MIME type, and charset. 134 short = `data:${sourceStr.substring(commaIndex + 1)}`.slice(0, 100); 135 const result = { short, long: sourceStr }; 136 gSourceNamesStore.set(source, result); 137 return result; 138 } 139 } 140 141 const parsedUrl = parseURL(sourceStr); 142 143 if (!parsedUrl) { 144 // Malformed URI. 145 long = sourceStr; 146 short = sourceStr.slice(0, 100); 147 } else { 148 host = parsedUrl.host; 149 150 long = parsedUrl.href; 151 if (parsedUrl.hash) { 152 long = long.replace(parsedUrl.hash, ""); 153 } 154 if (parsedUrl.search) { 155 long = long.replace(parsedUrl.search, ""); 156 } 157 158 short = parsedUrl.fileName; 159 // If `short` is just a slash, and we actually have a path, 160 // strip the slash and parse again to get a more useful short name. 161 // e.g. "http://foo.com/bar/" -> "bar", rather than "/" 162 if (short === "/" && parsedUrl.pathname !== "/") { 163 short = parseURL(long.replace(/\/$/, "")).fileName; 164 } 165 } 166 167 if (!short) { 168 if (!long) { 169 long = UNKNOWN_SOURCE_STRING; 170 } 171 short = long.slice(0, 100); 172 } 173 174 const result = { short, long, host }; 175 gSourceNamesStore.set(source, result); 176 return result; 177 } 178 179 // For the functions below, we assume that we will never access the location 180 // argument out of bounds, which is indeed the vast majority of cases. 181 // 182 // They are written this way because they are hot. Each frame is checked for 183 // being content or chrome when processing the profile. 184 185 function isColonSlashSlash(location, i = 0) { 186 return ( 187 location.charCodeAt(++i) === CHAR_CODE_COLON && 188 location.charCodeAt(++i) === CHAR_CODE_SLASH && 189 location.charCodeAt(++i) === CHAR_CODE_SLASH 190 ); 191 } 192 193 function isDataScheme(location, i = 0) { 194 return ( 195 location.charCodeAt(i) === CHAR_CODE_D && 196 location.charCodeAt(++i) === CHAR_CODE_A && 197 location.charCodeAt(++i) === CHAR_CODE_T && 198 location.charCodeAt(++i) === CHAR_CODE_A && 199 location.charCodeAt(++i) === CHAR_CODE_COLON 200 ); 201 } 202 203 function isContentScheme(location, i = 0) { 204 const firstChar = location.charCodeAt(i); 205 206 switch (firstChar) { 207 // "http://" or "https://" 208 case CHAR_CODE_H: 209 if ( 210 location.charCodeAt(++i) === CHAR_CODE_T && 211 location.charCodeAt(++i) === CHAR_CODE_T && 212 location.charCodeAt(++i) === CHAR_CODE_P 213 ) { 214 if (location.charCodeAt(i + 1) === CHAR_CODE_S) { 215 ++i; 216 } 217 return isColonSlashSlash(location, i); 218 } 219 return false; 220 221 // "file://" 222 case CHAR_CODE_F: 223 if ( 224 location.charCodeAt(++i) === CHAR_CODE_I && 225 location.charCodeAt(++i) === CHAR_CODE_L && 226 location.charCodeAt(++i) === CHAR_CODE_E 227 ) { 228 return isColonSlashSlash(location, i); 229 } 230 return false; 231 232 // "app://" 233 case CHAR_CODE_A: 234 if ( 235 location.charCodeAt(++i) == CHAR_CODE_P && 236 location.charCodeAt(++i) == CHAR_CODE_P 237 ) { 238 return isColonSlashSlash(location, i); 239 } 240 return false; 241 242 // "blob:" 243 case CHAR_CODE_B: 244 if ( 245 location.charCodeAt(++i) == CHAR_CODE_L && 246 location.charCodeAt(++i) == CHAR_CODE_O && 247 location.charCodeAt(++i) == CHAR_CODE_B && 248 location.charCodeAt(++i) == CHAR_CODE_COLON 249 ) { 250 return isContentScheme(location, i + 1); 251 } 252 return false; 253 254 default: 255 return false; 256 } 257 } 258 259 function isChromeString(location, i = 0) { 260 if ( 261 location.charCodeAt(i) === CHAR_CODE_C && 262 location.charCodeAt(++i) === CHAR_CODE_H && 263 location.charCodeAt(++i) === CHAR_CODE_R && 264 location.charCodeAt(++i) === CHAR_CODE_O && 265 location.charCodeAt(++i) === CHAR_CODE_M && 266 location.charCodeAt(++i) === CHAR_CODE_E 267 ) { 268 return isColonSlashSlash(location, i); 269 } 270 return false; 271 } 272 273 function isResourceString(location, i = 0) { 274 if ( 275 location.charCodeAt(i) === CHAR_CODE_R && 276 location.charCodeAt(++i) === CHAR_CODE_E && 277 location.charCodeAt(++i) === CHAR_CODE_S && 278 location.charCodeAt(++i) === CHAR_CODE_O && 279 location.charCodeAt(++i) === CHAR_CODE_U && 280 location.charCodeAt(++i) === CHAR_CODE_R && 281 location.charCodeAt(++i) === CHAR_CODE_C && 282 location.charCodeAt(++i) === CHAR_CODE_E 283 ) { 284 return isColonSlashSlash(location, i); 285 } 286 return false; 287 } 288 289 function isJarFileString(location, i = 0) { 290 if ( 291 location.charCodeAt(i) === CHAR_CODE_J && 292 location.charCodeAt(++i) === CHAR_CODE_A && 293 location.charCodeAt(++i) === CHAR_CODE_R && 294 location.charCodeAt(++i) === CHAR_CODE_COLON && 295 location.charCodeAt(++i) === CHAR_CODE_F && 296 location.charCodeAt(++i) === CHAR_CODE_I && 297 location.charCodeAt(++i) === CHAR_CODE_L && 298 location.charCodeAt(++i) === CHAR_CODE_E 299 ) { 300 return isColonSlashSlash(location, i); 301 } 302 return false; 303 } 304 305 function isChromeScheme(location, i = 0) { 306 return ( 307 isChromeString(location, i) || 308 isResourceString(location, i) || 309 isJarFileString(location, i) 310 ); 311 } 312 313 function isWASM(location, i = 0) { 314 return ( 315 // "wasm-function[" 316 location.charCodeAt(i) === CHAR_CODE_W && 317 location.charCodeAt(++i) === CHAR_CODE_A && 318 location.charCodeAt(++i) === CHAR_CODE_S && 319 location.charCodeAt(++i) === CHAR_CODE_M && 320 location.charCodeAt(++i) === CHAR_CODE_DASH && 321 location.charCodeAt(++i) === CHAR_CODE_F && 322 location.charCodeAt(++i) === CHAR_CODE_U && 323 location.charCodeAt(++i) === CHAR_CODE_N && 324 location.charCodeAt(++i) === CHAR_CODE_C && 325 location.charCodeAt(++i) === CHAR_CODE_T && 326 location.charCodeAt(++i) === CHAR_CODE_I && 327 location.charCodeAt(++i) === CHAR_CODE_O && 328 location.charCodeAt(++i) === CHAR_CODE_N && 329 location.charCodeAt(++i) === CHAR_CODE_L_SQUARE_BRACKET 330 ); 331 } 332 333 /** 334 * A utility method to get the file name from a sourcemapped location 335 * The sourcemap location can be in any form. This method returns a 336 * formatted file name for different cases like Windows or OSX. 337 * 338 * @param source 339 * @returns String 340 */ 341 function getSourceMappedFile(source) { 342 // If sourcemapped source is a OSX path, return 343 // the characters after last "/". 344 // If sourcemapped source is a Windowss path, return 345 // the characters after last "\\". 346 if (source.lastIndexOf("/") >= 0) { 347 source = source.slice(source.lastIndexOf("/") + 1); 348 } else if (source.lastIndexOf("\\") >= 0) { 349 source = source.slice(source.lastIndexOf("\\") + 1); 350 } 351 return source; 352 } 353 354 exports.parseURL = parseURL; 355 exports.getSourceNames = getSourceNames; 356 exports.isChromeScheme = isChromeScheme; 357 exports.isContentScheme = isContentScheme; 358 exports.isWASM = isWASM; 359 exports.isDataScheme = isDataScheme; 360 exports.getSourceMappedFile = getSourceMappedFile;