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