123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350
/*******************************************************************************

        copyright:      Copyright (c) 2006-2009 Lars Ivar Igesund, Thomas Kühne,
                                           Grzegorz Adam Hankiewicz, sleek

        license:        BSD style: $(LICENSE)

        version:        Initial release: December 2006
                        Updated and readded: August 2009

        author:         Lars Ivar Igesund, Thomas Kühne,
                        Grzegorz Adam Hankiewicz, sleek

        Since:          0.99.9

*******************************************************************************/

module tango.sys.HomeFolder;

import TextUtil = tango.text.Util;
import Path = tango.io.Path;
import tango.sys.Environment;

version (Posix)
{
    import tango.core.Exception;
    import tango.stdc.stdlib;
    import tango.stdc.posix.pwd;
    import tango.stdc.errno;

    private extern (C) size_t strlen (in char *);
}


/******************************************************************************

  Returns the home folder set in the current environment.

******************************************************************************/

@property char[] homeFolder()
{
    version (Windows)
        return Path.standard(Environment.get("USERPROFILE"));
    else
        return Path.standard(Environment.get("HOME"));
}

version (Posix)
{

    /******************************************************************************

        Performs tilde expansion in paths.

        There are two ways of using tilde expansion in a path. One
        involves using the tilde alone or followed by a path separator. In
        this case, the tilde will be expanded with the value of the
        environment variable <i>HOME</i>.  The second way is putting
        a username after the tilde (i.e. <tt>~john/Mail</tt>). Here,
        the username will be searched for in the user database
        (i.e. <tt>/etc/passwd</tt> on Unix systems) and will expand to
        whatever path is stored there.  The username is considered the
        string after the tilde ending at the first instance of a path
        separator.

        Note that using the <i>~user</i> syntax may give different
        values from just <i>~</i> if the environment variable doesn't
        match the value stored in the user database.

        When the environment variable version is used, the path won't
        be modified if the environment variable doesn't exist or it
        is empty. When the database version is used, the path won't be
        modified if the user doesn't exist in the database or there is
        not enough memory to perform the query.

        Returns: inputPath with the tilde expanded, or just inputPath
        if it could not be expanded.

        Throws: OutOfMemoryException if there is not enough memory to
                perform the database lookup for the <i>~user</i> syntax.

        Examples:
        -----
        import tango.sys.HomeFolder;

        void processFile(char[] filename)
        {
             char[] path = expandTilde(filename);
            ...
        }
        -----

        -----
        import tango.sys.HomeFolder;

        const char[] RESOURCE_DIR_TEMPLATE = "~/.applicationrc";
        char[] RESOURCE_DIR;    // This gets expanded below.

        static this()
        {
            RESOURCE_DIR = expandTilde(RESOURCE_DIR_TEMPLATE);
        }
        -----
    ******************************************************************************/

    inout(char)[] expandTilde (inout(char)[] inputPath)
    {
            // Return early if there is no tilde in path.
            if (inputPath.length < 1 || inputPath[0] != '~')
                return inputPath;

            if (inputPath.length == 1 || inputPath[1] == '/')
                return expandFromEnvironment(inputPath);
            else
                return expandFromDatabase(inputPath);
    }

    /*******************************************************************************

            Replaces the tilde from path with the environment variable
            HOME.

    ******************************************************************************/

    private inout(char)[] expandFromEnvironment(inout(char)[] path)
    in
    {
        assert(path.length >= 1);
        assert(path[0] == '~');
    }
    body
    {
        // Get HOME and use that to replace the tilde.
        char[] home = homeFolder;
        if (home is null)
            return cast(inout)path;

        if (home[$-1] == '/')
            home = home[0..$-1];

        return cast(inout)Path.join(home, path[1..$]);

    }

    /*******************************************************************************

            Replaces the tilde from path with the path from the user
            database.

    ******************************************************************************/

    private inout(char)[] expandFromDatabase(inout(char)[] path)
    {
        assert(path.length > 2 || (path.length == 2 && path[1] != '/'));
        assert(path[0] == '~');

        // Extract username, searching for path separator.
        char[] username;
        auto last_char = TextUtil.locate(path, '/');

        if (last_char == path.length)
        {
            username = path[1..$].dup ~ '\0';
        }
        else
        {
            username = path[1..last_char].dup ~ '\0';
        }

        assert(last_char > 1);

        // Reserve C memory for the getpwnam_r() function.
        passwd result;
        int extra_memory_size = 5 * 1024;
        void* extra_memory;

        scope (exit) if(extra_memory) tango.stdc.stdlib.free(extra_memory);

        while (1)
        {
            extra_memory = tango.stdc.stdlib.malloc(extra_memory_size);
            if (extra_memory is null)
                throw new OutOfMemoryError("Not enough memory for user lookup in tilde expansion.", __LINE__);

            // Obtain info from database.
            passwd *verify;
            tango.stdc.errno.errno(0);
            if (getpwnam_r(username.ptr, &result, cast(char*)extra_memory, extra_memory_size,
                &verify) == 0)
            {
                // Failure if verify doesn't point at result.
                if (verify == &result)
                {
                    auto pwdirlen = strlen(result.pw_dir);

                    path = cast(inout)Path.join(result.pw_dir[0..pwdirlen].dup, path[last_char..$]);
                }

                return cast(inout)path;
            }

            if (tango.stdc.errno.errno() != ERANGE)
                throw new OutOfMemoryError("Not enough memory for user lookup in tilde expansion.", __LINE__);

            // extra_memory isn't large enough
            tango.stdc.stdlib.free(extra_memory);
            extra_memory_size *= 2;
        }
    }

}

version (Windows)
{

    /******************************************************************************

        Performs tilde expansion in paths.

        There are two ways of using tilde expansion in a path. One
        involves using the tilde alone or followed by a path separator. In
        this case, the tilde will be expanded with the value of the
        environment variable <i>HOME</i>.  The second way is putting
        a username after the tilde (i.e. <tt>~john/Mail</tt>). Here,
        the username will be searched for in the user database
        (i.e. <tt>/etc/passwd</tt> on Unix systems) and will expand to
        whatever path is stored there.  The username is considered the
        string after the tilde ending at the first instance of a path
        separator.

        Note that using the <i>~user</i> syntax may give different
        values from just <i>~</i> if the environment variable doesn't
        match the value stored in the user database.

        When the environment variable version is used, the path won't
        be modified if the environment variable doesn't exist or it
        is empty. When the database version is used, the path won't be
        modified if the user doesn't exist in the database or there is
        not enough memory to perform the query.

        Returns: inputPath with the tilde expanded, or just inputPath
        if it could not be expanded.

        Throws: OutOfMemoryException if there is not enough memory to
                perform the database lookup for the <i>~user</i> syntax.

        Examples:
        -----
        import tango.sys.HomeFolder;

        void processFile(char[] filename)
        {
             char[] path = expandTilde(filename);
            ...
        }
        -----

        -----
        import tango.sys.HomeFolder;

        const char[] RESOURCE_DIR_TEMPLATE = "~/.applicationrc";
        char[] RESOURCE_DIR;    // This gets expanded below.

        static this()
        {
            RESOURCE_DIR = expandTilde(RESOURCE_DIR_TEMPLATE);
        }
        -----
    ******************************************************************************/

    inout(char)[] expandTilde(inout(char)[] inputPath)
    {
        inputPath = cast(inout(char)[])Path.standard(inputPath.dup);

        if (inputPath.length < 1 || inputPath[0] != '~') {
            return cast(inout)inputPath;
        }

        if (inputPath.length == 1 || inputPath[1] == '/') {
            return expandCurrentUser(inputPath);
        }

        return expandOtherUser(inputPath);
    }

    private inout(char)[] expandCurrentUser(inout(char)[] path)
    {
        auto userProfileDir = homeFolder;
        auto offset = TextUtil.locate(path, '/');

        if (offset == path.length) {
            return cast(inout(char)[])userProfileDir;
        }

        return cast(inout)Path.join(userProfileDir, path[offset+1..$]);
    }

    private inout(char)[] expandOtherUser(inout(char)[] path)
    {
        auto profileDir = Path.parse(homeFolder).parent;
        return cast(inout)Path.join(profileDir, path[1..$]);
    }
}

/*******************************************************************************

*******************************************************************************/

debug(UnitTest) {

unittest
{
    version (Posix)
    {
    // Retrieve the current home variable.
    char[] home = Environment.get("HOME");

    // Testing when there is no environment variable.
    Environment.set("HOME", null);
    assert(expandTilde("~/") == "~/");
    assert(expandTilde("~") == "~");

    // Testing when an environment variable is set.
    Environment.set("HOME", "tango/test");
    assert (Environment.get("HOME") == "tango/test");

    assert(expandTilde("~/") == "tango/test/");
    assert(expandTilde("~") == "tango/test");

    // The same, but with a variable ending in a slash.
    Environment.set("HOME", "tango/test/");
    assert(expandTilde("~/") == "tango/test/");
    assert(expandTilde("~") == "tango/test");

    // Recover original HOME variable before continuing.
    if (home)
        Environment.set("HOME", home);
    else
        Environment.set("HOME", null);

    // Test user expansion for root. Are there unices without /root?
    assert(expandTilde("~root") == "/root" || expandTilde("~root") == "/var/root");
    assert(expandTilde("~root/") == "/root/" || expandTilde("~root") == "/var/root");
    assert(expandTilde("~Idontexist/hey") == "~Idontexist/hey");
    }
}
}