1 /*******************************************************************************
2 
3         Copyright:
4             Copyright (c) 2006-2009 Lars Ivar Igesund, Thomas Kühne,
5               Grzegorz Adam Hankiewicz, sleek
6             Some parts copyright (c) 2009-2016 dunnhumby Germany GmbH.
7             All rights reserved.
8 
9         License:
10             Tango Dual License: 3-Clause BSD License / Academic Free License v3.0.
11             See LICENSE_TANGO.txt for details.
12 
13         Version:
14             Initial release: December 2006
15             Updated and readded: August 2009
16 
17         Authors:
18             Lars Ivar Igesund, Thomas Kühne,
19             Grzegorz Adam Hankiewicz, sleek
20 
21 *******************************************************************************/
22 
23 module ocean.sys.HomeFolder;
24 
25 import ocean.meta.types.Qualifiers;
26 import ocean.core.Verify;
27 import TextUtil = ocean.text.Util;
28 import Path = ocean.io.Path;
29 import ocean.sys.Environment;
30 version (unittest) import ocean.core.Test;
31 
32 import core.exception : onOutOfMemoryError;
33 import core.stdc.stdlib;
34 import core.sys.posix.pwd;
35 import core.stdc.errno;
36 
37 private extern (C) size_t strlen (in char *);
38 
39 
40 /******************************************************************************
41 
42   Returns the home folder set in the current environment.
43 
44 ******************************************************************************/
45 
46 char[] homeFolder()
47 {
48    return Path.standard(Environment.get("HOME").dup);
49 }
50 
51 /*******************************************************************************
52 
53    Performs tilde expansion in paths.
54 
55    There are two ways of using tilde expansion in a path. One
56    involves using the tilde alone or followed by a path separator. In
57    this case, the tilde will be expanded with the value of the
58    environment variable <i>HOME</i>.  The second way is putting
59    a username after the tilde (i.e. <tt>~john/Mail</tt>). Here,
60    the username will be searched for in the user database
61    (i.e. <tt>/etc/passwd</tt> on Unix systems) and will expand to
62    whatever path is stored there.  The username is considered the
63    string after the tilde ending at the first instance of a path
64    separator.
65 
66    Note that using the <i>~user</i> syntax may give different
67    values from just <i>~</i> if the environment variable doesn't
68    match the value stored in the user database.
69 
70    When the environment variable version is used, the path won't
71    be modified if the environment variable doesn't exist or it
72    is empty. When the database version is used, the path won't be
73    modified if the user doesn't exist in the database or there is
74    not enough memory to perform the query.
75 
76    Returns: inputPath with the tilde expanded, or just inputPath
77    if it could not be expanded.
78 
79    Throws: OutOfMemoryException if there is not enough memory to
80       perform the database lookup for the <i>~user</i> syntax.
81 
82    Examples:
83    -----
84    import ocean.sys.HomeFolder;
85 
86    void processFile(char[] filename)
87    {
88         char[] path = expandTilde(filename);
89        ...
90    }
91    -----
92 
93    -----
94    import ocean.sys.HomeFolder;
95 
96    const char[] RESOURCE_DIR_TEMPLATE = "~/.applicationrc";
97    char[] RESOURCE_DIR;    // This gets expanded below.
98 
99    static this()
100    {
101        RESOURCE_DIR = expandTilde(RESOURCE_DIR_TEMPLATE);
102    }
103    -----
104 ******************************************************************************/
105 
106 cstring expandTilde (cstring inputPath)
107 {
108     // Return early if there is no tilde in path.
109     if (inputPath.length < 1 || inputPath[0] != '~')
110         return inputPath;
111 
112     if (inputPath.length == 1 || inputPath[1] == '/')
113         return expandFromEnvironment(inputPath);
114     else
115         return expandFromDatabase(inputPath);
116 }
117 
118 /*******************************************************************************
119 
120     Replaces the tilde from path with the environment variable HOME.
121 
122 *******************************************************************************/
123 
124 private cstring expandFromEnvironment(cstring path)
125 {
126     verify(path.length >= 1);
127     verify(path[0] == '~');
128 
129     // Get HOME and use that to replace the tilde.
130     char[] home = homeFolder;
131     if (home is null)
132         return path;
133 
134     if (home[$-1] == '/')
135         home = home[0..$-1];
136 
137     return Path.join(home, path[1..$]);
138 
139 }
140 
141 /*******************************************************************************
142 
143     Replaces the tilde from path with the path from the user database.
144 
145 *******************************************************************************/
146 
147 private cstring expandFromDatabase(cstring path)
148 {
149     verify(path.length > 2 || (path.length == 2 && path[1] != '/'));
150     verify(path[0] == '~');
151 
152     // Extract username, searching for path separator.
153     cstring username;
154     auto last_char = TextUtil.locate(path, '/');
155 
156     if (last_char == path.length)
157     {
158         username = path[1..$] ~ '\0';
159     }
160     else
161     {
162         username = path[1..last_char] ~ '\0';
163     }
164 
165     verify(last_char > 1);
166 
167     // Reserve C memory for the getpwnam_r() function.
168     passwd result;
169     int extra_memory_size = 5 * 1024;
170     void* extra_memory;
171 
172     scope (exit) if(extra_memory) core.stdc.stdlib.free(extra_memory);
173 
174     while (1)
175     {
176         extra_memory = core.stdc.stdlib.malloc(extra_memory_size);
177         if (extra_memory is null)
178             onOutOfMemoryError();
179 
180         // Obtain info from database.
181         passwd *verify;
182         core.stdc.errno.errno = 0;
183         if (getpwnam_r(username.ptr, &result, cast(char*)extra_memory, extra_memory_size,
184                        &verify) == 0)
185         {
186             // Failure if verify doesn't point at result.
187             if (verify == &result)
188             {
189                 auto pwdirlen = strlen(result.pw_dir);
190 
191                 path = Path.join(result.pw_dir[0..pwdirlen].dup, path[last_char..$]);
192             }
193 
194             return path;
195         }
196 
197         if (core.stdc.errno.errno() != ERANGE)
198             onOutOfMemoryError();
199 
200         // extra_memory isn't large enough
201         core.stdc.stdlib.free(extra_memory);
202         extra_memory_size *= 2;
203     }
204 }
205 
206 ///
207 unittest
208 {
209     // Retrieve the current home variable.
210     auto home = Environment.get("HOME");
211 
212     // Testing when there is no environment variable.
213     Environment.set("HOME", null);
214     test(expandTilde("~/") == "~/");
215     test(expandTilde("~") == "~");
216 
217     // Testing when an environment variable is set.
218     Environment.set("HOME", "ocean/test");
219     test (Environment.get("HOME") == "ocean/test");
220 
221     test(expandTilde("~/") == "ocean/test/");
222     test(expandTilde("~") == "ocean/test");
223 
224     // The same, but with a variable ending in a slash.
225     Environment.set("HOME", "ocean/test/");
226     test(expandTilde("~/") == "ocean/test/");
227     test(expandTilde("~") == "ocean/test");
228 
229     // Recover original HOME variable before continuing.
230     if (home)
231         Environment.set("HOME", home);
232     else
233         Environment.set("HOME", null);
234 
235     // Test user expansion for root. Are there unices without /root?
236     test(expandTilde("~root") == "/root" || expandTilde("~root") == "/var/root");
237     test(expandTilde("~root/") == "/root/" || expandTilde("~root") == "/var/root");
238     test(expandTilde("~Idontexist/hey") == "~Idontexist/hey");
239 }