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