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.compilers.buildsettings : BuildSettings;
15 import dub.version_;
16 
17 // todo: cleanup imports.
18 import core.thread;
19 import std.algorithm : startsWith;
20 import std.array;
21 import std.conv;
22 import std.exception;
23 import std.file;
24 import std.process;
25 import std.string;
26 import std.traits : isIntegral;
27 import std.typecons;
28 import std.zip;
29 version(DubUseCurl) import std.net.curl;
30 
31 
32 private Path[] temporary_files;
33 
34 Path getTempDir()
35 {
36 	return Path(std.file.tempDir());
37 }
38 
39 Path getTempFile(string prefix, string extension = null)
40 {
41 	import std.uuid : randomUUID;
42 
43 	auto path = getTempDir() ~ (prefix ~ "-" ~ randomUUID.toString() ~ extension);
44 	temporary_files ~= path;
45 	return path;
46 }
47 
48 // lockfile based on atomic mkdir
49 struct LockFile
50 {
51 	bool opCast(T:bool)() { return !!path; }
52 	~this() { if (path) rmdir(path); }
53 	string path;
54 }
55 
56 auto tryLockFile(string path)
57 {
58 	import std.file;
59 	if (collectException(mkdir(path)))
60 		return LockFile(null);
61 	return LockFile(path);
62 }
63 
64 auto lockFile(string path, Duration wait)
65 {
66 	import std.datetime, std.file;
67 	auto t0 = Clock.currTime();
68 	auto dur = 1.msecs;
69 	while (true)
70 	{
71 		if (!collectException(mkdir(path)))
72 			return LockFile(path);
73 		enforce(Clock.currTime() - t0 < wait, "Failed to lock '"~path~"'.");
74 		if (dur < 1024.msecs) // exponentially increase sleep time
75 			dur *= 2;
76 		Thread.sleep(dur);
77 	}
78 }
79 
80 static ~this()
81 {
82 	foreach (path; temporary_files)
83 	{
84 		auto spath = path.toNativeString();
85 		if (spath.exists)
86 			std.file.remove(spath);
87 	}
88 }
89 
90 bool isEmptyDir(Path p) {
91 	foreach(DirEntry e; dirEntries(p.toNativeString(), SpanMode.shallow))
92 		return false;
93 	return true;
94 }
95 
96 bool isWritableDir(Path p, bool create_if_missing = false)
97 {
98 	import std.random;
99 	auto fname = p ~ format("__dub_write_test_%08X", uniform(0, uint.max));
100 	if (create_if_missing && !exists(p.toNativeString())) mkdirRecurse(p.toNativeString());
101 	try openFile(fname, FileMode.createTrunc).close();
102 	catch (Exception) return false;
103 	remove(fname.toNativeString());
104 	return true;
105 }
106 
107 Json jsonFromFile(Path file, bool silent_fail = false) {
108 	if( silent_fail && !existsFile(file) ) return Json.emptyObject;
109 	auto f = openFile(file.toNativeString(), FileMode.read);
110 	scope(exit) f.close();
111 	auto text = stripUTF8Bom(cast(string)f.readAll());
112 	return parseJsonString(text, file.toNativeString());
113 }
114 
115 Json jsonFromZip(Path zip, string filename) {
116 	auto f = openFile(zip, FileMode.read);
117 	ubyte[] b = new ubyte[cast(size_t)f.size];
118 	f.rawRead(b);
119 	f.close();
120 	auto archive = new ZipArchive(b);
121 	auto text = stripUTF8Bom(cast(string)archive.expand(archive.directory[filename]));
122 	return parseJsonString(text, zip.toNativeString~"/"~filename);
123 }
124 
125 void writeJsonFile(Path path, Json json)
126 {
127 	auto f = openFile(path, FileMode.createTrunc);
128 	scope(exit) f.close();
129 	f.writePrettyJsonString(json);
130 }
131 
132 /// Performs a write->delete->rename sequence to atomically "overwrite" the destination file
133 void atomicWriteJsonFile(Path path, Json json)
134 {
135 	import std.random : uniform;
136 	auto tmppath = path[0 .. $-1] ~ format("%s.%s.tmp", path.head, uniform(0, int.max));
137 	auto f = openFile(tmppath, FileMode.createTrunc);
138 	scope (failure) {
139 		f.close();
140 		removeFile(tmppath);
141 	}
142 	f.writePrettyJsonString(json);
143 	f.close();
144 	if (existsFile(path)) removeFile(path);
145 	moveFile(tmppath, path);
146 }
147 
148 bool isPathFromZip(string p) {
149 	enforce(p.length > 0);
150 	return p[$-1] == '/';
151 }
152 
153 bool existsDirectory(Path path) {
154 	if( !existsFile(path) ) return false;
155 	auto fi = getFileInfo(path);
156 	return fi.isDirectory;
157 }
158 
159 void runCommand(string command, string[string] env = null)
160 {
161 	runCommands((&command)[0 .. 1], env);
162 }
163 
164 void runCommands(in string[] commands, string[string] env = null)
165 {
166 	import std.stdio : stdin, stdout, stderr, File;
167 
168 	version(Windows) enum nullFile = "NUL";
169 	else version(Posix) enum nullFile = "/dev/null";
170 	else static assert(0);
171 
172 	auto childStdout = stdout;
173 	auto childStderr = stderr;
174 	auto config = Config.retainStdout | Config.retainStderr;
175 
176 	// Disable child's stdout/stderr depending on LogLevel
177 	auto logLevel = getLogLevel();
178 	if(logLevel >= LogLevel.warn)
179 		childStdout = File(nullFile, "w");
180 	if(logLevel >= LogLevel.none)
181 		childStderr = File(nullFile, "w");
182 
183 	foreach(cmd; commands){
184 		logDiagnostic("Running %s", cmd);
185 		Pid pid;
186 		pid = spawnShell(cmd, stdin, childStdout, childStderr, env, config);
187 		auto exitcode = pid.wait();
188 		enforce(exitcode == 0, "Command failed with exit code "~to!string(exitcode));
189 	}
190 }
191 
192 /**
193 	Downloads a file from the specified URL.
194 
195 	Any redirects will be followed until the actual file resource is reached or if the redirection
196 	limit of 10 is reached. Note that only HTTP(S) is currently supported.
197 */
198 void download(string url, string filename)
199 {
200 	version(DubUseCurl) {
201 		auto conn = HTTP();
202 		setupHTTPClient(conn);
203 		logDebug("Storing %s...", url);
204 		std.net.curl.download(url, filename, conn);
205 		enforce(conn.statusLine.code < 400,
206 			format("Failed to download %s: %s %s",
207 				url, conn.statusLine.code, conn.statusLine.reason));
208 	} else version (Have_vibe_d) {
209 		import vibe.inet.urltransfer;
210 		vibe.inet.urltransfer.download(url, filename);
211 	} else assert(false);
212 }
213 /// ditto
214 void download(URL url, Path filename)
215 {
216 	download(url.toString(), filename.toNativeString());
217 }
218 /// ditto
219 ubyte[] download(string url)
220 {
221 	version(DubUseCurl) {
222 		auto conn = HTTP();
223 		setupHTTPClient(conn);
224 		logDebug("Getting %s...", url);
225 		auto ret = cast(ubyte[])get(url, conn);
226 		enforce(conn.statusLine.code < 400,
227 			format("Failed to GET %s: %s %s",
228 				url, conn.statusLine.code, conn.statusLine.reason));
229 		return ret;
230 	} else version (Have_vibe_d) {
231 		import vibe.inet.urltransfer;
232 		import vibe.stream.operations;
233 		ubyte[] ret;
234 		vibe.inet.urltransfer.download(url, (scope input) { ret = input.readAll(); });
235 		return ret;
236 	} else assert(false);
237 }
238 /// ditto
239 ubyte[] download(URL url)
240 {
241 	return download(url.toString());
242 }
243 
244 /// Returns the current DUB version in semantic version format
245 string getDUBVersion()
246 {
247 	import dub.version_;
248 	// convert version string to valid SemVer format
249 	auto verstr = dubVersion;
250 	if (verstr.startsWith("v")) verstr = verstr[1 .. $];
251 	auto parts = verstr.split("-");
252 	if (parts.length >= 3) {
253 		// detect GIT commit suffix
254 		if (parts[$-1].length == 8 && parts[$-1][1 .. $].isHexNumber() && parts[$-2].isNumber())
255 			verstr = parts[0 .. $-2].join("-") ~ "+" ~ parts[$-2 .. $].join("-");
256 	}
257 	return verstr;
258 }
259 
260 version(DubUseCurl) {
261 	void setupHTTPClient(ref HTTP conn)
262 	{
263 		static if( is(typeof(&conn.verifyPeer)) )
264 			conn.verifyPeer = false;
265 
266 		auto proxy = environment.get("http_proxy", null);
267 		if (proxy.length) conn.proxy = proxy;
268 
269 		conn.addRequestHeader("User-Agent", "dub/"~getDUBVersion()~" (std.net.curl; +https://github.com/rejectedsoftware/dub)");
270 	}
271 }
272 
273 string stripUTF8Bom(string str)
274 {
275 	if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] )
276 		return str[3 ..$];
277 	return str;
278 }
279 
280 private bool isNumber(string str) {
281 	foreach (ch; str)
282 		switch (ch) {
283 			case '0': .. case '9': break;
284 			default: return false;
285 		}
286 	return true;
287 }
288 
289 private bool isHexNumber(string str) {
290 	foreach (ch; str)
291 		switch (ch) {
292 			case '0': .. case '9': break;
293 			case 'a': .. case 'f': break;
294 			case 'A': .. case 'F': break;
295 			default: return false;
296 		}
297 	return true;
298 }
299 
300 /**
301 	Get the closest match of $(D input) in the $(D array), where $(D distance)
302 	is the maximum levenshtein distance allowed between the compared strings.
303 	Returns $(D null) if no closest match is found.
304 */
305 string getClosestMatch(string[] array, string input, size_t distance)
306 {
307 	import std.algorithm : countUntil, map, levenshteinDistance;
308 	import std.uni : toUpper;
309 
310 	auto distMap = array.map!(elem =>
311 		levenshteinDistance!((a, b) => toUpper(a) == toUpper(b))(elem, input));
312 	auto idx = distMap.countUntil!(a => a <= distance);
313 	return (idx == -1) ? null : array[idx];
314 }
315 
316 /**
317 	Searches for close matches to input in range. R must be a range of strings
318 	Note: Sorts the strings range. Use std.range.indexed to avoid this...
319   */
320 auto fuzzySearch(R)(R strings, string input){
321 	import std.algorithm : levenshteinDistance, schwartzSort, partition3;
322 	import std.traits : isSomeString;
323 	import std.range : ElementType;
324 
325 	static assert(isSomeString!(ElementType!R), "Cannot call fuzzy search on non string rang");
326 	immutable threshold = input.length / 4;
327 	return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1]
328 			.schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper));
329 }
330 
331 /**
332 	If T is a bitfield-style enum, this function returns a string range
333 	listing the names of all members included in the given value.
334 
335 	Example:
336 	---------
337 	enum Bits {
338 		none = 0,
339 		a = 1<<0,
340 		b = 1<<1,
341 		c = 1<<2,
342 		a_c = a | c,
343 	}
344 
345 	assert( bitFieldNames(Bits.none).equals(["none"]) );
346 	assert( bitFieldNames(Bits.a).equals(["a"]) );
347 	assert( bitFieldNames(Bits.a_c).equals(["a", "c", "a_c"]) );
348 	---------
349   */
350 auto bitFieldNames(T)(T value) if(is(T==enum) && isIntegral!T)
351 {
352 	import std.algorithm : filter, map;
353 	import std.conv : to;
354 	import std.traits : EnumMembers;
355 
356 	return [ EnumMembers!(T) ]
357 		.filter!(member => member==0? value==0 : (value & member) == member)
358 		.map!(member => to!string(member));
359 }
360 
361 
362 bool isIdentChar(dchar ch)
363 {
364 	import std.ascii : isAlphaNum;
365 	return isAlphaNum(ch) || ch == '_';
366 }
367 
368 string stripDlangSpecialChars(string s)
369 {
370 	import std.array : appender;
371 	auto ret = appender!string();
372 	foreach(ch; s)
373 		ret.put(isIdentChar(ch) ? ch : '_');
374 	return ret.data;
375 }
376 
377 string determineModuleName(BuildSettings settings, Path file, Path base_path)
378 {
379 	import std.algorithm : map;
380 
381 	assert(base_path.absolute);
382 	if (!file.absolute) file = base_path ~ file;
383 
384 	size_t path_skip = 0;
385 	foreach (ipath; settings.importPaths.map!(p => Path(p))) {
386 		if (!ipath.absolute) ipath = base_path ~ ipath;
387 		assert(!ipath.empty);
388 		if (file.startsWith(ipath) && ipath.length > path_skip)
389 			path_skip = ipath.length;
390 	}
391 
392 	enforce(path_skip > 0,
393 		format("Source file '%s' not found in any import path.", file.toNativeString()));
394 
395 	auto mpath = file[path_skip .. file.length];
396 	auto ret = appender!string;
397 
398 	//search for module keyword in file
399 	string moduleName = getModuleNameFromFile(file.to!string);
400 
401 	if(moduleName.length) return moduleName;
402 
403 	//create module name from path
404 	foreach (i; 0 .. mpath.length) {
405 		import std.path;
406 		auto p = mpath[i].toString();
407 		if (p == "package.d") break;
408 		if (i > 0) ret ~= ".";
409 		if (i+1 < mpath.length) ret ~= p;
410 		else ret ~= p.baseName(".d");
411 	}
412 
413 	return ret.data;
414 }
415 
416 /**
417  * Search for module keyword in D Code
418  */
419 string getModuleNameFromContent(string content) {
420 	import std.regex;
421 	import std.string;
422 
423 	content = content.strip;
424 	if (!content.length) return null;
425 
426 	static bool regex_initialized = false;
427 	static Regex!char comments_pattern, module_pattern;
428 
429 	if (!regex_initialized) {
430 		comments_pattern = regex(`(/\*([^*]|[\r\n]|(\*+([^*/]|[\r\n])))*\*+/)|(//.*)`, "g");
431 		module_pattern = regex(`module\s+([\w\.]+)\s*;`, "g");
432 		regex_initialized = true;
433 	}
434 
435 	content = replaceAll(content, comments_pattern, "");
436 	auto result = matchFirst(content, module_pattern);
437 
438 	string moduleName;
439 	if(!result.empty) moduleName = result.front;
440 
441 	if (moduleName.length >= 7) moduleName = moduleName[7..$-1];
442 
443 	return moduleName;
444 }
445 
446 unittest {
447 	//test empty string
448 	string name = getModuleNameFromContent("");
449 	assert(name == "", "can't get module name from empty string");
450 
451 	//test simple name
452 	name = getModuleNameFromContent("module myPackage.myModule;");
453 	assert(name == "myPackage.myModule", "can't parse module name");
454 
455 	//test if it can ignore module inside comments
456 	name = getModuleNameFromContent("/**
457 	module fakePackage.fakeModule;
458 	*/
459 	module myPackage.myModule;");
460 
461 	assert(name == "myPackage.myModule", "can't parse module name");
462 
463 	name = getModuleNameFromContent("//module fakePackage.fakeModule;
464 	module myPackage.myModule;");
465 
466 	assert(name == "myPackage.myModule", "can't parse module name");
467 }
468 
469 /**
470  * Search for module keyword in file
471  */
472 string getModuleNameFromFile(string filePath) {
473 	string fileContent = filePath.readText;
474 
475 	logDiagnostic("Get module name from path: " ~ filePath);
476 	return getModuleNameFromContent(fileContent);
477 }