1 /**
2 Copyright: Copyright (c) 2020, Joakim Brännström. All rights reserved.
3 License: $(LINK2 http://www.boost.org/LICENSE_1_0.txt, Boost Software License 1.0)
4 Author: Joakim Brännström (joakim.brannstrom@gmx.com)
5 */
6 module my.file;
7 
8 import logger = std.experimental.logger;
9 import std.algorithm : canFind;
10 import std.file : mkdirRecurse, exists, copy, dirEntries, SpanMode;
11 import std.path : relativePath, buildPath, dirName;
12 
13 public import std.file : attrIsDir, attrIsFile, attrIsSymlink, isFile, isDir, isSymlink;
14 
15 import my.path;
16 import my.optional;
17 
18 /** A `nothrow` version of `getAttributes` in Phobos.
19  *
20  * man 7 inode search for S_IFMT
21  * S_ISUID     04000   set-user-ID bit
22  * S_ISGID     02000   set-group-ID bit (see below)
23  * S_ISVTX     01000   sticky bit (see below)
24  *
25  * S_IRWXU     00700   owner has read, write, and execute permission
26  * S_IRUSR     00400   owner has read permission
27  * S_IWUSR     00200   owner has write permission
28  * S_IXUSR     00100   owner has execute permission
29  *
30  * S_IRWXG     00070   group has read, write, and execute permission
31  * S_IRGRP     00040   group has read permission
32  * S_IWGRP     00020   group has write permission
33  * S_IXGRP     00010   group has execute permission
34  *
35  * S_IRWXO     00007   others (not in group) have read, write, and execute permission
36  * S_IROTH     00004   others have read permission
37  * S_IWOTH     00002   others have write permission
38  * S_IXOTH     00001   others have execute permission
39  *
40  * The idea of doing it like this is from WebFreaks001 pull request
41  * [DCD pullrequest](https://github.com/dlang-community/dsymbol/pull/151/files).
42  *
43  * Returns: true on success and thus `attributes` contains a valid value.
44  */
45 bool getAttrs(const Path file, ref uint attributes) @safe nothrow {
46     import core.sys.posix.sys.stat : stat, stat_t;
47     import my.cstring;
48 
49     static bool trustedAttrs(const Path file, ref stat_t st) @trusted {
50         return stat(file.toString.tempCString, &st) == 0;
51     }
52 
53     stat_t st;
54     bool status = trustedAttrs(file, st);
55     attributes = st.st_mode;
56     return status;
57 }
58 
59 /** A `nothrow` version of `getLinkAttributes` in Phobos.
60  *
61  * Returns: true on success and thus `attributes` contains a valid value.
62  */
63 bool getLinkAttrs(const Path file, ref uint attributes) @safe nothrow {
64     import core.sys.posix.sys.stat : lstat, stat_t;
65     import my.cstring;
66 
67     static bool trustedAttrs(const Path file, ref stat_t st) @trusted {
68         return lstat(file.toString.tempCString, &st) == 0;
69     }
70 
71     stat_t st;
72     bool status = trustedAttrs(file, st);
73     attributes = st.st_mode;
74     return status;
75 }
76 
77 /** A `nothrow` version of `setAttributes` in Phobos.
78  */
79 bool setAttrs(const Path file, const uint attributes) @trusted nothrow {
80     import core.sys.posix.sys.stat : chmod;
81     import my.cstring;
82 
83     return chmod(file.toString.tempCString, attributes) == 0;
84 }
85 
86 /// Returns: true if `file` exists.
87 bool exists(const Path file) @safe nothrow {
88     uint attrs;
89     return getAttrs(file, attrs);
90 }
91 
92 /** Returns: true if `file` exists and is a file.
93  *
94  * Source: [DCD](https://github.com/dlang-community/dsymbol/blob/master/src/dsymbol/modulecache.d)
95  */
96 bool existsAnd(alias pred : isFile)(const Path file) @safe nothrow {
97     uint attrs;
98     if (!getAttrs(file, attrs))
99         return false;
100     return attrIsFile(attrs);
101 }
102 
103 /** Returns: true if `file` exists and is a directory.
104  *
105  * Source: [DCD](https://github.com/dlang-community/dsymbol/blob/master/src/dsymbol/modulecache.d)
106  */
107 bool existsAnd(alias pred : isDir)(const Path file) @safe nothrow {
108     uint attrs;
109     if (!getAttrs(file, attrs))
110         return false;
111     return attrIsDir(attrs);
112 }
113 
114 /** Returns: true if `file` exists and is a symlink.
115  *
116  * Source: [DCD](https://github.com/dlang-community/dsymbol/blob/master/src/dsymbol/modulecache.d)
117  */
118 bool existsAnd(alias pred : isSymlink)(const Path file) @safe nothrow {
119     uint attrs;
120     if (!getLinkAttrs(file, attrs))
121         return false;
122     return attrIsSymlink(attrs);
123 }
124 
125 /// Example:
126 unittest {
127     import std.file : remove, symlink, mkdir;
128     import std.format : format;
129     import std.stdio : File;
130 
131     const base = Path(format!"%s_%s"(__FILE__, __LINE__)).baseName;
132     const Path fname = base ~ "_file";
133     const Path dname = base ~ "_dir";
134     const Path symname = base ~ "_symlink";
135     scope (exit)
136         () {
137         foreach (f; [fname, dname, symname]) {
138             if (exists(f))
139                 remove(f);
140         }
141     }();
142 
143     File(fname, "w").write("foo");
144     mkdir(dname);
145     symlink(fname, symname);
146 
147     assert(exists(fname));
148     assert(existsAnd!isFile(fname));
149     assert(existsAnd!isDir(dname));
150     assert(existsAnd!isSymlink(symname));
151 }
152 
153 /// Copy `src` into `dst` recursively.
154 void copyRecurse(Path src, Path dst) {
155     foreach (a; dirEntries(src.toString, SpanMode.depth)) {
156         const s = relativePath(a.name, src.toString);
157         const d = buildPath(dst.toString, s);
158         if (!exists(d.dirName)) {
159             mkdirRecurse(d.dirName);
160         }
161         if (!existsAnd!isDir(Path(a))) {
162             copy(a.name, d);
163         }
164     }
165 }
166 
167 /// Make a file executable by all users on the system.
168 void setExecutable(Path p) nothrow {
169     import core.sys.posix.sys.stat;
170     import std.file : getAttributes, setAttributes;
171 
172     uint attrs;
173     if (getAttrs(p, attrs)) {
174         setAttrs(p, attrs | S_IXUSR | S_IXGRP | S_IXOTH);
175     }
176 }
177 
178 /// Check if a file is executable.
179 bool isExecutable(Path p) nothrow {
180     import core.sys.posix.sys.stat;
181     import std.file : getAttributes;
182 
183     uint attrs;
184     if (getAttrs(p, attrs)) {
185         return (attrs & (S_IXUSR | S_IXGRP | S_IXOTH)) != 0;
186     }
187     return false;
188 }
189 
190 /** As the unix command `which` it searches in `dirs` for an executable `name`.
191  *
192  * The difference in the behavior is that this function returns all
193  * matches and supports globbing (use of `*`).
194  *
195  * Example:
196  * ---
197  * writeln(which([Path("/bin")], "ls"));
198  * writeln(which([Path("/bin")], "l*"));
199  * ---
200  */
201 AbsolutePath[] which(Path[] dirs, string name) nothrow {
202     import std.algorithm : map, filter, joiner, copy;
203     import std.array : appender;
204     import std.exception : collectException;
205     import std.file : dirEntries, SpanMode;
206     import std.path : baseName, globMatch;
207 
208     auto res = appender!(AbsolutePath[])();
209 
210     foreach (dir; dirs.filter!(a => existsAnd!isDir(a))) {
211         try {
212             dirEntries(dir, SpanMode.shallow).map!(a => Path(a))
213                 .filter!(a => isExecutable(a))
214                 .filter!(a => globMatch(a.baseName, name))
215                 .map!(a => AbsolutePath(a))
216                 .copy(res);
217         } catch (Exception e) {
218             logger.trace(e.msg).collectException;
219         }
220     }
221 
222     return res.data;
223 }
224 
225 @("shall return all locations of ls")
226 unittest {
227     assert(which([Path("/bin")], "mv") == [AbsolutePath("/bin/mv")]);
228     assert(which([Path("/bin")], "l*").length >= 1);
229 }
230 
231 AbsolutePath[] whichFromEnv(string envKey, string name) {
232     import std.algorithm : splitter, map, filter;
233     import std.process : environment;
234     import std.array : empty, array;
235 
236     auto dirs = environment.get(envKey, null).splitter(":").filter!(a => !a.empty)
237         .map!(a => Path(a))
238         .array;
239     return which(dirs, name);
240 }
241 
242 @("shall return all locations of ls by using the environment variable PATH")
243 unittest {
244     assert(canFind(whichFromEnv("PATH", "mv"), AbsolutePath("/bin/mv")));
245 }
246 
247 /** Follow a symlink until it reaches its target.
248  *
249  * Max depth guard against recursions or self referencing symlinks.
250  */
251 Optional!Path followSymlink(Path p, int maxDepth = 100) @safe nothrow {
252     import std.file : readLink;
253 
254     try {
255         int depth;
256         for (; depth < maxDepth && existsAnd!isSymlink(p); ++depth) {
257             p = Path(buildPath(p.dirName, readLink(p.toString)));
258         }
259         if (depth < maxDepth)
260             return some(p);
261     } catch (Exception e) {
262     }
263 
264     return none!Path;
265 }
266 
267 @("shall not infinite loop over a self referencing symlink")
268 unittest {
269     import std.file : remove, symlink;
270     import std.format : format;
271     import std.path : baseName;
272 
273     immutable l = format!"%s_%s_link"(__FILE__.baseName, __LINE__);
274     scope (exit)
275         remove(l);
276     symlink(l, l);
277     assert(!followSymlink(Path(l)).hasValue, "should be no value");
278 }