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