1 /** FileExtensions to std.file.
2 	Copyright: Per Nordlöw 2024-.
3 	License: $(WEB boost.org/LICENSE_1_0.txt, Boost License 1.0).
4 	Authors: $(WEB Per Nordlöw)
5 */
6 module nxt.file;
7 
8 import std.stdio : File;
9 import nxt.path : FileName, FilePath, DirPath;
10 import nxt.pattern : Node;
11 
12 @safe:
13 
14 /++ Type of file.
15  +/
16 struct FileType {
17 	DataFormat format; ///< Format associated with file type.
18 	alias format this;
19 	Node fileNamePattern; ///< Pattern matching file name(s) often associated.
20 }
21 
22 typeof(FileName.str) matchFirst(scope return /+ref+/ FileName input, const Node node) pure nothrow /+@nogc+/ {
23 	import nxt.pattern : matchFirst;
24 	return input.str.matchFirst(node);
25 }
26 
27 /++ Get pattern that matches a file name ending with '.'~`s`. +/
28 static Node fileExtension(string s) pure nothrow {
29 	import nxt.pattern : seq, lit, eob;
30 	return seq(lit('.'), lit(s), eob());
31 }
32 
33 /// ditto
34 @safe pure unittest {
35 	auto app_d = FileName("app.d");
36 	assert( app_d.matchFirst(fileExtension("d")));
37 	assert(!app_d.matchFirst(fileExtension("c")));
38 	assert(!app_d.matchFirst(fileExtension("cpp")));
39 	auto app = FileName("app");
40 	assert(!app.matchFirst(fileExtension("d")));
41 	assert(!app.matchFirst(fileExtension("c")));
42 	assert(!app.matchFirst(fileExtension("cpp")));
43 	auto app_c = FileName("app.c");
44 	assert(!app_c.matchFirst(fileExtension("d")));
45 	assert( app_c.matchFirst(fileExtension("c")));
46 	assert(!app_c.matchFirst(fileExtension("cpp")));
47 }
48 
49 /++ (Data) Format of file contents.
50 
51 	The `name` can be either a programming language such as "C",
52 	"C++", "D", etc or a data format such as "JSON", "XML" etc.
53 
54 	See: https://en.wikipedia.org/wiki/File_format
55  +/
56 struct DataFormat {
57 	string name;
58 	alias name this;
59 	/// Pattern matching file contents often associated.
60 	Node contentPattern;
61 }
62 
63 /++ Extension of filename.
64 	See: https://en.wikipedia.org/wiki/Filename_extension
65  +/
66 struct FileExtension {
67 	string value;
68 	alias value this;
69 }
70 
71 /** Read file $(D path) into raw array with one extra terminating zero byte.
72  *
73  * This extra terminating zero (`null`) byte at the end is typically used as a
74  * sentinel value to speed up textual parsers or when characters are sent to a
75  * C-function taking a zero-terminated string as input.
76  *
77  * TODO: Add or merge to Phobos?
78  *
79  * See_Also: https://en.wikipedia.org/wiki/Sentinel_value
80  * See_Also: http://forum.dlang.org/post/pdzxpkusvifelumkrtdb@forum.dlang.org
81  */
82 immutable(void)[] rawReadZ(FilePath path) @safe {
83 	return File(path.str, `rb`).rawReadZ();
84 }
85 /// ditto
86 immutable(void)[] rawReadZ(scope File file) @trusted
87 {
88 	import std.array : uninitializedArray;
89 
90 	alias Data = ubyte[];
91 	Data data = uninitializedArray!(Data)(file.size + 1); // one extra for terminator
92 
93 	file.rawRead(data);
94 	data[file.size] = '\0';	 // zero terminator for sentinel
95 
96 	import std.exception : assumeUnique;
97 	return assumeUnique(data);
98 }
99 
100 ///
101 version (Posix)
102 @safe unittest {
103 	import nxt.algorithm.searching : endsWith;
104 	const d = cast(const(char)[]) FilePath(`/etc/passwd`).rawReadZ();
105 	assert(d.endsWith('\0')); // has 0-terminator
106 }
107 
108 /++ Find path for `a` (or `FilePath.init` if not found) in `pathVariableName`.
109 	TODO: Add caching of result and detect changes via inotify.
110  +/
111 FilePath findExecutable(FileName a, scope const(char)[] pathVariableName = "PATH") {
112 	return findFileInPath(a, "PATH");
113 }
114 
115 ///
116 version (none)
117 @safe unittest {
118 	assert(findExecutable(FileName("ls")) == FilePath("/usr/bin/ls"));
119 }
120 
121 /++ Find path for `a` (or `FilePath.init` if not found) in `pathVariableName`.
122 	TODO: Add caching of result and detect changes via inotify.
123  +/
124 FilePath findFileInPath(FileName a, scope const(char)[] pathVariableName) {
125 	import std.algorithm : splitter;
126 	import std.process : environment;
127 	const envPATH = environment.get(pathVariableName, "");
128 	foreach (const p; envPATH.splitter(':')) {
129 		import nxt.path : buildPath, exists;
130 		const path = DirPath(p).buildPath(a);
131 		if (path.exists)
132 			return path; // pick first match
133 	}
134 	return typeof(return).init;
135 }
136 
137 /++ Get path to default temporary directory.
138 	See_Also: `std.file.tempDir`
139 	See: https://forum.dlang.org/post/gg9kds$1at0$1@digitalmars.com
140  +/
141 DirPath tempDir() {
142 	import std.file : std_tempDir = tempDir;
143 	return typeof(return)(std_tempDir);
144 }
145 
146 ///
147 @safe unittest {
148 	version (Posix) {
149 		assert(tempDir().str == "/tmp/");
150 	}
151 }
152 
153 /++ Get path to home directory.
154 	See_Also: `tempDir`
155 	See: https://forum.dlang.org/post/gg9kds$1at0$1@digitalmars.com
156  +/
157 DirPath homeDir() {
158 	import std.process : environment;
159     version(Windows) {
160         // On Windows, USERPROFILE is typically used, but HOMEPATH is an alternative
161 		if (const home = environment.get("USERPROFILE"))
162 			return typeof(return)(home);
163         // Fallback to HOMEDRIVE + HOMEPATH
164         const homeDrive = environment.get("HOMEDRIVE");
165         const homePath = environment.get("HOMEPATH");
166         if (homeDrive && homePath)
167             return typeof(return)(buildPath(homeDrive, homePath));
168     } else {
169         if (const home = environment.get("HOME"))
170 			return typeof(return)(home);
171     }
172     throw new Exception("Home directory environment variable is not set.");
173 }
174 
175 ///
176 @safe unittest {
177 	version (Posix) {
178 		import std.path : expandTilde;
179 		assert(homeDir().str == "~".expandTilde);
180 	}
181 }