using System;
using System.Linq;
using BadPath = System.IO.Path;
namespace LessIO
{
///
/// Represents a file system path.
///
public struct Path : IEquatable
{
private readonly string _path;
private static readonly string _pathEmpty = string.Empty;
public static readonly Path Empty = new Path();
///
/// This is the special prefix to prepend to paths to support up to 32,767 character paths.
/// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
///
private static readonly string Win32LongPathPrefix = @"\\?\";
///
/// This is the special prefix to prepend to paths to support long paths for UNC paths.
/// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247(v=vs.85).aspx#maxpath
///
private static readonly string Win32LongPathPrefixUNC = @"\\?\UNC\";
private static readonly string UNCPrefix = @"\\";
// TODO: Add validation using a strategy? Or just use it as a strongly typed path to force caller to be explicit?
public Path(string path)
{
//TODO: Consider doing a FileSystem.Normalize and FileSystem.Validate to allow the strategy to Normalize & Validate path
//To maintain sanity NEVER let the Path object store the long path prefixes. That is a hack for Win32 that should only ever be used just before calling the Win32 API and stripped out of any paths coming out of the Win32 API.
path = StripWin32PathPrefix(path);
path = StripDirectorySeperatorPostfix(path);
path = RemoveDoubleSeperators(path);
_path = path;
}
private static string RemoveDoubleSeperators(string path)
{
/*
"\\" is legit for UNC paths and as a prefix.
So don't remove "\\" if it is in the root.
*/
string root = GetPathRoot(path);
string remainder = path.Length > root.Length ? path.Substring(root.Length) : "";
Array.ForEach(DirectorySeperatorChars, sep => remainder = remainder.Replace(new string(new char[] { sep, sep }), new string(new char[] { sep })));
return root + remainder;
}
private static string StripDirectorySeperatorPostfix(string path)
{
/* Here we want to trim any trailing directory seperator charactars EXCEPT
in one case: When the path is a fully qualified root dir such as "x:\". See GetPathRoot and System.IO.Path.GetPathRoot
*/
// "X:/"(path specified an absolute path on a given drive).
if (path.Length == 3 && path[1] == ':' && IsDirectorySeparator(path[2]))
return path;
else
return path.TrimEnd(DirectorySeperatorChars);
}
private static string StripWin32PathPrefix(string pathString)
{
if (pathString.StartsWith(Win32LongPathPrefixUNC))
return UNCPrefix + pathString.Substring(Win32LongPathPrefixUNC.Length);
if (pathString.StartsWith(Win32LongPathPrefix))
return pathString.Substring(Win32LongPathPrefix.Length);
return pathString;
}
///
/// Returns the directory seperator characers.
///
internal static char[] DirectorySeperatorChars
{
get
{
return new char[] { BadPath.DirectorySeparatorChar, BadPath.AltDirectorySeparatorChar };
}
}
internal static bool IsDirectorySeparator(char ch)
{
return Array.Exists(DirectorySeperatorChars, c => c == ch);
}
///
/// Gets the normalized path string. May be rooted or may be relative.
/// For a rooted/qualified path use
///
public string PathString
{
get
{
return _path != null ? _path : _pathEmpty;
}
}
///
/// Returns the absolute path for the current path.
/// Compatible with .
///
public Path FullPath
{
get
{
return new Path(this.FullPathString);
}
}
///
/// Returns the absolute path for the current path.
/// Compatible with .
///
public string FullPathString
{
get
{
var pathString = this.PathString;
var pathRoot = this.PathRoot;
if (pathRoot == "")
{ // relative
return Combine(WorkingDirectory, pathString).PathString;
}
else if (pathRoot == @"\" || pathRoot == @"/")
{ // use the working directory's drive/root only.
pathString = pathString.TrimStart(DirectorySeperatorChars);//otherwise Combine will ignore the root
string workingRoot = new Path(WorkingDirectory).PathRoot;
return Combine(workingRoot, pathString).PathString;
}
else
{
return pathString;
}
}
}
private static string WorkingDirectory
{
get {
//TODO: There is a Win32 native equivelent for this:
return System.IO.Directory.GetCurrentDirectory();
}
}
public bool IsEmpty
{
get { return Equals(Path.Empty); }
}
///
/// Indicates if the two paths are equivelent and point to the same file or directory.
///
private static bool PathEquals(string pathA, string pathB)
{
/* Now we never let the Win32 long path prefix get into a Path instance:
pathA = StripWin32PathPrefix(pathA);
pathB = StripWin32PathPrefix(pathB);
*/
pathA = pathA.TrimEnd(DirectorySeperatorChars);
pathB = pathB.TrimEnd(DirectorySeperatorChars);
var partsA = pathA.Split(DirectorySeperatorChars);
var partsB = pathB.Split(DirectorySeperatorChars);
if (partsA.Length != partsB.Length)
return false;
for (var i = 0; i < partsA.Length; i++)
{
var areEqual = string.Equals(partsA[i], partsB[i], StringComparison.InvariantCultureIgnoreCase);
if (!areEqual)
return false;
}
return true;
}
public static bool operator ==(Path a, Path b)
{
return Path.Equals(a, b);
}
public static bool operator !=(Path a, Path b)
{
return !Path.Equals(a, b);
}
public override bool Equals(object obj)
{
if (obj == null || GetType() != obj.GetType())
return false;
return Equals((Path)obj);
}
public bool Equals(Path other)
{
return Path.PathEquals(this.PathString, other.PathString);
}
internal static bool Equals(Path a, Path b)
{
return PathEquals(a.PathString, b.PathString);
}
///
/// Long-form filenames are not supported by the .NET system libraries, so we do win32 calls.
/// See https://msdn.microsoft.com/en-us/library/windows/desktop/aa365247%28v=vs.85%29.aspx#maxpath
///
///
/// The object will never store the Win32 long path prefix. Instead use this method to add it back when necessary (i.e. when making direct calls into Win32 APIs).
///
public string WithWin32LongPathPrefix()
{
if (!PathString.StartsWith(Win32LongPathPrefix)) // More consistent to deal with if we just add it to all of them: if (!path.StartsWith(LongPathPrefix) && path.Length >= MAX_PATH)
{
if (PathString.StartsWith(UNCPrefix))
return Win32LongPathPrefixUNC + this.PathString.Substring(UNCPrefix.Length);
else
return Win32LongPathPrefix + this.PathString;
}
else
{
//NOTE that Win32LongPathPrefixUNC is a superset of Win32LongPathPrefix we just assume the right pathprefix is already there.
return this.PathString;
}
}
public override int GetHashCode()
{
return PathString.GetHashCode();
}
public override string ToString()
{
return PathString.ToString();
}
///
/// Gets the root directory information of the specified path.
///
public string PathRoot
{
get
{
return GetPathRoot(this.PathString);
}
}
///
/// Modeled after but supports long path names.
///
///
///
///
/// See https://msdn.microsoft.com/en-us/library/system.io.path.getpathroot%28v=vs.110%29.aspx
/// Possible patterns for the string returned by this method are as follows:
/// An empty string (path specified a relative path on the current drive or volume).
/// "/"(path specified an absolute path on the current drive).
/// "X:"(path specified a relative path on a drive, where X represents a drive or volume letter).
/// "X:/"(path specified an absolute path on a given drive).
/// "\\ComputerName\SharedFolder"(a UNC path).
///
internal static string GetPathRoot(string path)
{
// "X:/"(path specified an absolute path on a given drive).
if (path.Length >= 3 && path[1] == ':' && IsDirectorySeparator(path[2]))
return path.Substring(0, 3);
// "X:"(path specified a relative path on a drive, where X represents a drive or volume letter).
if (path.Length >= 2 && path[1] == ':')
{
return path.Substring(0, 2);
}
// "\\ComputerName\SharedFolder"(a UNC path).
// NOTE: UNC Path "root" includes the server/host AND have the root share folder too.
if (path.Length > 2
&& IsDirectorySeparator(path[0])
&& IsDirectorySeparator(path[1])
&& path.IndexOfAny(DirectorySeperatorChars, 2) > 2)
{
var beginShareName = path.IndexOfAny(DirectorySeperatorChars, 2);
var endShareName = path.IndexOfAny(DirectorySeperatorChars, beginShareName + 1);
if (endShareName < 0)
endShareName = path.Length;
if (beginShareName > 2 && endShareName > beginShareName)
return path.Substring(0, endShareName);
}
// "/"(path specified an absolute path on the current drive).
if (path.Length >= 1 && IsDirectorySeparator(path[0]))
{
return path.Substring(0, 1);
}
// path specified a relative path on the current drive or volume?
return "";
}
public static Path Combine(Path path1, params string[] pathParts)
{
if (path1.IsEmpty)
throw new ArgumentNullException("path1");
if (pathParts == null || pathParts.Length == 0)
throw new ArgumentNullException("pathParts");
string[] allStrings = new string[pathParts.Length + 1];
allStrings[0] = path1.PathString;
Array.Copy(pathParts, 0, allStrings, 1, pathParts.Length);
return Combine(allStrings);
}
public static Path Combine(params Path[] pathParts)
{
if (pathParts == null)
throw new ArgumentNullException();
var strs = pathParts.Select(p => p.PathString);
return Combine(strs.ToArray());
}
public static Path Combine(params string[] pathParts)
{
if (pathParts == null)
throw new ArgumentNullException();
if (pathParts.Length < 2)
throw new ArgumentException("Expected at least two parts to combine.");
var output = BadPath.Combine(pathParts[0], pathParts[1]);
for (var i = 2; i < pathParts.Length; i++)
{
output = BadPath.Combine(output, pathParts[i]);
}
return new Path(output);
}
public Path Parent
{
get
{
var path = this.PathString;
path = path.TrimEnd(Path.DirectorySeperatorChars);
var parentEnd = path.LastIndexOfAny(Path.DirectorySeperatorChars);
if (parentEnd >= 0 && parentEnd > GetPathRoot(path).Length)
{
var result = path.Substring(0, parentEnd);
return new Path(result);
}
else
return Path.Empty;
}
}
///
/// Indicates if the file or directory at the specified path exists.
/// For code compatibility with .
///
public bool Exists
{
get { return FileSystem.Exists(this); }
}
///
/// True if the path is a rooted/fully qualified path. Otherwise returns false if it is a relative path.
/// Compatible with .
///
public bool IsPathRooted
{
get
{
/* The IsPathRooted method returns true if the first character is a directory separator character such as "\", or if the path starts with a drive letter and colon (:).
* For example, it returns true for path strings such as "\\MyDir\\MyFile.txt", "C:\\MyDir", or "C: MyDir". It returns false for path strings such as "MyDir".
* - https://msdn.microsoft.com/en-us/library/system.io.path.ispathrooted%28v=vs.110%29.aspx
*/
var pathString = this.PathString;
bool rooted =
DirectorySeperatorChars.Any(c => c == pathString[0])
|| pathString.Length >= 2 && pathString[1] == ':';
return rooted;
}
}
///
/// For code compatibility with
///
public System.IO.StreamWriter CreateText()
{
var stream = FileSystem.CreateFile(this);
return new System.IO.StreamWriter(stream, System.Text.Encoding.UTF8);
}
///
/// For code compatibility with
///
public static string GetFileName(string path)
{
return BadPath.GetFileName(path);
}
}
}