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