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.data.json;
12 import dub.internal.vibecompat.inet.url;
13 import dub.compilers.buildsettings : BuildSettings;
14 import dub.version_;
15 import dub.internal.logging;
16 
17 import core.time : Duration;
18 import std.algorithm : canFind, startsWith;
19 import std.array : appender, array;
20 import std.conv : to;
21 import std.exception : enforce;
22 import std.file;
23 import std.format;
24 import std.range;
25 import std.string : format;
26 import std.process;
27 import std.traits : isIntegral;
28 version(DubUseCurl)
29 {
30 	import std.net.curl;
31 	public import std.net.curl : HTTPStatusException;
32 }
33 
34 public import dub.internal.temp_files;
35 
36 /**
37  * Obtain a lock for a file at the given path.
38  *
39  * If the file cannot be locked within the given duration,
40  * an exception is thrown. The file will be created if it does not yet exist.
41  * Deleting the file is not safe as another process could create a new file
42  * with the same name.
43  * The returned lock will get unlocked upon destruction.
44  *
45  * Params:
46  *   path = path to file that gets locked
47  *   timeout = duration after which locking failed
48  *
49  * Returns:
50  *   The locked file or an Exception on timeout.
51  */
52 auto lockFile(string path, Duration timeout)
53 {
54 	import core.thread : Thread;
55 	import std.datetime, std.stdio : File;
56 	import std.algorithm : move;
57 
58 	// Just a wrapper to hide (and destruct) the locked File.
59 	static struct LockFile
60 	{
61 		// The Lock can't be unlinked as someone could try to lock an already
62 		// opened fd while a new file with the same name gets created.
63 		// Exclusive file system locks (O_EXCL, mkdir) could be deleted but
64 		// aren't automatically freed when a process terminates, see #1149.
65 		private File f;
66 	}
67 
68 	auto file = File(path, "w");
69 	auto t0 = Clock.currTime();
70 	auto dur = 1.msecs;
71 	while (true)
72 	{
73 		if (file.tryLock())
74 			return LockFile(move(file));
75 		enforce(Clock.currTime() - t0 < timeout, "Failed to lock '"~path~"'.");
76 		if (dur < 1024.msecs) // exponentially increase sleep time
77 			dur *= 2;
78 		Thread.sleep(dur);
79 	}
80 }
81 
82 bool isWritableDir(NativePath p, bool create_if_missing = false)
83 {
84 	import std.random;
85 	auto fname = p ~ format("__dub_write_test_%08X", uniform(0, uint.max));
86 	if (create_if_missing)
87 		ensureDirectory(p);
88 	try writeFile(fname, "Canary");
89 	catch (Exception) return false;
90 	remove(fname.toNativeString());
91 	return true;
92 }
93 
94 Json jsonFromFile(NativePath file, bool silent_fail = false) {
95 	if( silent_fail && !existsFile(file) ) return Json.emptyObject;
96 	auto text = readText(file);
97 	return parseJsonString(text, file.toNativeString());
98 }
99 
100 /**
101 	Read package info file content from archive.
102 	File needs to be in root folder or in first
103 	sub folder.
104 
105 	Params:
106 		zip = path to archive file
107 		fileName = Package file name
108 	Returns:
109 		package file content.
110 */
111 string packageInfoFileFromZip(NativePath zip, out string fileName) {
112 	import std.zip : ZipArchive, ArchiveMember;
113 	import dub.package_ : packageInfoFiles;
114 
115 	auto b = readFile(zip);
116 	auto archive = new ZipArchive(b);
117 	alias PSegment = typeof (NativePath.init.head);
118 	foreach (ArchiveMember am; archive.directory) {
119 		auto path = NativePath(am.name).bySegment.array;
120 		foreach (fil; packageInfoFiles) {
121 			if ((path.length == 1 && path[0] == fil.filename) || (path.length == 2 && path[$-1].name == fil.filename)) {
122 				fileName = fil.filename;
123 				return stripUTF8Bom(cast(string) archive.expand(archive.directory[am.name]));
124 			}
125 		}
126 	}
127 	throw new Exception("No package descriptor found");
128 }
129 
130 void writeJsonFile(NativePath path, Json json)
131 {
132 	auto app = appender!string();
133 	app.writePrettyJsonString(json);
134 	writeFile(path, app.data);
135 }
136 
137 /// Performs a write->delete->rename sequence to atomically "overwrite" the destination file
138 void atomicWriteJsonFile(NativePath path, Json json)
139 {
140 	import std.random : uniform;
141 	auto tmppath = path.parentPath ~ format("%s.%s.tmp", path.head, uniform(0, int.max));
142 	auto app = appender!string();
143 	app.writePrettyJsonString(json);
144 	writeFile(tmppath, app.data);
145 	if (existsFile(path)) removeFile(path);
146 	moveFile(tmppath, path);
147 }
148 
149 void runCommand(string command, string[string] env, string workDir)
150 {
151 	runCommands((&command)[0 .. 1], env, workDir);
152 }
153 
154 void runCommands(in string[] commands, string[string] env, string workDir)
155 {
156 	import std.stdio : stdin, stdout, stderr, File;
157 
158 	version(Windows) enum nullFile = "NUL";
159 	else version(Posix) enum nullFile = "/dev/null";
160 	else static assert(0);
161 
162 	auto childStdout = stdout;
163 	auto childStderr = stderr;
164 	auto config = Config.retainStdout | Config.retainStderr;
165 
166 	// Disable child's stdout/stderr depending on LogLevel
167 	auto logLevel = getLogLevel();
168 	if(logLevel >= LogLevel.warn)
169 		childStdout = File(nullFile, "w");
170 	if(logLevel >= LogLevel.none)
171 		childStderr = File(nullFile, "w");
172 
173 	foreach(cmd; commands){
174 		logDiagnostic("Running %s", cmd);
175 		Pid pid;
176 		pid = spawnShell(cmd, stdin, childStdout, childStderr, env, config, workDir);
177 		auto exitcode = pid.wait();
178 		enforce(exitcode == 0, "Command failed with exit code "
179 			~ to!string(exitcode) ~ ": " ~ cmd);
180 	}
181 }
182 
183 version (Have_vibe_d_http)
184 	public import vibe.http.common : HTTPStatusException;
185 
186 /**
187 	Downloads a file from the specified URL.
188 
189 	Any redirects will be followed until the actual file resource is reached or if the redirection
190 	limit of 10 is reached. Note that only HTTP(S) is currently supported.
191 
192 	The download times out if a connection cannot be established within
193 	`timeout` ms, or if the average transfer rate drops below 10 bytes / s for
194 	more than `timeout` seconds.  Pass `0` as `timeout` to disable both timeout
195 	mechanisms.
196 
197 	Note: Timeouts are only implemented when curl is used (DubUseCurl).
198 */
199 private void download(string url, string filename, uint timeout = 8)
200 {
201 	version(DubUseCurl) {
202 		auto conn = HTTP();
203 		setupHTTPClient(conn, timeout);
204 		logDebug("Storing %s...", url);
205 		std.net.curl.download(url, filename, conn);
206 		// workaround https://issues.dlang.org/show_bug.cgi?id=18318
207 		auto sl = conn.statusLine;
208 		logDebug("Download %s %s", url, sl);
209 		if (sl.code / 100 != 2)
210 			throw new HTTPStatusException(sl.code,
211 				"Downloading %s failed with %d (%s).".format(url, sl.code, sl.reason));
212 	} else version (Have_vibe_d_http) {
213 		import vibe.inet.urltransfer;
214 		vibe.inet.urltransfer.download(url, filename);
215 	} else assert(false);
216 }
217 /// ditto
218 private void download(URL url, NativePath filename, uint timeout = 8)
219 {
220 	download(url.toString(), filename.toNativeString(), timeout);
221 }
222 /// ditto
223 private ubyte[] download(string url, uint timeout = 8)
224 {
225 	version(DubUseCurl) {
226 		auto conn = HTTP();
227 		setupHTTPClient(conn, timeout);
228 		logDebug("Getting %s...", url);
229 		return get!(HTTP, ubyte)(url, conn);
230 	} else version (Have_vibe_d_http) {
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 private ubyte[] download(URL url, uint timeout = 8)
240 {
241 	return download(url.toString(), timeout);
242 }
243 
244 /**
245 	Downloads a file from the specified URL with retry logic.
246 
247 	Downloads a file from the specified URL with up to n tries on failure
248 	Throws: `Exception` if the download failed or `HTTPStatusException` after the nth retry or
249 	on "unrecoverable failures" such as 404 not found
250 	Otherwise might throw anything else that `download` throws.
251 	See_Also: download
252 
253 	The download times out if a connection cannot be established within
254 	`timeout` ms, or if the average transfer rate drops below 10 bytes / s for
255 	more than `timeout` seconds.  Pass `0` as `timeout` to disable both timeout
256 	mechanisms.
257 
258 	Note: Timeouts are only implemented when curl is used (DubUseCurl).
259 **/
260 void retryDownload(URL url, NativePath filename, size_t retryCount = 3, uint timeout = 8)
261 {
262 	foreach(i; 0..retryCount) {
263 		version(DubUseCurl) {
264 			try {
265 				download(url, filename, timeout);
266 				return;
267 			}
268 			catch(HTTPStatusException e) {
269 				if (e.status == 404) throw e;
270 				else {
271 					logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
272 					if (i == retryCount - 1) throw e;
273 					else continue;
274 				}
275 			}
276 			catch(CurlException e) {
277 				logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
278 				continue;
279 			}
280 		}
281 		else
282 		{
283 			try {
284 				download(url, filename);
285 				return;
286 			}
287 			catch(HTTPStatusException e) {
288 				if (e.status == 404) throw e;
289 				else {
290 					logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
291 					if (i == retryCount - 1) throw e;
292 					else continue;
293 				}
294 			}
295 		}
296 	}
297 	throw new Exception("Failed to download %s".format(url));
298 }
299 
300 ///ditto
301 ubyte[] retryDownload(URL url, size_t retryCount = 3, uint timeout = 8)
302 {
303 	foreach(i; 0..retryCount) {
304 		version(DubUseCurl) {
305 			try {
306 				return download(url, timeout);
307 			}
308 			catch(HTTPStatusException e) {
309 				if (e.status == 404) throw e;
310 				else {
311 					logDebug("Failed to download %s (Attempt %s of %s): %s",
312 						url, i + 1, retryCount, e.message);
313 					if (i == retryCount - 1) throw e;
314 					else continue;
315 				}
316 			}
317 			catch(CurlException e) {
318 				logDebug("Failed to download %s (Attempt %s of %s): %s",
319 					url, i + 1, retryCount, e.message);
320 				continue;
321 			}
322 		}
323 		else
324 		{
325 			try {
326 				return download(url);
327 			}
328 			catch(HTTPStatusException e) {
329 				if (e.status == 404) throw e;
330 				else {
331 					logDebug("Failed to download %s (Attempt %s of %s)", url, i + 1, retryCount);
332 					if (i == retryCount - 1) throw e;
333 					else continue;
334 				}
335 			}
336 		}
337 	}
338 	throw new Exception("Failed to download %s".format(url));
339 }
340 
341 /// Returns the current DUB version in semantic version format
342 string getDUBVersion()
343 {
344 	import dub.version_;
345 	import std.array : split, join;
346 	// convert version string to valid SemVer format
347 	auto verstr = dubVersion;
348 	if (verstr.startsWith("v")) verstr = verstr[1 .. $];
349 	auto parts = verstr.split("-");
350 	if (parts.length >= 3) {
351 		// detect GIT commit suffix
352 		if (parts[$-1].length == 8 && parts[$-1][1 .. $].isHexNumber() && parts[$-2].isNumber())
353 			verstr = parts[0 .. $-2].join("-") ~ "+" ~ parts[$-2 .. $].join("-");
354 	}
355 	return verstr;
356 }
357 
358 
359 /**
360 	Get current executable's path if running as DUB executable,
361 	or find a DUB executable if DUB is used as a library.
362 	For the latter, the following locations are checked in order:
363 	$(UL
364 		$(LI current working directory)
365 		$(LI same directory as `compilerBinary` (if supplied))
366 		$(LI all components of the `$PATH` variable)
367 	)
368 	Params:
369 		compilerBinary = optional path to a D compiler executable, used to locate DUB executable
370 	Returns:
371 		The path to a valid DUB executable
372 	Throws:
373 		an Exception if no valid DUB executable is found
374 */
375 public NativePath getDUBExePath(in string compilerBinary=null)
376 {
377 	version(DubApplication) {
378 		import std.file : thisExePath;
379 		return NativePath(thisExePath());
380 	}
381 	else {
382 		// this must be dub as a library
383 		import std.algorithm : filter, map, splitter;
384 		import std.array : array;
385 		import std.file : exists, getcwd;
386 		import std.path : chainPath, dirName;
387 		import std.range : chain, only, take;
388 		import std.process : environment;
389 
390 		version(Windows) {
391 			enum exeName = "dub.exe";
392 			enum pathSep = ';';
393 		}
394 		else {
395 			enum exeName = "dub";
396 			enum pathSep = ':';
397 		}
398 
399 		auto dubLocs = only(
400 			getcwd().chainPath(exeName),
401 			compilerBinary.dirName.chainPath(exeName),
402 		)
403 		.take(compilerBinary.length ? 2 : 1)
404 		.chain(
405 			environment.get("PATH", "")
406 				.splitter(pathSep)
407 				.map!(p => p.chainPath(exeName))
408 		)
409 		.filter!exists;
410 
411 		enforce(!dubLocs.empty, "Could not find DUB executable");
412 		return NativePath(dubLocs.front.array);
413 	}
414 }
415 
416 
417 version(DubUseCurl) {
418 	void setupHTTPClient(ref HTTP conn, uint timeout)
419 	{
420 		static if( is(typeof(&conn.verifyPeer)) )
421 			conn.verifyPeer = false;
422 
423 		auto proxy = environment.get("http_proxy", null);
424 		if (proxy.length) conn.proxy = proxy;
425 
426 		auto noProxy = environment.get("no_proxy", null);
427 		if (noProxy.length) conn.handle.set(CurlOption.noproxy, noProxy);
428 
429 		conn.handle.set(CurlOption.encoding, "");
430 		if (timeout) {
431 			// connection (TLS+TCP) times out after 8s
432 			conn.handle.set(CurlOption.connecttimeout, timeout);
433 			// transfers time out after 8s below 10 byte/s
434 			conn.handle.set(CurlOption.low_speed_limit, 10);
435 			conn.handle.set(CurlOption.low_speed_time, timeout);
436 		}
437 
438 		conn.addRequestHeader("User-Agent", "dub/"~getDUBVersion()~" (std.net.curl; +https://github.com/rejectedsoftware/dub)");
439 
440 		enum CURL_NETRC_OPTIONAL = 1;
441 		conn.handle.set(CurlOption.netrc, CURL_NETRC_OPTIONAL);
442 	}
443 }
444 
445 private string stripUTF8Bom(string str)
446 {
447 	if( str.length >= 3 && str[0 .. 3] == [0xEF, 0xBB, 0xBF] )
448 		return str[3 ..$];
449 	return str;
450 }
451 
452 private bool isNumber(string str) {
453 	foreach (ch; str)
454 		switch (ch) {
455 			case '0': .. case '9': break;
456 			default: return false;
457 		}
458 	return true;
459 }
460 
461 private bool isHexNumber(string str) {
462 	foreach (ch; str)
463 		switch (ch) {
464 			case '0': .. case '9': break;
465 			case 'a': .. case 'f': break;
466 			case 'A': .. case 'F': break;
467 			default: return false;
468 		}
469 	return true;
470 }
471 
472 /**
473 	Get the closest match of $(D input) in the $(D array), where $(D distance)
474 	is the maximum levenshtein distance allowed between the compared strings.
475 	Returns $(D null) if no closest match is found.
476 */
477 string getClosestMatch(string[] array, string input, size_t distance)
478 {
479 	import std.algorithm : countUntil, map, levenshteinDistance;
480 	import std.uni : toUpper;
481 
482 	auto distMap = array.map!(elem =>
483 		levenshteinDistance!((a, b) => toUpper(a) == toUpper(b))(elem, input));
484 	auto idx = distMap.countUntil!(a => a <= distance);
485 	return (idx == -1) ? null : array[idx];
486 }
487 
488 /**
489 	Searches for close matches to input in range. R must be a range of strings
490 	Note: Sorts the strings range. Use std.range.indexed to avoid this...
491   */
492 auto fuzzySearch(R)(R strings, string input){
493 	import std.algorithm : levenshteinDistance, schwartzSort, partition3;
494 	import std.traits : isSomeString;
495 	import std.range : ElementType;
496 
497 	static assert(isSomeString!(ElementType!R), "Cannot call fuzzy search on non string rang");
498 	immutable threshold = input.length / 4;
499 	return strings.partition3!((a, b) => a.length + threshold < b.length)(input)[1]
500 			.schwartzSort!(p => levenshteinDistance(input.toUpper, p.toUpper));
501 }
502 
503 /**
504 	If T is a bitfield-style enum, this function returns a string range
505 	listing the names of all members included in the given value.
506 
507 	Example:
508 	---------
509 	enum Bits {
510 		none = 0,
511 		a = 1<<0,
512 		b = 1<<1,
513 		c = 1<<2,
514 		a_c = a | c,
515 	}
516 
517 	assert( bitFieldNames(Bits.none).equals(["none"]) );
518 	assert( bitFieldNames(Bits.a).equals(["a"]) );
519 	assert( bitFieldNames(Bits.a_c).equals(["a", "c", "a_c"]) );
520 	---------
521   */
522 auto bitFieldNames(T)(T value) if(is(T==enum) && isIntegral!T)
523 {
524 	import std.algorithm : filter, map;
525 	import std.conv : to;
526 	import std.traits : EnumMembers;
527 
528 	return [ EnumMembers!(T) ]
529 		.filter!(member => member==0? value==0 : (value & member) == member)
530 		.map!(member => to!string(member));
531 }
532 
533 
534 bool isIdentChar(dchar ch)
535 {
536 	import std.ascii : isAlphaNum;
537 	return isAlphaNum(ch) || ch == '_';
538 }
539 
540 string stripDlangSpecialChars(string s)
541 {
542 	import std.array : appender;
543 	auto ret = appender!string();
544 	foreach(ch; s)
545 		ret.put(isIdentChar(ch) ? ch : '_');
546 	return ret.data;
547 }
548 
549 string determineModuleName(BuildSettings settings, NativePath file, NativePath base_path)
550 {
551 	import std.algorithm : map;
552 	import std.array : array;
553 	import std.range : walkLength, chain;
554 
555 	assert(base_path.absolute);
556 	if (!file.absolute) file = base_path ~ file;
557 
558 	size_t path_skip = 0;
559 	foreach (ipath; chain(settings.importPaths, settings.cImportPaths).map!(p => NativePath(p))) {
560 		if (!ipath.absolute) ipath = base_path ~ ipath;
561 		assert(!ipath.empty);
562 		if (file.startsWith(ipath) && ipath.bySegment.walkLength > path_skip)
563 			path_skip = ipath.bySegment.walkLength;
564 	}
565 
566 	auto mpath = file.bySegment.array[path_skip .. $];
567 	auto ret = appender!string;
568 
569 	//search for module keyword in file
570 	string moduleName = getModuleNameFromFile(file.to!string);
571 
572 	if(moduleName.length) {
573 		assert(moduleName.length > 0, "Wasn't this module name already checked? what");
574 		return moduleName;
575 	}
576 
577 	//create module name from path
578 	if (path_skip == 0)
579 	{
580 		import std.path;
581 		ret ~= mpath[$-1].name.baseName(".d");
582 	}
583 	else
584 	{
585 		foreach (i; 0 .. mpath.length) {
586 			import std.path;
587 			auto p = mpath[i].name;
588 			if (p == "package.d") break ;
589 			if (ret.data.length > 0) ret ~= ".";
590 			if (i+1 < mpath.length) ret ~= p;
591 			else ret ~= p.baseName(".d");
592 		}
593 	}
594 
595 	assert(ret.data.length > 0, "A module name was expected to be computed, and none was.");
596 	return ret.data;
597 }
598 
599 /**
600  * Search for module keyword in D Code
601  * A primitive parser to skip comments and whitespace to get
602  * the module's name from the module declaration.
603  */
604 string getModuleNameFromContent(string content) {
605 	import std.ascii: isAlpha, isAlphaNum, isWhite;
606 	import std.algorithm: among;
607 	import core.exception: RangeError;
608 
609 	enum keyword = "module";
610 
611 	size_t i = 0;
612 	size_t startIndex = 0, endIndex = 0;
613 	auto foundKeyword = false;
614 
615 	auto ch() {
616 		return content[i];
617 	}
618 
619 	static bool isIdentChar(in char c) {
620 		return !isWhite(c) && c != '/' && c != ';';
621 	}
622 
623 	try {
624 		while(i < content.length) {
625 			if(!foundKeyword && ch == keyword[0] && content[i .. i + keyword.length] == keyword) {
626 				// -1 because the end of the loop will advance by 1
627 				i += keyword.length - 1;
628 				foundKeyword = true;
629 			}
630 			else if(ch == '/') {
631 				++i;
632 				// line comment?
633 				if(ch == '/') {
634 					while(ch != '\n')
635 						++i;
636 				}
637 				// block comment?
638 				else if(ch == '*') {
639 					++i;
640 					while(ch != '*' || content[i + 1] != '/')
641 						++i;
642 					++i; // skip over closing '/'
643 				}
644 				// nested comment?
645 				else if(ch == '+') {
646 					++i;
647 
648 					size_t level = 1;
649 
650 					while(level > 0) {
651 						if(ch == '/') {
652 							++i;
653 							if(ch == '+') {
654 								++i;
655 								++level;
656 							}
657 						}
658 						if(ch == '+') {
659 							++i;
660 							if(ch == '/') {
661 								--level;
662 							} else continue;
663 						}
664 						++i;
665 					}
666 				}
667 			}
668 			else if(isIdentChar(ch) && foundKeyword) {
669 				if(startIndex == 0)
670 					startIndex = i;
671 				++i; // skip the first char of the name
672 				while(isIdentChar(ch)) {
673 					++i;
674 				}
675 				// when we get here, either we're at the end of the module's identifier,
676 				// or there are comments afterwards
677 				if(endIndex == 0) {
678 					endIndex = i;
679 				}
680 				if(!isIdentChar(ch))
681 					return content[startIndex .. endIndex];
682 				else continue;
683 			} else if(!isIdentChar(ch) && foundKeyword && startIndex != 0) {
684 				return content[startIndex .. endIndex];
685 			}
686 			++i;
687 		}
688 		return "";
689 	} catch(RangeError) {
690 		return "";
691 	}
692 }
693 
694 unittest {
695 	assert(getModuleNameFromContent("") == "");
696 	assert(getModuleNameFromContent("module myPackage.myModule;") == "myPackage.myModule", getModuleNameFromContent("module myPackage.myModule;"));
697 	assert(getModuleNameFromContent("module \t\n myPackage.myModule \t\r\n;") == "myPackage.myModule");
698 	assert(getModuleNameFromContent("// foo\nmodule bar;") == "bar");
699 	assert(getModuleNameFromContent("/*\nfoo\n*/\nmodule bar;") == "bar");
700 	assert(getModuleNameFromContent("/+\nfoo\n+/\nmodule bar;") == "bar");
701 	assert(getModuleNameFromContent("/***\nfoo\n***/\nmodule bar;") == "bar");
702 	assert(getModuleNameFromContent("/+++\nfoo\n+++/\nmodule bar;") == "bar");
703 	assert(getModuleNameFromContent("// module foo;\nmodule bar;") == "bar");
704 	assert(getModuleNameFromContent("/* module foo; */\nmodule bar;") == "bar");
705 	assert(getModuleNameFromContent("/+ module foo; +/\nmodule bar;") == "bar");
706 	assert(getModuleNameFromContent("/+ /+ module foo; +/ +/\nmodule bar;") == "bar");
707 	assert(getModuleNameFromContent("// module foo;\nmodule bar; // module foo;") == "bar");
708 
709 	assert(getModuleNameFromContent("// module foo;\nmodule// module foo;\nbar//module foo;\n;// module foo;") == "bar");
710 
711 	assert(getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;") == "bar", getModuleNameFromContent("/* module foo; */\nmodule/*module foo;*/bar/*module foo;*/;"));
712 	assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar;") == "bar");
713 	assert(getModuleNameFromContent("/+ /+ module foo; +/ module foo; +/ module bar/++/;") == "bar");
714 	assert(getModuleNameFromContent("/*\nmodule sometest;\n*/\n\nmodule fakemath;\n") == "fakemath");
715 	assert(getModuleNameFromContent("module foo_bar;") == "foo_bar");
716 	assert(getModuleNameFromContent("module _foo_bar;") == "_foo_bar");
717 	assert(getModuleNameFromContent("/++ ++/\nmodule foo;") == "foo");
718 	assert(getModuleNameFromContent("module pokémon;") == "pokémon");
719 	assert(getModuleNameFromContent("module éclair;") == "éclair");
720 	assert(getModuleNameFromContent("/** module foo*/ module bar;") == "bar");
721 	assert(getModuleNameFromContent("/* / module foo*/ module bar;") == "bar");
722 
723 	assert(getModuleNameFromContent("module modules.foo;") == "modules.foo");
724 }
725 
726 /**
727  * Search for module keyword in file
728  */
729 string getModuleNameFromFile(string filePath) {
730 	if (!filePath.exists)
731 	{
732 		return null;
733 	}
734 	string fileContent = filePath.readText;
735 
736 	logDiagnostic("Get module name from path: %s", filePath);
737 	return getModuleNameFromContent(fileContent);
738 }
739 
740 /**
741  * Compare two instances of the same type for equality,
742  * providing a rich error message on failure.
743  *
744  * This function will recurse into composite types (struct, AA, arrays)
745  * and compare element / member wise, taking opEquals into account,
746  * to provide the most accurate reason why comparison failed.
747  */
748 void deepCompare (T) (
749 	in T result, in T expected, string file = __FILE__, size_t line = __LINE__)
750 {
751 	deepCompareImpl!T(result, expected, T.stringof, file, line);
752 }
753 
754 void deepCompareImpl (T) (
755 	in T result, in T expected, string path, string file, size_t line)
756 {
757 	static if (is(T == struct) && !is(typeof(T.init.opEquals(T.init)) : bool))
758 	{
759 		static foreach (idx; 0 .. T.tupleof.length)
760 			deepCompareImpl(result.tupleof[idx], expected.tupleof[idx],
761 							format("%s.%s", path, __traits(identifier, T.tupleof[idx])),
762 							file, line);
763 	}
764 	else static if (is(T : KeyT[ValueT], KeyT, ValueT))
765 	{
766 		if (result.length != expected.length)
767 			throw new Exception(
768 				format("%s: AA has different number of entries (%s != %s): %s != %s",
769 					   path, result.length, expected.length, result, expected),
770 				file, line);
771 		foreach (key, value; expected)
772 		{
773 			if (auto ptr = key in result)
774 				deepCompareImpl(*ptr, value, format("%s[%s]", path, key), file, line);
775 			else
776 				throw new Exception(
777 					format("Expected key %s[%s] not present in result. %s != %s",
778 						   path, key, result, expected), file, line);
779 		}
780 	}
781 	else if (result != expected) {
782 		static if (is(T == struct) && is(typeof(T.init.opEquals(T.init)) : bool))
783 			path ~= ".opEquals";
784 		throw new Exception(
785 			format("%s: result != expected: %s != %s", path, result, expected),
786 			file, line);
787 	}
788 }
789 
790 /** Filters a forward range with the given predicate and returns a prefix range.
791 
792 	This function filters elements in-place, as opposed to returning a new
793 	range. This can be particularly useful when working with arrays, as this
794 	does not require any memory allocations.
795 
796 	This function guarantees that `pred` is called exactly once per element and
797 	deterministically destroys any elements for which `pred` returns `false`.
798 */
799 auto filterInPlace(alias pred, R)(R elems)
800 	if (isForwardRange!R)
801 {
802 	import std.algorithm.mutation : move;
803 
804 	R telems = elems.save;
805 	bool any_removed = false;
806 	size_t nret = 0;
807 	foreach (ref el; elems.save) {
808 		if (pred(el)) {
809 			if (any_removed) move(el, telems.front);
810 			telems.popFront();
811 			nret++;
812 		} else any_removed = true;
813 	}
814 	return elems.takeExactly(nret);
815 }
816 
817 ///
818 unittest {
819 	int[] arr = [1, 2, 3, 4, 5, 6, 7, 8];
820 
821 	arr = arr.filterInPlace!(e => e % 2 == 0);
822 	assert(arr == [2, 4, 6, 8]);
823 
824 	arr = arr.filterInPlace!(e => e < 5);
825 	assert(arr == [2, 4]);
826 }