1 /++ File system path and name types and their operations.
2 
3 	See: https://en.cppreference.com/w/cpp/filesystem/path
4 	See: https://hackage.haskell.org/package/FileSystem-1.0.0/docs/System-FileSystem-Types.html
5 	See_Also: `dmd.root.filename`
6  +/
7 module nxt.path;
8 
9 @safe:
10 
11 /++ Path.
12 
13 	The concept of a "pure path" doesn't need to be modelled in D as
14 	it has `pure` functions.  See
15 	https://docs.python.org/3/library/pathlib.html#pure-paths.
16 
17 	See: SUMO:`ComputerPath`.
18  +/
19 struct Path {
20 	this(string str, in bool normalize = false) pure nothrow @nogc {
21 		import nxt.algorithm.mutation : stripRight;
22 		import std.path : dirSeparator;
23 		this.str = normalize ? str.stripRight(dirSeparator) : str;
24 	}
25 	string str;
26 pure nothrow @nogc:
27 	bool opCast(T : bool)() const scope => str !is null;
28 	string toString() const @property => str;
29 }
30 
31 ///
32 @safe pure nothrow unittest {
33 	assert(Path("/usr/bin/").toString == "/usr/bin/");
34 }
35 
36 /++ (Regular) file path (on local file system).
37 	See: https://hackage.haskell.org/package/filepath-1.5.0.0/docs/System-FilePath.html#t:FilePath
38  +/
39 struct FilePath {
40 	this(string str, in bool normalize = false) pure nothrow @nogc {
41 		this.path = Path(str, normalize);
42 	}
43 	Path path;
44 	alias path this;
45 }
46 
47 ///
48 @safe pure nothrow unittest {
49 	assert(FilePath("/usr/bin/") == FilePath("/usr/bin/"));
50 	assert(FilePath("/usr/bin/") == Path("/usr/bin/"));
51 	assert(FilePath("foo") == Path("foo"));
52 	assert(FilePath("foo", false).str == "foo");
53 	assert(FilePath("foo", true).str == "foo");
54 	assert(Path("/etc/", false).str == "/etc/");
55 	assert(Path("/etc/", true).str == "/etc");
56 	assert(Path("foo", true).str == "foo");
57 }
58 
59 /++ Directory path (on local file system).
60 	See: SUMO:`ComputerDirectory`.
61  +/
62 struct DirPath {
63 	this(string path, in bool normalize = false) pure nothrow @nogc {
64 		this.path = Path(path, normalize);
65 	}
66 	Path path;
67 	alias path this;
68 }
69 
70 ///
71 @safe pure nothrow unittest {
72 	assert(DirPath("/etc") == Path("/etc"));
73 	assert(DirPath("/etc") == DirPath("/etc"));
74 	assert(DirPath("/etc/", false).str == "/etc/");
75 	assert(DirPath("/etc/", true).str == "/etc");
76 }
77 
78 /++ File name (either local or remote).
79  +/
80 struct FileName {
81 	import nxt.algorithm.searching : canFind;
82 	this(string str) pure nothrow @nogc in (!str.canFind('/')) {
83 		this.str = str;
84 	}
85 	string str;
86 pure nothrow @nogc:
87 	bool opCast(T : bool)() const scope => str !is null;
88 	string toString() const @property => str;
89 }
90 
91 ///
92 pure nothrow @safe unittest {
93 	assert(FileName(".emacs").str == ".emacs");
94 	assert(FileName(".emacs").toString == ".emacs");
95 }
96 
97 /++ Directory name (either local or remote).
98  +/
99 struct DirName {
100 	import nxt.algorithm.searching : canFind;
101 	this(string str) pure nothrow @nogc in (!str.canFind('/')) {
102 		this.str = str;
103 	}
104 	string str;
105 pure nothrow @nogc:
106 	bool opCast(T : bool)() const scope => str !is null;
107 	string toString() const @property => str;
108 }
109 
110 ///
111 pure nothrow @safe unittest {
112 	assert(DirName(".emacs.d").str == ".emacs.d");
113 	assert(DirName(".emacs.d").toString == ".emacs.d");
114 }
115 
116 /++ Execute file path (on local file system).
117  +/
118 struct ExePath {
119 	this(string path) pure nothrow @nogc {
120 		this.path = Path(path);
121 	}
122 	Path path;
123 	alias path this;
124 }
125 
126 ///
127 @safe pure nothrow unittest {
128 	assert(ExePath("/usr/bin/") == Path("/usr/bin/"));
129 }
130 
131 /++ Written (input) path (on local file system). +/
132 struct RdPath {
133 	this(string str) pure nothrow @nogc {
134 		this.path = Path(str);
135 	}
136 	Path path;
137 	alias path this;
138 }
139 
140 ///
141 @safe pure nothrow unittest {
142 	assert(RdPath("/usr/bin/") == Path("/usr/bin/"));
143 }
144 
145 /++ Written (output) path (on local file system). +/
146 struct WrPath {
147 	this(string str) pure nothrow @nogc {
148 		this.path = Path(str);
149 	}
150 	Path path;
151 	alias path this;
152 }
153 
154 ///
155 @safe pure nothrow unittest {
156 	assert(WrPath("/usr/bin/") == Path("/usr/bin/"));
157 }
158 
159 private import std.path : std_expandTilde = expandTilde;
160 
161 /++ Expand tilde in `a`.
162 	TODO: remove `@trusted` when scope inference is works.
163  +/
164 FilePath expandTilde(scope return FilePath a) nothrow @trusted => typeof(return)(std_expandTilde(a.path.str));
165 /// ditto
166 DirPath expandTilde(scope return DirPath a) nothrow @trusted => typeof(return)(std_expandTilde(a.path.str));
167 
168 ///
169 @safe nothrow unittest {
170 	assert(FilePath("~").expandTilde);
171 	assert(DirPath("~").expandTilde);
172 }
173 
174 private import std.path : std_buildPath = buildPath;
175 
176 /// Build path `a`/`b`.
177 FilePath buildPath(DirPath a, FilePath b) pure nothrow => typeof(return)(std_buildPath(a.path.str, b.path.str));
178 /// ditto
179 DirPath buildPath(DirPath a, DirPath b) pure nothrow => typeof(return)(std_buildPath(a.path.str, b.path.str));
180 /// ditto
181 DirPath buildPath(DirPath a, DirName b) pure nothrow => typeof(return)(std_buildPath(a.path.str, b.str));
182 /// ditto
183 FilePath buildPath(DirPath a, FileName b) pure nothrow => typeof(return)(std_buildPath(a.path.str, b.str));
184 
185 ///
186 @safe pure nothrow unittest {
187 	assert(DirPath("/etc").buildPath(FileName("foo")) == FilePath("/etc/foo"));
188 	assert(DirPath("/etc").buildPath(FilePath("foo")) == FilePath("/etc/foo"));
189 	assert(DirPath("/usr").buildPath(DirName("bin")) == DirPath("/usr/bin"));
190 	assert(DirPath("/usr").buildPath(DirPath("bin")) == DirPath("/usr/bin"));
191 	assert(DirPath("/usr").buildPath(DirPath("/bin")) == DirPath("/bin"));
192 }
193 
194 private import std.path : std_buildNormalizedPath = buildNormalizedPath;
195 
196 /// Build path `a`/`b`.
197 FilePath buildNormalizedPath(DirPath a, FilePath b) pure nothrow => typeof(return)(std_buildNormalizedPath(a.path.str, b.path.str));
198 /// ditto
199 DirPath buildNormalizedPath(DirPath a, DirPath b) pure nothrow => typeof(return)(std_buildNormalizedPath(a.path.str, b.path.str));
200 /// ditto
201 DirPath buildNormalizedPath(DirPath a, DirName b) pure nothrow => typeof(return)(std_buildNormalizedPath(a.path.str, b.str));
202 /// ditto
203 FilePath buildNormalizedPath(DirPath a, FileName b) pure nothrow => typeof(return)(std_buildNormalizedPath(a.path.str, b.str));
204 
205 ///
206 @safe pure nothrow unittest {
207 	assert(DirPath("/etc").buildNormalizedPath(FileName("foo")) == FilePath("/etc/foo"));
208 	assert(DirPath("/etc").buildNormalizedPath(FilePath("foo")) == FilePath("/etc/foo"));
209 	assert(DirPath("/usr").buildNormalizedPath(DirName("bin")) == DirPath("/usr/bin"));
210 	assert(DirPath("/usr").buildNormalizedPath(DirPath("bin")) == DirPath("/usr/bin"));
211 	assert(DirPath("/usr").buildNormalizedPath(DirPath("/bin")) == DirPath("/bin"));
212 }
213 
214 /++ URL.
215  +/
216 struct URL {
217 	string str;
218 pure nothrow @nogc:
219 	bool opCast(T : bool)() const scope => str !is null;
220 	string toString() const @property => str;
221 }
222 
223 ///
224 @safe pure nothrow unittest {
225 	assert(URL("www.sunet.se").toString == "www.sunet.se");
226 }
227 
228 /++ File URL.
229  +/
230 struct FileURL {
231 	URL url;
232 pure nothrow @nogc:
233 	this(string str) { this.url = URL(str); }
234 	this(FilePath path) { this.url = URL(path.str); }
235 	bool opCast(T : bool)() const scope => url.str !is null;
236 	string toString() const @property => url.str;
237 }
238 
239 ///
240 @safe pure nothrow unittest {
241 	assert(FileURL("www.sunet.se").toString == "www.sunet.se");
242 	assert(FileURL(FilePath("/etc/passwd")).toString == "/etc/passwd");
243 }
244 
245 /++ Directory URL.
246  +/
247 struct DirURL {
248 	URL url;
249 pure nothrow @nogc:
250 	this(string str) { this.url = URL(str); }
251 	this(DirPath path) { this.url = URL(path.str); }
252 	bool opCast(T : bool)() const scope => url.str !is null;
253 	string toString() const @property => url.str;
254 }
255 
256 ///
257 @safe pure nothrow unittest {
258 	assert(DirURL("www.sunet.se").toString == "www.sunet.se");
259 	assert(DirURL(DirPath("/etc/")).toString == "/etc/");
260 }
261 
262 /++ File URL and Offset (in bytes).
263  +/
264 struct FileURLOffset {
265 	import nxt.offset : Offset;
266 	FileURL url;
267 	Offset offset;
268 }
269 
270 /++ File URL and Region (in bytes).
271  +/
272 struct FileURLRegion {
273 	import nxt.region : Region;
274 	FileURL url;
275 	Region region;
276 }
277 
278 private import std.path : std_baseName = baseName;
279 
280 /// Get basename of `a`.
281 FileName baseName(FilePath a) pure nothrow @nogc => typeof(return)(std_baseName(a.str));
282 /// ditto
283 DirName baseName(DirPath a) pure nothrow @nogc => typeof(return)(std_baseName(a.str));
284 /// ditto
285 DirName baseName(URL a) pure nothrow @nogc => typeof(return)(std_baseName(a.str));
286 /// ditto
287 FileName baseName(FileURL a) pure nothrow @nogc => typeof(return)(std_baseName(a.url.str));
288 /// ditto
289 DirName baseName(DirURL a) pure nothrow @nogc => typeof(return)(std_baseName(a.url.str));
290 
291 ///
292 version (Posix) nothrow @safe unittest {
293 	assert(FilePath("/etc/foo").baseName.str == "foo");
294 	assert(DirPath("/etc/").baseName.str == "etc");
295 	const dmd = "https://github.com/dlang/dmd/";
296 	assert(URL(dmd).baseName.str == "dmd");
297 	assert(FileURL(dmd).baseName.str == "dmd");
298 	assert(DirURL(dmd).baseName.str == "dmd");
299 }
300 
301 private import std.file : std_exists = exists;
302 
303 /// Check if `a` exists.
304 bool exists(in Path a) nothrow @nogc => typeof(return)(std_exists(a.str));
305 /// ditto
306 bool exists(in FilePath a) nothrow @nogc => typeof(return)(std_exists(a.str));
307 /// ditto
308 bool exists(in DirPath a) nothrow @nogc => typeof(return)(std_exists(a.str));
309 /// ditto
310 bool exists(in FileURL a) nothrow @nogc => typeof(return)(std_exists(a.url.str));
311 /// ditto
312 bool exists(in DirURL a) nothrow @nogc => typeof(return)(std_exists(a.url.str));
313 /// ditto
314 bool exists(in FileName a) nothrow @nogc => typeof(return)(std_exists(a.str));
315 /// ditto
316 bool exists(in DirName a) nothrow @nogc => typeof(return)(std_exists(a.str));
317 
318 /// verify `a.toString` when `a` being `scope` parameter
319 version (none) // TODO: Remove or make compile
320 @safe pure nothrow @nogc unittest {
321 	import std.meta : AliasSeq;
322 
323 	static foreach (T; AliasSeq!(Path, FilePath, DirPath, FileName, DirName)) {{
324 		static void f(in T a) { const _ = a.toString; }
325 		f(T.init);
326 	}}
327 }
328 
329 ///
330 version (Posix) nothrow @safe unittest {
331 	assert( Path("/etc/").exists);
332 	assert(!Path("/etcxyz/").exists);
333 	assert( DirPath("/etc/").exists);
334 	assert( DirURL("/etc/").exists);
335 	assert(!DirPath("/etcxyz/").exists);
336 	assert( FilePath("/etc/passwd").exists);
337 	assert( FileURL("/etc/passwd").exists);
338 	assert(!FileName("dsfdsfdsfdsfdfdsf").exists);
339 	assert(!DirName("dsfdsfdsfdsfdfdsf").exists);
340 }