util.js (11815B)
1 /* -*- Mode: js; js-indent-level: 2; -*- */ 2 /* 3 * Copyright 2011 Mozilla Foundation and contributors 4 * Licensed under the New BSD license. See LICENSE or: 5 * http://opensource.org/licenses/BSD-3-Clause 6 */ 7 8 const URL = require("./url"); 9 10 /** 11 * This is a helper function for getting values from parameter/options 12 * objects. 13 * 14 * @param args The object we are extracting values from 15 * @param name The name of the property we are getting. 16 * @param defaultValue An optional value to return if the property is missing 17 * from the object. If this is not specified and the property is missing, an 18 * error will be thrown. 19 */ 20 function getArg(aArgs, aName, aDefaultValue) { 21 if (aName in aArgs) { 22 return aArgs[aName]; 23 } else if (arguments.length === 3) { 24 return aDefaultValue; 25 } 26 throw new Error('"' + aName + '" is a required argument.'); 27 } 28 exports.getArg = getArg; 29 30 const supportsNullProto = (function () { 31 const obj = Object.create(null); 32 return !("__proto__" in obj); 33 })(); 34 35 function identity(s) { 36 return s; 37 } 38 39 /** 40 * Because behavior goes wacky when you set `__proto__` on objects, we 41 * have to prefix all the strings in our set with an arbitrary character. 42 * 43 * See https://github.com/mozilla/source-map/pull/31 and 44 * https://github.com/mozilla/source-map/issues/30 45 * 46 * @param String aStr 47 */ 48 function toSetString(aStr) { 49 if (isProtoString(aStr)) { 50 return "$" + aStr; 51 } 52 53 return aStr; 54 } 55 exports.toSetString = supportsNullProto ? identity : toSetString; 56 57 function fromSetString(aStr) { 58 if (isProtoString(aStr)) { 59 return aStr.slice(1); 60 } 61 62 return aStr; 63 } 64 exports.fromSetString = supportsNullProto ? identity : fromSetString; 65 66 function isProtoString(s) { 67 if (!s) { 68 return false; 69 } 70 71 const length = s.length; 72 73 if (length < 9 /* "__proto__".length */) { 74 return false; 75 } 76 77 /* eslint-disable no-multi-spaces */ 78 if ( 79 s.charCodeAt(length - 1) !== 95 /* '_' */ || 80 s.charCodeAt(length - 2) !== 95 /* '_' */ || 81 s.charCodeAt(length - 3) !== 111 /* 'o' */ || 82 s.charCodeAt(length - 4) !== 116 /* 't' */ || 83 s.charCodeAt(length - 5) !== 111 /* 'o' */ || 84 s.charCodeAt(length - 6) !== 114 /* 'r' */ || 85 s.charCodeAt(length - 7) !== 112 /* 'p' */ || 86 s.charCodeAt(length - 8) !== 95 /* '_' */ || 87 s.charCodeAt(length - 9) !== 95 /* '_' */ 88 ) { 89 return false; 90 } 91 /* eslint-enable no-multi-spaces */ 92 93 for (let i = length - 10; i >= 0; i--) { 94 if (s.charCodeAt(i) !== 36 /* '$' */) { 95 return false; 96 } 97 } 98 99 return true; 100 } 101 102 function strcmp(aStr1, aStr2) { 103 if (aStr1 === aStr2) { 104 return 0; 105 } 106 107 if (aStr1 === null) { 108 return 1; // aStr2 !== null 109 } 110 111 if (aStr2 === null) { 112 return -1; // aStr1 !== null 113 } 114 115 if (aStr1 > aStr2) { 116 return 1; 117 } 118 119 return -1; 120 } 121 122 /** 123 * Comparator between two mappings with inflated source and name strings where 124 * the generated positions are compared. 125 */ 126 function compareByGeneratedPositionsInflated(mappingA, mappingB) { 127 let cmp = mappingA.generatedLine - mappingB.generatedLine; 128 if (cmp !== 0) { 129 return cmp; 130 } 131 132 cmp = mappingA.generatedColumn - mappingB.generatedColumn; 133 if (cmp !== 0) { 134 return cmp; 135 } 136 137 cmp = strcmp(mappingA.source, mappingB.source); 138 if (cmp !== 0) { 139 return cmp; 140 } 141 142 cmp = mappingA.originalLine - mappingB.originalLine; 143 if (cmp !== 0) { 144 return cmp; 145 } 146 147 cmp = mappingA.originalColumn - mappingB.originalColumn; 148 if (cmp !== 0) { 149 return cmp; 150 } 151 152 return strcmp(mappingA.name, mappingB.name); 153 } 154 exports.compareByGeneratedPositionsInflated = 155 compareByGeneratedPositionsInflated; 156 157 /** 158 * Strip any JSON XSSI avoidance prefix from the string (as documented 159 * in the source maps specification), and then parse the string as 160 * JSON. 161 */ 162 function parseSourceMapInput(str) { 163 return JSON.parse(str.replace(/^\)]}'[^\n]*\n/, "")); 164 } 165 exports.parseSourceMapInput = parseSourceMapInput; 166 167 // We use 'http' as the base here because we want URLs processed relative 168 // to the safe base to be treated as "special" URLs during parsing using 169 // the WHATWG URL parsing. This ensures that backslash normalization 170 // applies to the path and such. 171 const PROTOCOL = "http:"; 172 const PROTOCOL_AND_HOST = `${PROTOCOL}//host`; 173 174 /** 175 * Make it easy to create small utilities that tweak a URL's path. 176 */ 177 function createSafeHandler(cb) { 178 return input => { 179 const type = getURLType(input); 180 const base = buildSafeBase(input); 181 const url = new URL(input, base); 182 183 cb(url); 184 185 const result = url.toString(); 186 187 if (type === "absolute") { 188 return result; 189 } else if (type === "scheme-relative") { 190 return result.slice(PROTOCOL.length); 191 } else if (type === "path-absolute") { 192 return result.slice(PROTOCOL_AND_HOST.length); 193 } 194 195 // This assumes that the callback will only change 196 // the path, search and hash values. 197 return computeRelativeURL(base, result); 198 }; 199 } 200 201 function withBase(url, base) { 202 return new URL(url, base).toString(); 203 } 204 205 function buildUniqueSegment(prefix, str) { 206 let id = 0; 207 do { 208 const ident = prefix + id++; 209 if (str.indexOf(ident) === -1) return ident; 210 } while (true); 211 } 212 213 function buildSafeBase(str) { 214 const maxDotParts = str.split("..").length - 1; 215 216 // If we used a segment that also existed in `str`, then we would be unable 217 // to compute relative paths. For example, if `segment` were just "a": 218 // 219 // const url = "../../a/" 220 // const base = buildSafeBase(url); // http://host/a/a/ 221 // const joined = "http://host/a/"; 222 // const result = relative(base, joined); 223 // 224 // Expected: "../../a/"; 225 // Actual: "a/" 226 // 227 const segment = buildUniqueSegment("p", str); 228 229 let base = `${PROTOCOL_AND_HOST}/`; 230 for (let i = 0; i < maxDotParts; i++) { 231 base += `${segment}/`; 232 } 233 return base; 234 } 235 236 const ABSOLUTE_SCHEME = /^[A-Za-z0-9\+\-\.]+:/; 237 function getURLType(url) { 238 if (url[0] === "/") { 239 if (url[1] === "/") return "scheme-relative"; 240 return "path-absolute"; 241 } 242 243 return ABSOLUTE_SCHEME.test(url) ? "absolute" : "path-relative"; 244 } 245 246 /** 247 * Given two URLs that are assumed to be on the same 248 * protocol/host/user/password build a relative URL from the 249 * path, params, and hash values. 250 * 251 * @param rootURL The root URL that the target will be relative to. 252 * @param targetURL The target that the relative URL points to. 253 * @return A rootURL-relative, normalized URL value. 254 */ 255 function computeRelativeURL(rootURL, targetURL) { 256 if (typeof rootURL === "string") rootURL = new URL(rootURL); 257 if (typeof targetURL === "string") targetURL = new URL(targetURL); 258 259 const targetParts = targetURL.pathname.split("/"); 260 const rootParts = rootURL.pathname.split("/"); 261 262 // If we've got a URL path ending with a "/", we remove it since we'd 263 // otherwise be relative to the wrong location. 264 if (rootParts.length > 0 && !rootParts[rootParts.length - 1]) { 265 rootParts.pop(); 266 } 267 268 while ( 269 targetParts.length > 0 && 270 rootParts.length > 0 && 271 targetParts[0] === rootParts[0] 272 ) { 273 targetParts.shift(); 274 rootParts.shift(); 275 } 276 277 const relativePath = rootParts 278 .map(() => "..") 279 .concat(targetParts) 280 .join("/"); 281 282 return relativePath + targetURL.search + targetURL.hash; 283 } 284 285 /** 286 * Given a URL, ensure that it is treated as a directory URL. 287 * 288 * @param url 289 * @return A normalized URL value. 290 */ 291 const ensureDirectory = createSafeHandler(url => { 292 url.pathname = url.pathname.replace(/\/?$/, "/"); 293 }); 294 295 /** 296 * Given a URL, strip off any filename if one is present. 297 * 298 * @param url 299 * @return A normalized URL value. 300 */ 301 const trimFilename = createSafeHandler(url => { 302 url.href = new URL(".", url.toString()).toString(); 303 }); 304 305 /** 306 * Normalize a given URL. 307 * * Convert backslashes. 308 * * Remove any ".." and "." segments. 309 * 310 * @param url 311 * @return A normalized URL value. 312 */ 313 const normalize = createSafeHandler(url => {}); 314 exports.normalize = normalize; 315 316 /** 317 * Joins two paths/URLs. 318 * 319 * All returned URLs will be normalized. 320 * 321 * @param aRoot The root path or URL. Assumed to reference a directory. 322 * @param aPath The path or URL to be joined with the root. 323 * @return A joined and normalized URL value. 324 */ 325 function join(aRoot, aPath) { 326 const pathType = getURLType(aPath); 327 const rootType = getURLType(aRoot); 328 329 aRoot = ensureDirectory(aRoot); 330 331 if (pathType === "absolute") { 332 return withBase(aPath, undefined); 333 } 334 if (rootType === "absolute") { 335 return withBase(aPath, aRoot); 336 } 337 338 if (pathType === "scheme-relative") { 339 return normalize(aPath); 340 } 341 if (rootType === "scheme-relative") { 342 return withBase(aPath, withBase(aRoot, PROTOCOL_AND_HOST)).slice( 343 PROTOCOL.length 344 ); 345 } 346 347 if (pathType === "path-absolute") { 348 return normalize(aPath); 349 } 350 if (rootType === "path-absolute") { 351 return withBase(aPath, withBase(aRoot, PROTOCOL_AND_HOST)).slice( 352 PROTOCOL_AND_HOST.length 353 ); 354 } 355 356 const base = buildSafeBase(aPath + aRoot); 357 const newPath = withBase(aPath, withBase(aRoot, base)); 358 return computeRelativeURL(base, newPath); 359 } 360 exports.join = join; 361 362 /** 363 * Make a path relative to a URL or another path. If returning a 364 * relative URL is not possible, the original target will be returned. 365 * All returned URLs will be normalized. 366 * 367 * @param aRoot The root path or URL. 368 * @param aPath The path or URL to be made relative to aRoot. 369 * @return A rootURL-relative (if possible), normalized URL value. 370 */ 371 function relative(rootURL, targetURL) { 372 const result = relativeIfPossible(rootURL, targetURL); 373 374 return typeof result === "string" ? result : normalize(targetURL); 375 } 376 exports.relative = relative; 377 378 function relativeIfPossible(rootURL, targetURL) { 379 const urlType = getURLType(rootURL); 380 if (urlType !== getURLType(targetURL)) { 381 return null; 382 } 383 384 const base = buildSafeBase(rootURL + targetURL); 385 const root = new URL(rootURL, base); 386 const target = new URL(targetURL, base); 387 388 try { 389 new URL("", target.toString()); 390 } catch (err) { 391 // Bail if the URL doesn't support things being relative to it, 392 // For example, data: and blob: URLs. 393 return null; 394 } 395 396 if ( 397 target.protocol !== root.protocol || 398 target.user !== root.user || 399 target.password !== root.password || 400 target.hostname !== root.hostname || 401 target.port !== root.port 402 ) { 403 return null; 404 } 405 406 return computeRelativeURL(root, target); 407 } 408 409 /** 410 * Compute the URL of a source given the the source root, the source's 411 * URL, and the source map's URL. 412 */ 413 function computeSourceURL(sourceRoot, sourceURL, sourceMapURL) { 414 // The source map spec states that "sourceRoot" and "sources" entries are to be appended. While 415 // that is a little vague, implementations have generally interpreted that as joining the 416 // URLs with a `/` between then, assuming the "sourceRoot" doesn't already end with one. 417 // For example, 418 // 419 // sourceRoot: "some-dir", 420 // sources: ["/some-path.js"] 421 // 422 // and 423 // 424 // sourceRoot: "some-dir/", 425 // sources: ["/some-path.js"] 426 // 427 // must behave as "some-dir/some-path.js". 428 // 429 // With this library's the transition to a more URL-focused implementation, that behavior is 430 // preserved here. To acheive that, we trim the "/" from absolute-path when a sourceRoot value 431 // is present in order to make the sources entries behave as if they are relative to the 432 // "sourceRoot", as they would have if the two strings were simply concated. 433 if (sourceRoot && getURLType(sourceURL) === "path-absolute") { 434 sourceURL = sourceURL.replace(/^\//, ""); 435 } 436 437 let url = normalize(sourceURL || ""); 438 439 // Parsing URLs can be expensive, so we only perform these joins when needed. 440 if (sourceRoot) url = join(sourceRoot, url); 441 if (sourceMapURL) url = join(trimFilename(sourceMapURL), url); 442 return url; 443 } 444 exports.computeSourceURL = computeSourceURL;