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