using System; using System.Collections.Generic; using NzbDrone.Common.Extensions; namespace NzbDrone.Common.Disk { public struct OsPath : IEquatable { private readonly string _path; private readonly OsPathKind _kind; public OsPath(string path) { if (path == null) { _kind = OsPathKind.Unknown; _path = string.Empty; } else { _kind = DetectPathKind(path); _path = FixSlashes(path, _kind); } } public OsPath(string path, OsPathKind kind) { if (path == null) { _kind = kind; _path = string.Empty; } else { _kind = kind; _path = FixSlashes(path, kind); } } private static OsPathKind DetectPathKind(string path) { if (path.StartsWith("/")) { return OsPathKind.Unix; } if (HasWindowsDriveLetter(path) || path.Contains('\\')) { return OsPathKind.Windows; } if (path.Contains('/')) { return OsPathKind.Unix; } return OsPathKind.Unknown; } private static bool HasWindowsDriveLetter(string path) { if (path.Length < 2) { return false; } if (!char.IsLetter(path[0]) || path[1] != ':') { return false; } if (path.Length > 2 && path[2] != '\\' && path[2] != '/') { return false; } return true; } private static string FixSlashes(string path, OsPathKind kind) { switch (kind) { case OsPathKind.Windows: return path.Replace('/', '\\'); case OsPathKind.Unix: path = path.Replace('\\', '/'); while (path.Contains("//")) { path = path.Replace("//", "/"); } return path; } return path; } public OsPathKind Kind => _kind; public bool IsWindowsPath => _kind == OsPathKind.Windows; public bool IsUnixPath => _kind == OsPathKind.Unix; public bool IsEmpty => _path.IsNullOrWhiteSpace(); public bool IsRooted { get { if (IsWindowsPath) { return _path.StartsWith(@"\\") || HasWindowsDriveLetter(_path); } if (IsUnixPath) { return _path.StartsWith("/"); } return false; } } public OsPath Directory { get { var index = GetFileNameIndex(); if (index == -1) { return new OsPath(null); } return new OsPath(_path.Substring(0, index), _kind).AsDirectory(); } } public string FullPath => _path; public string FileName { get { var index = GetFileNameIndex(); if (index == -1) { var path = _path.Trim('\\', '/'); if (path.Length == 0) { return null; } return path; } return _path.Substring(index).Trim('\\', '/'); } } public bool IsValid => _path.IsPathValid(PathValidationType.CurrentOs); private int GetFileNameIndex() { if (_path.Length < 2) { return -1; } var index = _path.LastIndexOfAny(new[] { '/', '\\' }, _path.Length - 2); if (index == -1) { return -1; } if (_path.StartsWith(@"\\") && index < 2) { return -1; } if (_path.StartsWith("/") && index == 0) { index++; } return index; } private string[] GetFragments() { return _path.Split(new char[] { '\\', '/' }, StringSplitOptions.RemoveEmptyEntries); } public override string ToString() { return _path; } public override int GetHashCode() { return _path.ToLowerInvariant().GetHashCode(); } public override bool Equals(object obj) { if (obj is OsPath) { return Equals((OsPath)obj); } if (obj is string) { return Equals(new OsPath(obj as string)); } return false; } public OsPath AsDirectory() { if (IsEmpty) { return this; } switch (Kind) { case OsPathKind.Windows: return new OsPath(_path.TrimEnd('\\') + "\\", _kind); case OsPathKind.Unix: return new OsPath(_path.TrimEnd('/') + "/", _kind); } return this; } public bool Contains(OsPath other) { if (!IsRooted || !other.IsRooted) { return false; } var leftFragments = GetFragments(); var rightFragments = other.GetFragments(); if (rightFragments.Length < leftFragments.Length) { return false; } var stringComparison = (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture; for (var i = 0; i < leftFragments.Length; i++) { if (!string.Equals(leftFragments[i], rightFragments[i], stringComparison)) { return false; } } return true; } public bool Equals(OsPath other) { if (ReferenceEquals(other, null)) { return false; } if (_path == other._path) { return true; } var left = _path; var right = other._path; if (Kind == OsPathKind.Windows || other.Kind == OsPathKind.Windows) { return string.Equals(left, right, StringComparison.InvariantCultureIgnoreCase); } return string.Equals(left, right, StringComparison.InvariantCulture); } public static bool operator ==(OsPath left, OsPath right) { if (ReferenceEquals(left, null)) { return ReferenceEquals(right, null); } return left.Equals(right); } public static bool operator !=(OsPath left, OsPath right) { if (ReferenceEquals(left, null)) { return !ReferenceEquals(right, null); } return !left.Equals(right); } public static OsPath operator +(OsPath left, OsPath right) { if (left.Kind != right.Kind && right.Kind != OsPathKind.Unknown) { throw new Exception(string.Format("Cannot combine OsPaths of different platforms ('{0}' + '{1}')", left, right)); } if (right.IsEmpty) { return left; } if (right.IsRooted) { return right; } if (left.Kind == OsPathKind.Windows || right.Kind == OsPathKind.Windows) { return new OsPath(string.Join("\\", left._path.TrimEnd('\\'), right._path.TrimStart('\\')), OsPathKind.Windows); } if (left.Kind == OsPathKind.Unix || right.Kind == OsPathKind.Unix) { return new OsPath(string.Join("/", left._path.TrimEnd('/'), right._path), OsPathKind.Unix); } return new OsPath(string.Join("/", left._path, right._path), OsPathKind.Unknown); } public static OsPath operator +(OsPath left, string right) { return left + new OsPath(right); } public static OsPath operator -(OsPath left, OsPath right) { if (!left.IsRooted || !right.IsRooted) { throw new ArgumentException("Cannot determine relative path for unrooted paths."); } var leftFragments = left.GetFragments(); var rightFragments = right.GetFragments(); var stringComparison = (left.Kind == OsPathKind.Windows || right.Kind == OsPathKind.Windows) ? StringComparison.InvariantCultureIgnoreCase : StringComparison.InvariantCulture; int i; for (i = 0; i < leftFragments.Length && i < rightFragments.Length; i++) { if (!string.Equals(leftFragments[i], rightFragments[i], stringComparison)) { break; } } if (i == 0) { return right; } var newFragments = new List(); for (var j = i; j < rightFragments.Length; j++) { newFragments.Add(".."); } for (var j = i; j < leftFragments.Length; j++) { newFragments.Add(leftFragments[j]); } if (left.FullPath.EndsWith("\\") || left.FullPath.EndsWith("/")) { newFragments.Add(string.Empty); } if (left.Kind == OsPathKind.Windows || right.Kind == OsPathKind.Windows) { return new OsPath(string.Join("\\", newFragments), OsPathKind.Unknown); } return new OsPath(string.Join("/", newFragments), OsPathKind.Unknown); } } public enum OsPathKind { Unknown, Windows, Unix } }