/** * Handling native paths. * * This module contains a number of functions destined to simplify * working with native paths through a cross-platform API. Functions * of this module will only work with the following assumptions: * * - paths are valid; * - paths are defined with one of the grammars that this module can * parse (see later); * - all path concatenations go through function |join|. * * Limitations of this implementation. * * Windows supports 6 distinct grammars for paths. For the moment, this * implementation supports the following subset: * * - drivename:backslash-separated components * - backslash-separated components * - \\drivename\ followed by backslash-separated components * * Additionally, |normalize| can convert a path containing slash- * separated components to a path containing backslash-separated * components. */ "use strict"; // Boilerplate used to be able to import this module both from the main // thread and from worker threads. if (typeof Components != "undefined") { Components.utils.importGlobalProperties(["URL"]); // Global definition of |exports|, to keep everybody happy. // In non-main thread, |exports| is provided by the module // loader. this.exports = {}; } else if (typeof module == "undefined" || typeof exports == "undefined") { throw new Error("Please load this module using require()"); } var EXPORTED_SYMBOLS = [ "basename", "dirname", "join", "normalize", "split", "winGetDrive", "winIsAbsolute", "toFileURI", "fromFileURI", ]; /** * Return the final part of the path. * The final part of the path is everything after the last "\\". */ var basename = function(path) { if (path.startsWith("\\\\")) { // UNC-style path let index = path.lastIndexOf("\\"); if (index != 1) { return path.slice(index + 1); } return ""; // Degenerate case } return path.slice(Math.max(path.lastIndexOf("\\"), path.lastIndexOf(":")) + 1); }; exports.basename = basename; /** * Return the directory part of the path. * * If the path contains no directory, return the drive letter, * or "." if the path contains no drive letter or if option * |winNoDrive| is set. * * Otherwise, return everything before the last backslash, * including the drive/server name. * * * @param {string} path The path. * @param {*=} options Platform-specific options controlling the behavior * of this function. This implementation supports the following options: * - |winNoDrive| If |true|, also remove the letter from the path name. */ var dirname = function(path, options) { let noDrive = (options && options.winNoDrive); // Find the last occurrence of "\\" let index = path.lastIndexOf("\\"); if (index == -1) { // If there is no directory component... if (!noDrive) { // Return the drive path if possible, falling back to "." return this.winGetDrive(path) || "."; } else { // Or just "." return "."; } } if (index == 1 && path.charAt(0) == "\\") { // The path is reduced to a UNC drive if (noDrive) { return "."; } else { return path; } } // Ignore any occurrence of "\\: immediately before that one while (index >= 0 && path[index] == "\\") { --index; } // Compute what is left, removing the drive name if necessary let start; if (noDrive) { start = (this.winGetDrive(path) || "").length; } else { start = 0; } return path.slice(start, index + 1); }; exports.dirname = dirname; /** * Join path components. * This is the recommended manner of getting the path of a file/subdirectory * in a directory. * * Example: Obtaining $TMP/foo/bar in an OS-independent manner * var tmpDir = OS.Constants.Path.tmpDir; * var path = OS.Path.join(tmpDir, "foo", "bar"); * * Under Windows, this will return "$TMP\foo\bar". * * Empty components are ignored, i.e. `OS.Path.join("foo", "", "bar)` is the * same as `OS.Path.join("foo", "bar")`. */ var join = function(...path) { let paths = []; let root; let absolute = false; for (let subpath of path) { if (subpath == null) { throw new TypeError("invalid path component"); } if (subpath == "") { continue; } let drive = this.winGetDrive(subpath); if (drive) { root = drive; let component = trimBackslashes(subpath.slice(drive.length)); if (component) { paths = [component]; } else { paths = []; } absolute = true; } else if (this.winIsAbsolute(subpath)) { paths = [trimBackslashes(subpath)]; absolute = true; } else { paths.push(trimBackslashes(subpath)); } } let result = ""; if (root) { result += root; } if (absolute) { result += "\\"; } result += paths.join("\\"); return result; }; exports.join = join; /** * Return the drive name of a path, or |null| if the path does * not contain a drive name. * * Drive name appear either as "DriveName:..." (the return drive * name includes the ":") or "\\\\DriveName..." (the returned drive name * includes "\\\\"). */ var winGetDrive = function(path) { if (path == null) { throw new TypeError("path is invalid"); } if (path.startsWith("\\\\")) { // UNC path if (path.length == 2) { return null; } let index = path.indexOf("\\", 2); if (index == -1) { return path; } return path.slice(0, index); } // Non-UNC path let index = path.indexOf(":"); if (index <= 0) return null; return path.slice(0, index + 1); }; exports.winGetDrive = winGetDrive; /** * Return |true| if the path is absolute, |false| otherwise. * * We consider that a path is absolute if it starts with "\\" * or "driveletter:\\". */ var winIsAbsolute = function(path) { let index = path.indexOf(":"); return path.length > index + 1 && path[index + 1] == "\\"; }; exports.winIsAbsolute = winIsAbsolute; /** * Normalize a path by removing any unneeded ".", "..", "\\". * Also convert any "/" to a "\\". */ var normalize = function(path) { let stack = []; if (!path.startsWith("\\\\")) { // Normalize "/" to "\\" path = path.replace(/\//g, "\\"); } // Remove the drive (we will put it back at the end) let root = this.winGetDrive(path); if (root) { path = path.slice(root.length); } // Remember whether we need to restore a leading "\\" or drive name. let absolute = this.winIsAbsolute(path); // And now, fill |stack| from the components, // popping whenever there is a ".." path.split("\\").forEach(function loop(v) { switch (v) { case "": case ".": // Ignore break; case "..": if (stack.length == 0) { if (absolute) { throw new Error("Path is ill-formed: attempting to go past root"); } else { stack.push(".."); } } else { if (stack[stack.length - 1] == "..") { stack.push(".."); } else { stack.pop(); } } break; default: stack.push(v); } }); // Put everything back together let result = stack.join("\\"); if (absolute || root) { result = "\\" + result; } if (root) { result = root + result; } return result; }; exports.normalize = normalize; /** * Return the components of a path. * You should generally apply this function to a normalized path. * * @return {{ * {bool} absolute |true| if the path is absolute, |false| otherwise * {array} components the string components of the path * {string?} winDrive the drive or server for this path * }} * * Other implementations may add additional OS-specific informations. */ var split = function(path) { return { absolute: this.winIsAbsolute(path), winDrive: this.winGetDrive(path), components: path.split("\\") }; }; exports.split = split; /** * Return the file:// URI file path of the given local file path. */ // The case of %3b is designed to match Services.io, but fundamentally doesn't matter. var toFileURIExtraEncodings = {';': '%3b', '?': '%3F', '#': '%23'}; var toFileURI = function toFileURI(path) { // URI-escape forward slashes and convert backward slashes to forward path = this.normalize(path).replace(/[\\\/]/g, m => (m=='\\')? '/' : '%2F'); // Per https://url.spec.whatwg.org we should not encode [] in the path let dontNeedEscaping = {'%5B': '[', '%5D': ']'}; let uri = encodeURI(path).replace(/%(5B|5D)/gi, match => dontNeedEscaping[match]); // add a prefix, and encodeURI doesn't escape a few characters that we do // want to escape, so fix that up let prefix = "file:///"; uri = prefix + uri.replace(/[;?#]/g, match => toFileURIExtraEncodings[match]); // turn e.g., file:///C: into file:///C:/ if (uri.charAt(uri.length - 1) === ':') { uri += "/" } return uri; }; exports.toFileURI = toFileURI; /** * Returns the local file path from a given file URI. */ var fromFileURI = function fromFileURI(uri) { let url = new URL(uri); if (url.protocol != 'file:') { throw new Error("fromFileURI expects a file URI"); } // strip leading slash, since Windows paths don't start with one uri = url.pathname.substr(1); let path = decodeURI(uri); // decode a few characters where URL's parsing is overzealous path = path.replace(/%(3b|3f|23)/gi, match => decodeURIComponent(match)); path = this.normalize(path); // this.normalize() does not remove the trailing slash if the path // component is a drive letter. eg. 'C:\'' will not get normalized. if (path.endsWith(":\\")) { path = path.substr(0, path.length - 1); } return this.normalize(path); }; exports.fromFileURI = fromFileURI; /** * Utility function: Remove any leading/trailing backslashes * from a string. */ var trimBackslashes = function trimBackslashes(string) { return string.replace(/^\\+|\\+$/g,''); }; //////////// Boilerplate if (typeof Components != "undefined") { this.EXPORTED_SYMBOLS = EXPORTED_SYMBOLS; for (let symbol of EXPORTED_SYMBOLS) { this[symbol] = exports[symbol]; } }