1 // The MIT License(MIT)
2 // =====================
3 //
4 // Copyright © `2015-2017` `Lucas Meijer`
5 //
6 // Permission is hereby granted, free of charge, to any person
7 // obtaining a copy of this software and associated documentation
8 // files (the “Software”), to deal in the Software without
9 // restriction, including without limitation the rights to use,
10 // copy, modify, merge, publish, distribute, sublicense, and/or sell
11 // copies of the Software, and to permit persons to whom the
12 // Software is furnished to do so, subject to the following
13 // conditions:
14 //
15 // The above copyright notice and this permission notice shall be
16 // included in all copies or substantial portions of the Software.
17 //
18 // THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
19 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20 // OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22 // HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23 // WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 // FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25 // OTHER DEALINGS IN THE SOFTWARE.
26 
27 using System;
28 using System.Collections.Generic;
29 using System.IO;
30 using System.Linq;
31 using System.Text;
32 
33 namespace Mono.Linker.Tests.Extensions
34 {
35 	public class NPath : IEquatable<NPath>, IComparable
36 	{
37 		private static readonly StringComparison PathStringComparison = IsLinux() ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
38 
39 		private readonly string[] _elements;
40 		private readonly bool _isRelative;
41 		private readonly string _driveLetter;
42 
43 		#region construction
44 
NPath(string path)45 		public NPath(string path)
46 		{
47 			if (path == null)
48 				throw new ArgumentNullException();
49 
50 			path = ParseDriveLetter(path, out _driveLetter);
51 
52 			if (path == "/")
53 			{
54 				_isRelative = false;
55 				_elements = new string[] { };
56 			}
57 			else
58 			{
59 				var split = path.Split('/', '\\');
60 
61 				_isRelative = _driveLetter == null && IsRelativeFromSplitString(split);
62 
63 				_elements = ParseSplitStringIntoElements(split.Where(s => s.Length > 0).ToArray());
64 			}
65 		}
66 
NPath(string[] elements, bool isRelative, string driveLetter)67 		private NPath(string[] elements, bool isRelative, string driveLetter)
68 		{
69 			_elements = elements;
70 			_isRelative = isRelative;
71 			_driveLetter = driveLetter;
72 		}
73 
ParseSplitStringIntoElements(IEnumerable<string> inputs)74 		private string[] ParseSplitStringIntoElements(IEnumerable<string> inputs)
75 		{
76 			var stack = new List<string>();
77 
78 			foreach (var input in inputs.Where(input => input.Length != 0))
79 			{
80 				if (input == ".")
81 				{
82 					if ((stack.Count > 0) && (stack.Last() != "."))
83 						continue;
84 				}
85 				else if (input == "..")
86 				{
87 					if (HasNonDotDotLastElement(stack))
88 					{
89 						stack.RemoveAt(stack.Count - 1);
90 						continue;
91 					}
92 					if (!_isRelative)
93 						throw new ArgumentException("You cannot create a path that tries to .. past the root");
94 				}
95 				stack.Add(input);
96 			}
97 			return stack.ToArray();
98 		}
99 
HasNonDotDotLastElement(List<string> stack)100 		private static bool HasNonDotDotLastElement(List<string> stack)
101 		{
102 			return stack.Count > 0 && stack[stack.Count - 1] != "..";
103 		}
104 
ParseDriveLetter(string path, out string driveLetter)105 		private string ParseDriveLetter(string path, out string driveLetter)
106 		{
107 			if (path.Length >= 2 && path[1] == ':')
108 			{
109 				driveLetter = path[0].ToString();
110 				return path.Substring(2);
111 			}
112 
113 			driveLetter = null;
114 			return path;
115 		}
116 
IsRelativeFromSplitString(string[] split)117 		private static bool IsRelativeFromSplitString(string[] split)
118 		{
119 			if (split.Length < 2)
120 				return true;
121 
122 			return split[0].Length != 0 || !split.Any(s => s.Length > 0);
123 		}
124 
Combine(params string[] append)125 		public NPath Combine(params string[] append)
126 		{
127 			return Combine(append.Select(a => new NPath(a)).ToArray());
128 		}
129 
Combine(params NPath[] append)130 		public NPath Combine(params NPath[] append)
131 		{
132 			if (!append.All(p => p.IsRelative))
133 				throw new ArgumentException("You cannot .Combine a non-relative path");
134 
135 			return new NPath(ParseSplitStringIntoElements(_elements.Concat(append.SelectMany(p => p._elements))), _isRelative, _driveLetter);
136 		}
137 
138 		public NPath Parent
139 		{
140 			get
141 			{
142 				if (_elements.Length == 0)
143 					throw new InvalidOperationException("Parent is called on an empty path");
144 
145 				var newElements = _elements.Take(_elements.Length - 1).ToArray();
146 
147 				return new NPath(newElements, _isRelative, _driveLetter);
148 			}
149 		}
150 
RelativeTo(NPath path)151 		public NPath RelativeTo(NPath path)
152 		{
153 			if (!IsChildOf(path))
154 			{
155 				if (!IsRelative && !path.IsRelative && _driveLetter != path._driveLetter)
156 					throw new ArgumentException("Path.RelativeTo() was invoked with two paths that are on different volumes. invoked on: " + ToString() + " asked to be made relative to: " + path);
157 
158 				NPath commonParent = null;
159 				foreach (var parent in RecursiveParents)
160 				{
161 					commonParent = path.RecursiveParents.FirstOrDefault(otherParent => otherParent == parent);
162 
163 					if (commonParent != null)
164 						break;
165 				}
166 
167 				if (commonParent == null)
168 					throw new ArgumentException("Path.RelativeTo() was unable to find a common parent between " + ToString() + " and " + path);
169 
170 				if (IsRelative && path.IsRelative && commonParent.IsEmpty())
171 					throw new ArgumentException("Path.RelativeTo() was invoked with two relative paths that do not share a common parent.  Invoked on: " + ToString() + " asked to be made relative to: " + path);
172 
173 				var depthDiff = path.Depth - commonParent.Depth;
174 				return new NPath(Enumerable.Repeat("..", depthDiff).Concat(_elements.Skip(commonParent.Depth)).ToArray(), true, null);
175 			}
176 
177 			return new NPath(_elements.Skip(path._elements.Length).ToArray(), true, null);
178 		}
179 
ChangeExtension(string extension)180 		public NPath ChangeExtension(string extension)
181 		{
182 			ThrowIfRoot();
183 
184 			var newElements = (string[])_elements.Clone();
185 			newElements[newElements.Length - 1] = Path.ChangeExtension(_elements[_elements.Length - 1], WithDot(extension));
186 			if (extension == string.Empty)
187 				newElements[newElements.Length - 1] = newElements[newElements.Length - 1].TrimEnd('.');
188 			return new NPath(newElements, _isRelative, _driveLetter);
189 		}
190 		#endregion construction
191 
192 		#region inspection
193 
194 		public bool IsRelative
195 		{
196 			get { return _isRelative; }
197 		}
198 
199 		public string FileName
200 		{
201 			get
202 			{
203 				ThrowIfRoot();
204 
205 				return _elements.Last();
206 			}
207 		}
208 
209 		public string FileNameWithoutExtension
210 		{
211 			get { return Path.GetFileNameWithoutExtension(FileName); }
212 		}
213 
214 		public IEnumerable<string> Elements
215 		{
216 			get { return _elements; }
217 		}
218 
219 		public int Depth
220 		{
221 			get { return _elements.Length; }
222 		}
223 
Exists(string append = R)224 		public bool Exists(string append = "")
225 		{
226 			return Exists(new NPath(append));
227 		}
228 
Exists(NPath append)229 		public bool Exists(NPath append)
230 		{
231 			return FileExists(append) || DirectoryExists(append);
232 		}
233 
DirectoryExists(string append = R)234 		public bool DirectoryExists(string append = "")
235 		{
236 			return DirectoryExists(new NPath(append));
237 		}
238 
DirectoryExists(NPath append)239 		public bool DirectoryExists(NPath append)
240 		{
241 			return Directory.Exists(Combine(append).ToString());
242 		}
243 
FileExists(string append = R)244 		public bool FileExists(string append = "")
245 		{
246 			return FileExists(new NPath(append));
247 		}
248 
FileExists(NPath append)249 		public bool FileExists(NPath append)
250 		{
251 			return File.Exists(Combine(append).ToString());
252 		}
253 
254 		public string ExtensionWithDot
255 		{
256 			get
257 			{
258 				if (IsRoot)
259 					throw new ArgumentException("A root directory does not have an extension");
260 
261 				var last = _elements.Last();
262 				var index = last.LastIndexOf(".");
263 				if (index < 0) return String.Empty;
264 				return last.Substring(index);
265 			}
266 		}
267 
InQuotes()268 		public string InQuotes()
269 		{
270 			return "\"" + ToString() + "\"";
271 		}
272 
InQuotes(SlashMode slashMode)273 		public string InQuotes(SlashMode slashMode)
274 		{
275 			return "\"" + ToString(slashMode) + "\"";
276 		}
277 
ToString()278 		public override string ToString()
279 		{
280 			return ToString(SlashMode.Native);
281 		}
282 
ToString(SlashMode slashMode)283 		public string ToString(SlashMode slashMode)
284 		{
285 			// Check if it's linux root /
286 			if (IsRoot && string.IsNullOrEmpty(_driveLetter))
287 				return Slash(slashMode).ToString();
288 
289 			if (_isRelative && _elements.Length == 0)
290 				return ".";
291 
292 			var sb = new StringBuilder();
293 			if (_driveLetter != null)
294 			{
295 				sb.Append(_driveLetter);
296 				sb.Append(":");
297 			}
298 			if (!_isRelative)
299 				sb.Append(Slash(slashMode));
300 			var first = true;
301 			foreach (var element in _elements)
302 			{
303 				if (!first)
304 					sb.Append(Slash(slashMode));
305 
306 				sb.Append(element);
307 				first = false;
308 			}
309 			return sb.ToString();
310 		}
311 
operator string(NPath path)312 		public static implicit operator string(NPath path)
313 		{
314 			return path.ToString();
315 		}
316 
Slash(SlashMode slashMode)317 		static char Slash(SlashMode slashMode)
318 		{
319 			switch (slashMode)
320 			{
321 				case SlashMode.Backward:
322 					return '\\';
323 				case SlashMode.Forward:
324 					return '/';
325 				default:
326 					return Path.DirectorySeparatorChar;
327 			}
328 		}
329 
Equals(Object obj)330 		public override bool Equals(Object obj)
331 		{
332 			if (obj == null)
333 				return false;
334 
335 			// If parameter cannot be cast to Point return false.
336 			var p = obj as NPath;
337 			if ((Object)p == null)
338 				return false;
339 
340 			return Equals(p);
341 		}
342 
Equals(NPath p)343 		public bool Equals(NPath p)
344 		{
345 			if (p._isRelative != _isRelative)
346 				return false;
347 
348 			if (!string.Equals(p._driveLetter, _driveLetter, PathStringComparison))
349 				return false;
350 
351 			if (p._elements.Length != _elements.Length)
352 				return false;
353 
354 			for (var i = 0; i != _elements.Length; i++)
355 				if (!string.Equals(p._elements[i], _elements[i], PathStringComparison))
356 					return false;
357 
358 			return true;
359 		}
360 
operator ==(NPath a, NPath b)361 		public static bool operator ==(NPath a, NPath b)
362 		{
363 			// If both are null, or both are same instance, return true.
364 			if (ReferenceEquals(a, b))
365 				return true;
366 
367 			// If one is null, but not both, return false.
368 			if (((object)a == null) || ((object)b == null))
369 				return false;
370 
371 			// Return true if the fields match:
372 			return a.Equals(b);
373 		}
374 
GetHashCode()375 		public override int GetHashCode()
376 		{
377 			unchecked
378 			{
379 				int hash = 17;
380 				// Suitable nullity checks etc, of course :)
381 				hash = hash * 23 + _isRelative.GetHashCode();
382 				foreach (var element in _elements)
383 					hash = hash * 23 + element.GetHashCode();
384 				if (_driveLetter != null)
385 					hash = hash * 23 + _driveLetter.GetHashCode();
386 				return hash;
387 			}
388 		}
389 
CompareTo(object obj)390 		public int CompareTo(object obj)
391 		{
392 			if (obj == null)
393 				return -1;
394 
395 			return this.ToString().CompareTo(((NPath)obj).ToString());
396 		}
397 
operator !=(NPath a, NPath b)398 		public static bool operator !=(NPath a, NPath b)
399 		{
400 			return !(a == b);
401 		}
402 
HasExtension(params string[] extensions)403 		public bool HasExtension(params string[] extensions)
404 		{
405 			var extensionWithDotLower = ExtensionWithDot.ToLower();
406 			return extensions.Any(e => WithDot(e).ToLower() == extensionWithDotLower);
407 		}
408 
WithDot(string extension)409 		private static string WithDot(string extension)
410 		{
411 			return extension.StartsWith(".") ? extension : "." + extension;
412 		}
413 
IsEmpty()414 		private bool IsEmpty()
415 		{
416 			return _elements.Length == 0;
417 		}
418 
419 		public bool IsRoot
420 		{
421 			get { return _elements.Length == 0 && !_isRelative; }
422 		}
423 
424 		#endregion inspection
425 
426 		#region directory enumeration
427 
Files(string filter, bool recurse = false)428 		public IEnumerable<NPath> Files(string filter, bool recurse = false)
429 		{
430 			return Directory.GetFiles(ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s));
431 		}
432 
Files(bool recurse = false)433 		public IEnumerable<NPath> Files(bool recurse = false)
434 		{
435 			return Files("*", recurse);
436 		}
437 
Contents(string filter, bool recurse = false)438 		public IEnumerable<NPath> Contents(string filter, bool recurse = false)
439 		{
440 			return Files(filter, recurse).Concat(Directories(filter, recurse));
441 		}
442 
Contents(bool recurse = false)443 		public IEnumerable<NPath> Contents(bool recurse = false)
444 		{
445 			return Contents("*", recurse);
446 		}
447 
Directories(string filter, bool recurse = false)448 		public IEnumerable<NPath> Directories(string filter, bool recurse = false)
449 		{
450 			return Directory.GetDirectories(ToString(), filter, recurse ? SearchOption.AllDirectories : SearchOption.TopDirectoryOnly).Select(s => new NPath(s));
451 		}
452 
Directories(bool recurse = false)453 		public IEnumerable<NPath> Directories(bool recurse = false)
454 		{
455 			return Directories("*", recurse);
456 		}
457 
458 		#endregion
459 
460 		#region filesystem writing operations
CreateFile()461 		public NPath CreateFile()
462 		{
463 			ThrowIfRelative();
464 			ThrowIfRoot();
465 			EnsureParentDirectoryExists();
466 			File.WriteAllBytes(ToString(), new byte[0]);
467 			return this;
468 		}
469 
CreateFile(string file)470 		public NPath CreateFile(string file)
471 		{
472 			return CreateFile(new NPath(file));
473 		}
474 
CreateFile(NPath file)475 		public NPath CreateFile(NPath file)
476 		{
477 			if (!file.IsRelative)
478 				throw new ArgumentException("You cannot call CreateFile() on an existing path with a non relative argument");
479 			return Combine(file).CreateFile();
480 		}
481 
CreateDirectory()482 		public NPath CreateDirectory()
483 		{
484 			ThrowIfRelative();
485 
486 			if (IsRoot)
487 				throw new NotSupportedException("CreateDirectory is not supported on a root level directory because it would be dangerous:" + ToString());
488 
489 			Directory.CreateDirectory(ToString());
490 			return this;
491 		}
492 
CreateDirectory(string directory)493 		public NPath CreateDirectory(string directory)
494 		{
495 			return CreateDirectory(new NPath(directory));
496 		}
497 
CreateDirectory(NPath directory)498 		public NPath CreateDirectory(NPath directory)
499 		{
500 			if (!directory.IsRelative)
501 				throw new ArgumentException("Cannot call CreateDirectory with an absolute argument");
502 
503 			return Combine(directory).CreateDirectory();
504 		}
505 
Copy(string dest)506 		public NPath Copy(string dest)
507 		{
508 			return Copy(new NPath(dest));
509 		}
510 
Copy(string dest, Func<NPath, bool> fileFilter)511 		public NPath Copy(string dest, Func<NPath, bool> fileFilter)
512 		{
513 			return Copy(new NPath(dest), fileFilter);
514 		}
515 
Copy(NPath dest)516 		public NPath Copy(NPath dest)
517 		{
518 			return Copy(dest, p => true);
519 		}
520 
Copy(NPath dest, Func<NPath, bool> fileFilter)521 		public NPath Copy(NPath dest, Func<NPath, bool> fileFilter)
522 		{
523 			ThrowIfRelative();
524 			if (dest.IsRelative)
525 				dest = Parent.Combine(dest);
526 
527 			if (dest.DirectoryExists())
528 				return CopyWithDeterminedDestination(dest.Combine(FileName), fileFilter);
529 
530 			return CopyWithDeterminedDestination(dest, fileFilter);
531 		}
532 
MakeAbsolute()533 		public NPath MakeAbsolute()
534 		{
535 			if (!IsRelative)
536 				return this;
537 
538 			return NPath.CurrentDirectory.Combine(this);
539 		}
540 
CopyWithDeterminedDestination(NPath absoluteDestination, Func<NPath, bool> fileFilter)541 		NPath CopyWithDeterminedDestination(NPath absoluteDestination, Func<NPath, bool> fileFilter)
542 		{
543 			if (absoluteDestination.IsRelative)
544 				throw new ArgumentException("absoluteDestination must be absolute");
545 
546 			if (FileExists())
547 			{
548 				if (!fileFilter(absoluteDestination))
549 					return null;
550 
551 				absoluteDestination.EnsureParentDirectoryExists();
552 
553 				File.Copy(ToString(), absoluteDestination.ToString(), true);
554 				return absoluteDestination;
555 			}
556 
557 			if (DirectoryExists())
558 			{
559 				absoluteDestination.EnsureDirectoryExists();
560 				foreach (var thing in Contents())
561 					thing.CopyWithDeterminedDestination(absoluteDestination.Combine(thing.RelativeTo(this)), fileFilter);
562 				return absoluteDestination;
563 			}
564 
565 			throw new ArgumentException("Copy() called on path that doesnt exist: " + ToString());
566 		}
567 
Delete(DeleteMode deleteMode = DeleteMode.Normal)568 		public void Delete(DeleteMode deleteMode = DeleteMode.Normal)
569 		{
570 			ThrowIfRelative();
571 
572 			if (IsRoot)
573 				throw new NotSupportedException("Delete is not supported on a root level directory because it would be dangerous:" + ToString());
574 
575 			if (FileExists())
576 				File.Delete(ToString());
577 			else if (DirectoryExists())
578 				try
579 				{
580 					Directory.Delete(ToString(), true);
581 				}
582 				catch (IOException)
583 				{
584 					if (deleteMode == DeleteMode.Normal)
585 						throw;
586 				}
587 			else
588 				throw new InvalidOperationException("Trying to delete a path that does not exist: " + ToString());
589 		}
590 
DeleteIfExists(DeleteMode deleteMode = DeleteMode.Normal)591 		public void DeleteIfExists(DeleteMode deleteMode = DeleteMode.Normal)
592 		{
593 			ThrowIfRelative();
594 
595 			if (FileExists() || DirectoryExists())
596 				Delete(deleteMode);
597 		}
598 
DeleteContents()599 		public NPath DeleteContents()
600 		{
601 			ThrowIfRelative();
602 
603 			if (IsRoot)
604 				throw new NotSupportedException("DeleteContents is not supported on a root level directory because it would be dangerous:" + ToString());
605 
606 			if (FileExists())
607 				throw new InvalidOperationException("It is not valid to perform this operation on a file");
608 
609 			if (DirectoryExists())
610 			{
611 				try
612 				{
613 					Files().Delete();
614 					Directories().Delete();
615 				}
616 				catch (IOException)
617 				{
618 					if (Files(true).Any())
619 						throw;
620 				}
621 
622 				return this;
623 			}
624 
625 			return EnsureDirectoryExists();
626 		}
627 
CreateTempDirectory(string myprefix)628 		public static NPath CreateTempDirectory(string myprefix)
629 		{
630 			var random = new Random();
631 			while (true)
632 			{
633 				var candidate = new NPath(Path.GetTempPath() + "/" + myprefix + "_" + random.Next());
634 				if (!candidate.Exists())
635 					return candidate.CreateDirectory();
636 			}
637 		}
638 
Move(string dest)639 		public NPath Move(string dest)
640 		{
641 			return Move(new NPath(dest));
642 		}
643 
Move(NPath dest)644 		public NPath Move(NPath dest)
645 		{
646 			ThrowIfRelative();
647 
648 			if (IsRoot)
649 				throw new NotSupportedException("Move is not supported on a root level directory because it would be dangerous:" + ToString());
650 
651 			if (dest.IsRelative)
652 				return Move(Parent.Combine(dest));
653 
654 			if (dest.DirectoryExists())
655 				return Move(dest.Combine(FileName));
656 
657 			if (FileExists())
658 			{
659 				dest.EnsureParentDirectoryExists();
660 				File.Move(ToString(), dest.ToString());
661 				return dest;
662 			}
663 
664 			if (DirectoryExists())
665 			{
666 				Directory.Move(ToString(), dest.ToString());
667 				return dest;
668 			}
669 
670 			throw new ArgumentException("Move() called on a path that doesn't exist: " + ToString());
671 		}
672 
673 		#endregion
674 
675 		#region special paths
676 
677 		public static NPath CurrentDirectory
678 		{
679 			get
680 			{
681 				return new NPath(Directory.GetCurrentDirectory());
682 			}
683 		}
684 
685 		public static NPath HomeDirectory
686 		{
687 			get
688 			{
689 				if (Path.DirectorySeparatorChar == '\\')
690 					return new NPath(Environment.GetEnvironmentVariable("USERPROFILE"));
691 				return new NPath(Environment.GetEnvironmentVariable("HOME"));
692 			}
693 		}
694 
695 		public static NPath SystemTemp
696 		{
697 			get
698 			{
699 				return new NPath(Path.GetTempPath());
700 			}
701 		}
702 
703 		#endregion
704 
ThrowIfRelative()705 		private void ThrowIfRelative()
706 		{
707 			if (_isRelative)
708 				throw new ArgumentException("You are attempting an operation on a Path that requires an absolute path, but the path is relative");
709 		}
710 
ThrowIfRoot()711 		private void ThrowIfRoot()
712 		{
713 			if (IsRoot)
714 				throw new ArgumentException("You are attempting an operation that is not valid on a root level directory");
715 		}
716 
EnsureDirectoryExists(string append = R)717 		public NPath EnsureDirectoryExists(string append = "")
718 		{
719 			return EnsureDirectoryExists(new NPath(append));
720 		}
721 
EnsureDirectoryExists(NPath append)722 		public NPath EnsureDirectoryExists(NPath append)
723 		{
724 			var combined = Combine(append);
725 			if (combined.DirectoryExists())
726 				return combined;
727 			combined.EnsureParentDirectoryExists();
728 			combined.CreateDirectory();
729 			return combined;
730 		}
731 
EnsureParentDirectoryExists()732 		public NPath EnsureParentDirectoryExists()
733 		{
734 			var parent = Parent;
735 			parent.EnsureDirectoryExists();
736 			return parent;
737 		}
738 
FileMustExist()739 		public NPath FileMustExist()
740 		{
741 			if (!FileExists())
742 				throw new FileNotFoundException("File was expected to exist : " + ToString());
743 
744 			return this;
745 		}
746 
DirectoryMustExist()747 		public NPath DirectoryMustExist()
748 		{
749 			if (!DirectoryExists())
750 				throw new DirectoryNotFoundException("Expected directory to exist : " + ToString());
751 
752 			return this;
753 		}
754 
IsChildOf(string potentialBasePath)755 		public bool IsChildOf(string potentialBasePath)
756 		{
757 			return IsChildOf(new NPath(potentialBasePath));
758 		}
759 
IsChildOf(NPath potentialBasePath)760 		public bool IsChildOf(NPath potentialBasePath)
761 		{
762 			if ((IsRelative && !potentialBasePath.IsRelative) || !IsRelative && potentialBasePath.IsRelative)
763 				throw new ArgumentException("You can only call IsChildOf with two relative paths, or with two absolute paths");
764 
765 			// If the other path is the root directory, then anything is a child of it as long as it's not a Windows path
766 			if (potentialBasePath.IsRoot)
767 			{
768 				if (_driveLetter != potentialBasePath._driveLetter)
769 					return false;
770 				return true;
771 			}
772 
773 			if (IsEmpty())
774 				return false;
775 
776 			if (Equals(potentialBasePath))
777 				return true;
778 
779 			return Parent.IsChildOf(potentialBasePath);
780 		}
781 
782 		public IEnumerable<NPath> RecursiveParents
783 		{
784 			get
785 			{
786 				var candidate = this;
787 				while (true)
788 				{
789 					if (candidate.IsEmpty())
790 						yield break;
791 
792 					candidate = candidate.Parent;
793 					yield return candidate;
794 				}
795 			}
796 		}
797 
ParentContaining(string needle)798 		public NPath ParentContaining(string needle)
799 		{
800 			return ParentContaining(new NPath(needle));
801 		}
802 
ParentContaining(NPath needle)803 		public NPath ParentContaining(NPath needle)
804 		{
805 			ThrowIfRelative();
806 
807 			return RecursiveParents.FirstOrDefault(p => p.Exists(needle));
808 		}
809 
WriteAllText(string contents)810 		public NPath WriteAllText(string contents)
811 		{
812 			ThrowIfRelative();
813 			EnsureParentDirectoryExists();
814 			File.WriteAllText(ToString(), contents);
815 			return this;
816 		}
817 
ReadAllText()818 		public string ReadAllText()
819 		{
820 			ThrowIfRelative();
821 			return File.ReadAllText(ToString());
822 		}
823 
WriteAllLines(string[] contents)824 		public NPath WriteAllLines(string[] contents)
825 		{
826 			ThrowIfRelative();
827 			EnsureParentDirectoryExists();
828 			File.WriteAllLines(ToString(), contents);
829 			return this;
830 		}
831 
ReadAllLines()832 		public string[] ReadAllLines()
833 		{
834 			ThrowIfRelative();
835 			return File.ReadAllLines(ToString());
836 		}
837 
CopyFiles(NPath destination, bool recurse, Func<NPath, bool> fileFilter = null)838 		public IEnumerable<NPath> CopyFiles(NPath destination, bool recurse, Func<NPath, bool> fileFilter = null)
839 		{
840 			destination.EnsureDirectoryExists();
841 			return Files(recurse).Where(fileFilter ?? AlwaysTrue).Select(file => file.Copy(destination.Combine(file.RelativeTo(this)))).ToArray();
842 		}
843 
MoveFiles(NPath destination, bool recurse, Func<NPath, bool> fileFilter = null)844 		public IEnumerable<NPath> MoveFiles(NPath destination, bool recurse, Func<NPath, bool> fileFilter = null)
845 		{
846 			if (IsRoot)
847 				throw new NotSupportedException("MoveFiles is not supported on this directory because it would be dangerous:" + ToString());
848 
849 			destination.EnsureDirectoryExists();
850 			return Files(recurse).Where(fileFilter ?? AlwaysTrue).Select(file => file.Move(destination.Combine(file.RelativeTo(this)))).ToArray();
851 		}
852 
AlwaysTrue(NPath p)853 		static bool AlwaysTrue(NPath p)
854 		{
855 			return true;
856 		}
857 
IsLinux()858 		private static bool IsLinux()
859 		{
860 			return Directory.Exists("/proc");
861 		}
862 	}
863 
864 	public static class Extensions
865 	{
Copy(this IEnumerable<NPath> self, string dest)866 		public static IEnumerable<NPath> Copy(this IEnumerable<NPath> self, string dest)
867 		{
868 			return Copy(self, new NPath(dest));
869 		}
870 
Copy(this IEnumerable<NPath> self, NPath dest)871 		public static IEnumerable<NPath> Copy(this IEnumerable<NPath> self, NPath dest)
872 		{
873 			if (dest.IsRelative)
874 				throw new ArgumentException("When copying multiple files, the destination cannot be a relative path");
875 			dest.EnsureDirectoryExists();
876 			return self.Select(p => p.Copy(dest.Combine(p.FileName))).ToArray();
877 		}
878 
Move(this IEnumerable<NPath> self, string dest)879 		public static IEnumerable<NPath> Move(this IEnumerable<NPath> self, string dest)
880 		{
881 			return Move(self, new NPath(dest));
882 		}
883 
Move(this IEnumerable<NPath> self, NPath dest)884 		public static IEnumerable<NPath> Move(this IEnumerable<NPath> self, NPath dest)
885 		{
886 			if (dest.IsRelative)
887 				throw new ArgumentException("When moving multiple files, the destination cannot be a relative path");
888 			dest.EnsureDirectoryExists();
889 			return self.Select(p => p.Move(dest.Combine(p.FileName))).ToArray();
890 		}
891 
Delete(this IEnumerable<NPath> self)892 		public static IEnumerable<NPath> Delete(this IEnumerable<NPath> self)
893 		{
894 			foreach (var p in self)
895 				p.Delete();
896 			return self;
897 		}
898 
InQuotes(this IEnumerable<NPath> self, SlashMode forward = SlashMode.Native)899 		public static IEnumerable<string> InQuotes(this IEnumerable<NPath> self, SlashMode forward = SlashMode.Native)
900 		{
901 			return self.Select(p => p.InQuotes(forward));
902 		}
903 
ToNPath(this string path)904 		public static NPath ToNPath(this string path)
905 		{
906 			return new NPath(path);
907 		}
908 	}
909 
910 	public enum SlashMode
911 	{
912 		Native,
913 		Forward,
914 		Backward
915 	}
916 
917 	public enum DeleteMode
918 	{
919 		Normal,
920 		Soft
921 	}
922 }