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 import core.time : Duration;
18 import std.algorithm : canFind, startsWith;
19 import std.array : appender;
20 import std.conv : to;
21 import std.exception : enforce;
22 import std.file;
23 import std.string : format;
24 import std.process;
25 import std.traits : isIntegral;
26 version(DubUseCurl)
27 {
28 	import std.net.curl;
29 	static if (__VERSION__ > 2075) public import std.net.curl : HTTPStatusException;
30 }
31 
32 
33 private NativePath[] temporary_files;
34 
35 NativePath getTempDir()
36 {
37 	return NativePath(std.file.tempDir());
38 }
39 
40 NativePath getTempFile(string prefix, string extension = null)
41 {
42 	import std.uuid : randomUUID;
43 
44 	auto path = getTempDir() ~ (prefix ~ "-" ~ randomUUID.toString() ~ extension);
45 	temporary_files ~= path;
46 	return path;
47 }
48 
49 /**
50    Obtain a lock for a file at the given path. If the file cannot be locked
51    within the given duration, an exception is thrown.  The file will be created
52    if it does not yet exist. Deleting the file is not safe as another process
53    could create a new file with the same name.
54    The returned lock will get unlocked upon destruction.
55 
56    Params:
57      path = path to file that gets locked
58      timeout = duration after which locking failed
59    Returns:
60      The locked file or an Exception on timeout.
61 */
62 auto lockFile(string path, Duration timeout)
63 {
64 	import core.thread : Thread;
65 	import std.datetime, std.stdio : File;
66 	import std.algorithm : move;
67 
68 	// Just a wrapper to hide (and destruct) the locked File.
69 	static struct LockFile
70 	{
71 		// The Lock can't be unlinked as someone could try to lock an already
72 		// opened fd while a new file with the same name gets created.
73 		// Exclusive filesystem locks (O_EXCL, mkdir) could be deleted but
74 		// aren't automatically freed when a process terminates, see #1149.
75 		private File f;
76 	}
77 
78 	auto file = File(path, "w");
79 	auto t0 = Clock.currTime();
80 	auto dur = 1.msecs;
81 	while (true)
82 	{
83 		if (file.tryLock())
84 			return LockFile(move(file));
85 		enforce(Clock.currTime() - t0 < timeout, "Failed to lock '"~path~"'.");
86 		if (dur < 1024.msecs) // exponentially increase sleep time
87 			dur *= 2;
88 		Thread.sleep(dur);
89 	}
90 }
91 
92 static ~this()
93 {
94 	foreach (path; temporary_files)
95 	{
96 		auto spath = path.toNativeString();
97 		if (spath.exists)
98 			std.file.remove(spath);
99 	}
100 }
101 
102 bool isEmptyDir(NativePath p) {
103 	foreach(DirEntry e; dirEntries(p.toNativeString(), SpanMode.shallow))
104 		return false;
105 	return true;
106 }
107 
108 bool isWritableDir(NativePath p, bool create_if_missing = false)
109 {
110 	import std.random;
111 	auto fname = p ~ format("__dub_write_test_%08X", uniform(0, uint.max));
112 	if (create_if_missing && !exists(p.toNativeString())) mkdirRecurse(p.toNativeString());
113 	try openFile(fname, FileMode.createTrunc).close();
114 	catch (Exception) return false;
115 	remove(fname.toNativeString());
116 	return true;
117 }
118 
119 Json jsonFromFile(NativePath file, bool silent_fail = false) {
120 	if( silent_fail && !existsFile(file) ) return Json.emptyObject;
121 	auto f = openFile(file.toNativeString(), FileMode.read);
122 	scope(exit) f.close();
123 	auto text = stripUTF8Bom(cast(string)f.readAll());
124 	return parseJsonString(text, file.toNativeString());
125 }
126 
127 Json jsonFromZip(NativePath zip, string filename) {
128 	import std.zip : ZipArchive;
129 	auto f = openFile(zip, FileMode.read);
130 	ubyte[] b = new ubyte[cast(size_t)f.size];
131 	f.rawRead(b);
132 	f.close();
133 	auto archive = new ZipArchive(b);
134 	auto text = stripUTF8Bom(cast(string)archive.expand(archive.directory[filename]));
135 	return parseJsonString(text, zip.toNativeString~"/"~filename);
136 }
137 
138 void writeJsonFile(NativePath path, Json json)
139 {
140 	auto f = openFile(path, FileMode.createTrunc);
141 	scope(exit) f.close();
142 	f.writePrettyJsonString(json);
143 }
144 
145 /// Performs a write->delete->rename sequence to atomically "overwrite" the destination file
146 void atomicWriteJsonFile(NativePath path, Json json)
147 {
148 	import std.random : uniform;
149 	auto tmppath = path.parentPath ~ format("%s.%s.tmp", path.head, uniform(0, int.max));
150 	auto f = openFile(tmppath, FileMode.createTrunc);
151 	scope (failure) {
152 		f.close();
153 		removeFile(tmppath);
154 	}
155 	f.writePrettyJsonString(json);
156 	f.close();
157 	if (existsFile(path)) removeFile(path);
158 	moveFile(tmppath, path);
159 }
160 
161 bool isPathFromZip(string p) {
162 	enforce(p.length > 0);
163 	return p[$-1] == '/';
164 }
165 
166 bool existsDirectory(NativePath path) {
167 	if( !existsFile(path) ) return false;
168 	auto fi = getFileInfo(path);
169 	return fi.isDirectory;
170 }
171 
172 void runCommand(string command, string[string] env = null)
173 {
174 	runCommands((&command)[0 .. 1], env);
175 }
176 
177 void runCommands(in string[] commands, string[string] env = null)
178 {
179 	import std.stdio : stdin, stdout, stderr, File;
180 
181 	version(Windows) enum nullFile = "NUL";
182 	else version(Posix) enum nullFile = "/dev/null";
183 	else static assert(0);
184 
185 	auto childStdout = stdout;
186 	auto childStderr = stderr;
187 	auto config = Config.retainStdout | Config.retainStderr;
188 
189 	// Disable child's stdout/stderr depending on LogLevel
190 	auto logLevel = getLogLevel();
191 	if(logLevel >= LogLevel.warn)
192 		childStdout = File(nullFile, "w");
193 	if(logLevel >= LogLevel.none)
194 		childStderr = File(nullFile, "w");
195 
196 	foreach(cmd; commands){
197 		logDiagnostic("Running %s", cmd);
198 		Pid pid;
199 		pid = spawnShell(cmd, stdin, childStdout, childStderr, env, config);
200 		auto exitcode = pid.wait();
201 		enforce(exitcode == 0, "Command failed with exit code "~to!string(exitcode));
202 	}
203 }
204 
205 version(DubUseCurl) {
206 	/++
207 	 Exception thrown on HTTP request failures, e.g. 404 Not Found.
208 	 +/
209 	static if (__VERSION__ <= 2075) class HTTPStatusException : CurlException
210 	{
211 		/++
212 		 Params:
213 		 status = The HTTP status code.
214 		 msg  = The message for the exception.
215 		 file = The file where the exception occurred.
216 		 line = The line number where the exception occurred.
217 		 next = The previous exception in the chain of exceptions, if any.
218 		 +/
219 		@safe pure nothrow
220 			this(
221 				int status,
222 				string msg,
223 				string file = __FILE__,
224 				size_t line = __LINE__,
225 				Throwable next = null)
226 		{
227 			this.status = status;
228 			super(msg, file, line, next);
229 		}
230 
231 		int status; /// The HTTP status code
232 	}
233 } else version (Have_vibe_d_http) {
234 	public import vibe.http.common : HTTPStatusException;
235 }
236 
237 /**
238 	Downloads a file from the specified URL.
239 
240 	Any redirects will be followed until the actual file resource is reached or if the redirection
241 	limit of 10 is reached. Note that only HTTP(S) is currently supported.
242 
243 	The download times out if a connection cannot be established within
244 	`timeout` ms, or if the average transfer rate drops below 10 bytes / s for
245 	more than `timeout` seconds.  Pass `0` as `timeout` to disable both timeout
246 	mechanisms.
247 
248 	Note: Timeouts are only implemented when curl is used (DubUseCurl).
249 */
250 void download(string url, string filename, uint timeout = 8)
251 {
252 	version(DubUseCurl) {
253 		auto conn = HTTP();
254 		setupHTTPClient(conn, timeout);
255 		logDebug("Storing %s...", url);
256 		static if (__VERSION__ <= 2075)
257 		{
258 			try
259 				std.net.curl.download(url, filename, conn);
260 			catch (CurlException e)
261 			{
262 				if (e.msg.canFind("404"))
263 					throw new HTTPStatusException(404, e.msg);
264 				throw e;
265 			}
266 		}
267 		else
268 		{
269 			std.net.curl.download(url, filename, conn);
270 			// workaround https://issues.dlang.org/show_bug.cgi?id=18318
271 			auto sl = conn.statusLine;
272 			logDebug("Download %s %s", url, sl);
273 			if (sl.code / 100 != 2)
274 				throw new HTTPStatusException(sl.code,
275 					"Downloading %s failed with %d (%s).".format(url, sl.code, sl.reason));
276 		}
277 	} else version (Have_vibe_d_http) {
278 		import vibe.inet.urltransfer;
279 		vibe.inet.urltransfer.download(url, filename);
280 	} else assert(false);
281 }
282 /// ditto
283 void download(URL url, NativePath filename, uint timeout = 8)
284 {
285 	download(url.toString(), filename.toNativeString(), timeout);
286 }
287 /// ditto
288 ubyte[] download(string url, uint timeout = 8)
289 {
290 	version(DubUseCurl) {
291 		auto conn = HTTP();
292 		setupHTTPClient(conn, timeout);
293 		logDebug("Getting %s...", url);
294 		static if (__VERSION__ <= 2075)
295 		{
296 			try
297 				return cast(ubyte[])get(url, conn);
298 			catch (CurlException e)
299 			{
300 				if (e.msg.canFind("404"))
301 					throw new HTTPStatusException(404, e.msg);
302 				throw e;
303 			}
304 		}
305 		else
306 			return cast(ubyte[])get(url, conn);
307 	} else version (Have_vibe_d_http) {
308 		import vibe.inet.urltransfer;
309 		import vibe.stream.operations;
310 		ubyte[] ret;
311 		vibe.inet.urltransfer.download(url, (scope input) { ret = input.readAll(); });
312 		return ret;
313 	} else assert(false);
314 }
315 /// ditto
316 ubyte[] download(URL url, uint timeout = 8)
317 {
318 	return download(url.toString(), timeout);
319 }
320 
321 /// Returns the current DUB version in semantic version format
322 string getDUBVersion()
323 {
324 	import dub.version_;
325 	import std.array : split, join;
326 	// convert version string to valid SemVer format
327 	auto verstr = dubVersion;
328 	if (verstr.startsWith("v")) verstr = verstr[1 .. $];
329 	auto parts = verstr.split("-");
330 	if (parts.length >= 3) {
331 		// detect GIT commit suffix
332 		if (parts[$-1].length == 8 && parts[$-1][1 .. $].isHexNumber() && parts[$-2].isNumber())
333 			verstr = parts[0 .. $-2].join("-") ~ "+" ~ parts[$-2 .. $].join("-");
334 	}
335 	return verstr;
336 }
337 
338 version(DubUseCurl) {
339 	void setupHTTPClient(ref HTTP conn, uint timeout)
340 	{
341 		static if( is(typeof(&conn.verifyPeer)) )
342 			conn.verifyPeer = false;
343 
344 		auto proxy = environment.get("http_proxy", null);
345 		if (proxy.length) conn.proxy = proxy;
346 
347 		auto noProxy = environment.get("no_proxy", null);
348 		if (noProxy.length) conn.handle.set(CurlOption.noproxy, noProxy);
349 
350 		conn.handle.set(CurlOption.encoding, "");
351 		if (timeout) {
352 			// connection (TLS+TCP) times out after 8s
353 			conn.handle.set(CurlOption.connecttimeout, timeout);
354 			// transfers time out after 8s below 10 byte/s
355 			conn.handle.set(CurlOption.low_speed_limit, 10);
356 			conn.handle.set(CurlOption.low_speed_time, 5);
357 		}
358 
359 		conn.addRequestHeader("User-Agent", "dub/"~getDUBVersion()~" (std.net.curl; +https://github.com/rejectedsoftware/dub)");
360 	}
361 }
362 
363 string stripUTF8Bom(string str)
364 {
365 	if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] )
366 		return str[3 ..$];
367 	return str;
368 }
369 
370 private bool isNumber(string str) {
371 	foreach (ch; str)
372 		switch (ch) {
373 			case '0': .. case '9': break;
374 			default: return false;
375 		}
376 	return true;
377 }
378 
379 private bool isHexNumber(string str) {
380 	foreach (ch; str)
381 		switch (ch) {
382 			case '0': .. case '9': break;
383 			case 'a': .. case 'f': break;
384 			case 'A': .. case 'F': break;
385 			default: return false;
386 		}
387 	return true;
388 }
389 
390 /**
391 	Get the closest match of $(D input) in the $(D array), where $(D distance)
392 	is the maximum levenshtein distance allowed between the compared strings.
393 	Returns $(D null) if no closest match is found.
394 */
395 string getClosestMatch(string[] array, string input, size_t distance)
396 {
397 	import std.algorithm : countUntil, map, levenshteinDistance;
398 	import std.uni : toUpper;
399 
400 	auto distMap = array.map!(elem =>
401 		levenshteinDistance!((a, b) => toUpper(a) == toUpper(b))(elem, input));
402 	auto idx = distMap.countUntil!(a => a <= distance);
403 	return (idx == -1) ? null : array[idx];
404 }
405 
406 /**
407 	Searches for close matches to input in range. R must be a range of strings
408 	Note: Sorts the strings range. Use std.range.indexed to avoid this...
409   */
410 auto fuzzySearch(R)(R strings, string input){
411 	import std.algorithm : levenshteinDistance, schwartzSort, partition3;
412 	import std.traits : isSomeString;
413 	import std.range : ElementType;
414 
415 	static assert(isSomeString!(ElementType!R), "Cannot call fuzzy search on non string rang");
416 	immutable threshold = input.length / 4;
417 	return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1]
418 			.schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper));
419 }
420 
421 /**
422 	If T is a bitfield-style enum, this function returns a string range
423 	listing the names of all members included in the given value.
424 
425 	Example:
426 	---------
427 	enum Bits {
428 		none = 0,
429 		a = 1<<0,
430 		b = 1<<1,
431 		c = 1<<2,
432 		a_c = a | c,
433 	}
434 
435 	assert( bitFieldNames(Bits.none).equals(["none"]) );
436 	assert( bitFieldNames(Bits.a).equals(["a"]) );
437 	assert( bitFieldNames(Bits.a_c).equals(["a", "c", "a_c"]) );
438 	---------
439   */
440 auto bitFieldNames(T)(T value) if(is(T==enum) && isIntegral!T)
441 {
442 	import std.algorithm : filter, map;
443 	import std.conv : to;
444 	import std.traits : EnumMembers;
445 
446 	return [ EnumMembers!(T) ]
447 		.filter!(member => member==0? value==0 : (value & member) == member)
448 		.map!(member => to!string(member));
449 }
450 
451 
452 bool isIdentChar(dchar ch)
453 {
454 	import std.ascii : isAlphaNum;
455 	return isAlphaNum(ch) || ch == '_';
456 }
457 
458 string stripDlangSpecialChars(string s)
459 {
460 	import std.array : appender;
461 	auto ret = appender!string();
462 	foreach(ch; s)
463 		ret.put(isIdentChar(ch) ? ch : '_');
464 	return ret.data;
465 }
466 
467 string determineModuleName(BuildSettings settings, NativePath file, NativePath base_path)
468 {
469 	import std.algorithm : map;
470 	import std.array : array;
471 	import std.range : walkLength;
472 
473 	assert(base_path.absolute);
474 	if (!file.absolute) file = base_path ~ file;
475 
476 	size_t path_skip = 0;
477 	foreach (ipath; settings.importPaths.map!(p => NativePath(p))) {
478 		if (!ipath.absolute) ipath = base_path ~ ipath;
479 		assert(!ipath.empty);
480 		if (file.startsWith(ipath) && ipath.bySegment.walkLength > path_skip)
481 			path_skip = ipath.bySegment.walkLength;
482 	}
483 
484 	enforce(path_skip > 0,
485 		format("Source file '%s' not found in any import path.", file.toNativeString()));
486 
487 	auto mpath = file.bySegment.array[path_skip .. $];
488 	auto ret = appender!string;
489 
490 	//search for module keyword in file
491 	string moduleName = getModuleNameFromFile(file.to!string);
492 
493 	if(moduleName.length) return moduleName;
494 
495 	//create module name from path
496 	foreach (i; 0 .. mpath.length) {
497 		import std.path;
498 		auto p = mpath[i].toString();
499 		if (p == "package.d") break;
500 		if (i > 0) ret ~= ".";
501 		if (i+1 < mpath.length) ret ~= p;
502 		else ret ~= p.baseName(".d");
503 	}
504 
505 	return ret.data;
506 }
507 
508 /**
509  * Search for module keyword in D Code
510  */
511 string getModuleNameFromContent(string content) {
512 	import std.regex;
513 	import std.string;
514 
515 	content = content.strip;
516 	if (!content.length) return null;
517 
518 	static bool regex_initialized = false;
519 	static Regex!char comments_pattern, module_pattern;
520 
521 	if (!regex_initialized) {
522 		comments_pattern = regex(`//[^\r\n]*\r?\n?|/\*.*?\*/|/\+.*\+/`, "g");
523 		module_pattern = regex(`module\s+([\w\.]+)\s*;`, "g");
524 		regex_initialized = true;
525 	}
526 
527 	content = replaceAll(content, comments_pattern, " ");
528 	auto result = matchFirst(content, module_pattern);
529 
530 	if (!result.empty) return result[1];
531 
532 	return null;
533 }
534 
535 unittest {
536 	assert(getModuleNameFromContent("") == "");
537 	assert(getModuleNameFromContent("module myPackage.myModule;") == "myPackage.myModule");
538 	assert(getModuleNameFromContent("module \t\n myPackage.myModule \t\r\n;") == "myPackage.myModule");
539 	assert(getModuleNameFromContent("// foo\nmodule bar;") == "bar");
540 	assert(getModuleNameFromContent("/*\nfoo\n*/\nmodule bar;") == "bar");
541 	assert(getModuleNameFromContent("/+\nfoo\n+/\nmodule bar;") == "bar");
542 	assert(getModuleNameFromContent("/***\nfoo\n***/\nmodule bar;") == "bar");
543 	assert(getModuleNameFromContent("/+++\nfoo\n+++/\nmodule bar;") == "bar");
544 	assert(getModuleNameFromContent("// module foo;\nmodule bar;") == "bar");
545 	assert(getModuleNameFromContent("/* module foo; */\nmodule bar;") == "bar");
546 	assert(getModuleNameFromContent("/+ module foo; +/\nmodule bar;") == "bar");
547 	assert(getModuleNameFromContent("/+ /+ module foo; +/ +/\nmodule bar;") == "bar");
548 	assert(getModuleNameFromContent("// module foo;\nmodule bar; // module foo;") == "bar");
549 	assert(getModuleNameFromContent("// module foo;\nmodule// module foo;\nbar//module foo;\n;// module foo;") == "bar");
550 	assert(getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;") == "bar", getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;"));
551 	assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar;") == "bar");
552 	//assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar/++/;") == "bar"); // nested comments require a context-free parser!
553 }
554 
555 /**
556  * Search for module keyword in file
557  */
558 string getModuleNameFromFile(string filePath) {
559 	string fileContent = filePath.readText;
560 
561 	logDiagnostic("Get module name from path: " ~ filePath);
562 	return getModuleNameFromContent(fileContent);
563 }