using System;
using System.IO;
using System.Runtime.CompilerServices;
using System.Text;
namespace NETMetaCoder
{
///
/// A helper class for manipulating file paths.
///
public static class PathHelper
{
///
/// Get the path to a file, relative to another path.
///
///
///
///
public static string GetRelativePath(string relativeTo, string path)
{
return GetRelativePath(relativeTo, path, StringComparison);
}
private static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType)
{
if (relativeTo == null)
{
throw new ArgumentNullException(nameof(relativeTo));
}
if (relativeTo.AsSpan().IsEmpty)
{
throw new ArgumentException("The relative path must not be empty.", nameof(relativeTo));
}
if (path == null)
{
throw new ArgumentNullException(nameof(path));
}
if (path.AsSpan().IsEmpty)
{
throw new ArgumentException("The relative path must not be empty.", nameof(path));
}
System.Diagnostics.Debug.Assert(comparisonType == StringComparison.Ordinal ||
comparisonType == StringComparison.OrdinalIgnoreCase);
relativeTo = Path.GetFullPath(relativeTo);
path = Path.GetFullPath(path);
// Need to check if the roots are different- if they are we need to return the "to" path.
if (!AreRootsEqual(relativeTo, path, comparisonType))
{
return path;
}
var commonLength =
GetCommonPathLength(relativeTo, path, comparisonType == StringComparison.OrdinalIgnoreCase);
// If there is nothing in common they can't share the same root, return the "to" path as is.
if (commonLength == 0)
{
return path;
}
// Trailing separators aren't significant for comparison
var relativeToLength = relativeTo.Length;
if (EndsInDirectorySeparator(relativeTo.AsSpan()))
{
relativeToLength--;
}
var pathEndsInSeparator = EndsInDirectorySeparator(path.AsSpan());
var pathLength = path.Length;
if (pathEndsInSeparator)
{
pathLength--;
}
// If we have effectively the same path, return "."
if (relativeToLength == pathLength && commonLength >= relativeToLength)
{
return ".";
}
var sb = new StringBuilder(Math.Max(relativeTo.Length, path.Length));
// Add parent segments for segments past the common on the "from" path
if (commonLength < relativeToLength)
{
sb.Append("..");
for (var i = commonLength + 1; i < relativeToLength; i++)
{
if (IsDirectorySeparatorChar(relativeTo[i]))
{
sb.Append(Path.DirectorySeparatorChar);
sb.Append("..");
}
}
}
else if (IsDirectorySeparatorChar(path[commonLength]))
{
// No parent segments and we need to eat the initial separator
// (C:\Foo C:\Foo\Bar case)
commonLength++;
}
// Now add the rest of the "to" path, adding back the trailing separator
var differenceLength = pathLength - commonLength;
if (pathEndsInSeparator)
{
differenceLength++;
}
if (differenceLength > 0)
{
if (sb.Length > 0)
{
sb.Append(Path.DirectorySeparatorChar);
}
sb.Append(path, commonLength, differenceLength);
}
return sb.ToString();
}
private static bool EndsInDirectorySeparator(ReadOnlySpan path)
=> path.Length > 0 && IsDirectorySeparatorChar(path[path.Length - 1]);
private static StringComparison StringComparison =>
IsCaseSensitive ? StringComparison.Ordinal : StringComparison.OrdinalIgnoreCase;
private static bool IsCaseSensitive
{
get
{
#if PLATFORM_WINDOWS || PLATFORM_OSX
return false;
#else
return true;
#endif
}
}
private static bool AreRootsEqual(string first, string second, StringComparison comparisonType)
{
var firstRootLength = GetRootLength(first.AsSpan());
var secondRootLength = GetRootLength(second.AsSpan());
return firstRootLength == secondRootLength &&
string.Compare(
strA: first,
indexA: 0,
strB: second,
indexB: 0,
length: firstRootLength,
comparisonType: comparisonType) ==
0;
}
private static int GetRootLength(ReadOnlySpan path) =>
path.Length > 0 && IsDirectorySeparatorChar(path[0]) ? 1 : 0;
private static int GetCommonPathLength(string first, string second, bool ignoreCase)
{
var commonChars = EqualStartingCharacterCount(first, second, ignoreCase: ignoreCase);
// If nothing matches
if (commonChars == 0)
{
return commonChars;
}
// Or we're a full string and equal length or match to a separator
if (commonChars == first.Length &&
(commonChars == second.Length || IsDirectorySeparatorChar(second[commonChars])))
{
return commonChars;
}
if (commonChars == second.Length && IsDirectorySeparatorChar(first[commonChars]))
{
return commonChars;
}
// It's possible we matched somewhere in the middle of a segment e.g. C:\Foodie and C:\Foobar.
while (commonChars > 0 && first[commonChars - 1] != Path.DirectorySeparatorChar)
{
commonChars--;
}
return commonChars;
}
private static unsafe int EqualStartingCharacterCount(string first, string second, bool ignoreCase)
{
if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second))
{
return 0;
}
var commonChars = 0;
fixed (char* f = first)
fixed (char* s = second)
{
char* l = f;
char* r = s;
char* leftEnd = l + first.Length;
char* rightEnd = r + second.Length;
while (l != leftEnd &&
r != rightEnd &&
(*l == *r || (ignoreCase && char.ToUpperInvariant((*l)) == char.ToUpperInvariant((*r)))))
{
commonChars++;
l++;
r++;
}
}
return commonChars;
}
[MethodImpl(MethodImplOptions.AggressiveInlining)]
private static bool IsDirectorySeparatorChar(char c) => c == Path.DirectorySeparatorChar;
}
}