1 /**
2 	...
3 
4 	Copyright: © 2012 Matthias Dondorff
5 	License: Subject to the terms of the MIT license, as written in the included LICENSE.txt file.
6 	Authors: Matthias Dondorff
7 */
8 module dub.internal.utils;
9 
10 import dub.internal.vibecompat.core.file;
11 import dub.internal.vibecompat.core.log;
12 import dub.internal.vibecompat.data.json;
13 import dub.internal.vibecompat.inet.url;
14 import dub.version_;
15 
16 // todo: cleanup imports.
17 import std.algorithm : startsWith;
18 import std.array;
19 import std.conv;
20 import std.exception;
21 import std.file;
22 import std.process;
23 import std..string;
24 import std.traits : isIntegral;
25 import std.typecons;
26 import std.zip;
27 version(DubUseCurl) import std.net.curl;
28 
29 
30 Path getTempDir()
31 {
32 	return Path(std.file.tempDir());
33 }
34 
35 private Path[] temporary_files;
36 
37 Path getTempFile(string prefix, string extension = null)
38 {
39 	import std.uuid : randomUUID;
40 
41 	auto path = getTempDir() ~ (prefix ~ "-" ~ randomUUID.toString() ~ extension);
42 	temporary_files ~= path;
43 	return path;
44 }
45 
46 static ~this()
47 {
48 	foreach (path; temporary_files)
49 	{
50 		std.file.remove(path.toNativeString());
51 	}
52 }
53 
54 bool isEmptyDir(Path p) {
55 	foreach(DirEntry e; dirEntries(p.toNativeString(), SpanMode.shallow))
56 		return false;
57 	return true;
58 }
59 
60 bool isWritableDir(Path p, bool create_if_missing = false)
61 {
62 	import std.random;
63 	auto fname = p ~ format("__dub_write_test_%08X", uniform(0, uint.max));
64 	if (create_if_missing && !exists(p.toNativeString())) mkdirRecurse(p.toNativeString());
65 	try openFile(fname, FileMode.CreateTrunc).close();
66 	catch (Exception) return false;
67 	remove(fname.toNativeString());
68 	return true;
69 }
70 
71 Json jsonFromFile(Path file, bool silent_fail = false) {
72 	if( silent_fail && !existsFile(file) ) return Json.emptyObject;
73 	auto f = openFile(file.toNativeString(), FileMode.Read);
74 	scope(exit) f.close();
75 	auto text = stripUTF8Bom(cast(string)f.readAll());
76 	return parseJsonString(text, file.toNativeString());
77 }
78 
79 Json jsonFromZip(Path zip, string filename) {
80 	auto f = openFile(zip, FileMode.Read);
81 	ubyte[] b = new ubyte[cast(size_t)f.size];
82 	f.rawRead(b);
83 	f.close();
84 	auto archive = new ZipArchive(b);
85 	auto text = stripUTF8Bom(cast(string)archive.expand(archive.directory[filename]));
86 	return parseJsonString(text, zip.toNativeString~"/"~filename);
87 }
88 
89 void writeJsonFile(Path path, Json json)
90 {
91 	auto f = openFile(path, FileMode.CreateTrunc);
92 	scope(exit) f.close();
93 	f.writePrettyJsonString(json);
94 }
95 
96 bool isPathFromZip(string p) {
97 	enforce(p.length > 0);
98 	return p[$-1] == '/';
99 }
100 
101 bool existsDirectory(Path path) {
102 	if( !existsFile(path) ) return false;
103 	auto fi = getFileInfo(path);
104 	return fi.isDirectory;
105 }
106 
107 void runCommands(in string[] commands, string[string] env = null)
108 {
109 	import std.stdio : stdin, stdout, stderr, File;
110 
111 	version(Windows) enum nullFile = "NUL";
112 	else version(Posix) enum nullFile = "/dev/null";
113 	else static assert(0);
114 	
115 	auto childStdout = stdout;
116 	auto childStderr = stderr;
117 	auto config = Config.retainStdout | Config.retainStderr;
118 	
119 	// Disable child's stdout/stderr depending on LogLevel
120 	auto logLevel = getLogLevel();
121 	if(logLevel >= LogLevel.warn)
122 		childStdout = File(nullFile, "w");
123 	if(logLevel >= LogLevel.none)
124 		childStderr = File(nullFile, "w");
125 	
126 	foreach(cmd; commands){
127 		logDiagnostic("Running %s", cmd);
128 		Pid pid;
129 		pid = spawnShell(cmd, stdin, childStdout, childStderr, env, config);
130 		auto exitcode = pid.wait();
131 		enforce(exitcode == 0, "Command failed with exit code "~to!string(exitcode));
132 	}
133 }
134 
135 /**
136 	Downloads a file from the specified URL.
137 
138 	Any redirects will be followed until the actual file resource is reached or if the redirection
139 	limit of 10 is reached. Note that only HTTP(S) is currently supported.
140 */
141 void download(string url, string filename)
142 {
143 	version(DubUseCurl) {
144 		auto conn = HTTP();
145 		setupHTTPClient(conn);
146 		logDebug("Storing %s...", url);
147 		std.net.curl.download(url, filename, conn);
148 		enforce(conn.statusLine.code < 400,
149 			format("Failed to download %s: %s %s",
150 				url, conn.statusLine.code, conn.statusLine.reason));
151 	} else version (Have_vibe_d) {
152 		import vibe.inet.urltransfer;
153 		vibe.inet.urltransfer.download(url, filename);
154 	} else assert(false);
155 }
156 /// ditto
157 void download(URL url, Path filename)
158 {
159 	download(url.toString(), filename.toNativeString());
160 }
161 /// ditto
162 ubyte[] download(string url)
163 {
164 	version(DubUseCurl) {
165 		auto conn = HTTP();
166 		setupHTTPClient(conn);
167 		logDebug("Getting %s...", url);
168 		auto ret = cast(ubyte[])get(url, conn);
169 		enforce(conn.statusLine.code < 400,
170 			format("Failed to GET %s: %s %s",
171 				url, conn.statusLine.code, conn.statusLine.reason));
172 		return ret;
173 	} else version (Have_vibe_d) {
174 		import vibe.inet.urltransfer;
175 		import vibe.stream.operations;
176 		ubyte[] ret;
177 		download(url, (scope input) { ret = input.readAll(); });
178 		return ret;
179 	} else assert(false);
180 }
181 /// ditto
182 ubyte[] download(URL url)
183 {
184 	return download(url.toString());
185 }
186 
187 /// Returns the current DUB version in semantic version format
188 string getDUBVersion()
189 {
190 	import dub.version_;
191 	// convert version string to valid SemVer format
192 	auto verstr = dubVersion;
193 	if (verstr.startsWith("v")) verstr = verstr[1 .. $];
194 	auto parts = verstr.split("-");
195 	if (parts.length >= 3) {
196 		// detect GIT commit suffix
197 		if (parts[$-1].length == 8 && parts[$-1][1 .. $].isHexNumber() && parts[$-2].isNumber())
198 			verstr = parts[0 .. $-2].join("-") ~ "+" ~ parts[$-2 .. $].join("-");
199 	}
200 	return verstr;
201 }
202 
203 version(DubUseCurl) {
204 	void setupHTTPClient(ref HTTP conn)
205 	{
206 		static if( is(typeof(&conn.verifyPeer)) )
207 			conn.verifyPeer = false;
208 
209 		auto proxy = environment.get("http_proxy", null);
210 		if (proxy.length) conn.proxy = proxy;
211 
212 		conn.addRequestHeader("User-Agent", "dub/"~getDUBVersion()~" (std.net.curl; +https://github.com/rejectedsoftware/dub)");
213 	}
214 }
215 
216 string stripUTF8Bom(string str)
217 {
218 	if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] )
219 		return str[3 ..$];
220 	return str;
221 }
222 
223 private bool isNumber(string str) {
224 	foreach (ch; str)
225 		switch (ch) {
226 			case '0': .. case '9': break;
227 			default: return false;
228 		}
229 	return true;
230 }
231 
232 private bool isHexNumber(string str) {
233 	foreach (ch; str)
234 		switch (ch) {
235 			case '0': .. case '9': break;
236 			case 'a': .. case 'f': break;
237 			case 'A': .. case 'F': break;
238 			default: return false;
239 		}
240 	return true;
241 }
242 
243 /**
244 	Get the closest match of $(D input) in the $(D array), where $(D distance)
245 	is the maximum levenshtein distance allowed between the compared strings.
246 	Returns $(D null) if no closest match is found.
247 */
248 string getClosestMatch(string[] array, string input, size_t distance)
249 {
250 	import std.algorithm : countUntil, map, levenshteinDistance;
251 	import std.uni : toUpper;
252 
253 	auto distMap = array.map!(elem =>
254 		levenshteinDistance!((a, b) => toUpper(a) == toUpper(b))(elem, input));
255 	auto idx = distMap.countUntil!(a => a <= distance);
256 	return (idx == -1) ? null : array[idx];
257 }
258 
259 /**
260 	Searches for close matches to input in range. R must be a range of strings
261 	Note: Sorts the strings range. Use std.range.indexed to avoid this...
262   */
263 auto fuzzySearch(R)(R strings, string input){
264 	import std.algorithm : levenshteinDistance, schwartzSort, partition3;
265 	import std.traits : isSomeString;
266 	import std.range : ElementType;
267 
268 	static assert(isSomeString!(ElementType!R), "Cannot call fuzzy search on non string rang");
269 	immutable threshold = input.length / 4;
270 	return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1]
271 			.schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper));
272 }
273 
274 /**
275 	If T is a bitfield-style enum, this function returns a string range
276 	listing the names of all members included in the given value.
277 	
278 	Example:
279 	---------
280 	enum Bits {
281 		none = 0,
282 		a = 1<<0,
283 		b = 1<<1,
284 		c = 1<<2,
285 		a_c = a | c,
286 	}
287 	
288 	assert( bitFieldNames(Bits.none).equals(["none"]) );
289 	assert( bitFieldNames(Bits.a).equals(["a"]) );
290 	assert( bitFieldNames(Bits.a_c).equals(["a", "c", "a_c"]) );
291 	---------
292   */
293 auto bitFieldNames(T)(T value) if(is(T==enum) && isIntegral!T)
294 {
295 	import std.algorithm : filter, map;
296 	import std.conv : to;
297 	import std.traits : EnumMembers;
298 
299 	return [ EnumMembers!(T) ]
300 		.filter!(member => member==0? value==0 : (value & member) == member)
301 		.map!(member => to!string(member));
302 }