1 /**
2  * File stuff
3  *
4  * License:
5  *   This Source Code Form is subject to the terms of
6  *   the Mozilla Public License, v. 2.0. If a copy of
7  *   the MPL was not distributed with this file, You
8  *   can obtain one at http://mozilla.org/MPL/2.0/.
9  *
10  * Authors:
11  *   Vladimir Panteleev <vladimir@thecybershadow.net>
12  */
13 
14 module ae.sys.file;
15 
16 import core.stdc.wchar_;
17 import core.thread;
18 
19 import std.array;
20 import std.conv;
21 import std.file;
22 import std.path;
23 import std.stdio : File;
24 import std.string;
25 import std.typecons;
26 import std.utf;
27 
28 import ae.sys.cmd : getCurrentThreadID;
29 
30 public import std.typecons : No, Yes;
31 
32 alias wcscmp = core.stdc.wchar_.wcscmp;
33 alias wcslen = core.stdc.wchar_.wcslen;
34 
35 version(Windows) import ae.sys.windows.imports;
36 
37 // ************************************************************************
38 
39 version (Windows)
40 {
41 	// Work around std.file overload
42 	mixin(importWin32!(q{winnt}, null, q{FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT}));
43 }
44 
45 // ************************************************************************
46 
47 version(Windows)
48 {
49 	string[] fastListDir(bool recursive = false, bool symlinks=false)(string pathname, string pattern = null)
50 	{
51 		import core.sys.windows.windows;
52 
53 		static if (recursive)
54 			enforce(!pattern, "TODO: recursive fastListDir with pattern");
55 
56 		string[] result;
57 		string c;
58 		HANDLE h;
59 
60 		c = buildPath(pathname, pattern ? pattern : "*.*");
61 		WIN32_FIND_DATAW fileinfo;
62 
63 		h = FindFirstFileW(toUTF16z(c), &fileinfo);
64 		if (h != INVALID_HANDLE_VALUE)
65 		{
66 			scope(exit) FindClose(h);
67 
68 			do
69 			{
70 				// Skip "." and ".."
71 				if (wcscmp(fileinfo.cFileName.ptr, ".") == 0 ||
72 					wcscmp(fileinfo.cFileName.ptr, "..") == 0)
73 					continue;
74 
75 				static if (!symlinks)
76 				{
77 					if (fileinfo.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT)
78 						continue;
79 				}
80 
81 				size_t clength = wcslen(fileinfo.cFileName.ptr);
82 				string name = std.utf.toUTF8(fileinfo.cFileName[0 .. clength]);
83 				string path = buildPath(pathname, name);
84 
85 				static if (recursive)
86 				{
87 					if (fileinfo.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY)
88 					{
89 						result ~= fastListDir!recursive(path);
90 						continue;
91 					}
92 				}
93 
94 				result ~= path;
95 			} while (FindNextFileW(h,&fileinfo) != FALSE);
96 		}
97 		return result;
98 	}
99 }
100 else
101 version (Posix)
102 {
103 	private import core.stdc.errno;
104 	private import core.sys.posix.dirent;
105 	private import core.stdc.string;
106 
107 	string[] fastListDir(bool recursive=false, bool symlinks=false)(string pathname, string pattern = null)
108 	{
109 		string[] result;
110 		DIR* h;
111 		dirent* fdata;
112 
113 		h = opendir(toStringz(pathname));
114 		if (h)
115 		{
116 			try
117 			{
118 				while((fdata = readdir(h)) != null)
119 				{
120 					// Skip "." and ".."
121 					if (!core.stdc..string.strcmp(fdata.d_name.ptr, ".") ||
122 						!core.stdc..string.strcmp(fdata.d_name.ptr, ".."))
123 							continue;
124 
125 					static if (!symlinks)
126 					{
127 						if (fdata.d_type == DT_LNK)
128 							continue;
129 					}
130 
131 					size_t len = core.stdc..string.strlen(fdata.d_name.ptr);
132 					string name = fdata.d_name[0 .. len].idup;
133 					if (pattern && !globMatch(name, pattern))
134 						continue;
135 					string path = buildPath(pathname, name);
136 
137 					static if (recursive)
138 					{
139 						if (fdata.d_type & DT_DIR)
140 						{
141 							result ~= fastListDir!(recursive, symlinks)(path);
142 							continue;
143 						}
144 					}
145 
146 					result ~= path;
147 				}
148 			}
149 			finally
150 			{
151 				closedir(h);
152 			}
153 		}
154 		else
155 		{
156 			throw new std.file.FileException(pathname, errno);
157 		}
158 		return result;
159 	}
160 }
161 else
162 	static assert(0, "TODO");
163 
164 // ************************************************************************
165 
166 string buildPath2(string[] segments...) { return segments.length ? buildPath(segments) : null; }
167 
168 /// Shell-like expansion of ?, * and ** in path components
169 DirEntry[] fileList(string pattern)
170 {
171 	auto components = cast(string[])array(pathSplitter(pattern));
172 	foreach (i, component; components[0..$-1])
173 		if (component.contains("?") || component.contains("*")) // TODO: escape?
174 		{
175 			DirEntry[] expansions; // TODO: filter range instead?
176 			auto dir = buildPath2(components[0..i]);
177 			if (component == "**")
178 				expansions = array(dirEntries(dir, SpanMode.depth));
179 			else
180 				expansions = array(dirEntries(dir, component, SpanMode.shallow));
181 
182 			DirEntry[] result;
183 			foreach (expansion; expansions)
184 				if (expansion.isDir())
185 					result ~= fileList(buildPath(expansion.name ~ components[i+1..$]));
186 			return result;
187 		}
188 
189 	auto dir = buildPath2(components[0..$-1]);
190 	if (!dir || exists(dir))
191 		return array(dirEntries(dir, components[$-1], SpanMode.shallow));
192 	else
193 		return null;
194 }
195 
196 /// ditto
197 DirEntry[] fileList(string pattern0, string[] patterns...)
198 {
199 	DirEntry[] result;
200 	foreach (pattern; [pattern0] ~ patterns)
201 		result ~= fileList(pattern);
202 	return result;
203 }
204 
205 /// ditto
206 string[] fastFileList(string pattern)
207 {
208 	auto components = cast(string[])array(pathSplitter(pattern));
209 	foreach (i, component; components[0..$-1])
210 		if (component.contains("?") || component.contains("*")) // TODO: escape?
211 		{
212 			string[] expansions; // TODO: filter range instead?
213 			auto dir = buildPath2(components[0..i]);
214 			if (component == "**")
215 				expansions = fastListDir!true(dir);
216 			else
217 				expansions = fastListDir(dir, component);
218 
219 			string[] result;
220 			foreach (expansion; expansions)
221 				if (expansion.isDir())
222 					result ~= fastFileList(buildPath(expansion ~ components[i+1..$]));
223 			return result;
224 		}
225 
226 	auto dir = buildPath2(components[0..$-1]);
227 	if (!dir || exists(dir))
228 		return fastListDir(dir, components[$-1]);
229 	else
230 		return null;
231 }
232 
233 /// ditto
234 string[] fastFileList(string pattern0, string[] patterns...)
235 {
236 	string[] result;
237 	foreach (pattern; [pattern0] ~ patterns)
238 		result ~= fastFileList(pattern);
239 	return result;
240 }
241 
242 // ************************************************************************
243 
244 import std.datetime;
245 import std.exception;
246 
247 deprecated SysTime getMTime(string name)
248 {
249 	return timeLastModified(name);
250 }
251 
252 /// If target exists, update its modification time;
253 /// otherwise create it as an empty file.
254 void touch(in char[] target)
255 {
256 	if (exists(target))
257 	{
258 		auto now = Clock.currTime();
259 		setTimes(target, now, now);
260 	}
261 	else
262 		std.file.write(target, "");
263 }
264 
265 /// Returns true if the target file doesn't exist,
266 /// or source is newer than the target.
267 bool newerThan(string source, string target)
268 {
269 	if (!target.exists)
270 		return true;
271 	return source.timeLastModified() > target.timeLastModified();
272 }
273 
274 /// Returns true if the target file doesn't exist,
275 /// or any of the sources are newer than the target.
276 bool anyNewerThan(string[] sources, string target)
277 {
278 	if (!target.exists)
279 		return true;
280 	auto targetTime = target.timeLastModified();
281 	return sources.any!(source => source.timeLastModified() > targetTime)();
282 }
283 
284 version (Posix)
285 {
286 	import core.sys.posix.sys.stat;
287 	import core.sys.posix.unistd;
288 
289 	int getOwner(string fn)
290 	{
291 		stat_t s;
292 		errnoEnforce(stat(toStringz(fn), &s) == 0, "stat: " ~ fn);
293 		return s.st_uid;
294 	}
295 
296 	int getGroup(string fn)
297 	{
298 		stat_t s;
299 		errnoEnforce(stat(toStringz(fn), &s) == 0, "stat: " ~ fn);
300 		return s.st_gid;
301 	}
302 
303 	void setOwner(string fn, int uid, int gid)
304 	{
305 		errnoEnforce(chown(toStringz(fn), uid, gid) == 0, "chown: " ~ fn);
306 	}
307 }
308 
309 /// Try to rename; copy/delete if rename fails
310 void move(string src, string dst)
311 {
312 	try
313 		src.rename(dst);
314 	catch (Exception e)
315 	{
316 		atomicCopy(src, dst);
317 		src.remove();
318 	}
319 }
320 
321 /// Make sure that the given directory exists
322 /// (and create parent directories as necessary).
323 void ensureDirExists(string path)
324 {
325 	if (!path.exists)
326 		path.mkdirRecurse();
327 }
328 
329 /// Make sure that the path to the given file name
330 /// exists (and create directories as necessary).
331 void ensurePathExists(string fn)
332 {
333 	fn.dirName.ensureDirExists();
334 }
335 
336 import ae.utils.text;
337 
338 /// Forcibly remove a file or directory.
339 /// If atomic is true, the entire directory is deleted "atomically"
340 /// (it is first moved/renamed to another location).
341 /// On Windows, this will move the file/directory out of the way,
342 /// if it is in use and cannot be deleted (but can be renamed).
343 void forceDelete(Flag!"atomic" atomic=Yes.atomic)(string fn, Flag!"recursive" recursive = No.recursive)
344 {
345 	import std.process : environment;
346 	version(Windows)
347 	{
348 		mixin(importWin32!q{winnt});
349 		mixin(importWin32!q{winbase});
350 	}
351 
352 	auto name = fn.baseName();
353 	fn = fn.absolutePath().longPath();
354 
355 	version(Windows)
356 	{
357 		auto fnW = toUTF16z(fn);
358 		auto attr = GetFileAttributesW(fnW);
359 		wenforce(attr != INVALID_FILE_ATTRIBUTES, "GetFileAttributes");
360 		if (attr & FILE_ATTRIBUTE_READONLY)
361 			SetFileAttributesW(fnW, attr & ~FILE_ATTRIBUTE_READONLY).wenforce("SetFileAttributes");
362 	}
363 
364 	static if (atomic)
365 	{
366 		// To avoid zombifying locked directories, try renaming it first.
367 		// Attempting to delete a locked directory will make it inaccessible.
368 
369 		bool tryMoveTo(string target)
370 		{
371 			target = target.longPath();
372 			if (target.endsWith(dirSeparator))
373 				target = target[0..$-1];
374 			if (target.length && !target.exists)
375 				return false;
376 
377 			string newfn;
378 			do
379 				newfn = format("%s%sdeleted-%s.%s.%s", target, dirSeparator, name, thisProcessID, randomString());
380 			while (newfn.exists);
381 
382 			version(Windows)
383 			{
384 				auto newfnW = toUTF16z(newfn);
385 				if (!MoveFileW(fnW, newfnW))
386 					return false;
387 			}
388 			else
389 			{
390 				try
391 					rename(fn, newfn);
392 				catch (FileException e)
393 					return false;
394 			}
395 
396 			fn = newfn;
397 			version(Windows) fnW = newfnW;
398 			return true;
399 		}
400 
401 		void tryMove()
402 		{
403 			auto tmp = environment.get("TEMP");
404 			if (tmp)
405 				if (tryMoveTo(tmp))
406 					return;
407 
408 			version(Windows)
409 				string tempDir = fn[0..7]~"Temp";
410 			else
411 				enum tempDir = "/tmp";
412 
413 			if (tryMoveTo(tempDir))
414 				return;
415 
416 			if (tryMoveTo(fn.dirName()))
417 				return;
418 
419 			throw new Exception("Unable to delete " ~ fn ~ " atomically (all rename attempts failed)");
420 		}
421 
422 		tryMove();
423 	}
424 
425 	version(Windows)
426 	{
427 		if (attr & FILE_ATTRIBUTE_DIRECTORY)
428 		{
429 			if (recursive && (attr & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
430 			{
431 				foreach (de; fn.dirEntries(SpanMode.shallow))
432 					forceDelete!(No.atomic)(de.name, Yes.recursive);
433 			}
434 			// Will fail if !recursive and directory is not empty
435 			RemoveDirectoryW(fnW).wenforce("RemoveDirectory");
436 		}
437 		else
438 			DeleteFileW(fnW).wenforce("DeleteFile");
439 	}
440 	else
441 	{
442 		if (recursive)
443 			fn.removeRecurse();
444 		else
445 			if (fn.isDir)
446 				fn.rmdir();
447 			else
448 				fn.remove();
449 	}
450 }
451 
452 
453 deprecated void forceDelete(bool atomic)(string fn, bool recursive = false) { forceDelete!(cast(Flag!"atomic")atomic)(fn, cast(Flag!"recursive")recursive); }
454 //deprecated void forceDelete()(string fn, bool recursive) { forceDelete!(Yes.atomic)(fn, cast(Flag!"recursive")recursive); }
455 
456 deprecated unittest
457 {
458 	mkdir("testdir"); touch("testdir/b"); forceDelete!(false     )("testdir", true);
459 	mkdir("testdir"); touch("testdir/b"); forceDelete!(true      )("testdir", true);
460 }
461 
462 unittest
463 {
464 	mkdir("testdir"); touch("testdir/b"); forceDelete             ("testdir", Yes.recursive);
465 	mkdir("testdir"); touch("testdir/b"); forceDelete!(No .atomic)("testdir", Yes.recursive);
466 	mkdir("testdir"); touch("testdir/b"); forceDelete!(Yes.atomic)("testdir", Yes.recursive);
467 }
468 
469 /// If fn is a directory, delete it recursively.
470 /// Otherwise, delete the file or symlink fn.
471 void removeRecurse(string fn)
472 {
473 	auto attr = fn.getAttributes();
474 	if (attr.attrIsSymlink)
475 	{
476 		version (Windows)
477 			if (attr.attrIsDir)
478 				fn.rmdir();
479 			else
480 				fn.remove();
481 		else
482 			fn.remove();
483 	}
484 	else
485 	if (attr.attrIsDir)
486 		version (Windows)
487 			fn.forceDelete!(No.atomic)(Yes.recursive); // For read-only files
488 		else
489 			fn.rmdirRecurse();
490 	else
491 		fn.remove();
492 }
493 
494 /// Create an empty directory, deleting
495 /// all its contents if it already exists.
496 void recreateEmptyDirectory()(string dir)
497 {
498 	if (dir.exists)
499 		dir.forceDelete(Yes.recursive);
500 	mkdir(dir);
501 }
502 
503 bool isHidden()(string fn)
504 {
505 	if (baseName(fn).startsWith("."))
506 		return true;
507 	version (Windows)
508 	{
509 		mixin(importWin32!q{winnt});
510 		if (getAttributes(fn) & FILE_ATTRIBUTE_HIDDEN)
511 			return true;
512 	}
513 	return false;
514 }
515 
516 /// Return a file's unique ID.
517 ulong getFileID()(string fn)
518 {
519 	version (Windows)
520 	{
521 		mixin(importWin32!q{winnt});
522 		mixin(importWin32!q{winbase});
523 
524 		auto fnW = toUTF16z(fn);
525 		auto h = CreateFileW(fnW, FILE_READ_ATTRIBUTES, 0, null, OPEN_EXISTING, 0, HANDLE.init);
526 		wenforce(h!=INVALID_HANDLE_VALUE, fn);
527 		scope(exit) CloseHandle(h);
528 		BY_HANDLE_FILE_INFORMATION fi;
529 		GetFileInformationByHandle(h, &fi).wenforce("GetFileInformationByHandle");
530 
531 		ULARGE_INTEGER li;
532 		li.LowPart  = fi.nFileIndexLow;
533 		li.HighPart = fi.nFileIndexHigh;
534 		auto result = li.QuadPart;
535 		enforce(result, "Null file ID");
536 		return result;
537 	}
538 	else
539 	{
540 		return DirEntry(fn).statBuf.st_ino;
541 	}
542 }
543 
544 unittest
545 {
546 	touch("a");
547 	scope(exit) remove("a");
548 	hardLink("a", "b");
549 	scope(exit) remove("b");
550 	touch("c");
551 	scope(exit) remove("c");
552 	assert(getFileID("a") == getFileID("b"));
553 	assert(getFileID("a") != getFileID("c"));
554 }
555 
556 deprecated alias std.file.getSize getSize2;
557 
558 /// Using UNC paths bypasses path length limitation when using Windows wide APIs.
559 string longPath(string s)
560 {
561 	version (Windows)
562 	{
563 		if (!s.startsWith(`\\`))
564 			return `\\?\` ~ s.absolutePath().buildNormalizedPath().replace(`/`, `\`);
565 	}
566 	return s;
567 }
568 
569 version (Windows)
570 {
571 	static if (__traits(compiles, { mixin importWin32!q{winnt}; }))
572 		static mixin(importWin32!q{winnt});
573 
574 	void createReparsePoint(string reparseBufferName, string extraInitialization, string reparseTagName)(in char[] target, in char[] print, in char[] link)
575 	{
576 		mixin(importWin32!q{winbase});
577 		mixin(importWin32!q{windef});
578 		mixin(importWin32!q{winioctl});
579 
580 		enum SYMLINK_FLAG_RELATIVE = 1;
581 
582 		HANDLE hLink = CreateFileW(link.toUTF16z(), GENERIC_READ | GENERIC_WRITE, 0, null, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, null);
583 		wenforce(hLink && hLink != INVALID_HANDLE_VALUE, "CreateFileW");
584 		scope(exit) CloseHandle(hLink);
585 
586 		enum pathOffset =
587 			mixin(q{REPARSE_DATA_BUFFER..} ~ reparseBufferName)            .offsetof +
588 			mixin(q{REPARSE_DATA_BUFFER..} ~ reparseBufferName)._PathBuffer.offsetof;
589 
590 		auto targetW = target.toUTF16();
591 		auto printW  = print .toUTF16();
592 
593 		// Despite MSDN, two NUL-terminating characters are needed, one for each string.
594 
595 		auto pathBufferSize = targetW.length + 1 + printW.length + 1; // in chars
596 		auto buf = new ubyte[pathOffset + pathBufferSize * WCHAR.sizeof];
597 		auto r = cast(REPARSE_DATA_BUFFER*)buf.ptr;
598 
599 		r.ReparseTag = mixin(reparseTagName);
600 		r.ReparseDataLength = to!WORD(buf.length - mixin(q{r..} ~ reparseBufferName).offsetof);
601 
602 		auto pathBuffer = mixin(q{r..} ~ reparseBufferName).PathBuffer;
603 		auto p = pathBuffer;
604 
605 		mixin(q{r..} ~ reparseBufferName).SubstituteNameOffset = to!WORD((p-pathBuffer) * WCHAR.sizeof);
606 		mixin(q{r..} ~ reparseBufferName).SubstituteNameLength = to!WORD(targetW.length * WCHAR.sizeof);
607 		p[0..targetW.length] = targetW;
608 		p += targetW.length;
609 		*p++ = 0;
610 
611 		mixin(q{r..} ~ reparseBufferName).PrintNameOffset      = to!WORD((p-pathBuffer) * WCHAR.sizeof);
612 		mixin(q{r..} ~ reparseBufferName).PrintNameLength      = to!WORD(printW .length * WCHAR.sizeof);
613 		p[0..printW.length] = printW;
614 		p += printW.length;
615 		*p++ = 0;
616 
617 		assert(p-pathBuffer == pathBufferSize);
618 
619 		mixin(extraInitialization);
620 
621 		DWORD dwRet; // Needed despite MSDN
622 		DeviceIoControl(hLink, FSCTL_SET_REPARSE_POINT, buf.ptr, buf.length.to!DWORD(), null, 0, &dwRet, null).wenforce("DeviceIoControl");
623 	}
624 
625 	void acquirePrivilege(S)(S name)
626 	{
627 		mixin(importWin32!q{winbase});
628 		mixin(importWin32!q{windef});
629 
630 		import ae.sys.windows;
631 
632 		HANDLE hToken = null;
633 		wenforce(OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken));
634 		scope(exit) CloseHandle(hToken);
635 
636 		TOKEN_PRIVILEGES tp;
637 		wenforce(LookupPrivilegeValue(null, name.toUTF16z(), &tp.Privileges[0].Luid), "LookupPrivilegeValue");
638 
639 		tp.PrivilegeCount = 1;
640 		tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
641 		wenforce(AdjustTokenPrivileges(hToken, FALSE, &tp, cast(DWORD)TOKEN_PRIVILEGES.sizeof, null, null), "AdjustTokenPrivileges");
642 	}
643 
644 	/// Link a directory.
645 	/// Uses symlinks on POSIX, and directory junctions on Windows.
646 	void dirLink()(in char[] original, in char[] link)
647 	{
648 		mkdir(link);
649 		scope(failure) rmdir(link);
650 
651 		auto target = `\??\` ~ original.idup.absolutePath();
652 		if (target[$-1] != '\\')
653 			target ~= '\\';
654 
655 		createReparsePoint!(q{MountPointReparseBuffer}, q{}, q{IO_REPARSE_TAG_MOUNT_POINT})(target, null, link);
656 	}
657 
658 	void symlink()(in char[] original, in char[] link)
659 	{
660 		mixin(importWin32!q{winnt});
661 
662 		acquirePrivilege(SE_CREATE_SYMBOLIC_LINK_NAME);
663 
664 		touch(link);
665 		scope(failure) remove(link);
666 
667 		createReparsePoint!(q{SymbolicLinkReparseBuffer}, q{r.SymbolicLinkReparseBuffer.Flags = link.isAbsolute() ? 0 : SYMLINK_FLAG_RELATIVE;}, q{IO_REPARSE_TAG_SYMLINK})(original, original, link);
668 	}
669 }
670 else
671 	alias std.file.symlink dirLink;
672 
673 version(Windows) version(unittest) static mixin(importWin32!q{winnt});
674 
675 unittest
676 {
677 	mkdir("a"); scope(exit) rmdir("a"[]);
678 	touch("a/f"); scope(exit) remove("a/f");
679 	dirLink("a", "b"); scope(exit) version(Windows) rmdir("b"); else remove("b");
680 	//symlink("a/f", "c"); scope(exit) remove("c");
681 	assert("b".isSymlink());
682 	//assert("c".isSymlink());
683 	assert("b/f".exists());
684 }
685 
686 version (Windows)
687 {
688 	void hardLink()(string src, string dst)
689 	{
690 		mixin(importWin32!q{w32api});
691 
692 		static assert(_WIN32_WINNT >= 0x501, "CreateHardLinkW not available for target Windows platform. Specify -version=WindowsXP");
693 
694 		mixin(importWin32!q{winnt});
695 		mixin(importWin32!q{winbase});
696 
697 		wenforce(CreateHardLinkW(toUTF16z(dst), toUTF16z(src), null), "CreateHardLink failed: " ~ src ~ " -> " ~ dst);
698 	}
699 }
700 version (Posix)
701 {
702 	void hardLink()(string src, string dst)
703 	{
704 		import core.sys.posix.unistd;
705 		enforce(link(toUTFz!(const char*)(src), toUTFz!(const char*)(dst)) == 0, "link() failed: " ~ dst);
706 	}
707 }
708 
709 version (Posix)
710 {
711 	string realPath(string path)
712 	{
713 		// TODO: Windows version
714 		import core.sys.posix.stdlib;
715 		auto p = realpath(toUTFz!(const char*)(path), null);
716 		errnoEnforce(p, "realpath");
717 		string result = fromStringz(p).idup;
718 		free(p);
719 		return result;
720 	}
721 }
722 
723 // /proc/self/mounts parsing
724 version (linux)
725 {
726 	struct MountInfo
727 	{
728 		string spec; /// device path
729 		string file; /// mount path
730 		string vfstype; /// file system
731 		string mntops; /// options
732 		int freq; /// dump flag
733 		int passno; /// fsck order
734 	}
735 
736 	string unescapeMountString(in char[] s)
737 	{
738 		string result;
739 
740 		size_t p = 0;
741 		for (size_t i=0; i+3<s.length;)
742 		{
743 			auto c = s[i];
744 			if (c == '\\')
745 			{
746 				result ~= s[p..i];
747 				result ~= to!int(s[i+1..i+4], 8);
748 				i += 4;
749 				p = i;
750 			}
751 			else
752 				i++;
753 		}
754 		result ~= s[p..$];
755 		return result;
756 	}
757 
758 	unittest
759 	{
760 		assert(unescapeMountString(`a\040b\040c`) == "a b c");
761 		assert(unescapeMountString(`\040`) == " ");
762 	}
763 
764 	MountInfo parseMountInfo(in char[] line)
765 	{
766 		const(char)[][6] parts;
767 		copy(line.splitter(" "), parts[]);
768 		return MountInfo(
769 			unescapeMountString(parts[0]),
770 			unescapeMountString(parts[1]),
771 			unescapeMountString(parts[2]),
772 			unescapeMountString(parts[3]),
773 			parts[4].to!int,
774 			parts[5].to!int,
775 		);
776 	}
777 
778 	/// Returns an iterator of MountInfo structs.
779 	auto getMounts()
780 	{
781 		return File("/proc/self/mounts", "rb").byLine().map!parseMountInfo();
782 	}
783 
784 	/// Get the name of the filesystem that the given path is mounted under.
785 	string getPathFilesystem(string path)
786 	{
787 		path = realPath(path);
788 		size_t bestLength; string bestFS;
789 		foreach (ref info; getMounts())
790 		{
791 			if (path.startsWith(info.file) && (path.length == info.file.length || path[info.file.length] == '/'))
792 			{
793 				if (bestLength < info.file.length)
794 				{
795 					bestLength = info.file.length;
796 					bestFS = info.vfstype;
797 				}
798 			}
799 		}
800 		return bestFS;
801 	}
802 }
803 
804 // ****************************************************************************
805 
806 version (linux)
807 {
808 	import core.sys.linux.sys.xattr;
809 	import core.stdc.errno;
810 	alias ENOATTR = ENODATA;
811 
812 	/// AA-like object for accessing a file's extended attributes.
813 	struct XAttrs(Obj, string funPrefix)
814 	{
815 		Obj obj;
816 
817 		mixin("alias getFun = " ~ funPrefix ~ "getxattr;");
818 		mixin("alias setFun = " ~ funPrefix ~ "setxattr;");
819 		mixin("alias removeFun = " ~ funPrefix ~ "removexattr;");
820 		mixin("alias listFun = " ~ funPrefix ~ "listxattr;");
821 
822 		void[] opIndex(string key)
823 		{
824 			auto cKey = key.toStringz();
825 			auto size = getFun(obj, cKey, null, 0);
826 			errnoEnforce(size >= 0);
827 			auto result = new void[size];
828 			// TODO: race condition, retry
829 			size = getFun(obj, cKey, result.ptr, result.length);
830 			errnoEnforce(size == result.length);
831 			return result;
832 		}
833 
834 		bool opIn_r(string key)
835 		{
836 			auto cKey = key.toStringz();
837 			auto size = getFun(obj, cKey, null, 0);
838 			if (size >= 0)
839 				return true;
840 			else
841 			if (errno == ENOATTR)
842 				return false;
843 			else
844 				errnoEnforce(false, "Error reading file xattrs");
845 			assert(false);
846 		}
847 
848 		void opIndexAssign(in void[] value, string key)
849 		{
850 			auto ret = setFun(obj, key.toStringz(), value.ptr, value.length, 0);
851 			errnoEnforce(ret == 0);
852 		}
853 
854 		void remove(string key)
855 		{
856 			auto ret = removeFun(obj, key.toStringz());
857 			errnoEnforce(ret == 0);
858 		}
859 
860 		string[] keys()
861 		{
862 			auto size = listFun(obj, null, 0);
863 			errnoEnforce(size >= 0);
864 			auto buf = new char[size];
865 			// TODO: race condition, retry
866 			size = listFun(obj, buf.ptr, buf.length);
867 			errnoEnforce(size == buf.length);
868 
869 			char[][] result;
870 			size_t start;
871 			foreach (p, c; buf)
872 				if (!c)
873 				{
874 					result ~= buf[start..p];
875 					start = p+1;
876 				}
877 
878 			return cast(string[])result;
879 		}
880 	}
881 
882 	auto xAttrs(string path)
883 	{
884 		return XAttrs!(const(char)*, "")(path.toStringz());
885 	}
886 
887 	auto linkXAttrs(string path)
888 	{
889 		return XAttrs!(const(char)*, "l")(path.toStringz());
890 	}
891 
892 	auto xAttrs(in ref File f)
893 	{
894 		return XAttrs!(int, "f")(f.fileno);
895 	}
896 
897 	unittest
898 	{
899 		enum fn = "test.txt";
900 		std.file.write(fn, "test");
901 		scope(exit) remove(fn);
902 
903 		auto attrs = xAttrs(fn);
904 		enum key = "user.foo";
905 		assert(key !in attrs);
906 		assert(attrs.keys == []);
907 
908 		attrs[key] = "bar";
909 		assert(key in attrs);
910 		assert(attrs[key] == "bar");
911 		assert(attrs.keys == [key]);
912 
913 		attrs.remove(key);
914 		assert(key !in attrs);
915 		assert(attrs.keys == []);
916 	}
917 }
918 
919 // ****************************************************************************
920 
921 version (Windows)
922 {
923 	/// Enumerate all hard links to the specified file.
924 	// TODO: Return a range
925 	string[] enumerateHardLinks()(string fn)
926 	{
927 		mixin(importWin32!q{winnt});
928 		mixin(importWin32!q{winbase});
929 
930 		alias extern(System) HANDLE function(LPCWSTR lpFileName, DWORD dwFlags, LPDWORD StringLength, PWCHAR LinkName) TFindFirstFileNameW;
931 		alias extern(System) BOOL function(HANDLE hFindStream, LPDWORD StringLength, PWCHAR LinkName) TFindNextFileNameW;
932 
933 		auto kernel32 = GetModuleHandle("kernel32.dll");
934 		auto FindFirstFileNameW = cast(TFindFirstFileNameW)GetProcAddress(kernel32, "FindFirstFileNameW").wenforce("GetProcAddress(FindFirstFileNameW)");
935 		auto FindNextFileNameW = cast(TFindNextFileNameW)GetProcAddress(kernel32, "FindNextFileNameW").wenforce("GetProcAddress(FindNextFileNameW)");
936 
937 		static WCHAR[0x8000] buf;
938 		DWORD len = buf.length;
939 		auto h = FindFirstFileNameW(toUTF16z(fn), 0, &len, buf.ptr);
940 		wenforce(h != INVALID_HANDLE_VALUE, "FindFirstFileNameW");
941 		scope(exit) FindClose(h);
942 
943 		string[] result;
944 		do
945 		{
946 			enforce(len > 0 && len < buf.length && buf[len-1] == 0, "Bad FindFirst/NextFileNameW result");
947 			result ~= buf[0..len-1].toUTF8();
948 			len = buf.length;
949 			auto ok = FindNextFileNameW(h, &len, buf.ptr);
950 			if (!ok && GetLastError() == ERROR_HANDLE_EOF)
951 				break;
952 			wenforce(ok, "FindNextFileNameW");
953 		} while(true);
954 		return result;
955 	}
956 }
957 
958 uint hardLinkCount(string fn)
959 {
960 	version (Windows)
961 	{
962 		// TODO: Optimize (don't transform strings)
963 		return cast(uint)fn.enumerateHardLinks.length;
964 	}
965 	else
966 	{
967 		import core.sys.posix.sys.stat;
968 
969 		stat_t s;
970 		errnoEnforce(stat(fn.toStringz(), &s) == 0, "stat");
971 		return s.st_nlink.to!uint;
972 	}
973 }
974 
975 unittest
976 {
977 	touch("a.test");
978 	scope(exit) remove("a.test");
979 	assert("a.test".hardLinkCount() == 1);
980 
981 	hardLink("a.test", "b.test");
982 	scope(exit) remove("b.test");
983 	assert("a.test".hardLinkCount() == 2);
984 	assert("b.test".hardLinkCount() == 2);
985 
986 	version(Windows)
987 	{
988 		auto paths = enumerateHardLinks("a.test");
989 		assert(paths.length == 2);
990 		paths.sort();
991 		assert(paths[0].endsWith(`\a.test`), paths[0]);
992 		assert(paths[1].endsWith(`\b.test`));
993 	}
994 }
995 
996 void toFile(in void[] data, in char[] name)
997 {
998 	std.file.write(name, data);
999 }
1000 
1001 /// Uses UNC paths to open a file.
1002 /// Requires https://github.com/D-Programming-Language/phobos/pull/1888
1003 File openFile()(string fn, string mode = "rb")
1004 {
1005 	File f;
1006 	static if (is(typeof(&f.windowsHandleOpen)))
1007 	{
1008 		import core.sys.windows.windows;
1009 		import ae.sys.windows.exception;
1010 
1011 		string winMode;
1012 		foreach (c; mode)
1013 			switch (c)
1014 			{
1015 				case 'r':
1016 				case 'w':
1017 				case 'a':
1018 				case '+':
1019 					winMode ~= c;
1020 					break;
1021 				case 'b':
1022 				case 't':
1023 					break;
1024 				default:
1025 					assert(false, "Unknown character in mode");
1026 			}
1027 		DWORD access, creation;
1028 		bool append;
1029 		switch (winMode)
1030 		{
1031 			case "r" : access = GENERIC_READ                ; creation = OPEN_EXISTING; break;
1032 			case "r+": access = GENERIC_READ | GENERIC_WRITE; creation = OPEN_EXISTING; break;
1033 			case "w" : access =                GENERIC_WRITE; creation = OPEN_ALWAYS  ; break;
1034 			case "w+": access = GENERIC_READ | GENERIC_WRITE; creation = OPEN_ALWAYS  ; break;
1035 			case "a" : access =                GENERIC_WRITE; creation = OPEN_ALWAYS  ; append = true; break;
1036 			case "a+": assert(false, "Not implemented"); // requires two file pointers
1037 			default: assert(false, "Bad file mode: " ~ mode);
1038 		}
1039 
1040 		auto pathW = toUTF16z(longPath(fn));
1041 		auto h = CreateFileW(pathW, access, FILE_SHARE_READ, null, creation, 0, HANDLE.init);
1042 		wenforce(h != INVALID_HANDLE_VALUE);
1043 
1044 		if (append)
1045 			h.SetFilePointer(0, null, FILE_END);
1046 
1047 		f.windowsHandleOpen(h, mode);
1048 	}
1049 	else
1050 		f.open(fn, mode);
1051 	return f;
1052 }
1053 
1054 auto fileDigest(Digest)(string fn)
1055 {
1056 	import std.range.primitives;
1057 	Digest context;
1058 	context.start();
1059 	put(context, openFile(fn, "rb").byChunk(64 * 1024));
1060 	auto digest = context.finish();
1061 	return digest;
1062 }
1063 
1064 template mdFile()
1065 {
1066 	import std.digest.md;
1067 	alias mdFile = fileDigest!MD5;
1068 }
1069 
1070 version (HAVE_WIN32)
1071 unittest
1072 {
1073 	import std.digest.digest : toHexString;
1074 	write("test.txt", "Hello, world!");
1075 	scope(exit) remove("test.txt");
1076 	assert(mdFile("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839");
1077 }
1078 
1079 auto fileDigestCached(Digest)(string fn)
1080 {
1081 	static typeof(Digest.init.finish())[ulong] cache;
1082 	auto id = getFileID(fn);
1083 	auto phash = id in cache;
1084 	if (phash)
1085 		return *phash;
1086 	return cache[id] = fileDigest!Digest(fn);
1087 }
1088 
1089 template mdFileCached()
1090 {
1091 	import std.digest.md;
1092 	alias mdFileCached = fileDigestCached!MD5;
1093 }
1094 
1095 version (HAVE_WIN32)
1096 unittest
1097 {
1098 	import std.digest.digest : toHexString;
1099 	write("test.txt", "Hello, world!");
1100 	scope(exit) remove("test.txt");
1101 	assert(mdFileCached("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839");
1102 	write("test.txt", "Something else");
1103 	assert(mdFileCached("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839");
1104 }
1105 
1106 /// Read a File (which might be a stream) into an array
1107 void[] readFile(File f)
1108 {
1109 	import std.range.primitives;
1110 	auto result = appender!(ubyte[]);
1111 	put(result, f.byChunk(64*1024));
1112 	return result.data;
1113 }
1114 
1115 unittest
1116 {
1117 	auto s = "0123456789".replicate(10_000);
1118 	write("test.txt", s);
1119 	scope(exit) remove("test.txt");
1120 	assert(readFile(File("test.txt")) == s);
1121 }
1122 
1123 /// Like std.file.readText for non-UTF8
1124 ascii readAscii()(string fileName)
1125 {
1126 	return cast(ascii)readFile(openFile(fileName, "rb"));
1127 }
1128 
1129 // http://d.puremagic.com/issues/show_bug.cgi?id=7016
1130 version(Posix) static import ae.sys.signals;
1131 
1132 /// Start a thread which writes data to f asynchronously.
1133 Thread writeFileAsync(File f, in void[] data)
1134 {
1135 	static class Writer : Thread
1136 	{
1137 		File target;
1138 		const void[] data;
1139 
1140 		this(ref File f, in void[] data)
1141 		{
1142 			this.target = f;
1143 			this.data = data;
1144 			super(&run);
1145 		}
1146 
1147 		void run()
1148 		{
1149 			version (Posix)
1150 			{
1151 				import ae.sys.signals;
1152 				collectSignal(SIGPIPE, &write);
1153 			}
1154 			else
1155 				write();
1156 		}
1157 
1158 		void write()
1159 		{
1160 			target.rawWrite(data);
1161 			target.close();
1162 		}
1163 	}
1164 
1165 	auto t = new Writer(f, data);
1166 	t.start();
1167 	return t;
1168 }
1169 
1170 /// Write data to a file, and ensure it gets written to disk
1171 /// before this function returns.
1172 /// Consider using as atomic!syncWrite.
1173 /// See also: syncUpdate
1174 void syncWrite()(string target, in void[] data)
1175 {
1176 	auto f = File(target, "wb");
1177 	f.rawWrite(data);
1178 	version (Windows)
1179 	{
1180 		mixin(importWin32!q{windows});
1181 		FlushFileBuffers(f.windowsHandle);
1182 	}
1183 	else
1184 	{
1185 		import core.sys.posix.unistd;
1186 		fsync(f.fileno);
1187 	}
1188 	f.close();
1189 }
1190 
1191 /// Atomically save data to a file (if the file doesn't exist,
1192 /// or its contents differs). The update operation as a whole
1193 /// is not atomic, only the write is.
1194 void syncUpdate()(string fn, in void[] data)
1195 {
1196 	if (!fn.exists || fn.read() != data)
1197 		atomic!(syncWrite!())(fn, data);
1198 }
1199 
1200 version(Windows) import ae.sys.windows.exception;
1201 
1202 struct NamedPipeImpl
1203 {
1204 	immutable string fileName;
1205 
1206 	/// Create a named pipe, and reserve a filename.
1207 	this()(string name)
1208 	{
1209 		version(Windows)
1210 		{
1211 			mixin(importWin32!q{winbase});
1212 
1213 			fileName = `\\.\pipe\` ~ name;
1214 			auto h = CreateNamedPipeW(fileName.toUTF16z, PIPE_ACCESS_OUTBOUND, PIPE_TYPE_BYTE, 10, 4096, 4096, 0, null).wenforce("CreateNamedPipeW");
1215 			f.windowsHandleOpen(h, "wb");
1216 		}
1217 		else
1218 		{
1219 			import core.sys.posix.sys.stat;
1220 
1221 			fileName = `/tmp/` ~ name ~ `.fifo`;
1222 			mkfifo(fileName.toStringz, S_IWUSR | S_IRUSR);
1223 		}
1224 	}
1225 
1226 	/// Wait for a peer to open the other end of the pipe.
1227 	File connect()()
1228 	{
1229 		version(Windows)
1230 		{
1231 			mixin(importWin32!q{winbase});
1232 			mixin(importWin32!q{windef});
1233 
1234 			BOOL bSuccess = ConnectNamedPipe(f.windowsHandle, null);
1235 
1236 			// "If a client connects before the function is called, the function returns zero
1237 			// and GetLastError returns ERROR_PIPE_CONNECTED. This can happen if a client
1238 			// connects in the interval between the call to CreateNamedPipe and the call to
1239 			// ConnectNamedPipe. In this situation, there is a good connection between client
1240 			// and server, even though the function returns zero."
1241 			if (!bSuccess)
1242 				wenforce(GetLastError() == ERROR_PIPE_CONNECTED, "ConnectNamedPipe");
1243 
1244 			return f;
1245 		}
1246 		else
1247 		{
1248 			return File(fileName, "w");
1249 		}
1250 	}
1251 
1252 	~this()
1253 	{
1254 		version(Windows)
1255 		{
1256 			// File.~this will take care of cleanup
1257 		}
1258 		else
1259 			fileName.remove();
1260 	}
1261 
1262 private:
1263 	File f;
1264 }
1265 alias NamedPipe = RefCounted!NamedPipeImpl;
1266 
1267 import ae.utils.textout : StringBuilder;
1268 
1269 /// Avoid std.stdio.File.readln's memory corruption bug
1270 /// https://issues.dlang.org/show_bug.cgi?id=13856
1271 string safeReadln(File f)
1272 {
1273 	StringBuilder buf;
1274 	char[1] arr;
1275 	while (true)
1276 	{
1277 		auto result = f.rawRead(arr[]);
1278 		if (!result.length)
1279 			break;
1280 		buf.put(result);
1281 		if (result[0] == '\x0A')
1282 			break;
1283 	}
1284 	return buf.get();
1285 }
1286 
1287 // ****************************************************************************
1288 
1289 /// Change the current directory to the given directory. Does nothing if dir is null.
1290 /// Return a scope guard which, upon destruction, restores the previous directory.
1291 /// Asserts that only one thread has changed the process's current directory at any time.
1292 auto pushd(string dir)
1293 {
1294 	import core.atomic;
1295 
1296 	static int threadCount = 0;
1297 	static shared int processCount = 0;
1298 
1299 	static struct Popd
1300 	{
1301 		string oldPath;
1302 		this(string cwd) { oldPath = cwd; }
1303 		~this() { if (oldPath) pop(); }
1304 		@disable this();
1305 		@disable this(this);
1306 
1307 		void pop()
1308 		{
1309 			assert(oldPath);
1310 			scope(exit) oldPath = null;
1311 			chdir(oldPath);
1312 
1313 			auto newThreadCount = --threadCount;
1314 			auto newProcessCount = atomicOp!"-="(processCount, 1);
1315 			assert(newThreadCount == newProcessCount); // Shouldn't happen
1316 		}
1317 	}
1318 
1319 	string cwd;
1320 	if (dir)
1321 	{
1322 		auto newThreadCount = ++threadCount;
1323 		auto newProcessCount = atomicOp!"+="(processCount, 1);
1324 		assert(newThreadCount == newProcessCount, "Another thread already has an active pushd");
1325 
1326 		cwd = getcwd();
1327 		chdir(dir);
1328 	}
1329 	return Popd(cwd);
1330 }
1331 
1332 // ****************************************************************************
1333 
1334 import std.algorithm;
1335 import std.process : thisProcessID;
1336 import std.traits;
1337 import std.typetuple;
1338 import ae.utils.meta;
1339 
1340 enum targetParameterNames = "target/to/name/dst";
1341 
1342 /// Wrap an operation which creates a file or directory,
1343 /// so that it is created safely and, for files, atomically
1344 /// (by performing the underlying operation to a temporary
1345 /// location, then renaming the completed file/directory to
1346 /// the actual target location). targetName specifies the name
1347 /// of the parameter containing the target file/directory.
1348 auto atomic(alias impl, string targetName = targetParameterNames)(staticMap!(Unqual, ParameterTypeTuple!impl) args)
1349 {
1350 	enum targetIndex = findParameter([ParameterIdentifierTuple!impl], targetName, __traits(identifier, impl));
1351 	return atomic!(impl, targetIndex)(args);
1352 }
1353 
1354 /// ditto
1355 auto atomic(alias impl, size_t targetIndex)(staticMap!(Unqual, ParameterTypeTuple!impl) args)
1356 {
1357 	// idup for https://d.puremagic.com/issues/show_bug.cgi?id=12503
1358 	auto target = args[targetIndex].idup;
1359 	auto temp = "%s.%s.%s.temp".format(target, thisProcessID, getCurrentThreadID);
1360 	if (temp.exists) temp.removeRecurse();
1361 	scope(success) rename(temp, target);
1362 	scope(failure) if (temp.exists) temp.removeRecurse();
1363 	args[targetIndex] = temp;
1364 	return impl(args);
1365 }
1366 
1367 /// ditto
1368 // Workaround for https://d.puremagic.com/issues/show_bug.cgi?id=12230
1369 // Can't be an overload because of https://issues.dlang.org/show_bug.cgi?id=13374
1370 //R atomicDg(string targetName = "target", R, Args...)(R delegate(Args) impl, staticMap!(Unqual, Args) args)
1371 auto atomicDg(size_t targetIndexA = size_t.max, Impl, Args...)(Impl impl, Args args)
1372 {
1373 	enum targetIndex = targetIndexA == size_t.max ? ParameterTypeTuple!impl.length-1 : targetIndexA;
1374 	return atomic!(impl, targetIndex)(args);
1375 }
1376 
1377 deprecated alias safeUpdate = atomic;
1378 
1379 unittest
1380 {
1381 	enum fn = "atomic.tmp";
1382 	scope(exit) if (fn.exists) fn.remove();
1383 
1384 	atomic!touch(fn);
1385 	assert(fn.exists);
1386 	fn.remove();
1387 
1388 	atomicDg(&touch, fn);
1389 	assert(fn.exists);
1390 }
1391 
1392 /// Wrap an operation so that it is skipped entirely
1393 /// if the target already exists. Implies atomic.
1394 auto cached(alias impl, string targetName = targetParameterNames)(ParameterTypeTuple!impl args)
1395 {
1396 	enum targetIndex = findParameter([ParameterIdentifierTuple!impl], targetName, __traits(identifier, impl));
1397 	auto target = args[targetIndex];
1398 	if (!target.exists)
1399 		atomic!(impl, targetIndex)(args);
1400 	return target;
1401 }
1402 
1403 /// ditto
1404 // Exists due to the same reasons as atomicDg
1405 auto cachedDg(size_t targetIndexA = size_t.max, Impl, Args...)(Impl impl, Args args)
1406 {
1407 	enum targetIndex = targetIndexA == size_t.max ? ParameterTypeTuple!impl.length-1 : targetIndexA;
1408 	auto target = args[targetIndex];
1409 	if (!target.exists)
1410 		atomic!(impl, targetIndex)(args);
1411 	return target;
1412 }
1413 
1414 deprecated alias obtainUsing = cached;
1415 
1416 /// Create a file, or replace an existing file's contents
1417 /// atomically.
1418 /// Note: Consider using atomic!syncWrite or
1419 /// atomic!syncUpdate instead.
1420 alias atomic!writeProxy atomicWrite;
1421 deprecated alias safeWrite = atomicWrite;
1422 void writeProxy(string target, in void[] data)
1423 {
1424 	std.file.write(target, data);
1425 }
1426 
1427 // Work around for https://github.com/D-Programming-Language/phobos/pull/2784#issuecomment-68117241
1428 private void copy2(string source, string target) { std.file.copy(source, target); }
1429 
1430 /// Copy a file, or replace an existing file's contents
1431 /// with another file's, atomically.
1432 alias atomic!copy2 atomicCopy;
1433 
1434 unittest
1435 {
1436 	enum fn = "cached.tmp";
1437 	scope(exit) if (fn.exists) fn.remove();
1438 
1439 	cached!touch(fn);
1440 	assert(fn.exists);
1441 
1442 	std.file.write(fn, "test");
1443 
1444 	cachedDg!0(&writeProxy, fn, "test2");
1445 	assert(fn.readText() == "test");
1446 }
1447 
1448 // ****************************************************************************
1449 
1450 template withTarget(alias targetGen, alias fun)
1451 {
1452 	auto withTarget(Args...)(auto ref Args args)
1453 	{
1454 		auto target = targetGen(args);
1455 		fun(args, target);
1456 		return target;
1457 	}
1458 }
1459 
1460 /// Two-argument buildPath with reversed arguments.
1461 /// Useful for UFCS chaining.
1462 string prependPath(string target, string path)
1463 {
1464 	return buildPath(path, target);
1465 }