using System; using System.IO; using System.Runtime.CompilerServices; using System.Text; using JetBrains.Annotations; using UnityEngine; namespace NugetForUnity.Helper { /// /// Helper class to handle path operations. /// internal static class PathHelper { /// /// The path comparison type to use for the current platform the editor is running on. /// internal static readonly StringComparison PathComparisonType = Application.platform == RuntimePlatform.WindowsEditor || Application.platform == RuntimePlatform.OSXEditor ? StringComparison.OrdinalIgnoreCase : StringComparison.Ordinal; /// /// Create a relative path from one path to another. Paths will be resolved before calculating the difference. /// Default path comparison for the active platform will be used (OrdinalIgnoreCase for Windows or Mac, Ordinal for Unix). /// /// /// Should the project upgrade to Unity version that has .NET Standard 2.1, this can be removed and instead we will use: /// Path.GetRelativePath(relativeTo, path). /// /// . /// /// The source path the output should be relative to. This path is always considered to be a directory. /// The destination path. /// The relative path or if the paths don't share the same root. [NotNull] internal static string GetRelativePath([NotNull] string relativeTo, [NotNull] string path) { if (string.IsNullOrEmpty(relativeTo)) { throw new ArgumentNullException(nameof(relativeTo)); } if (string.IsNullOrEmpty(path)) { return path; } 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)) { return path; } var commonLength = GetCommonPathLength(relativeTo, path, PathComparisonType == 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)) { relativeToLength--; } var pathEndsInSeparator = EndsInDirectorySeparator(path); var pathLength = path.Length; if (pathEndsInSeparator) { pathLength--; } // If we have effectively the same path, return "." if (relativeToLength == pathLength && commonLength >= relativeToLength) { return "."; } // We have the same root, we need to calculate the difference now using the // common Length and Segment count past the length. // // Some examples: // // C:\Foo C:\Bar L3, S1 -> ..\Bar // C:\Foo C:\Foo\Bar L6, S0 -> Bar // C:\Foo\Bar C:\Bar\Bar L3, S2 -> ..\..\Bar\Bar // C:\Foo\Foo C:\Foo\Bar L7, S1 -> ..\Bar var sb = new StringBuilder(); sb.EnsureCapacity(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 (IsDirectorySeparator(relativeTo[i])) { sb.Append(Path.DirectorySeparatorChar); sb.Append(".."); } } } else if (IsDirectorySeparator(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.Substring(commonLength, differenceLength)); } return sb.ToString(); } /// /// Get the common path length from the start of the string. /// private static int GetCommonPathLength([NotNull] string first, [NotNull] string second, bool ignoreCase) { var commonChars = EqualStartingCharacterCount(first, second, 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 || IsDirectorySeparator(second[commonChars]))) { return commonChars; } if (commonChars == second.Length && IsDirectorySeparator(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 && !IsDirectorySeparator(first[commonChars - 1])) { commonChars--; } return commonChars; } /// /// Gets the count of common characters from the left optionally ignoring case. /// private static int EqualStartingCharacterCount([CanBeNull] string first, [CanBeNull] string second, bool ignoreCase) { if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second)) { return 0; } var commonChars = 0; for (; commonChars < first.Length && commonChars < second.Length; ++commonChars) { if (first[commonChars] == second[commonChars] || (ignoreCase && char.ToUpperInvariant(first[commonChars]) == char.ToUpperInvariant(second[commonChars]))) { continue; } break; } return commonChars; } /// /// Returns true if the two paths have the same root. /// private static bool AreRootsEqual([NotNull] string first, [NotNull] string second) { var firstRoot = Path.GetPathRoot(first); var secondRoot = Path.GetPathRoot(second); return string.Equals(firstRoot, secondRoot, PathComparisonType); } /// /// True if the given character is a directory separator. /// /// The char to compare. /// True if is a separator. [MethodImpl(MethodImplOptions.AggressiveInlining)] private static bool IsDirectorySeparator(char c) { return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; } /// /// Returns true if the path ends in a directory separator. /// private static bool EndsInDirectorySeparator([CanBeNull] string path) { return !string.IsNullOrEmpty(path) && IsDirectorySeparator(path[path.Length - 1]); } } }