michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this file, michael@0: * You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: 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: 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: "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: return path.slice(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: * The directory part of the path is everything before the last michael@0: * "/". If the last few characters of this part are also "/", michael@0: * they are ignored. michael@0: * michael@0: * If the path contains no directory, return ".". michael@0: */ michael@0: let dirname = function(path) { michael@0: let index = path.lastIndexOf("/"); michael@0: if (index == -1) { michael@0: return "."; michael@0: } michael@0: while (index >= 0 && path[index] == "/") { michael@0: --index; michael@0: } michael@0: return path.slice(0, 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 Unix, this will return "/tmp/foo/bar". michael@0: */ michael@0: let join = function(...path) { michael@0: // If there is a path that starts with a "/", eliminate everything before michael@0: let paths = []; michael@0: for (let subpath of path) { michael@0: if (subpath == null) { michael@0: throw new TypeError("invalid path component"); michael@0: } michael@0: if (subpath.length != 0 && subpath[0] == "/") { michael@0: paths = [subpath]; michael@0: } else { michael@0: paths.push(subpath); michael@0: } michael@0: } michael@0: return paths.join("/"); michael@0: }; michael@0: exports.join = join; michael@0: michael@0: /** michael@0: * Normalize a path by removing any unneeded ".", "..", "//". michael@0: */ michael@0: let normalize = function(path) { michael@0: let stack = []; michael@0: let absolute; michael@0: if (path.length >= 0 && path[0] == "/") { michael@0: absolute = true; michael@0: } else { michael@0: absolute = false; michael@0: } michael@0: path.split("/").forEach(function(v) { michael@0: switch (v) { michael@0: case "": case ".":// fallthrough 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: let string = stack.join("/"); michael@0: return absolute ? "/" + string : string; 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: * }} 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: path.length && path[0] == "/", michael@0: components: path.split("/") michael@0: }; michael@0: }; michael@0: exports.split = split; michael@0: michael@0: /** michael@0: * Returns 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: let uri = encodeURI(this.normalize(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: 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: let path = this.normalize(decodeURIComponent(url.pathname)); michael@0: return path; michael@0: }; michael@0: exports.fromFileURI = fromFileURI; 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: }