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