<commit_before>// Copyright (c) 2006-2008 by <NAME> <<EMAIL>>
// Copyright (c) 2007-2009 by <NAME> <<EMAIL>>
// Copyright (c) 2008-2010 by <NAME> <<EMAIL>>
// Some code based on Venkman
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
"use strict";

try {

Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("io", {
    exports: ["IO", "io"],
    require: ["services"],
    use: ["config", "storage", "styles", "template", "util"]
}, this);

// TODO: why are we passing around strings rather than file objects?
/**
 * Provides a basic interface to common system I/O operations.
 * @instance io
 */
var IO = Module("io", {
    init: function () {
        this._processDir = services.directory.get("CurWorkD", Ci.nsIFile);
        this._cwd = this._processDir.path;
        this._oldcwd = null;
        this.config = config;
    },

    Local: function (dactyl, modules, window) let ({ Script, io, plugins } = modules) ({

        init: function init() {
            this.config = modules.config;
            this._processDir = services.directory.get("CurWorkD", Ci.nsIFile);
            this._cwd = this._processDir.path;
            this._oldcwd = null;

            this._lastRunCommand = ""; // updated whenever the users runs a command with :!
            this._scriptNames = [];

            this.downloadListener = {
                onDownloadStateChange: function (state, download) {
                    if (download.state == services.downloadManager.DOWNLOAD_FINISHED) {
                        let url   = download.source.spec;
                        let title = download.displayName;
                        let file  = download.targetFile.path;
                        let size  = download.size;

                        dactyl.echomsg({ domains: [util.getHost(url)], message: "Download of " + title + " to " + file + " finished" },
                                       1, modules.commandline.ACTIVE_WINDOW);
                        modules.autocommands.trigger("DownloadPost", { url: url, title: title, file: file, size: size });
                    }
                },
                onStateChange:    function () {},
                onProgressChange: function () {},
                onSecurityChange: function () {}
            };

            services.downloadManager.addListener(this.downloadListener);
        },

        destroy: function destroy() {
            services.downloadManager.removeListener(this.downloadListener);
            for (let [, plugin] in Iterator(plugins.contexts))
                if (plugin.onUnload)
                    plugin.onUnload();
        },

        /**
         * Returns all directories named *name* in 'runtimepath'.
         *
         * @param {string} name
         * @returns {nsIFile[])
         */
        getRuntimeDirectories: function getRuntimeDirectories(name) {
            return modules.options.get("runtimepath").files
                .map(function (dir) dir.child(name))
                .filter(function (dir) dir.exists() && dir.isDirectory() && dir.isReadable());
        },

        // FIXME: multiple paths?
        /**
         * Sources files found in 'runtimepath'. For each relative path in *paths*
         * each directory in 'runtimepath' is searched and if a matching file is
         * found it is sourced. Only the first file found (per specified path) is
         * sourced unless *all* is specified, then all found files are sourced.
         *
         * @param {string[]} paths An array of relative paths to source.
         * @param {boolean} all Whether all found files should be sourced.
         */
        sourceFromRuntimePath: function sourceFromRuntimePath(paths, all) {
            let dirs = modules.options.get("runtimepath").files;
            let found = false;

            dactyl.echomsg("Searching for " + paths.join(" ").quote() + " in " + modules.options.get("runtimepath").stringValue, 2);

        outer:
            for (let dir in values(dirs)) {
                for (let [,path] in Iterator(paths)) {
                    let file = dir.child(path);

                    dactyl.echomsg("Searching for " + file.path.quote(), 3);

                    if (file.exists() && file.isFile() && file.isReadable()) {
                        io.source(file.path, false);
                        found = true;

                        if (!all)
                            break outer;
                    }
                }
            }

            if (!found)
                dactyl.echomsg("not found in 'runtimepath': " + paths.join(" ").quote(), 1);

            return found;
        },

        /**
         * Reads Ex commands, JavaScript or CSS from *filename*.
         *
         * @param {string} filename The name of the file to source.
         * @param {boolean} silent Whether errors should be reported.
         */
        source: function source(filename, silent) {
            defineModule.loadLog.push("sourcing " + filename);
            let time = Date.now();
            this.withSavedValues(["sourcing"], function _source() {
                this.sourcing = null;
                try {
                    var file = util.getFile(filename) || io.File(filename);

                    if (!file.exists() || !file.isReadable() || file.isDirectory()) {
                        if (!silent)
                            dactyl.echoerr("E484: Can't open file " + filename.quote());
                        return;
                    }

                    dactyl.echomsg("sourcing " + filename.quote(), 2);

                    let uri = services.io.newFileURI(file);

                    // handle pure JavaScript files specially
                    if (/\.js$/.test(filename)) {
                        try {
                            dactyl.loadScript(uri.spec, Script(file));
                            dactyl.helpInitialized = false;
                        }
                        catch (e) {
                            if (e.fileName)
                                try {
                                    e.fileName = e.fileName.replace(/^(chrome|resource):.*? -> /, "");
                                    if (e.fileName == uri.spec)
                                        e.fileName = filename;
                                    e.echoerr = <>{e.fileName}:{e.lineNumber}: {e}</>;
                                }
                                catch (e) {}
                            throw e;
                        }
                    }
                    else if (/\.css$/.test(filename))
                        styles.registerSheet(uri.spec, false, true);
                    else {
                        modules.commands.execute(file.read(), null, silent || "loud", null,
                            { file: file.path, line: 1 });
                    }

                    if (this._scriptNames.indexOf(file.path) == -1)
                        this._scriptNames.push(file.path);

                    dactyl.echomsg("finished sourcing " + filename.quote(), 2);

                    dactyl.log("Sourced: " + filename, 3);
                }
                catch (e) {
                    if (!(e instanceof FailedAssertion))
                        dactyl.reportError(e);
                    let message = "Sourcing file: " + (e.echoerr || file.path + ": " + e);
                    if (!silent)
                        dactyl.echoerr(message);
                }
                finally {
                    defineModule.loadLog.push("done sourcing " + filename + ": " + (Date.now() - time) + "ms");
                }
            });
        },
    }),

    // TODO: there seems to be no way, short of a new component, to change
    // the process's CWD - see https://bugzilla.mozilla.org/show_bug.cgi?id=280953
    /**
     * Returns the current working directory.
     *
     * It's not possible to change the real CWD of the process so this
     * state is maintained internally. External commands run via
     * {@link #system} are executed in this directory.
     *
     * @returns {nsIFile}
     */
    get cwd() {
        let dir = File(this._cwd);

        // NOTE: the directory could have been deleted underneath us so
        // fallback to the process's CWD
        if (dir.exists() && dir.isDirectory())
            return dir;
        else
            return this._processDir.clone();
    },

    /**
     * Sets the current working directory.
     *
     * @param {string} newDir The new CWD. This may be a relative or
     *     absolute path and is expanded by {@link #expandPath}.
     */
    set cwd(newDir) {
        newDir = newDir && newDir.path || newDir || "~";

        if (newDir == "-") {
            util.assert(this._oldcwd != null, "E186: No previous directory");
            [this._cwd, this._oldcwd] = [this._oldcwd, this.cwd];
        }
        else {
            let dir = io.File(newDir);
            util.assert(dir.exists() && dir.isDirectory(), "E344: Can't find directory " + dir.path.quote());
            dir.normalize();
            [this._cwd, this._oldcwd] = [dir.path, this.cwd];
        }
        return this.cwd;
    },

    /**
     * @property {function} File class.
     * @final
     */
    File: Class.memoize(function () let (io = this)
        Class("File", File, {
            init: function init(path, checkCWD)
                init.supercall(this, path, (arguments.length < 2 || checkCWD) && io.cwd)
        })),

    /**
     * @property {Object} The current file sourcing context. As a file is
     *     being sourced the 'file' and 'line' properties of this context
     *     object are updated appropriately.
     */
    sourcing: null,

    expandPath: deprecated("File.expandPath", function expandPath() File.expandPath.apply(File, arguments)),

    /**
     * Returns the first user RC file found in *dir*.
     *
     * @param {File|string} dir The directory to search.
     * @param {boolean} always When true, return a path whether
     *     the file exists or not.
     * @default $HOME.
     * @returns {nsIFile} The RC file or null if none is found.
     */
    getRCFile: function (dir, always) {
        dir = this.File(dir || "~");

        let rcFile1 = dir.child("." + config.name + "rc");
        let rcFile2 = dir.child("_" + config.name + "rc");

        if (util.OS.isWindows)
            [rcFile1, rcFile2] = [rcFile2, rcFile1];

        if (rcFile1.exists() && rcFile1.isFile())
            return rcFile1;
        else if (rcFile2.exists() && rcFile2.isFile())
            return rcFile2;
        else if (always)
            return rcFile1;
        return null;
    },

    // TODO: make secure
    /**
     * Creates a temporary file.
     *
     * @returns {File}
     */
    createTempFile: function () {
        let file = services.directory.get("TmpD", Ci.nsIFile);
        file.append(this.config.tempFile);
        file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, octal(600));

        Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
            .getService(Ci.nsPIExternalAppLauncher).deleteTemporaryFileOnExit(file);

        return File(file);
    },

    /**
     * Determines whether the given URL string resolves to a JAR URL and
     * returns the matching nsIJARURI object if it does.
     *
     * @param {string} url The URL to check.
     * @returns {nsIJARURI|null}
     */
    isJarURL: function isJarURL(url) {
        try {
            let uri = util.newURI(url);
            let channel = services.io.newChannelFromURI(uri);
            channel.cancel(Cr.NS_BINDING_ABORTED);
            if (channel instanceof Ci.nsIJARChannel)
                return channel.URI.QueryInterface(Ci.nsIJARURI);
        }
        catch (e) {}
        return null;
    },

    /**
     * Returns a list of the contents of the given JAR file which are
     * children of the given path.
     *
     * @param {nsIURI|string} file The URI of the JAR file to list.
     * @param {string} path The prefix path to search.
     */
    listJar: function listJar(file, path) {
        file = util.getFile(file);
        if (file) {
            // let jar = services.zipReader.getZip(file); Crashes.
            let jar = services.ZipReader(file);
            try {
                let filter = RegExp("^" + util.regexp.escape(decodeURI(path))
                                    + "[^/]*/?$");

                for (let entry in iter(jar.findEntries("*")))
                    if (filter.test(entry))
                        yield entry;
            }
            finally {
                jar.close();
            }
        }
    },

    readHeredoc: function (end) {
        return "";
    },

    /**
     * Searches for the given executable file in the system executable
     * file paths as specified by the PATH environment variable.
     *
     * On Windows, if the unadorned filename cannot be found, the
     * extensions in the semicolon-separated list in the PATHSEP
     * environment variable are successively appended to the original
     * name and searched for in turn.
     *
     * @param {string} bin The name of the executable to find.
     */
    pathSearch: function (bin) {
        if (bin instanceof File || File.isAbsolutePath(bin))
            return this.File(bin);

        let dirs = services.environment.get("PATH").split(util.OS.isWindows ? ";" : ":");
        // Windows tries the CWD first TODO: desirable?
        if (util.OS.isWindows)
            dirs = [io.cwd].concat(dirs);

        for (let [, dir] in Iterator(dirs))
            try {
                dir = this.File(dir, true);

                let file = dir.child(bin);
                if (file.exists())
                    return file;

                // TODO: couldn't we just palm this off to the start command?
                // automatically try to add the executable path extensions on windows
                if (util.OS.isWindows) {
                    let extensions = services.environment.get("PATHEXT").split(";");
                    for (let [, extension] in Iterator(extensions)) {
                        file = dir.child(bin + extension);
                        if (file.exists())
                            return file;
                    }
                }
            }
            catch (e) {}
        return null;
    },

    /**
     * Runs an external program.
     *
     * @param {File|string} program The program to run.
     * @param {string[]} args An array of arguments to pass to *program*.
     */
    run: function (program, args, blocking) {
        args = args || [];

        let file = this.pathSearch(program);

        if (!file || !file.exists()) {
            util.dactyl.echoerr("Command not found: " + program);
            return -1;
        }

        let process = services.Process(file);
        process.run(false, args.map(String), args.length);
        try {
            if (callable(blocking))
                var timer = services.Timer(
                    function () {
                        if (!process.isRunning) {
                            timer.cancel();
                            util.trapErrors(blocking);
                        }
                    },
                    100, services.Timer.TYPE_REPEATING_SLACK);
            else if (blocking)
                while (process.isRunning)
                    util.threadYield(false, true);
        }
        catch (e) {
            process.kill();
            throw e;
        }

        return process.exitValue;
    },

    // TODO: when https://bugzilla.mozilla.org/show_bug.cgi?id=68702 is
    // fixed use that instead of a tmpfile
    /**
     * Runs *command* in a subshell and returns the output in a string. The
     * shell used is that specified by the 'shell' option.
     *
     * @param {string} command The command to run.
     * @param {string} input Any input to be provided to the command on stdin.
     * @returns {string}
     */
    system: function (command, input) {
        util.dactyl.echomsg("Calling shell to execute: " + command, 4);

        function escape(str) '"' + str.replace(/[\\"$]/g, "\\$&") + '"';

        return this.withTempFiles(function (stdin, stdout, cmd) {
            if (input instanceof File)
                stdin = input;
            else if (input)
                stdin.write(input);

            let shell = io.pathSearch(storage["options"].get("shell").value);
            let shcf = storage["options"].get("shellcmdflag").value;
            util.assert(shell, "Invalid 'shell'");

            if (isArray(command))
                command = command.map(escape).join(" ");

            // TODO: implement 'shellredir'
            if (util.OS.isWindows && !/sh/.test(shell.leafName)) {
                command = "cd /D " + this.cwd.path + " && " + command + " > " + stdout.path + " 2>&1" + " < " + stdin.path;
                var res = this.run(shell, shcf.split(/\s+/).concat(command), true);
            }
            else {
                cmd.write("cd " + escape(this.cwd.path) + "\n" +
                        ["exec", ">" + escape(stdout.path), "2>&1", "<" + escape(stdin.path),
                         escape(shell.path), shcf, escape(command)].join(" "));
                res = this.run("/bin/sh", ["-e", cmd.path], true);
            }

            return {
                __noSuchMethod__: function (meth, args) this.output[meth].apply(this.output, args),
                valueOf: function () this.output,
                output: stdout.read().replace(/^(.*)\n$/, "$1"),
                returnValue: res
            };
        }) || "";
    },

    /**
     * Creates a temporary file context for executing external commands.
     * *func* is called with a temp file, created with {@link #createTempFile},
     * for each explicit argument. Ensures that all files are removed when
     * *func* returns.
     *
     * @param {function} func The function to execute.
     * @param {Object} self The 'this' object used when executing func.
     * @returns {boolean} false if temp files couldn't be created,
     *     otherwise, the return value of *func*.
     */
    withTempFiles: function (func, self, checked) {
        let args = array(util.range(0, func.length)).map(this.createTempFile).array;
        try {
            if (!args.every(util.identity))
                return false;
            var res = func.apply(self || this, args);
        }
        finally {
            if (!checked || res !== true)
                args.forEach(function (f) f && f.remove(false));
        }
        return res;
    }
}, {
    /**
     * @property {string} The value of the $PENTADACTYL_RUNTIME environment
     *     variable.
     */<commit_msg>Fix bug caused by last commit.<commit_after>// Copyright (c) 2006-2008 by <NAME> <<EMAIL>>
// Copyright (c) 2007-2009 by <NAME> <<EMAIL>>
// Copyright (c) 2008-2010 by <NAME> <<EMAIL>>
// Some code based on Venkman
//
// This work is licensed for reuse under an MIT license. Details are
// given in the LICENSE.txt file included with this file.
"use strict";

try {

Components.utils.import("resource://dactyl/bootstrap.jsm");
defineModule("io", {
    exports: ["IO", "io"],
    require: ["services"],
    use: ["config", "storage", "styles", "template", "util"]
}, this);

// TODO: why are we passing around strings rather than file objects?
/**
 * Provides a basic interface to common system I/O operations.
 * @instance io
 */
var IO = Module("io", {
    init: function () {
        this._processDir = services.directory.get("CurWorkD", Ci.nsIFile);
        this._cwd = this._processDir.path;
        this._oldcwd = null;
        this.config = config;
    },

    Local: function (dactyl, modules, window) let ({ Script, io, plugins } = modules) ({

        init: function init() {
            this.config = modules.config;
            this._processDir = services.directory.get("CurWorkD", Ci.nsIFile);
            this._cwd = this._processDir.path;
            this._oldcwd = null;

            this._lastRunCommand = ""; // updated whenever the users runs a command with :!
            this._scriptNames = [];

            this.downloadListener = {
                onDownloadStateChange: function (state, download) {
                    if (download.state == services.downloadManager.DOWNLOAD_FINISHED) {
                        let url   = download.source.spec;
                        let title = download.displayName;
                        let file  = download.targetFile.path;
                        let size  = download.size;

                        dactyl.echomsg({ domains: [util.getHost(url)], message: "Download of " + title + " to " + file + " finished" },
                                       1, modules.commandline.ACTIVE_WINDOW);
                        modules.autocommands.trigger("DownloadPost", { url: url, title: title, file: file, size: size });
                    }
                },
                onStateChange:    function () {},
                onProgressChange: function () {},
                onSecurityChange: function () {}
            };

            services.downloadManager.addListener(this.downloadListener);
        },

        destroy: function destroy() {
            services.downloadManager.removeListener(this.downloadListener);
            for (let [, plugin] in Iterator(plugins.contexts))
                if (plugin.onUnload)
                    plugin.onUnload();
        },

        /**
         * Returns all directories named *name* in 'runtimepath'.
         *
         * @param {string} name
         * @returns {nsIFile[])
         */
        getRuntimeDirectories: function getRuntimeDirectories(name) {
            return modules.options.get("runtimepath").files
                .map(function (dir) dir.child(name))
                .filter(function (dir) dir.exists() && dir.isDirectory() && dir.isReadable());
        },

        // FIXME: multiple paths?
        /**
         * Sources files found in 'runtimepath'. For each relative path in *paths*
         * each directory in 'runtimepath' is searched and if a matching file is
         * found it is sourced. Only the first file found (per specified path) is
         * sourced unless *all* is specified, then all found files are sourced.
         *
         * @param {string[]} paths An array of relative paths to source.
         * @param {boolean} all Whether all found files should be sourced.
         */
        sourceFromRuntimePath: function sourceFromRuntimePath(paths, all) {
            let dirs = modules.options.get("runtimepath").files;
            let found = false;

            dactyl.echomsg("Searching for " + paths.join(" ").quote() + " in " + modules.options.get("runtimepath").stringValue, 2);

        outer:
            for (let dir in values(dirs)) {
                for (let [,path] in Iterator(paths)) {
                    let file = dir.child(path);

                    dactyl.echomsg("Searching for " + file.path.quote(), 3);

                    if (file.exists() && file.isFile() && file.isReadable()) {
                        io.source(file.path, false);
                        found = true;

                        if (!all)
                            break outer;
                    }
                }
            }

            if (!found)
                dactyl.echomsg("not found in 'runtimepath': " + paths.join(" ").quote(), 1);

            return found;
        },

        /**
         * Reads Ex commands, JavaScript or CSS from *filename*.
         *
         * @param {string} filename The name of the file to source.
         * @param {boolean} silent Whether errors should be reported.
         */
        source: function source(filename, silent) {
            defineModule.loadLog.push("sourcing " + filename);
            let time = Date.now();
            this.withSavedValues(["sourcing"], function _source() {
                this.sourcing = null;
                try {
                    var file = util.getFile(filename) || io.File(filename);

                    if (!file.exists() || !file.isReadable() || file.isDirectory()) {
                        if (!silent)
                            dactyl.echoerr("E484: Can't open file " + filename.quote());
                        return;
                    }

                    dactyl.echomsg("sourcing " + filename.quote(), 2);

                    let uri = services.io.newFileURI(file);

                    // handle pure JavaScript files specially
                    if (/\.js$/.test(filename)) {
                        try {
                            dactyl.loadScript(uri.spec, Script(file));
                            dactyl.helpInitialized = false;
                        }
                        catch (e) {
                            if (e.fileName)
                                try {
                                    e.fileName = e.fileName.replace(/^(chrome|resource):.*? -> /, "");
                                    if (e.fileName == uri.spec)
                                        e.fileName = filename;
                                    e.echoerr = <>{e.fileName}:{e.lineNumber}: {e}</>;
                                }
                                catch (e) {}
                            throw e;
                        }
                    }
                    else if (/\.css$/.test(filename))
                        styles.registerSheet(uri.spec, false, true);
                    else {
                        modules.commands.execute(file.read(), null, silent || "loud", null,
                            { file: file.path, line: 1 });
                    }

                    if (this._scriptNames.indexOf(file.path) == -1)
                        this._scriptNames.push(file.path);

                    dactyl.echomsg("finished sourcing " + filename.quote(), 2);

                    dactyl.log("Sourced: " + filename, 3);
                }
                catch (e) {
                    if (!(e instanceof FailedAssertion))
                        dactyl.reportError(e);
                    let message = "Sourcing file: " + (e.echoerr || file.path + ": " + e);
                    if (!silent)
                        dactyl.echoerr(message);
                }
                finally {
                    defineModule.loadLog.push("done sourcing " + filename + ": " + (Date.now() - time) + "ms");
                }
            });
        },
    }),

    // TODO: there seems to be no way, short of a new component, to change
    // the process's CWD - see https://bugzilla.mozilla.org/show_bug.cgi?id=280953
    /**
     * Returns the current working directory.
     *
     * It's not possible to change the real CWD of the process so this
     * state is maintained internally. External commands run via
     * {@link #system} are executed in this directory.
     *
     * @returns {nsIFile}
     */
    get cwd() {
        let dir = File(this._cwd);

        // NOTE: the directory could have been deleted underneath us so
        // fallback to the process's CWD
        if (dir.exists() && dir.isDirectory())
            return dir;
        else
            return this._processDir.clone();
    },

    /**
     * Sets the current working directory.
     *
     * @param {string} newDir The new CWD. This may be a relative or
     *     absolute path and is expanded by {@link #expandPath}.
     */
    set cwd(newDir) {
        newDir = newDir && newDir.path || newDir || "~";

        if (newDir == "-") {
            util.assert(this._oldcwd != null, "E186: No previous directory");
            [this._cwd, this._oldcwd] = [this._oldcwd, this.cwd];
        }
        else {
            let dir = io.File(newDir);
            util.assert(dir.exists() && dir.isDirectory(), "E344: Can't find directory " + dir.path.quote());
            dir.normalize();
            [this._cwd, this._oldcwd] = [dir.path, this.cwd];
        }
        return this.cwd;
    },

    /**
     * @property {function} File class.
     * @final
     */
    File: Class.memoize(function () let (io = this)
        Class("File", File, {
            init: function init(path, checkCWD)
                init.supercall(this, path, (arguments.length < 2 || checkCWD) && io.cwd)
        })),

    /**
     * @property {Object} The current file sourcing context. As a file is
     *     being sourced the 'file' and 'line' properties of this context
     *     object are updated appropriately.
     */
    sourcing: null,

    expandPath: deprecated("File.expandPath", function expandPath() File.expandPath.apply(File, arguments)),

    /**
     * Returns the first user RC file found in *dir*.
     *
     * @param {File|string} dir The directory to search.
     * @param {boolean} always When true, return a path whether
     *     the file exists or not.
     * @default $HOME.
     * @returns {nsIFile} The RC file or null if none is found.
     */
    getRCFile: function (dir, always) {
        dir = this.File(dir || "~");

        let rcFile1 = dir.child("." + config.name + "rc");
        let rcFile2 = dir.child("_" + config.name + "rc");

        if (util.OS.isWindows)
            [rcFile1, rcFile2] = [rcFile2, rcFile1];

        if (rcFile1.exists() && rcFile1.isFile())
            return rcFile1;
        else if (rcFile2.exists() && rcFile2.isFile())
            return rcFile2;
        else if (always)
            return rcFile1;
        return null;
    },

    // TODO: make secure
    /**
     * Creates a temporary file.
     *
     * @returns {File}
     */
    createTempFile: function () {
        let file = services.directory.get("TmpD", Ci.nsIFile);
        file.append(this.config.tempFile);
        file.createUnique(Ci.nsIFile.NORMAL_FILE_TYPE, octal(600));

        Cc["@mozilla.org/uriloader/external-helper-app-service;1"]
            .getService(Ci.nsPIExternalAppLauncher).deleteTemporaryFileOnExit(file);

        return File(file);
    },

    /**
     * Determines whether the given URL string resolves to a JAR URL and
     * returns the matching nsIJARURI object if it does.
     *
     * @param {string} url The URL to check.
     * @returns {nsIJARURI|null}
     */
    isJarURL: function isJarURL(url) {
        try {
            let uri = util.newURI(url);
            let channel = services.io.newChannelFromURI(uri);
            channel.cancel(Cr.NS_BINDING_ABORTED);
            if (channel instanceof Ci.nsIJARChannel)
                return channel.URI.QueryInterface(Ci.nsIJARURI);
        }
        catch (e) {}
        return null;
    },

    /**
     * Returns a list of the contents of the given JAR file which are
     * children of the given path.
     *
     * @param {nsIURI|string} file The URI of the JAR file to list.
     * @param {string} path The prefix path to search.
     */
    listJar: function listJar(file, path) {
        file = util.getFile(file);
        if (file) {
            // let jar = services.zipReader.getZip(file); Crashes.
            let jar = services.ZipReader(file);
            try {
                let filter = RegExp("^" + util.regexp.escape(decodeURI(path))
                                    + "[^/]*/?$");

                for (let entry in iter(jar.findEntries("*")))
                    if (filter.test(entry))
                        yield entry;
            }
            finally {
                jar.close();
            }
        }
    },

    readHeredoc: function (end) {
        return "";
    },

    /**
     * Searches for the given executable file in the system executable
     * file paths as specified by the PATH environment variable.
     *
     * On Windows, if the unadorned filename cannot be found, the
     * extensions in the semicolon-separated list in the PATHSEP
     * environment variable are successively appended to the original
     * name and searched for in turn.
     *
     * @param {string} bin The name of the executable to find.
     */
    pathSearch: function (bin) {
        if (bin instanceof File || File.isAbsolutePath(bin))
            return this.File(bin);

        let dirs = services.environment.get("PATH").split(util.OS.isWindows ? ";" : ":");
        // Windows tries the CWD first TODO: desirable?
        if (util.OS.isWindows)
            dirs = [io.cwd].concat(dirs);

        for (let [, dir] in Iterator(dirs))
            try {
                dir = this.File(dir, true);

                let file = dir.child(bin);
                if (file.exists())
                    return file;

                // TODO: couldn't we just palm this off to the start command?
                // automatically try to add the executable path extensions on windows
                if (util.OS.isWindows) {
                    let extensions = services.environment.get("PATHEXT").split(";");
                    for (let [, extension] in Iterator(extensions)) {
                        file = dir.child(bin + extension);
                        if (file.exists())
                            return file;
                    }
                }
            }
            catch (e) {}
        return null;
    },

    /**
     * Runs an external program.
     *
     * @param {File|string} program The program to run.
     * @param {string[]} args An array of arguments to pass to *program*.
     */
    run: function (program, args, blocking) {
        args = args || [];

        let file = this.pathSearch(program);

        if (!file || !file.exists()) {
            util.dactyl.echoerr("Command not found: " + program);
            return -1;
        }

        let process = services.Process(file);
        process.run(false, args.map(String), args.length);
        try {
            if (callable(blocking))
                var timer = services.Timer(
                    function () {
                        if (!process.isRunning) {
                            timer.cancel();
                            util.trapErrors(blocking);
                        }
                    },
                    100, services.Timer.TYPE_REPEATING_SLACK);
            else if (blocking)
                while (process.isRunning)
                    util.threadYield(false, true);
        }
        catch (e) {
            process.kill();
            throw e;
        }

        return process.exitValue;
    },

    // TODO: when https://bugzilla.mozilla.org/show_bug.cgi?id=68702 is
    // fixed use that instead of a tmpfile
    /**
     * Runs *command* in a subshell and returns the output in a string. The
     * shell used is that specified by the 'shell' option.
     *
     * @param {string} command The command to run.
     * @param {string} input Any input to be provided to the command on stdin.
     * @returns {string}
     */
    system: function (command, input) {
        util.dactyl.echomsg("Calling shell to execute: " + command, 4);

        function escape(str) '"' + str.replace(/[\\"$]/g, "\\$&") + '"';

        return this.withTempFiles(function (stdin, stdout, cmd) {
            if (input instanceof File)
                stdin = input;
            else if (input)
                stdin.write(input);

            let shell = io.pathSearch(storage["options"].get("shell").value);
            let shcf = storage["options"].get("shellcmdflag").value;
            util.assert(shell, "Invalid 'shell'");

            if (isArray(command))
                command = command.map(escape).join(" ");

            // TODO: implement 'shellredir'
            if (util.OS.isWindows && !/sh/.test(shell.leafName)) {
                command = "cd /D " + this.cwd.path + " && " + command + " > " + stdout.path + " 2>&1" + " < " + stdin.path;
                var res = this.run(shell, shcf.split(/\s+/).concat(command), true);
            }
            else {
                cmd.write("cd " + escape(this.cwd.path) + "\n" +
                        ["exec", ">" + escape(stdout.path), "2>&1", "<" + escape(stdin.path),
                         escape(shell.path), shcf, escape(command)].join(" "));
                res = this.run("/bin/sh", ["-e", cmd.path], true);
            }

            return {
                __noSuchMethod__: function (meth, args) this.output[meth].apply(this.output, args),
                valueOf: function () this.output,
                output: stdout.read().replace(/^(.*)\n$/, "$1"),
                returnValue: res
            };
        }) || "";
    },

    /**
     * Creates a temporary file context for executing external commands.
     * *func* is called with a temp file, created with {@link #createTempFile},
     * for each explicit argument. Ensures that all files are removed when
     * *func* returns.
     *
     * @param {function} func The function to execute.
     * @param {Object} self The 'this' object used when executing func.
     * @returns {boolean} false if temp files couldn't be created,
     *     otherwise, the return value of *func*.
     */
    withTempFiles: function (func, self, checked) {
        let args = array(util.range(0, func.length)).map(this.closure.createTempFile).array;
        try {
            if (!args.every(util.identity))
                return false;
            var res = func.apply(self || this, args);
        }
        finally {
            if (!checked || res !== true)
                args.forEach(function (f) f && f.remove(false));
        }
        return res;
    }
}, {
    /**
     * @property {string} The value of the $PENTADACTYL_RUNTIME environment
     *     variable.
     */
{'fraction_non_alphanumeric': 0.12225680312824196, 'fraction_numerical': 0.004362514297874604, 'mean_word_length': 1.543916632832589, 'pattern_counts': {'":': 6, '<': 31, '<?xml version=': 0, '>': 35, 'https://': 4, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 46, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'dir': 'git-commits-cleaned', 'id': '880275', 'n_tokens_mistral': 10476, 'n_tokens_neox': 9903, 'n_words': 2767}