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