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 <ae@cy.md>
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.range.primitives;
24 import std.stdio : File;
25 import std.string;
26 import std.typecons;
27 import std.utf;
28 
29 import ae.sys.cmd : getCurrentThreadID;
30 import ae.utils.path;
31 
32 public import std.typecons : No, Yes;
33 
34 deprecated alias wcscmp = core.stdc.wchar_.wcscmp;
35 deprecated alias wcslen = core.stdc.wchar_.wcslen;
36 
37 version(Windows) import ae.sys.windows.imports;
38 
39 // ************************************************************************
40 
41 version (Windows)
42 {
43 	// Work around std.file overload
44 	mixin(importWin32!(q{winnt}, null, q{FILE_ATTRIBUTE_DIRECTORY, FILE_ATTRIBUTE_REPARSE_POINT}));
45 }
46 version (Posix)
47 {
48 	private import core.stdc.errno;
49 	private import core.sys.posix.dirent;
50 	private import core.stdc.string;
51 }
52 
53 // ************************************************************************
54 
55 deprecated string[] fastListDir(bool recursive = false, bool symlinks=false)(string pathname, string pattern = null)
56 {
57 	string[] result;
58 
59 	listDir!((e) {
60 		static if (!symlinks)
61 		{
62 			// Note: shouldn't this just skip recursion?
63 			if (e.isSymlink)
64 				return;
65 		}
66 
67 		if (pattern && !globMatch(e.baseName, pattern))
68 			return;
69 
70 		static if (recursive)
71 		{
72 			if (e.entryIsDir)
73 			{
74 				// Note: why exclude directories from results?
75 				e.recurse();
76 				return;
77 			}
78 		}
79 
80 		result ~= e.fullName;
81 	})(pathname);
82 	return result;
83 }
84 
85 // ************************************************************************
86 
87 version (Windows)
88 {
89 	mixin(importWin32!(q{winnt}, null, q{WCHAR}));
90 	mixin(importWin32!(q{winbase}, null, q{WIN32_FIND_DATAW}));
91 }
92 
93 /// The OS's "native" filesystem character type (private in Phobos).
94 version (Windows)
95 	alias FSChar = WCHAR;
96 else version (Posix)
97 	alias FSChar = char;
98 else
99 	static assert(0);
100 
101 /// Reads a time field from a stat_t with full precision (private in Phobos).
102 SysTime statTimeToStdTime(string which)(ref const stat_t statbuf)
103 {
104 	auto unixTime = mixin(`statbuf.st_` ~ which ~ `time`);
105 	auto stdTime = unixTimeToStdTime(unixTime);
106 
107 	static if (is(typeof(mixin(`statbuf.st_` ~ which ~ `tim`))))
108 		stdTime += mixin(`statbuf.st_` ~ which ~ `tim.tv_nsec`) / 100;
109 	else
110 	static if (is(typeof(mixin(`statbuf.st_` ~ which ~ `timensec`))))
111 		stdTime += mixin(`statbuf.st_` ~ which ~ `timensec`) / 100;
112 	else
113 	static if (is(typeof(mixin(`statbuf.st_` ~ which ~ `time_nsec`))))
114 		stdTime += mixin(`statbuf.st_` ~ which ~ `time_nsec`) / 100;
115 	else
116 	static if (is(typeof(mixin(`statbuf.__st_` ~ which ~ `timensec`))))
117 		stdTime += mixin(`statbuf.__st_` ~ which ~ `timensec`) / 100;
118 
119 	return SysTime(stdTime);
120 }
121 
122 version (OSX)
123     version = Darwin;
124 else version (iOS)
125     version = Darwin;
126 else version (TVOS)
127     version = Darwin;
128 else version (WatchOS)
129     version = Darwin;
130 
131 private
132 version (Posix)
133 {
134 	// TODO: upstream into Druntime
135 	extern (C)
136 	{
137 		int dirfd(DIR *dirp) pure nothrow @nogc;
138 		int openat(int fd, const char *path, int oflag, ...) nothrow @nogc;
139 
140 		version (Darwin)
141 		{
142 			pragma(mangle, "fstatat$INODE64")
143 			int fstatat(int fd, const char *path, stat_t *buf, int flag) nothrow @nogc;
144 
145 			pragma(mangle, "fdopendir$INODE64")
146 			DIR *fdopendir(int fd) nothrow @nogc;
147 		}
148 		else
149 		{
150 			int fstatat(int fd, const(char)* path, stat_t* buf, int flag) nothrow @nogc;
151 			DIR *fdopendir(int fd) nothrow @nogc;
152 		}
153 	}
154 	version (linux)
155 	{
156 		enum AT_SYMLINK_NOFOLLOW = 0x100;
157 		enum O_DIRECTORY = 0x10000;
158 	}
159 	version (Darwin)
160 	{
161 		enum AT_SYMLINK_NOFOLLOW = 0x20;
162 		enum O_DIRECTORY = 0x100000;
163 	}
164 	version (FreeBSD)
165 	{
166 		enum AT_SYMLINK_NOFOLLOW = 0x200;
167 		enum O_DIRECTORY = 0x20000;
168 	}
169 }
170 
171 import ae.utils.range : nullTerminated;
172 
173 // https://issues.dlang.org/show_bug.cgi?id=7016
174 version (Windows) static import ae.sys.windows.misc;
175 
176 /**
177    Fast templated directory iterator
178 
179    Example:
180    ---
181    string[] entries;
182    listDir!((e) {
183 	   entries ~= e.fullName.relPath(tmpDir);
184 	   if (e.entryIsDir)
185 		   e.recurse();
186    })(tmpDir);
187    ---
188 */
189 template listDir(alias handler)
190 {
191 private: // (This is an eponymous template, so this is to aid documentation generators.)
192 	/*non-static*/ struct Context
193 	{
194 		// Tether to handler alias context
195 		void callHandler(Entry* e) { handler(e); }
196 
197 		bool timeToStop = false;
198 
199 		FSChar[] pathBuf;
200 	}
201 
202 	/// A pointer to this type will be passed to the `listDir` predicate.
203 	public static struct Entry
204 	{
205 		version (Posix)
206 		{
207 			dirent* ent; /// POSIX `dirent`.
208 
209 			private stat_t[enumLength!StatTarget] statBuf;
210 
211 			/// Result of `stat` call.
212 			/// Other values are the same as `errno`.
213 			enum StatResult : int
214 			{
215 				noInfo       =       0, /// Not called yet.
216 				statOK       = int.max, /// All OK
217 				unknownError = int.min, /// `errno` returned 0 or `int.max`
218 			}
219 
220 			int dirFD; /// POSIX directory file descriptor.
221 		}
222 		version (Windows)
223 		{
224 			WIN32_FIND_DATAW findData; /// Windows `WIN32_FIND_DATAW`.
225 		}
226 
227 		// Cached values.
228 		// Cleared (memset to 0) for every directory entry.
229 		struct Data
230 		{
231 			FSChar[] baseNameFS;
232 			string baseName;
233 			string fullName;
234 			size_t pathTailPos;
235 
236 			version (Posix)
237 			{
238 				StatResult[enumLength!StatTarget] statResult;
239 			}
240 		}
241 		Data data;
242 
243 		// Recursion
244 
245 		Entry* parent; ///
246 		private Context* context;
247 
248 		/// Request recursion on the current `entry`.
249 		version (Posix)
250 		{
251 			void recurse()
252 			{
253 				import core.sys.posix.fcntl;
254 				int flags = O_RDONLY;
255 				static if (is(typeof(O_DIRECTORY)))
256 					flags |= O_DIRECTORY;
257 				auto fd = openat(dirFD, this.ent.d_name.ptr, flags);
258 				errnoEnforce(fd >= 0,
259 					"Failed to open %s as subdirectory of directory %s"
260 					.format(this.baseNameFS, this.parent.fullName));
261 				auto subdir = fdopendir(fd);
262 				errnoEnforce(subdir,
263 					"Failed to open subdirectory %s of directory %s as directory"
264 					.format(this.baseNameFS, this.parent.fullName));
265 				scan(subdir, fd, &this);
266 			}
267 		}
268 		else
269 		version (Windows)
270 		{
271 			void recurse()
272 			{
273 				needFullPath();
274 				appendString(context.pathBuf,
275 					data.pathTailPos, "\\*.*\0"w);
276 				scan(&this);
277 			}
278 		}
279 
280 		/// Stop iteration.
281 		void stop() { context.timeToStop = true; }
282 
283 		// Name
284 
285 		/// Returns a pointer to the base file name, as a
286 		/// null-terminated string, in the operating system's
287 		/// character type.  Fastest.
288 		const(FSChar)* baseNameFSPtr() pure nothrow @nogc
289 		{
290 			version (Posix) return ent.d_name.ptr;
291 			version (Windows) return findData.cFileName.ptr;
292 		}
293 
294 		// Bounded variant of std.string.fromStringz for static arrays.
295 		private static T[] fromStringz(T, size_t n)(ref T[n] buf)
296 		{
297 			foreach (i, c; buf)
298 				if (!c)
299 					return buf[0 .. i];
300 			// This should only happen in case of an OS / libc bug.
301 			assert(false, "File name buffer is not null-terminated");
302 		}
303 
304 		/// Returns the base file name, as a D character array, in the
305 		/// operating system's character type.  Fast.
306 		const(FSChar)[] baseNameFS() pure nothrow @nogc
307 		{
308 			if (!data.baseNameFS)
309 			{
310 				version (Posix) data.baseNameFS = fromStringz(ent.d_name);
311 				version (Windows) data.baseNameFS = fromStringz(findData.cFileName);
312 			}
313 			return data.baseNameFS;
314 		}
315 
316 		/// Returns the base file name as a D string.  Allocates.
317 		string baseName() // allocates
318 		{
319 			if (!data.baseName)
320 				data.baseName = baseNameFS.to!string;
321 			return data.baseName;
322 		}
323 
324 		private void needFullPath() nothrow @nogc
325 		{
326 			if (!data.pathTailPos)
327 			{
328 				version (Posix)
329 					parent.needFullPath();
330 				version (Windows)
331 				{
332 					// directory separator was added during recursion
333 					auto startPos = parent.data.pathTailPos + 1;
334 				}
335 				version (Posix)
336 				{
337 					immutable FSChar[] separator = "/";
338 					auto startPos = appendString(context.pathBuf,
339 						parent.data.pathTailPos, separator);
340 				}
341 				data.pathTailPos = appendString(context.pathBuf,
342 					startPos,
343 					baseNameFSPtr.nullTerminated
344 				);
345 			}
346 		}
347 
348 		/// Returns the full file name, as a D character array, in the
349 		/// operating system's character type.  Fast.
350 		const(FSChar)[] fullNameFS() nothrow @nogc // fast
351 		{
352 			needFullPath();
353 			return context.pathBuf[0 .. data.pathTailPos];
354 		}
355 
356 		/// Returns the full file name as a D string.  Allocates.
357 		string fullName() // allocates
358 		{
359 			if (!data.fullName)
360 				data.fullName = fullNameFS.to!string;
361 			return data.fullName;
362 		}
363 
364 		// Attributes
365 
366 		version (Posix)
367 		{
368 			/// We can stat two different things on POSIX: the directory entry itself,
369 			/// or the link target (if the directory entry is a symbolic link).
370 			enum StatTarget
371 			{
372 				dirEntry,   /// do not dereference (lstat)
373 				linkTarget, /// dereference
374 			}
375 
376 			private bool tryStat(StatTarget target)() nothrow @nogc
377 			{
378 				if (data.statResult[target] == StatResult.noInfo)
379 				{
380 					// If we already did the other kind of stat, can we reuse its result?
381 					if (data.statResult[1 - target] != StatResult.noInfo)
382 					{
383 						// Yes, if we know this isn't a link from the directory entry.
384 						static if (__traits(compiles, ent.d_type))
385 							if (ent.d_type != DT_UNKNOWN && ent.d_type != DT_LNK)
386 								goto reuse;
387 						// Yes, if we already found out this isn't a link from an lstat call.
388 						static if (target == StatTarget.linkTarget)
389 							if (data.statResult[StatTarget.dirEntry] == StatResult.statOK
390 								&& (statBuf[StatTarget.dirEntry].st_mode & S_IFMT) != S_IFLNK)
391 								goto reuse;
392 					}
393 
394 					if (false)
395 					{
396 					reuse:
397 						statBuf[target] = statBuf[1 - target];
398 						data.statResult[target] = data.statResult[1 - target];
399 					}
400 					else
401 					{
402 						int flags = target == StatTarget.dirEntry ? AT_SYMLINK_NOFOLLOW : 0;
403 						auto res = fstatat(dirFD, ent.d_name.ptr, &statBuf[target], flags);
404 						if (res)
405 						{
406 							auto error = errno;
407 							data.statResult[target] = cast(StatResult)error;
408 							if (error == StatResult.noInfo || error == StatResult.statOK)
409 								data.statResult[target] = StatResult.unknownError; // unknown error?
410 						}
411 						else
412 							data.statResult[target] = StatResult.statOK; // no error
413 					}
414 				}
415 				return data.statResult[target] == StatResult.statOK;
416 			}
417 
418 			private ErrnoException statError(StatTarget target)()
419 			{
420 				errno = data.statResult[target];
421 				return new ErrnoException("Failed to stat " ~
422 					(target == StatTarget.linkTarget ? "link target" : "directory entry") ~
423 					": " ~ fullName);
424 			}
425 
426 			/// Return the result of `stat` / `lstat` (depending on `target`)
427 			/// for this `Entry`, performing it first if necessary.
428 			stat_t* needStat(StatTarget target)()
429 			{
430 				if (!tryStat!target)
431 					throw statError!target();
432 				return &statBuf[target];
433 			}
434 
435 			// Check if this is an object of the given type.
436 			private bool deIsType(typeof(DT_REG) dType, typeof(S_IFREG) statType)
437 			{
438 				static if (__traits(compiles, ent.d_type))
439 					if (ent.d_type != DT_UNKNOWN)
440 						return ent.d_type == dType;
441 
442 				return (needStat!(StatTarget.dirEntry)().st_mode & S_IFMT) == statType;
443 			}
444 
445 			/// Returns true if this is a symlink.
446 			@property bool isSymlink()
447 			{
448 				return deIsType(DT_LNK, S_IFLNK);
449 			}
450 
451 			/// Returns true if this is a directory.
452 			/// You probably want to use this one to decide whether to recurse.
453 			@property bool entryIsDir()
454 			{
455 				return deIsType(DT_DIR, S_IFDIR);
456 			}
457 
458 			// Check if this is an object of the given type, or a link pointing to one.
459 			private bool ltIsType(typeof(DT_REG) dType, typeof(S_IFREG) statType)
460 			{
461 				static if (__traits(compiles, ent.d_type))
462 					if (ent.d_type != DT_UNKNOWN && ent.d_type != DT_LNK)
463 						return ent.d_type == dType;
464 
465 				if (tryStat!(StatTarget.linkTarget)())
466 					return (statBuf[StatTarget.linkTarget].st_mode & S_IFMT) == statType;
467 
468 				if (isSymlink()) // broken symlink?
469 					return false; // a broken symlink does not point at anything.
470 
471 				throw statError!(StatTarget.linkTarget)();
472 			}
473 
474 			/// Returns true if this is a file, or a link pointing to one.
475 			@property bool isFile()
476 			{
477 				return ltIsType(DT_REG, S_IFREG);
478 			}
479 
480 			/// Returns true if this is a directory, or a link pointing to one.
481 			@property bool isDir()
482 			{
483 				return ltIsType(DT_DIR, S_IFDIR);
484 			}
485 
486 			/// Returns the raw POSIX attributes of this directory entry.
487 			@property uint attributes()
488 			{
489 				return needStat!(StatTarget.linkTarget)().st_mode;
490 			}
491 
492 			/// Returns the raw POSIX attributes of this directory entry,
493 			/// or the link target if this directory entry is a symlink.
494 			@property uint linkAttributes()
495 			{
496 				return needStat!(StatTarget.dirEntry)().st_mode;
497 			}
498 
499 			// Other attributes
500 
501 			/// Returns the "c" time of this directory entry,
502 			/// or the link target if this directory entry is a symlink.
503 			@property SysTime timeStatusChanged()
504 			{
505 				return statTimeToStdTime!"c"(*needStat!(StatTarget.linkTarget)());
506 			}
507 
508 			/// Returns the "a" time of this directory entry,
509 			/// or the link target if this directory entry is a symlink.
510 			@property SysTime timeLastAccessed()
511 			{
512 				return statTimeToStdTime!"a"(*needStat!(StatTarget.linkTarget)());
513 			}
514 
515 			/// Returns the "m" time of this directory entry,
516 			/// or the link target if this directory entry is a symlink.
517 			@property SysTime timeLastModified()
518 			{
519 				return statTimeToStdTime!"m"(*needStat!(StatTarget.linkTarget)());
520 			}
521 
522 			/// Returns the "birth" time of this directory entry,
523 			/// or the link target if this directory entry is a symlink.
524 			static if (is(typeof(&statTimeToStdTime!"birth")))
525 			@property SysTime timeCreated()
526 			{
527 				return statTimeToStdTime!"birth"(*needStat!(StatTarget.linkTarget)());
528 			}
529 
530 			/// Returns the size in bytes of this directory entry,
531 			/// or the link target if this directory entry is a symlink.
532 			@property ulong size()
533 			{
534 				return needStat!(StatTarget.linkTarget)().st_size;
535 			}
536 
537 			/// Returns the inode number of this directory entry,
538 			/// or the link target if this directory entry is a symlink.
539 			@property ulong fileID()
540 			{
541 				static if (__traits(compiles, ent.d_ino))
542 					return ent.d_ino;
543 				else
544 					return needStat!(StatTarget.linkTarget)().st_ino;
545 			}
546 		}
547 
548 		version (Windows)
549 		{
550 			/// Returns true if this is a directory, or a reparse point pointing to one.
551 			@property bool isDir() const pure nothrow
552 			{
553 				return (findData.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY) != 0;
554 			}
555 
556 			/// Returns true if this is a file, or a reparse point pointing to one.
557 			@property bool isFile() const pure nothrow
558 			{
559 				return !isDir;
560 			}
561 
562 			/// Returns true if this is a reparse point.
563 			@property bool isSymlink() const pure nothrow
564 			{
565 				return (findData.dwFileAttributes & FILE_ATTRIBUTE_REPARSE_POINT) != 0;
566 			}
567 
568 			/// Returns true if this is a directory.
569 			/// You probably want to use this one to decide whether to recurse.
570 			@property bool entryIsDir() const pure nothrow
571 			{
572 				return isDir && !isSymlink;
573 			}
574 
575 			/// Returns the raw Windows attributes of this directory entry.
576 			@property uint attributes() const pure nothrow
577 			{
578 				return findData.dwFileAttributes;
579 			}
580 
581 			/// Returns the size in bytes of this directory entry.
582 			@property ulong size() const pure nothrow
583 			{
584 				return makeUlong(findData.nFileSizeLow, findData.nFileSizeHigh);
585 			}
586 
587 			/// Returns the creation time of this directory entry.
588 			@property SysTime timeCreated() const
589 			{
590 				return FILETIMEToSysTime(&findData.ftCreationTime);
591 			}
592 
593 			/// Returns the last access time of this directory entry.
594 			@property SysTime timeLastAccessed() const
595 			{
596 				return FILETIMEToSysTime(&findData.ftLastAccessTime);
597 			}
598 
599 			/// Returns the last modification time of this directory entry.
600 			@property SysTime timeLastModified() const
601 			{
602 				return FILETIMEToSysTime(&findData.ftLastWriteTime);
603 			}
604 
605 			/// Returns the 64-bit unique file index of this file.
606 			@property ulong fileID()
607 			{
608 				return getFileID(fullName);
609 			}
610 		}
611 	}
612 
613 	version (Posix)
614 	{
615 		// The length of the buffer on the stack.
616 		enum initialPathBufLength = 256;
617 
618 		private static void scan(DIR* dir, int dirFD, Entry* parentEntry)
619 		{
620 			Entry entry = void;
621 			entry.parent = parentEntry;
622 			entry.context = entry.parent.context;
623 			entry.dirFD = dirFD;
624 
625 			scope(exit) closedir(dir);
626 
627 			dirent* ent;
628 			while ((ent = readdir(dir)) != null)
629 			{
630 				// Apparently happens on some OS X versions.
631 				enforce(ent.d_name[0],
632 					"Empty dir entry name (OS bug?)");
633 
634 				// Skip "." and ".."
635 				if (ent.d_name[0] == '.' && (
636 						ent.d_name[1] == 0 ||
637 						(ent.d_name[1] == '.' && ent.d_name[2] == 0)))
638 					continue;
639 
640 				entry.ent = ent;
641 				entry.data = Entry.Data.init;
642 				entry.context.callHandler(&entry);
643 				if (entry.context.timeToStop)
644 					break;
645 			}
646 		}
647 	}
648 
649 	enum isPath(Path) = (isForwardRange!Path || isSomeString!Path) &&
650 		isSomeChar!(ElementEncodingType!Path);
651 
652 	import core.stdc.stdlib : malloc, realloc, free;
653 
654 	static FSChar[] reallocPathBuf(FSChar[] buf, size_t newLength) nothrow @nogc
655 	{
656 		if (buf.length == initialPathBufLength) // current buffer is on stack
657 		{
658 			auto ptr = cast(FSChar*) malloc(newLength * FSChar.sizeof);
659 			ptr[0 .. buf.length] = buf[];
660 			return ptr[0 .. newLength];
661 		}
662 		else // current buffer on C heap (malloc'd above)
663 		{
664 			auto ptr = cast(FSChar*) realloc(buf.ptr, newLength * FSChar.sizeof);
665 			return ptr[0 .. newLength];
666 		}
667 	}
668 
669 	// Append a string to the buffer, reallocating as necessary.
670 	// Returns the new length of the string in the buffer.
671 	static size_t appendString(Str)(ref FSChar[] buf, size_t pos, Str str) nothrow @nogc
672 	if (isPath!Str)
673 	{
674 		static if (ElementEncodingType!Str.sizeof == FSChar.sizeof
675 			&& is(typeof(str.length)))
676 		{
677 			// No transcoding needed and length known
678 			auto remainingSpace = buf.length - pos;
679 			if (str.length > remainingSpace)
680 				buf = reallocPathBuf(buf, (pos + str.length) * 3 / 2);
681 			buf[pos .. pos + str.length] = str[];
682 			pos += str.length;
683 		}
684 		else
685 		{
686 			// Need to transcode
687 			auto p = buf.ptr + pos;
688 			auto bufEnd = buf.ptr + buf.length;
689 			foreach (c; byUTF!FSChar(str))
690 			{
691 				if (p == bufEnd) // out of room
692 				{
693 					auto newBuf = reallocPathBuf(buf, buf.length * 3 / 2);
694 
695 					// Update pointers to point into the new buffer.
696 					p = newBuf.ptr + (p - buf.ptr);
697 					buf = newBuf;
698 					bufEnd = buf.ptr + buf.length;
699 				}
700 				*p++ = c;
701 			}
702 			pos = p - buf.ptr;
703 		}
704 		return pos;
705 	}
706 
707 	version (Windows)
708 	{
709 		mixin(importWin32!(q{winbase}));
710 		import ae.sys.windows.misc : makeUlong;
711 
712 		// The length of the buffer on the stack.
713 		enum initialPathBufLength = MAX_PATH;
714 
715 		enum FIND_FIRST_EX_LARGE_FETCH = 2;
716 		enum FindExInfoBasic = cast(FINDEX_INFO_LEVELS)1;
717 
718 		static void scan(Entry* parentEntry)
719 		{
720 			Entry entry = void;
721 			entry.parent = parentEntry;
722 			entry.context = parentEntry.context;
723 
724 			HANDLE hFind = FindFirstFileExW(
725 				entry.context.pathBuf.ptr,
726 				FindExInfoBasic,
727 				&entry.findData,
728 				FINDEX_SEARCH_OPS.FindExSearchNameMatch,
729 				null,
730 				FIND_FIRST_EX_LARGE_FETCH, // https://blogs.msdn.microsoft.com/oldnewthing/20131024-00/?p=2843
731 			);
732 			if (hFind == INVALID_HANDLE_VALUE)
733 				throw new WindowsException(GetLastError(),
734 					text("FindFirstFileW: ", parentEntry.fullNameFS));
735 			scope(exit) FindClose(hFind);
736 			do
737 			{
738 				// Skip "." and ".."
739 				auto fn = entry.findData.cFileName.ptr;
740 				if (fn[0] == '.' && (
741 						fn[1] == 0 ||
742 						(fn[1] == '.' && fn[2] == 0)))
743 					continue;
744 
745 				entry.data = Entry.Data.init;
746 				entry.context.callHandler(&entry);
747 				if (entry.context.timeToStop)
748 					break;
749 			}
750 			while (FindNextFileW(hFind, &entry.findData));
751 			if (GetLastError() != ERROR_NO_MORE_FILES)
752 				throw new WindowsException(GetLastError(),
753 					text("FindNextFileW: ", parentEntry.fullNameFS));
754 		}
755 	}
756 
757 	public void listDir(Path)(Path dirPath)
758 	if (isPath!Path)
759 	{
760 		import std.internal.cstring;
761 
762 		if (dirPath.empty)
763 			return listDir(".");
764 
765 		Context context;
766 
767 		FSChar[initialPathBufLength] pathBufStore = void;
768 		context.pathBuf = pathBufStore[];
769 
770 		scope (exit)
771 		{
772 			if (context.pathBuf.length != initialPathBufLength)
773 				free(context.pathBuf.ptr);
774 		}
775 
776 		Entry rootEntry = void;
777 		rootEntry.context = &context;
778 
779 		auto endPos = appendString(context.pathBuf, 0, dirPath);
780 		rootEntry.data.pathTailPos = endPos - (endPos > 0 && context.pathBuf[endPos - 1].isDirSeparator() ? 1 : 0);
781 		assert(rootEntry.data.pathTailPos > 0);
782 
783 		version (Posix)
784 		{
785 			auto dir = opendir(tempCString(dirPath));
786 			checkDir(dir, dirPath);
787 
788 			scan(dir, dirfd(dir), &rootEntry);
789 		}
790 		else
791 		version (Windows)
792 		{
793 			const WCHAR[] tailString = endPos == 0 || context.pathBuf[endPos - 1].isDirSeparator() ? "*.*\0"w : "\\*.*\0"w;
794 			appendString(context.pathBuf, endPos, tailString);
795 
796 			scan(&rootEntry);
797 		}
798 	}
799 
800 	// Workaround for https://github.com/ldc-developers/ldc/issues/2960
801 	version (Posix)
802 	private void checkDir(Path)(DIR* dir, auto ref Path dirPath)
803 	{
804 		errnoEnforce(dir, "Failed to open directory " ~ dirPath);
805 	}
806 }
807 
808 unittest
809 {
810 	auto tmpDir = deleteme ~ "-dir";
811 	if (tmpDir.exists) tmpDir.removeRecurse();
812 	mkdirRecurse(tmpDir);
813 	scope(exit) rmdirRecurse(tmpDir);
814 
815 	touch(tmpDir ~ "/a");
816 	touch(tmpDir ~ "/b");
817 	mkdir(tmpDir ~ "/c");
818 	touch(tmpDir ~ "/c/1");
819 	touch(tmpDir ~ "/c/2");
820 
821 	string[] entries;
822 	listDir!((e) {
823 		assert(equal(e.fullNameFS, e.fullName));
824 		entries ~= e.fullName.relPath(tmpDir);
825 		if (e.entryIsDir)
826 			e.recurse();
827 	})(tmpDir);
828 
829 	assert(equal(
830 		entries.sort,
831 		["a", "b", "c", "c/1", "c/2"].map!(name => name.replace("/", dirSeparator)),
832 	), text(entries));
833 
834 	entries = null;
835 	import std.ascii : isDigit;
836 	listDir!((e) {
837 		entries ~= e.fullName.relPath(tmpDir);
838 		if (e.baseNameFS[0].isDigit)
839 			e.stop();
840 		else
841 		if (e.entryIsDir)
842 			e.recurse();
843 	})(tmpDir);
844 
845 	assert(entries.length < 5 && entries[$-1][$-1].isDigit, text(entries));
846 
847 	// Symlink test
848 	(){
849 		// Wine's implementation of symlinks/junctions is incomplete
850 		version (Windows)
851 			if (getWineVersion())
852 				return;
853 
854 		dirLink("c", tmpDir ~ "/d");
855 		dirLink("x", tmpDir ~ "/e");
856 
857 		string[] entries;
858 		listDir!((e) {
859 			entries ~= e.fullName.relPath(tmpDir);
860 			if (e.entryIsDir)
861 				e.recurse();
862 		})(tmpDir);
863 
864 		assert(equal(
865 			entries.sort,
866 			["a", "b", "c", "c/1", "c/2", "d", "e"].map!(name => name.replace("/", dirSeparator)),
867 		));
868 
869 		// Recurse into symlinks
870 
871 		entries = null;
872 		listDir!((e) {
873 			entries ~= e.fullName.relPath(tmpDir);
874 			if (e.isDir)
875 				try
876 					e.recurse();
877 				catch (Exception e) // broken junctions on Windows throw
878 					{}
879 		})(tmpDir);
880 
881 		assert(equal(
882 			entries.sort,
883 			["a", "b", "c", "c/1", "c/2", "d", "d/1", "d/2", "e"].map!(name => name.replace("/", dirSeparator)),
884 		));
885 	}();
886 }
887 
888 // ************************************************************************
889 
890 private string buildPath2(string[] segments...) { return segments.length ? buildPath(segments) : null; }
891 
892 /// Shell-like expansion of ?, * and ** in path components
893 DirEntry[] fileList(string pattern)
894 {
895 	auto components = cast(string[])array(pathSplitter(pattern));
896 	foreach (i, component; components[0..$-1])
897 		if (component.contains("?") || component.contains("*")) // TODO: escape?
898 		{
899 			DirEntry[] expansions; // TODO: filter range instead?
900 			auto dir = buildPath2(components[0..i]);
901 			if (component == "**")
902 				expansions = array(dirEntries(dir, SpanMode.depth));
903 			else
904 				expansions = array(dirEntries(dir, component, SpanMode.shallow));
905 
906 			DirEntry[] result;
907 			foreach (expansion; expansions)
908 				if (expansion.isDir())
909 					result ~= fileList(buildPath(expansion.name ~ components[i+1..$]));
910 			return result;
911 		}
912 
913 	auto dir = buildPath2(components[0..$-1]);
914 	if (!dir || exists(dir))
915 		return array(dirEntries(dir, components[$-1], SpanMode.shallow));
916 	else
917 		return null;
918 }
919 
920 /// ditto
921 DirEntry[] fileList(string pattern0, string[] patterns...)
922 {
923 	DirEntry[] result;
924 	foreach (pattern; [pattern0] ~ patterns)
925 		result ~= fileList(pattern);
926 	return result;
927 }
928 
929 /// ditto
930 deprecated string[] fastFileList(string pattern)
931 {
932 	auto components = cast(string[])array(pathSplitter(pattern));
933 	foreach (i, component; components[0..$-1])
934 		if (component.contains("?") || component.contains("*")) // TODO: escape?
935 		{
936 			string[] expansions; // TODO: filter range instead?
937 			auto dir = buildPath2(components[0..i]);
938 			if (component == "**")
939 				expansions = fastListDir!true(dir);
940 			else
941 				expansions = fastListDir(dir, component);
942 
943 			string[] result;
944 			foreach (expansion; expansions)
945 				if (expansion.isDir())
946 					result ~= fastFileList(buildPath(expansion ~ components[i+1..$]));
947 			return result;
948 		}
949 
950 	auto dir = buildPath2(components[0..$-1]);
951 	if (!dir || exists(dir))
952 		return fastListDir(dir, components[$-1]);
953 	else
954 		return null;
955 }
956 
957 /// ditto
958 deprecated string[] fastFileList(string pattern0, string[] patterns...)
959 {
960 	string[] result;
961 	foreach (pattern; [pattern0] ~ patterns)
962 		result ~= fastFileList(pattern);
963 	return result;
964 }
965 
966 // ************************************************************************
967 
968 import std.datetime;
969 import std.exception;
970 
971 deprecated SysTime getMTime(string name)
972 {
973 	return timeLastModified(name);
974 }
975 
976 /// If target exists, update its modification time;
977 /// otherwise create it as an empty file.
978 void touch(in char[] target)
979 {
980 	if (exists(target))
981 	{
982 		auto now = Clock.currTime();
983 		setTimes(target, now, now);
984 	}
985 	else
986 		std.file.write(target, "");
987 }
988 
989 /// Returns true if the target file doesn't exist,
990 /// or source is newer than the target.
991 bool newerThan(string source, string target)
992 {
993 	if (!target.exists)
994 		return true;
995 	return source.timeLastModified() > target.timeLastModified();
996 }
997 
998 /// Returns true if the target file doesn't exist,
999 /// or any of the sources are newer than the target.
1000 bool anyNewerThan(string[] sources, string target)
1001 {
1002 	if (!target.exists)
1003 		return true;
1004 	auto targetTime = target.timeLastModified();
1005 	return sources.any!(source => source.timeLastModified() > targetTime)();
1006 }
1007 
1008 version (Posix)
1009 {
1010 	import core.sys.posix.sys.stat;
1011 	import core.sys.posix.unistd;
1012 
1013 	/// Get the ID of the user owning this file.
1014 	int getOwner(string fn)
1015 	{
1016 		stat_t s;
1017 		errnoEnforce(stat(toStringz(fn), &s) == 0, "stat: " ~ fn);
1018 		return s.st_uid;
1019 	}
1020 
1021 	/// Get the ID of the group owning this file.
1022 	int getGroup(string fn)
1023 	{
1024 		stat_t s;
1025 		errnoEnforce(stat(toStringz(fn), &s) == 0, "stat: " ~ fn);
1026 		return s.st_gid;
1027 	}
1028 
1029 	/// Set the owner user and group of this file.
1030 	void setOwner(string fn, int uid, int gid)
1031 	{
1032 		errnoEnforce(chown(toStringz(fn), uid, gid) == 0, "chown: " ~ fn);
1033 	}
1034 }
1035 
1036 /// Try to rename; copy/delete if rename fails
1037 void move(string src, string dst)
1038 {
1039 	try
1040 		src.rename(dst);
1041 	catch (Exception e)
1042 	{
1043 		atomicCopy(src, dst);
1044 		src.remove();
1045 	}
1046 }
1047 
1048 /// Make sure that the given directory exists
1049 /// (and create parent directories as necessary).
1050 void ensureDirExists(string path)
1051 {
1052 	if (!path.exists)
1053 		path.mkdirRecurse();
1054 }
1055 
1056 /// Make sure that the path to the given file name
1057 /// exists (and create directories as necessary).
1058 void ensurePathExists(string fn)
1059 {
1060 	fn.dirName.ensureDirExists();
1061 }
1062 
1063 static import core.stdc.errno;
1064 version (Windows)
1065 {
1066 	static import core.sys.windows.winerror;
1067 	static import std.windows.syserror;
1068 	static import ae.sys.windows.exception;
1069 }
1070 
1071 /// Catch common Phobos exception types corresponding to file operations.
1072 bool collectOSError(alias checkCError, alias checkWinError)(scope void delegate() operation)
1073 {
1074 	mixin(() {
1075 		string code = q{
1076 			try
1077 			{
1078 				operation();
1079 				return true;
1080 			}
1081 			catch (FileException e)
1082 			{
1083 				version (Windows)
1084 					bool collect = checkWinError(e.errno);
1085 				else
1086 					bool collect = checkCError(e.errno);
1087 				if (collect)
1088 					return false;
1089 				else
1090 					throw e;
1091 			}
1092 			catch (ErrnoException e)
1093 			{
1094 				if (checkCError(e.errno))
1095 					return false;
1096 				else
1097 					throw e;
1098 			}
1099 		};
1100 		version(Windows) code ~= q{
1101 			catch (std.windows.syserror.WindowsException e)
1102 			{
1103 				if (checkWinError(e.code))
1104 					return false;
1105 				else
1106 					throw e;
1107 			}
1108 			catch (ae.sys.windows.exception.WindowsException e)
1109 			{
1110 				if (checkWinError(e.code))
1111 					return false;
1112 				else
1113 					throw e;
1114 			}
1115 		};
1116 		return code;
1117 	}());
1118 }
1119 
1120 /// Collect a "file not found" error.
1121 alias collectNotFoundError = collectOSError!(
1122 	errno => errno == core.stdc.errno.ENOENT,
1123 	(code) { version(Windows) return
1124 			 code == core.sys.windows.winerror.ERROR_FILE_NOT_FOUND ||
1125 			 code == core.sys.windows.winerror.ERROR_PATH_NOT_FOUND; },
1126 );
1127 
1128 ///
1129 unittest
1130 {
1131 	auto fn = deleteme;
1132 	if (fn.exists) fn.removeRecurse();
1133 	foreach (dg; [
1134 		{ openFile(fn, "rb"); },
1135 		{ mkdir(fn.buildPath("b")); },
1136 		{ hardLink(fn, fn ~ "2"); },
1137 	])
1138 		assert(!dg.collectNotFoundError);
1139 }
1140 
1141 /// Collect a "file already exists" error.
1142 alias collectFileExistsError = collectOSError!(
1143 	errno => errno == core.stdc.errno.EEXIST,
1144 	(code) { version(Windows) return
1145 			 code == core.sys.windows.winerror.ERROR_FILE_EXISTS ||
1146 			 code == core.sys.windows.winerror.ERROR_ALREADY_EXISTS; },
1147 );
1148 
1149 ///
1150 unittest
1151 {
1152 	auto fn = deleteme;
1153 	foreach (dg; [
1154 		{ mkdir(fn); },
1155 		{ openFile(fn, "wxb"); },
1156 		{ touch(fn ~ "2"); hardLink(fn ~ "2", fn); },
1157 	])
1158 	{
1159 		if (fn.exists) fn.removeRecurse();
1160 		assert( dg.collectFileExistsError);
1161 		assert(!dg.collectFileExistsError);
1162 	}
1163 }
1164 
1165 import ae.utils.text;
1166 
1167 /// Forcibly remove a file or directory.
1168 /// If atomic is true, the entire directory is deleted "atomically"
1169 /// (it is first moved/renamed to another location).
1170 /// On Windows, this will move the file/directory out of the way,
1171 /// if it is in use and cannot be deleted (but can be renamed).
1172 void forceDelete(Flag!"atomic" atomic=Yes.atomic)(string fn, Flag!"recursive" recursive = No.recursive)
1173 {
1174 	import std.process : environment;
1175 	version(Windows)
1176 	{
1177 		mixin(importWin32!q{winnt});
1178 		mixin(importWin32!q{winbase});
1179 	}
1180 
1181 	auto name = fn.baseName();
1182 	fn = fn.absolutePath().longPath();
1183 
1184 	version(Windows)
1185 	{
1186 		auto fnW = toUTF16z(fn);
1187 		auto attr = GetFileAttributesW(fnW);
1188 		wenforce(attr != INVALID_FILE_ATTRIBUTES, "GetFileAttributes");
1189 		if (attr & FILE_ATTRIBUTE_READONLY)
1190 			SetFileAttributesW(fnW, attr & ~FILE_ATTRIBUTE_READONLY).wenforce("SetFileAttributes");
1191 	}
1192 
1193 	static if (atomic)
1194 	{
1195 		// To avoid zombifying locked directories, try renaming it first.
1196 		// Attempting to delete a locked directory will make it inaccessible.
1197 
1198 		bool tryMoveTo(string target)
1199 		{
1200 			target = target.longPath();
1201 			if (target.endsWith(dirSeparator))
1202 				target = target[0..$-1];
1203 			if (target.length && !target.exists)
1204 				return false;
1205 
1206 			string newfn;
1207 			do
1208 				newfn = format("%s%sdeleted-%s.%s.%s", target, dirSeparator, name, thisProcessID, randomString());
1209 			while (newfn.exists);
1210 
1211 			version(Windows)
1212 			{
1213 				auto newfnW = toUTF16z(newfn);
1214 				if (!MoveFileW(fnW, newfnW))
1215 					return false;
1216 			}
1217 			else
1218 			{
1219 				try
1220 					rename(fn, newfn);
1221 				catch (FileException e)
1222 					return false;
1223 			}
1224 
1225 			fn = newfn;
1226 			version(Windows) fnW = newfnW;
1227 			return true;
1228 		}
1229 
1230 		void tryMove()
1231 		{
1232 			auto tmp = environment.get("TEMP");
1233 			if (tmp)
1234 				if (tryMoveTo(tmp))
1235 					return;
1236 
1237 			version(Windows)
1238 				string tempDir = fn[0..7]~"Temp";
1239 			else
1240 				enum tempDir = "/tmp";
1241 
1242 			if (tryMoveTo(tempDir))
1243 				return;
1244 
1245 			if (tryMoveTo(fn.dirName()))
1246 				return;
1247 
1248 			throw new Exception("Unable to delete " ~ fn ~ " atomically (all rename attempts failed)");
1249 		}
1250 
1251 		tryMove();
1252 	}
1253 
1254 	version(Windows)
1255 	{
1256 		if (attr & FILE_ATTRIBUTE_DIRECTORY)
1257 		{
1258 			if (recursive && (attr & FILE_ATTRIBUTE_REPARSE_POINT) == 0)
1259 			{
1260 				foreach (de; fn.dirEntries(SpanMode.shallow))
1261 					forceDelete!(No.atomic)(de.name, Yes.recursive);
1262 			}
1263 			// Will fail if !recursive and directory is not empty
1264 			RemoveDirectoryW(fnW).wenforce("RemoveDirectory");
1265 		}
1266 		else
1267 			DeleteFileW(fnW).wenforce("DeleteFile");
1268 	}
1269 	else
1270 	{
1271 		if (recursive)
1272 			fn.removeRecurse();
1273 		else
1274 			if (fn.isDir)
1275 				fn.rmdir();
1276 			else
1277 				fn.remove();
1278 	}
1279 }
1280 
1281 
1282 deprecated void forceDelete(bool atomic)(string fn, bool recursive = false) { forceDelete!(cast(Flag!"atomic")atomic)(fn, cast(Flag!"recursive")recursive); }
1283 //deprecated void forceDelete()(string fn, bool recursive) { forceDelete!(Yes.atomic)(fn, cast(Flag!"recursive")recursive); }
1284 
1285 deprecated unittest
1286 {
1287 	mkdir("testdir"); touch("testdir/b"); forceDelete!(false     )("testdir", true);
1288 	mkdir("testdir"); touch("testdir/b"); forceDelete!(true      )("testdir", true);
1289 }
1290 
1291 unittest
1292 {
1293 	mkdir("testdir"); touch("testdir/b"); forceDelete             ("testdir", Yes.recursive);
1294 	mkdir("testdir"); touch("testdir/b"); forceDelete!(No .atomic)("testdir", Yes.recursive);
1295 	mkdir("testdir"); touch("testdir/b"); forceDelete!(Yes.atomic)("testdir", Yes.recursive);
1296 }
1297 
1298 /// If fn is a directory, delete it recursively.
1299 /// Otherwise, delete the file or symlink fn.
1300 void removeRecurse(string fn)
1301 {
1302 	auto attr = fn.getAttributes();
1303 	if (attr.attrIsSymlink)
1304 	{
1305 		version (Windows)
1306 			if (attr.attrIsDir)
1307 				fn.rmdir();
1308 			else
1309 				fn.remove();
1310 		else
1311 			fn.remove();
1312 	}
1313 	else
1314 	if (attr.attrIsDir)
1315 		version (Windows)
1316 			fn.forceDelete!(No.atomic)(Yes.recursive); // For read-only files
1317 		else
1318 			fn.rmdirRecurse();
1319 	else
1320 		fn.remove();
1321 }
1322 
1323 /// Create an empty directory, deleting
1324 /// all its contents if it already exists.
1325 void recreateEmptyDirectory()(string dir)
1326 {
1327 	if (dir.exists)
1328 		dir.forceDelete(Yes.recursive);
1329 	mkdir(dir);
1330 }
1331 
1332 /// Copy a directory recursively.
1333 void copyRecurse(DirEntry src, string dst)
1334 {
1335 	version (Posix)
1336 		if (src.isSymlink)
1337 			return symlink(dst, readLink(src));
1338 	if (src.isFile)
1339 		return copy(src, dst, PreserveAttributes.yes);
1340 	dst.mkdir();
1341 	foreach (de; src.dirEntries(SpanMode.shallow))
1342 		copyRecurse(de, dst.buildPath(de.baseName));
1343 }
1344 void copyRecurse(string src, string dst) { copyRecurse(DirEntry(src), dst); } /// ditto
1345 
1346 /// Return true if the given file would be hidden from directory listings.
1347 /// Returns true for files starting with `'.'`, and, on Windows, hidden files.
1348 bool isHidden()(string fn)
1349 {
1350 	if (baseName(fn).startsWith("."))
1351 		return true;
1352 	version (Windows)
1353 	{
1354 		mixin(importWin32!q{winnt});
1355 		if (getAttributes(fn) & FILE_ATTRIBUTE_HIDDEN)
1356 			return true;
1357 	}
1358 	return false;
1359 }
1360 
1361 /// Return a file's unique ID.
1362 ulong getFileID()(string fn)
1363 {
1364 	version (Windows)
1365 	{
1366 		mixin(importWin32!q{winnt});
1367 		mixin(importWin32!q{winbase});
1368 
1369 		auto fnW = toUTF16z(fn);
1370 		auto h = CreateFileW(fnW, FILE_READ_ATTRIBUTES, 0, null, OPEN_EXISTING, 0, HANDLE.init);
1371 		wenforce(h!=INVALID_HANDLE_VALUE, fn);
1372 		scope(exit) CloseHandle(h);
1373 		BY_HANDLE_FILE_INFORMATION fi;
1374 		GetFileInformationByHandle(h, &fi).wenforce("GetFileInformationByHandle");
1375 
1376 		ULARGE_INTEGER li;
1377 		li.LowPart  = fi.nFileIndexLow;
1378 		li.HighPart = fi.nFileIndexHigh;
1379 		auto result = li.QuadPart;
1380 		enforce(result, "Null file ID");
1381 		return result;
1382 	}
1383 	else
1384 	{
1385 		return DirEntry(fn).statBuf.st_ino;
1386 	}
1387 }
1388 
1389 unittest
1390 {
1391 	auto base = deleteme;
1392 	touch(base ~ "a");
1393 	scope(exit) remove(base ~ "a");
1394 	hardLink(base ~ "a", base ~ "b");
1395 	scope(exit) remove(base ~ "b");
1396 	touch(base ~ "c");
1397 	scope(exit) remove(base ~ "c");
1398 	assert(getFileID(base ~ "a") == getFileID(base ~ "b"));
1399 	assert(getFileID(base ~ "a") != getFileID(base ~ "c"));
1400 }
1401 
1402 deprecated alias std.file.getSize getSize2;
1403 
1404 /// Using UNC paths bypasses path length limitation when using Windows wide APIs.
1405 string longPath(string s)
1406 {
1407 	version (Windows)
1408 	{
1409 		if (!s.startsWith(`\\`))
1410 			return `\\?\` ~ s.absolutePath().buildNormalizedPath().replace(`/`, `\`);
1411 	}
1412 	return s;
1413 }
1414 
1415 version (Windows)
1416 {
1417 	static if (__traits(compiles, { mixin importWin32!q{winnt}; }))
1418 		static mixin(importWin32!q{winnt});
1419 
1420 	/// Common code for creating Windows reparse points.
1421 	private void createReparsePoint(string reparseBufferName, string extraInitialization, string reparseTagName)(in char[] target, in char[] print, in char[] link)
1422 	{
1423 		mixin(importWin32!q{winbase});
1424 		mixin(importWin32!q{windef});
1425 		mixin(importWin32!q{winioctl});
1426 
1427 		enum SYMLINK_FLAG_RELATIVE = 1;
1428 
1429 		HANDLE hLink = CreateFileW(link.toUTF16z(), GENERIC_READ | GENERIC_WRITE, 0, null, OPEN_EXISTING, FILE_FLAG_OPEN_REPARSE_POINT | FILE_FLAG_BACKUP_SEMANTICS, null);
1430 		wenforce(hLink && hLink != INVALID_HANDLE_VALUE, "CreateFileW");
1431 		scope(exit) CloseHandle(hLink);
1432 
1433 		enum pathOffset =
1434 			mixin(q{REPARSE_DATA_BUFFER..} ~ reparseBufferName)            .offsetof +
1435 			mixin(q{REPARSE_DATA_BUFFER..} ~ reparseBufferName)._PathBuffer.offsetof;
1436 
1437 		auto targetW = target.toUTF16();
1438 		auto printW  = print .toUTF16();
1439 
1440 		// Despite MSDN, two NUL-terminating characters are needed, one for each string.
1441 
1442 		auto pathBufferSize = targetW.length + 1 + printW.length + 1; // in chars
1443 		auto buf = new ubyte[pathOffset + pathBufferSize * WCHAR.sizeof];
1444 		auto r = cast(REPARSE_DATA_BUFFER*)buf.ptr;
1445 
1446 		r.ReparseTag = mixin(reparseTagName);
1447 		r.ReparseDataLength = to!WORD(buf.length - mixin(q{r..} ~ reparseBufferName).offsetof);
1448 
1449 		auto pathBuffer = mixin(q{r..} ~ reparseBufferName).PathBuffer;
1450 		auto p = pathBuffer;
1451 
1452 		mixin(q{r..} ~ reparseBufferName).SubstituteNameOffset = to!WORD((p-pathBuffer) * WCHAR.sizeof);
1453 		mixin(q{r..} ~ reparseBufferName).SubstituteNameLength = to!WORD(targetW.length * WCHAR.sizeof);
1454 		p[0..targetW.length] = targetW;
1455 		p += targetW.length;
1456 		*p++ = 0;
1457 
1458 		mixin(q{r..} ~ reparseBufferName).PrintNameOffset      = to!WORD((p-pathBuffer) * WCHAR.sizeof);
1459 		mixin(q{r..} ~ reparseBufferName).PrintNameLength      = to!WORD(printW .length * WCHAR.sizeof);
1460 		p[0..printW.length] = printW;
1461 		p += printW.length;
1462 		*p++ = 0;
1463 
1464 		assert(p-pathBuffer == pathBufferSize);
1465 
1466 		mixin(extraInitialization);
1467 
1468 		DWORD dwRet; // Needed despite MSDN
1469 		DeviceIoControl(hLink, FSCTL_SET_REPARSE_POINT, buf.ptr, buf.length.to!DWORD(), null, 0, &dwRet, null).wenforce("DeviceIoControl");
1470 	}
1471 
1472 	/// Attempt to acquire the specified privilege.
1473 	void acquirePrivilege(S)(S name)
1474 	{
1475 		mixin(importWin32!q{winbase});
1476 		mixin(importWin32!q{windef});
1477 
1478 		import ae.sys.windows;
1479 
1480 		HANDLE hToken = null;
1481 		wenforce(OpenProcessToken(GetCurrentProcess(), TOKEN_ADJUST_PRIVILEGES, &hToken));
1482 		scope(exit) CloseHandle(hToken);
1483 
1484 		TOKEN_PRIVILEGES tp;
1485 		wenforce(LookupPrivilegeValue(null, name.toUTF16z(), &tp.Privileges[0].Luid), "LookupPrivilegeValue");
1486 
1487 		tp.PrivilegeCount = 1;
1488 		tp.Privileges[0].Attributes = SE_PRIVILEGE_ENABLED;
1489 		wenforce(AdjustTokenPrivileges(hToken, FALSE, &tp, cast(DWORD)TOKEN_PRIVILEGES.sizeof, null, null), "AdjustTokenPrivileges");
1490 	}
1491 
1492 	/// Link a directory.
1493 	/// Uses symlinks on POSIX, and directory junctions on Windows.
1494 	void dirLink()(in char[] original, in char[] link)
1495 	{
1496 		mkdir(link);
1497 		scope(failure) rmdir(link);
1498 
1499 		auto target = `\??\` ~ (cast(string)original).absolutePath((cast(string)link.dirName).absolutePath).buildNormalizedPath;
1500 		if (target[$-1] != '\\')
1501 			target ~= '\\';
1502 
1503 		createReparsePoint!(q{MountPointReparseBuffer}, q{}, q{IO_REPARSE_TAG_MOUNT_POINT})(target, null, link);
1504 	}
1505 
1506 	/// Windows implementation of `std.file.symlink`.
1507 	void symlink()(in char[] original, in char[] link)
1508 	{
1509 		mixin(importWin32!q{winnt});
1510 
1511 		acquirePrivilege(SE_CREATE_SYMBOLIC_LINK_NAME);
1512 
1513 		touch(link);
1514 		scope(failure) remove(link);
1515 
1516 		createReparsePoint!(q{SymbolicLinkReparseBuffer}, q{r.SymbolicLinkReparseBuffer.Flags = link.isAbsolute() ? 0 : SYMLINK_FLAG_RELATIVE;}, q{IO_REPARSE_TAG_SYMLINK})(original, original, link);
1517 	}
1518 }
1519 else
1520 	alias std.file.symlink dirLink; /// `std.file.symlink` is used to implement `dirLink` on POSIX.
1521 
1522 version(Windows) version(unittest) static mixin(importWin32!q{winnt});
1523 
1524 unittest
1525 {
1526 	// Wine's implementation of symlinks/junctions is incomplete
1527 	version (Windows)
1528 		if (getWineVersion())
1529 			return;
1530 
1531 	mkdir("a"); scope(exit) rmdir("a"[]);
1532 	touch("a/f"); scope(exit) remove("a/f");
1533 	dirLink("a", "b"); scope(exit) version(Windows) rmdir("b"); else remove("b");
1534 	//symlink("a/f", "c"); scope(exit) remove("c");
1535 	assert("b".isSymlink());
1536 	//assert("c".isSymlink());
1537 	assert("b/f".exists());
1538 }
1539 
1540 version (Windows)
1541 {
1542 	/// Create a hard link.
1543 	void hardLink()(string src, string dst)
1544 	{
1545 		mixin(importWin32!q{w32api});
1546 
1547 		static assert(_WIN32_WINNT >= 0x501, "CreateHardLinkW not available for target Windows platform. Specify -version=WindowsXP");
1548 
1549 		mixin(importWin32!q{winnt});
1550 		mixin(importWin32!q{winbase});
1551 
1552 		wenforce(CreateHardLinkW(toUTF16z(dst), toUTF16z(src), null), "CreateHardLink failed: " ~ src ~ " -> " ~ dst);
1553 	}
1554 
1555 	/// Deletes a file, which might be a read-only hard link
1556 	/// (thus, deletes the read-only file/link without affecting other links to it).
1557 	void deleteHardLink()(string fn)
1558 	{
1559 		mixin(importWin32!q{winbase});
1560 
1561 		auto fnW = toUTF16z(fn);
1562 
1563 		DWORD attrs = GetFileAttributesW(fnW);
1564 		wenforce(attrs != INVALID_FILE_ATTRIBUTES, "GetFileAttributesW failed: " ~ fn);
1565 
1566 		if (attrs & FILE_ATTRIBUTE_READONLY)
1567 			SetFileAttributesW(fnW, attrs & ~FILE_ATTRIBUTE_READONLY)
1568 			.wenforce("SetFileAttributesW failed: " ~ fn);
1569 		HANDLE h = CreateFileW(fnW, GENERIC_READ|GENERIC_WRITE, 7, null, OPEN_EXISTING,
1570 					FILE_FLAG_DELETE_ON_CLOSE, null);
1571 		wenforce(h != INVALID_HANDLE_VALUE, "CreateFileW failed: " ~ fn);
1572 		if (attrs & FILE_ATTRIBUTE_READONLY)
1573 			SetFileAttributesW(fnW, attrs)
1574 			.wenforce("SetFileAttributesW failed: " ~ fn);
1575 		CloseHandle(h).wenforce("CloseHandle failed: " ~ fn);
1576 	}
1577 }
1578 version (Posix)
1579 {
1580 	/// Create a hard link.
1581 	void hardLink()(string src, string dst)
1582 	{
1583 		import core.sys.posix.unistd;
1584 		errnoEnforce(link(toUTFz!(const char*)(src), toUTFz!(const char*)(dst)) == 0, "link() failed: " ~ dst);
1585 	}
1586 
1587 	alias deleteHardLink = remove; /// `std.file.remove` is used to implement `deleteHardLink` on POSIX.
1588 }
1589 
1590 unittest
1591 {
1592 	write("a", "foo"); scope(exit) remove("a");
1593 	hardLink("a", "b");
1594 	assert("b".readText == "foo");
1595 	deleteHardLink("b");
1596 	assert(!"b".exists);
1597 }
1598 
1599 version (Posix)
1600 {
1601 	/// Wrapper around the C `realpath` function.
1602 	string realPath(string path)
1603 	{
1604 		// TODO: Windows version
1605 		import core.sys.posix.stdlib;
1606 		auto p = realpath(toUTFz!(const char*)(path), null);
1607 		errnoEnforce(p, "realpath");
1608 		string result = fromStringz(p).idup;
1609 		free(p);
1610 		return result;
1611 	}
1612 }
1613 
1614 // /proc/self/mounts parsing
1615 version (linux)
1616 {
1617 	/// A parsed line from /proc/self/mounts.
1618 	struct MountInfo
1619 	{
1620 		string spec; /// device path
1621 		string file; /// mount path
1622 		string vfstype; /// file system
1623 		string mntops; /// options
1624 		int freq; /// dump flag
1625 		int passno; /// fsck order
1626 	}
1627 
1628 	private string unescapeMountString(in char[] s)
1629 	{
1630 		string result;
1631 
1632 		size_t p = 0;
1633 		for (size_t i=0; i+3<s.length;)
1634 		{
1635 			auto c = s[i];
1636 			if (c == '\\')
1637 			{
1638 				result ~= s[p..i];
1639 				result ~= to!int(s[i+1..i+4], 8);
1640 				i += 4;
1641 				p = i;
1642 			}
1643 			else
1644 				i++;
1645 		}
1646 		result ~= s[p..$];
1647 		return result;
1648 	}
1649 
1650 	unittest
1651 	{
1652 		assert(unescapeMountString(`a\040b\040c`) == "a b c");
1653 		assert(unescapeMountString(`\040`) == " ");
1654 	}
1655 
1656 	/// Parse a line from /proc/self/mounts.
1657 	MountInfo parseMountInfo(in char[] line)
1658 	{
1659 		const(char)[][6] parts;
1660 		copy(line.splitter(" "), parts[]);
1661 		return MountInfo(
1662 			unescapeMountString(parts[0]),
1663 			unescapeMountString(parts[1]),
1664 			unescapeMountString(parts[2]),
1665 			unescapeMountString(parts[3]),
1666 			parts[4].to!int,
1667 			parts[5].to!int,
1668 		);
1669 	}
1670 
1671 	/// Returns an iterator of MountInfo structs.
1672 	auto getMounts()
1673 	{
1674 		return File("/proc/self/mounts", "rb").byLine().map!parseMountInfo();
1675 	}
1676 
1677 	/// Get MountInfo with longest mount point matching path.
1678 	/// Returns MountInfo.init if none match.
1679 	MountInfo getPathMountInfo(string path)
1680 	{
1681 		path = realPath(path);
1682 		size_t bestLength; MountInfo bestInfo;
1683 		foreach (ref info; getMounts())
1684 		{
1685 			if (path.pathStartsWith(info.file))
1686 			{
1687 				if (bestLength < info.file.length)
1688 				{
1689 					bestLength = info.file.length;
1690 					bestInfo = info;
1691 				}
1692 			}
1693 		}
1694 		return bestInfo;
1695 	}
1696 
1697 	/// Get the name of the filesystem that the given path is mounted under.
1698 	/// Returns null if none match.
1699 	string getPathFilesystem(string path)
1700 	{
1701 		return getPathMountInfo(path).vfstype;
1702 	}
1703 }
1704 
1705 // ****************************************************************************
1706 
1707 version (linux)
1708 {
1709 	import core.sys.linux.sys.xattr;
1710 	import core.stdc.errno;
1711 	private alias ENOATTR = ENODATA;
1712 
1713 	/// AA-like object for accessing a file's extended attributes.
1714 	struct XAttrs(Obj, string funPrefix)
1715 	{
1716 		private Obj obj;
1717 
1718 		mixin("alias getFun = " ~ funPrefix ~ "getxattr;");
1719 		mixin("alias setFun = " ~ funPrefix ~ "setxattr;");
1720 		mixin("alias removeFun = " ~ funPrefix ~ "removexattr;");
1721 		mixin("alias listFun = " ~ funPrefix ~ "listxattr;");
1722 
1723 		/// True if extended attributes are supported on this filesystem.
1724 		bool supported()
1725 		{
1726 			auto size = getFun(obj, "user.\x01", null, 0);
1727 			return size >= 0 || errno != EOPNOTSUPP;
1728 		}
1729 
1730 		/// Read an extended attribute.
1731 		void[] opIndex(string key)
1732 		{
1733 			auto cKey = key.toStringz();
1734 			size_t size = 0;
1735 			void[] buf;
1736 			do
1737 			{
1738 				buf.length = size;
1739 				size = getFun(obj, cKey, buf.ptr, buf.length);
1740 				errnoEnforce(size >= 0, __traits(identifier, getFun));
1741 			} while (size != buf.length);
1742 			return buf;
1743 		}
1744 
1745 		/// Check if an extended attribute is present.
1746 		bool opBinaryRight(string op)(string key)
1747 		if (op == "in")
1748 		{
1749 			auto cKey = key.toStringz();
1750 			auto size = getFun(obj, cKey, null, 0);
1751 			if (size >= 0)
1752 				return true;
1753 			else
1754 			if (errno == ENOATTR)
1755 				return false;
1756 			else
1757 				errnoEnforce(false, __traits(identifier, getFun));
1758 			assert(false);
1759 		}
1760 
1761 		/// Write an extended attribute.
1762 		void opIndexAssign(in void[] value, string key)
1763 		{
1764 			auto ret = setFun(obj, key.toStringz(), value.ptr, value.length, 0);
1765 			errnoEnforce(ret == 0, __traits(identifier, setFun));
1766 		}
1767 
1768 		/// Delete an extended attribute.
1769 		void remove(string key)
1770 		{
1771 			auto ret = removeFun(obj, key.toStringz());
1772 			errnoEnforce(ret == 0, __traits(identifier, removeFun));
1773 		}
1774 
1775 		/// Return a list of all extended attribute names.
1776 		string[] keys()
1777 		{
1778 			size_t size = 0;
1779 			char[] buf;
1780 			do
1781 			{
1782 				buf.length = size;
1783 				size = listFun(obj, buf.ptr, buf.length);
1784 				errnoEnforce(size >= 0, __traits(identifier, listFun));
1785 			} while (size != buf.length);
1786 
1787 			char[][] result;
1788 			size_t start;
1789 			foreach (p, c; buf)
1790 				if (!c)
1791 				{
1792 					result ~= buf[start..p];
1793 					start = p+1;
1794 				}
1795 
1796 			return cast(string[])result;
1797 		}
1798 	}
1799 
1800 	/// Return `XAttrs` for the given path,
1801 	/// or the link destination if the path leads to as symbolic link.
1802 	auto xAttrs(string path)
1803 	{
1804 		return XAttrs!(const(char)*, "")(path.toStringz());
1805 	}
1806 
1807 	/// Return `XAttrs` for the given path.
1808 	auto linkXAttrs(string path)
1809 	{
1810 		return XAttrs!(const(char)*, "l")(path.toStringz());
1811 	}
1812 
1813 	/// Return `XAttrs` for the given open file.
1814 	auto xAttrs(ref const File f)
1815 	{
1816 		return XAttrs!(int, "f")(f.fileno);
1817 	}
1818 
1819 	///
1820 	unittest
1821 	{
1822 		if (!xAttrs(".").supported)
1823 		{
1824 			import std.stdio : stderr;
1825 			stderr.writeln("ae.sys.file: xattrs not supported on current filesystem, skipping test.");
1826 			return;
1827 		}
1828 
1829 		enum fn = "test.txt";
1830 		std.file.write(fn, "test");
1831 		scope(exit) remove(fn);
1832 
1833 		auto attrs = xAttrs(fn);
1834 		enum key = "user.foo";
1835 		assert(key !in attrs);
1836 		assert(attrs.keys == []);
1837 
1838 		attrs[key] = "bar";
1839 		assert(key in attrs);
1840 		assert(attrs[key] == "bar");
1841 		assert(attrs.keys == [key]);
1842 
1843 		attrs.remove(key);
1844 		assert(key !in attrs);
1845 		assert(attrs.keys == []);
1846 	}
1847 }
1848 
1849 // ****************************************************************************
1850 
1851 version (Windows)
1852 {
1853 	/// Enumerate all hard links to the specified file.
1854 	// TODO: Return a range
1855 	string[] enumerateHardLinks()(string fn)
1856 	{
1857 		mixin(importWin32!q{winnt});
1858 		mixin(importWin32!q{winbase});
1859 
1860 		alias extern(System) HANDLE function(LPCWSTR lpFileName, DWORD dwFlags, LPDWORD StringLength, PWCHAR LinkName) TFindFirstFileNameW;
1861 		alias extern(System) BOOL function(HANDLE hFindStream, LPDWORD StringLength, PWCHAR LinkName) TFindNextFileNameW;
1862 
1863 		auto kernel32 = GetModuleHandle("kernel32.dll");
1864 		auto FindFirstFileNameW = cast(TFindFirstFileNameW)GetProcAddress(kernel32, "FindFirstFileNameW").wenforce("GetProcAddress(FindFirstFileNameW)");
1865 		auto FindNextFileNameW = cast(TFindNextFileNameW)GetProcAddress(kernel32, "FindNextFileNameW").wenforce("GetProcAddress(FindNextFileNameW)");
1866 
1867 		static WCHAR[0x8000] buf;
1868 		DWORD len = buf.length;
1869 		auto h = FindFirstFileNameW(toUTF16z(fn), 0, &len, buf.ptr);
1870 		wenforce(h != INVALID_HANDLE_VALUE, "FindFirstFileNameW");
1871 		scope(exit) FindClose(h);
1872 
1873 		string[] result;
1874 		do
1875 		{
1876 			enforce(len > 0 && len < buf.length && buf[len-1] == 0, "Bad FindFirst/NextFileNameW result");
1877 			result ~= buf[0..len-1].toUTF8();
1878 			len = buf.length;
1879 			auto ok = FindNextFileNameW(h, &len, buf.ptr);
1880 			if (!ok && GetLastError() == ERROR_HANDLE_EOF)
1881 				break;
1882 			wenforce(ok, "FindNextFileNameW");
1883 		} while(true);
1884 		return result;
1885 	}
1886 }
1887 
1888 /// Obtain the hard link count for the given file.
1889 uint hardLinkCount(string fn)
1890 {
1891 	version (Windows)
1892 	{
1893 		// TODO: Optimize (don't transform strings)
1894 		return cast(uint)fn.enumerateHardLinks.length;
1895 	}
1896 	else
1897 	{
1898 		import core.sys.posix.sys.stat;
1899 
1900 		stat_t s;
1901 		errnoEnforce(stat(fn.toStringz(), &s) == 0, "stat");
1902 		return s.st_nlink.to!uint;
1903 	}
1904 }
1905 
1906 // https://issues.dlang.org/show_bug.cgi?id=7016
1907 version (unittest)
1908 	version (Windows)
1909 		import ae.sys.windows.misc : getWineVersion;
1910 
1911 unittest
1912 {
1913 	// FindFirstFileNameW not implemented in Wine
1914 	version (Windows)
1915 		if (getWineVersion())
1916 			return;
1917 
1918 	touch("a.test");
1919 	scope(exit) remove("a.test");
1920 	assert("a.test".hardLinkCount() == 1);
1921 
1922 	hardLink("a.test", "b.test");
1923 	scope(exit) remove("b.test");
1924 	assert("a.test".hardLinkCount() == 2);
1925 	assert("b.test".hardLinkCount() == 2);
1926 
1927 	version(Windows)
1928 	{
1929 		auto paths = enumerateHardLinks("a.test");
1930 		assert(paths.length == 2);
1931 		paths.sort();
1932 		assert(paths[0].endsWith(`\a.test`), paths[0]);
1933 		assert(paths[1].endsWith(`\b.test`));
1934 	}
1935 }
1936 
1937 /// Argument-reversed version of `std.file.write`,
1938 /// usable at the end of an UFCS chain.
1939 static if (is(typeof({ import std.stdio : toFile; })))
1940 {
1941 	static import std.stdio;
1942 	alias toFile = std.stdio.toFile;
1943 }
1944 else
1945 {
1946 	void toFile(in void[] data, in char[] name)
1947 	{
1948 		std.file.write(name, data);
1949 	}
1950 }
1951 
1952 /// Same as toFile, but accepts void[] and does not conflict with the
1953 /// std.stdio function.
1954 void writeTo(in void[] data, in char[] target)
1955 {
1956 	std.file.write(target, data);
1957 }
1958 
1959 /// Polyfill for Windows fopen implementations with support for UNC
1960 /// paths and the 'x' subspecifier.
1961 File openFile()(string fn, string mode = "rb")
1962 {
1963 	File f;
1964 	static if (is(typeof(&f.windowsHandleOpen)))
1965 	{
1966 		import core.sys.windows.windows;
1967 		import ae.sys.windows.exception;
1968 
1969 		string winMode, cMode;
1970 		foreach (c; mode)
1971 		{
1972 			switch (c)
1973 			{
1974 				case 'r':
1975 				case 'w':
1976 				case 'a':
1977 				case '+':
1978 				case 'x':
1979 					winMode ~= c;
1980 					break;
1981 				case 'b':
1982 				case 't':
1983 					break;
1984 				default:
1985 					assert(false, "Unknown character in mode");
1986 			}
1987 			if (c != 'x')
1988 				cMode ~= c;
1989 		}
1990 		DWORD access, creation;
1991 		bool append;
1992 		switch (winMode)
1993 		{
1994 			case "r"  : access = GENERIC_READ                ; creation = OPEN_EXISTING; break;
1995 			case "r+" : access = GENERIC_READ | GENERIC_WRITE; creation = OPEN_EXISTING; break;
1996 			case "w"  : access =                GENERIC_WRITE; creation = CREATE_ALWAYS; break;
1997 			case "w+" : access = GENERIC_READ | GENERIC_WRITE; creation = CREATE_ALWAYS; break;
1998 			case "a"  : access =                GENERIC_WRITE; creation = OPEN_ALWAYS  ; version (CRuntime_Microsoft) append = true; break;
1999 			case "a+" : access = GENERIC_READ | GENERIC_WRITE; creation = OPEN_ALWAYS  ; version (CRuntime_Microsoft) assert(false, "MSVCRT can't fdopen with a+"); else break;
2000 			case "wx" : access =                GENERIC_WRITE; creation = CREATE_NEW   ; break;
2001 			case "w+x": access = GENERIC_READ | GENERIC_WRITE; creation = CREATE_NEW   ; break;
2002 			case "ax" : access =                GENERIC_WRITE; creation = CREATE_NEW   ; version (CRuntime_Microsoft) append = true; break;
2003 			case "a+x": access = GENERIC_READ | GENERIC_WRITE; creation = CREATE_NEW   ; version (CRuntime_Microsoft) assert(false, "MSVCRT can't fdopen with a+"); else break;
2004 			default: assert(false, "Bad file mode: " ~ mode);
2005 		}
2006 
2007 		auto pathW = toUTF16z(longPath(fn));
2008 		auto h = CreateFileW(pathW, access, FILE_SHARE_READ, null, creation, 0, HANDLE.init);
2009 		wenforce(h != INVALID_HANDLE_VALUE);
2010 
2011 		if (append)
2012 			h.SetFilePointer(0, null, FILE_END);
2013 
2014 		f.windowsHandleOpen(h, cMode);
2015 	}
2016 	else
2017 		f.open(fn, mode);
2018 	return f;
2019 }
2020 
2021 unittest
2022 {
2023 	enum Existence { any, mustExist, mustNotExist }
2024 	enum Pos { none /* not readable/writable */, start, end, empty }
2025 	static struct Behavior
2026 	{
2027 		Existence existence;
2028 		bool truncating;
2029 		Pos read, write;
2030 	}
2031 
2032 	void test(string mode, in Behavior expected)
2033 	{
2034 		static if (isVersion!q{CRuntime_Microsoft} || isVersion!q{OSX})
2035 			if (mode == "a+" || mode == "a+x")
2036 				return;
2037 
2038 		Behavior behavior;
2039 
2040 		static int counter;
2041 		auto fn = text(deleteme, counter++);
2042 
2043 		collectException(fn.remove());
2044 		bool mustExist    = !!collectException(openFile(fn, mode));
2045 		touch(fn);
2046 		bool mustNotExist = !!collectException(openFile(fn, mode));
2047 
2048 		if (!mustExist)
2049 			if (!mustNotExist)
2050 				behavior.existence = Existence.any;
2051 			else
2052 				behavior.existence = Existence.mustNotExist;
2053 		else
2054 			if (!mustNotExist)
2055 				behavior.existence = Existence.mustExist;
2056 			else
2057 				assert(false, "Can't open file whether it exists or not");
2058 
2059 		void create()
2060 		{
2061 			if (mustNotExist)
2062 				collectException(fn.remove());
2063 			else
2064 				write(fn, "foo");
2065 		}
2066 
2067 		create();
2068 		openFile(fn, mode);
2069 		behavior.truncating = getSize(fn) == 0;
2070 
2071 		create();
2072 		{
2073 			auto f = openFile(fn, mode);
2074 			ubyte[] buf;
2075 			if (collectException(f.rawRead(new ubyte[1]), buf))
2076 			{
2077 				behavior.read = Pos.none;
2078 				// Work around https://issues.dlang.org/show_bug.cgi?id=19751
2079 				f.reopen(fn, "w");
2080 			}
2081 			else
2082 			if (buf.length)
2083 				behavior.read = Pos.start;
2084 			else
2085 			if (f.size)
2086 				behavior.read = Pos.end;
2087 			else
2088 				behavior.read = Pos.empty;
2089 		}
2090 
2091 		create();
2092 		{
2093 			string s;
2094 			{
2095 				auto f = openFile(fn, mode);
2096 				if (collectException(f.rawWrite("b")))
2097 				{
2098 					s = null;
2099 					// Work around https://issues.dlang.org/show_bug.cgi?id=19751
2100 					f.reopen(fn, "w");
2101 				}
2102 				else
2103 				{
2104 					f.close();
2105 					s = fn.readText;
2106 				}
2107 			}
2108 
2109 			if (s is null)
2110 				behavior.write = Pos.none;
2111 			else
2112 			if (s == "b")
2113 				behavior.write = Pos.empty;
2114 			else
2115 			if (s.endsWith("b"))
2116 				behavior.write = Pos.end;
2117 			else
2118 			if (s.startsWith("b"))
2119 				behavior.write = Pos.start;
2120 			else
2121 				assert(false, "Can't detect write position");
2122 		}
2123 
2124 
2125 		if (behavior != expected)
2126 		{
2127 			import ae.utils.array : isOneOf;
2128 			version (Windows)
2129 				if (getWineVersion() && mode.isOneOf("w", "a", "wx", "ax"))
2130 				{
2131 					// Ignore bug in Wine msvcrt implementation
2132 					return;
2133 				}
2134 
2135 			assert(false, text(mode, ": expected ", expected, ", got ", behavior));
2136 		}
2137 	}
2138 
2139 	test("r"  , Behavior(Existence.mustExist   , false, Pos.start, Pos.none ));
2140 	test("r+" , Behavior(Existence.mustExist   , false, Pos.start, Pos.start));
2141 	test("w"  , Behavior(Existence.any         , true , Pos.none , Pos.empty));
2142 	test("w+" , Behavior(Existence.any         , true , Pos.empty, Pos.empty));
2143 	test("a"  , Behavior(Existence.any         , false, Pos.none , Pos.end  ));
2144 	test("a+" , Behavior(Existence.any         , false, Pos.start, Pos.end  ));
2145 	test("wx" , Behavior(Existence.mustNotExist, true , Pos.none , Pos.empty));
2146 	test("w+x", Behavior(Existence.mustNotExist, true , Pos.empty, Pos.empty));
2147 	test("ax" , Behavior(Existence.mustNotExist, true , Pos.none , Pos.empty));
2148 	test("a+x", Behavior(Existence.mustNotExist, true , Pos.empty, Pos.empty));
2149 }
2150 
2151 private version(Windows)
2152 {
2153 	version (CRuntime_Microsoft)
2154 	{
2155 		alias chsize_size_t = long;
2156 		extern(C) int _chsize_s(int fd, chsize_size_t size);
2157 		alias chsize = _chsize_s;
2158 	}
2159 	else
2160 	{
2161 		import core.stdc.config : c_long;
2162 		alias chsize_size_t = c_long;
2163 		extern(C) int chsize(int fd, c_long size);
2164 	}
2165 }
2166 
2167 /// Truncate the given file to the given size.
2168 void truncate(File f, ulong length)
2169 {
2170 	f.flush();
2171 	version (Windows)
2172 		chsize(f.fileno, length.to!chsize_size_t);
2173 	else
2174 		ftruncate(f.fileno, length.to!off_t);
2175 }
2176 
2177 unittest
2178 {
2179 	write("test.txt", "abcde");
2180 	auto f = File("test.txt", "r+b");
2181 	f.write("xyz");
2182 	f.truncate(f.tell);
2183 	f.close();
2184 	assert("test.txt".readText == "xyz");
2185 }
2186 
2187 /// Calculate the digest of a file.
2188 auto fileDigest(Digest)(string fn)
2189 {
2190 	import std.range.primitives;
2191 	Digest context;
2192 	context.start();
2193 	put(context, openFile(fn, "rb").byChunk(64 * 1024));
2194 	auto digest = context.finish();
2195 	return digest;
2196 }
2197 
2198 /// Calculate the MD5 hash of a file.
2199 template mdFile()
2200 {
2201 	import std.digest.md;
2202 	alias mdFile = fileDigest!MD5;
2203 }
2204 
2205 version (HAVE_WIN32)
2206 unittest
2207 {
2208 	import std.digest : toHexString;
2209 	write("test.txt", "Hello, world!");
2210 	scope(exit) remove("test.txt");
2211 	assert(mdFile("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839");
2212 }
2213 
2214 /// Calculate the digest of a file, and memoize it.
2215 auto fileDigestCached(Digest)(string fn)
2216 {
2217 	static typeof(Digest.init.finish())[ulong] cache;
2218 	auto id = getFileID(fn);
2219 	auto phash = id in cache;
2220 	if (phash)
2221 		return *phash;
2222 	return cache[id] = fileDigest!Digest(fn);
2223 }
2224 
2225 /// Calculate the MD5 hash of a file, and memoize it.
2226 template mdFileCached()
2227 {
2228 	import std.digest.md;
2229 	alias mdFileCached = fileDigestCached!MD5;
2230 }
2231 
2232 version (HAVE_WIN32)
2233 unittest
2234 {
2235 	import std.digest : toHexString;
2236 	write("test.txt", "Hello, world!");
2237 	scope(exit) remove("test.txt");
2238 	assert(mdFileCached("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839");
2239 	write("test.txt", "Something else");
2240 	assert(mdFileCached("test.txt").toHexString() == "6CD3556DEB0DA54BCA060B4C39479839");
2241 }
2242 
2243 /// Read a File (which might be a stream) into an array
2244 ubyte[] readFile(File f)
2245 {
2246 	import std.range.primitives;
2247 	auto result = appender!(ubyte[]);
2248 	put(result, f.byChunk(64*1024));
2249 	return result.data;
2250 }
2251 
2252 unittest
2253 {
2254 	auto s = "0123456789".replicate(10_000);
2255 	write("test.txt", s);
2256 	scope(exit) remove("test.txt");
2257 	assert(readFile(File("test.txt")) == s);
2258 }
2259 
2260 /// Read exactly `buf.length` bytes and return true.
2261 /// On EOF, return false.
2262 bool readExactly(File f, ubyte[] buf)
2263 {
2264 	if (!buf.length)
2265 		return true;
2266 	auto read = f.rawRead(buf);
2267 	if (read.length==0) return false;
2268 	enforce(read.length == buf.length, "Unexpected end of file");
2269 	return true;
2270 }
2271 
2272 private
2273 version (Windows)
2274 {
2275 	version (CRuntime_DigitalMars)
2276 		extern(C) sizediff_t read(int, void*, size_t);
2277 	else
2278 	{
2279 		extern(C) sizediff_t _read(int, void*, size_t);
2280 		alias read = _read;
2281 	}
2282 }
2283 else
2284 	import core.sys.posix.unistd : read;
2285 
2286 /// Like `File.rawRead`, but returns as soon as any data is available.
2287 void[] readPartial(File f, void[] buf)
2288 {
2289 	assert(buf.length);
2290 	auto numRead = read(f.fileno, buf.ptr, buf.length);
2291 	errnoEnforce(numRead >= 0);
2292 	return buf[0 .. numRead];
2293 }
2294 
2295 /// Like std.file.readText for non-UTF8
2296 ascii readAscii()(string fileName)
2297 {
2298 	return cast(ascii)readFile(openFile(fileName, "rb"));
2299 }
2300 
2301 // https://issues.dlang.org/show_bug.cgi?id=7016
2302 version(Posix) static import ae.sys.signals;
2303 
2304 /// Start a thread which writes data to f asynchronously.
2305 Thread writeFileAsync(File f, in void[] data)
2306 {
2307 	static class Writer : Thread
2308 	{
2309 		File target;
2310 		const void[] data;
2311 
2312 		this(ref File f, in void[] data)
2313 		{
2314 			this.target = f;
2315 			this.data = data;
2316 			super(&run);
2317 		}
2318 
2319 		void run()
2320 		{
2321 			version (Posix)
2322 			{
2323 				import ae.sys.signals;
2324 				collectSignal(SIGPIPE, &write);
2325 			}
2326 			else
2327 				write();
2328 		}
2329 
2330 		void write()
2331 		{
2332 			target.rawWrite(data);
2333 			target.close();
2334 		}
2335 	}
2336 
2337 	auto t = new Writer(f, data);
2338 	t.start();
2339 	return t;
2340 }
2341 
2342 /// Start a thread which reads data from f asynchronously.
2343 /// Call the returning delegate to obtain the read data,
2344 /// blocking until the read completes.
2345 ubyte[] delegate() readFileAsync(File f)
2346 {
2347 	static class Reader : Thread
2348 	{
2349 		File target;
2350 		ubyte[] data;
2351 
2352 		this(ref File f)
2353 		{
2354 			this.target = f;
2355 			this.data = data;
2356 			super(&run);
2357 		}
2358 
2359 		void run()
2360 		{
2361 			data = readFile(target);
2362 		}
2363 	}
2364 
2365 	auto t = new Reader(f);
2366 	t.start();
2367 	return {
2368 		t.join();
2369 		return t.data;
2370 	};
2371 }
2372 
2373 /// Write data to a file, and ensure it gets written to disk
2374 /// before this function returns.
2375 /// Consider using as atomic!syncWrite.
2376 /// See also: syncUpdate
2377 void syncWrite()(string target, in void[] data)
2378 {
2379 	auto f = File(target, "wb");
2380 	f.rawWrite(data);
2381 	version (Windows)
2382 	{
2383 		mixin(importWin32!q{windows});
2384 		FlushFileBuffers(f.windowsHandle);
2385 	}
2386 	else
2387 	{
2388 		import core.sys.posix.unistd;
2389 		fsync(f.fileno);
2390 	}
2391 	f.close();
2392 }
2393 
2394 /// Atomically save data to a file (if the file doesn't exist,
2395 /// or its contents differs). The update operation as a whole
2396 /// is not atomic, only the write is.
2397 void syncUpdate()(string fn, in void[] data)
2398 {
2399 	if (!fn.exists || fn.read() != data)
2400 		atomic!(syncWrite!())(fn, data);
2401 }
2402 
2403 version(Windows) import ae.sys.windows.exception;
2404 
2405 /// Create a named pipe, and allow interacting with it using a `std.stdio.File`.
2406 struct NamedPipeImpl
2407 {
2408 	immutable string fileName; ///
2409 
2410 	/// Create a named pipe, and reserve a filename.
2411 	this()(string name)
2412 	{
2413 		version(Windows)
2414 		{
2415 			mixin(importWin32!q{winbase});
2416 
2417 			fileName = `\\.\pipe\` ~ name;
2418 			auto h = CreateNamedPipeW(fileName.toUTF16z, PIPE_ACCESS_OUTBOUND, PIPE_TYPE_BYTE, 10, 4096, 4096, 0, null).wenforce("CreateNamedPipeW");
2419 			f.windowsHandleOpen(h, "wb");
2420 		}
2421 		else
2422 		{
2423 			import core.sys.posix.sys.stat;
2424 
2425 			fileName = `/tmp/` ~ name ~ `.fifo`;
2426 			mkfifo(fileName.toStringz, S_IWUSR | S_IRUSR);
2427 		}
2428 	}
2429 
2430 	/// Wait for a peer to open the other end of the pipe.
2431 	File connect()()
2432 	{
2433 		version(Windows)
2434 		{
2435 			mixin(importWin32!q{winbase});
2436 			mixin(importWin32!q{windef});
2437 
2438 			BOOL bSuccess = ConnectNamedPipe(f.windowsHandle, null);
2439 
2440 			// "If a client connects before the function is called, the function returns zero
2441 			// and GetLastError returns ERROR_PIPE_CONNECTED. This can happen if a client
2442 			// connects in the interval between the call to CreateNamedPipe and the call to
2443 			// ConnectNamedPipe. In this situation, there is a good connection between client
2444 			// and server, even though the function returns zero."
2445 			if (!bSuccess)
2446 				wenforce(GetLastError() == ERROR_PIPE_CONNECTED, "ConnectNamedPipe");
2447 
2448 			return f;
2449 		}
2450 		else
2451 		{
2452 			return File(fileName, "w");
2453 		}
2454 	}
2455 
2456 	~this()
2457 	{
2458 		version(Windows)
2459 		{
2460 			// File.~this will take care of cleanup
2461 		}
2462 		else
2463 			fileName.remove();
2464 	}
2465 
2466 private:
2467 	File f;
2468 }
2469 alias NamedPipe = RefCounted!NamedPipeImpl; /// ditto
2470 
2471 import ae.utils.textout : StringBuilder;
2472 
2473 /// Avoid std.stdio.File.readln's memory corruption bug
2474 /// https://issues.dlang.org/show_bug.cgi?id=13856
2475 string safeReadln(File f)
2476 {
2477 	StringBuilder buf;
2478 	char[1] arr;
2479 	while (true)
2480 	{
2481 		auto result = f.rawRead(arr[]);
2482 		if (!result.length)
2483 			break;
2484 		buf.put(result);
2485 		if (result[0] == '\x0A')
2486 			break;
2487 	}
2488 	return buf.get();
2489 }
2490 
2491 // ****************************************************************************
2492 
2493 /// Change the current directory to the given directory. Does nothing if dir is null.
2494 /// Return a scope guard which, upon destruction, restores the previous directory.
2495 /// Asserts that only one thread has changed the process's current directory at any time.
2496 auto pushd(string dir)
2497 {
2498 	import core.atomic;
2499 
2500 	static int threadCount = 0;
2501 	static shared int processCount = 0;
2502 
2503 	static struct Popd
2504 	{
2505 		string oldPath;
2506 		this(string cwd) { oldPath = cwd; }
2507 		~this() { if (oldPath) pop(); }
2508 		@disable this();
2509 		@disable this(this);
2510 
2511 		void pop()
2512 		{
2513 			assert(oldPath);
2514 			scope(exit) oldPath = null;
2515 			chdir(oldPath);
2516 
2517 			auto newThreadCount = --threadCount;
2518 			auto newProcessCount = atomicOp!"-="(processCount, 1);
2519 			assert(newThreadCount == newProcessCount); // Shouldn't happen
2520 		}
2521 	}
2522 
2523 	string cwd;
2524 	if (dir)
2525 	{
2526 		auto newThreadCount = ++threadCount;
2527 		auto newProcessCount = atomicOp!"+="(processCount, 1);
2528 		assert(newThreadCount == newProcessCount, "Another thread already has an active pushd");
2529 
2530 		cwd = getcwd();
2531 		chdir(dir);
2532 	}
2533 	return Popd(cwd);
2534 }
2535 
2536 // ****************************************************************************
2537 
2538 import std.algorithm;
2539 import std.process : thisProcessID;
2540 import std.traits;
2541 import std.typetuple;
2542 import ae.utils.meta;
2543 
2544 /// Parameter names that `atomic` assumes
2545 /// indicate a destination file by default.
2546 enum targetParameterNames = "target/to/name/dst";
2547 
2548 /// Wrap an operation which creates a file or directory,
2549 /// so that it is created safely and, for files, atomically
2550 /// (by performing the underlying operation to a temporary
2551 /// location, then renaming the completed file/directory to
2552 /// the actual target location). targetName specifies the name
2553 /// of the parameter containing the target file/directory.
2554 auto atomic(alias impl, string targetName = targetParameterNames)(staticMap!(Unqual, ParameterTypeTuple!impl) args)
2555 {
2556 	enum targetIndex = findParameter([ParameterIdentifierTuple!impl], targetName, __traits(identifier, impl));
2557 	return atomic!(impl, targetIndex)(args);
2558 }
2559 
2560 /// ditto
2561 auto atomic(alias impl, size_t targetIndex)(staticMap!(Unqual, ParameterTypeTuple!impl) args)
2562 {
2563 	// idup for https://issues.dlang.org/show_bug.cgi?id=12503
2564 	auto target = args[targetIndex].idup;
2565 	auto temp = "%s.%s.%s.temp".format(target, thisProcessID, getCurrentThreadID);
2566 	if (temp.exists) temp.removeRecurse();
2567 	scope(success) rename(temp, target);
2568 	scope(failure) if (temp.exists) temp.removeRecurse();
2569 	args[targetIndex] = temp;
2570 	return impl(args);
2571 }
2572 
2573 /// ditto
2574 // Workaround for https://issues.dlang.org/show_bug.cgi?id=12230
2575 // Can't be an overload because of https://issues.dlang.org/show_bug.cgi?id=13374
2576 //R atomicDg(string targetName = "target", R, Args...)(R delegate(Args) impl, staticMap!(Unqual, Args) args)
2577 auto atomicDg(size_t targetIndexA = size_t.max, Impl, Args...)(Impl impl, Args args)
2578 {
2579 	enum targetIndex = targetIndexA == size_t.max ? ParameterTypeTuple!impl.length-1 : targetIndexA;
2580 	return atomic!(impl, targetIndex)(args);
2581 }
2582 
2583 deprecated alias safeUpdate = atomic;
2584 
2585 unittest
2586 {
2587 	enum fn = "atomic.tmp";
2588 	scope(exit) if (fn.exists) fn.remove();
2589 
2590 	atomic!touch(fn);
2591 	assert(fn.exists);
2592 	fn.remove();
2593 
2594 	atomicDg(&touch, fn);
2595 	assert(fn.exists);
2596 }
2597 
2598 /// Wrap an operation so that it is skipped entirely
2599 /// if the target already exists. Implies atomic.
2600 auto cached(alias impl, string targetName = targetParameterNames)(ParameterTypeTuple!impl args)
2601 {
2602 	enum targetIndex = findParameter([ParameterIdentifierTuple!impl], targetName, __traits(identifier, impl));
2603 	auto target = args[targetIndex];
2604 	if (!target.exists)
2605 		atomic!(impl, targetIndex)(args);
2606 	return target;
2607 }
2608 
2609 /// ditto
2610 // Exists due to the same reasons as atomicDg
2611 auto cachedDg(size_t targetIndexA = size_t.max, Impl, Args...)(Impl impl, Args args)
2612 {
2613 	enum targetIndex = targetIndexA == size_t.max ? ParameterTypeTuple!impl.length-1 : targetIndexA;
2614 	auto target = args[targetIndex];
2615 	if (!target.exists)
2616 		atomic!(impl, targetIndex)(args);
2617 	return target;
2618 }
2619 
2620 deprecated alias obtainUsing = cached;
2621 
2622 /// Create a file, or replace an existing file's contents
2623 /// atomically.
2624 /// Note: Consider using atomic!syncWrite or
2625 /// atomic!syncUpdate instead.
2626 alias atomicWrite = atomic!_writeProxy;
2627 deprecated alias safeWrite = atomicWrite;
2628 /*private*/ void _writeProxy(string target, in void[] data)
2629 {
2630 	std.file.write(target, data);
2631 }
2632 
2633 // Work around for https://github.com/D-Programming-Language/phobos/pull/2784#issuecomment-68117241
2634 private void copy2(string source, string target) { std.file.copy(source, target); }
2635 
2636 /// Copy a file, or replace an existing file's contents
2637 /// with another file's, atomically.
2638 alias atomic!copy2 atomicCopy;
2639 
2640 unittest
2641 {
2642 	enum fn = "cached.tmp";
2643 	scope(exit) if (fn.exists) fn.remove();
2644 
2645 	cached!touch(fn);
2646 	assert(fn.exists);
2647 
2648 	std.file.write(fn, "test");
2649 
2650 	cachedDg!0(&_writeProxy, fn, "test2");
2651 	assert(fn.readText() == "test");
2652 }
2653 
2654 // ****************************************************************************
2655 
2656 /// Composes a function which generates a file name
2657 /// with a function which creates the file.
2658 /// Returns the file name.
2659 template withTarget(alias targetGen, alias fun)
2660 {
2661 	auto withTarget(Args...)(auto ref Args args)
2662 	{
2663 		auto target = targetGen(args);
2664 		fun(args, target);
2665 		return target;
2666 	}
2667 }
2668 
2669 /// Two-argument buildPath with reversed arguments.
2670 /// Useful for UFCS chaining.
2671 string prependPath(string target, string path)
2672 {
2673 	return buildPath(path, target);
2674 }