| File: Backchannel\AppHostPathComparer.cs | Web Access |
| Project: src\src\Aspire.Cli\Aspire.Cli.csproj (aspire) |
// Licensed to the .NET Foundation under one or more agreements. // The .NET Foundation licenses this file to you under the MIT license. using Aspire.Hosting.Utils; namespace Aspire.Cli.Backchannel; /// <summary> /// Provides the platform-specific path identity rules used for AppHost selection. /// </summary> internal static class AppHostPathComparer { public static bool PathsEqual(string leftPath, string rightPath) { return PathsEqual(leftPath, rightPath, Comparer); } internal static bool PathsEqual(string leftPath, string rightPath, StringComparer fallbackComparer) { if (StringComparer.Ordinal.Equals(leftPath, rightPath)) { return true; } var leftCanonicalized = TryGetCanonicalPath(leftPath, out var canonicalLeftPath); var rightCanonicalized = TryGetCanonicalPath(rightPath, out var canonicalRightPath); if (leftCanonicalized || rightCanonicalized) { if (!leftCanonicalized || !rightCanonicalized) { return false; } // Stored filesystem spelling is authoritative even on Windows because an individual // directory can opt into case-sensitive semantics. The volume root is identity rather // than a directory segment, so canonicalize only that root before the ordinal comparison. return StringComparer.Ordinal.Equals( NormalizeRootIdentity(canonicalLeftPath), NormalizeRootIdentity(canonicalRightPath)); } return fallbackComparer.Equals( PathNormalizer.ResolveToFilesystemPath(PathNormalizer.ResolveSymlinks(leftPath)), PathNormalizer.ResolveToFilesystemPath(PathNormalizer.ResolveSymlinks(rightPath))); } internal static string NormalizeRootIdentity(string path) { var driveLetterIndex = path.Length >= 3 && path[1] == ':' && path[2] is '\\' or '/' && char.IsAsciiLetter(path[0]) ? 0 : path.Length >= 7 && path[0] is '\\' or '/' && path[1] is '\\' or '/' && path[2] is '?' or '.' && path[3] is '\\' or '/' && path[5] == ':' && path[6] is '\\' or '/' && char.IsAsciiLetter(path[4]) ? 4 : -1; if (driveLetterIndex >= 0) { var normalizedDriveLetter = char.ToUpperInvariant(path[driveLetterIndex]); return normalizedDriveLetter == path[driveLetterIndex] ? path : $"{path[..driveLetterIndex]}{normalizedDriveLetter}{path[(driveLetterIndex + 1)..]}"; } var rootSegmentStart = path.Length >= 8 && path[0] is '\\' or '/' && path[1] is '\\' or '/' && path[2] is '?' or '.' && path[3] is '\\' or '/' && path.AsSpan(4, 3).Equals("UNC", StringComparison.OrdinalIgnoreCase) && path[7] is '\\' or '/' ? 8 : path.Length >= 2 && path[0] is '\\' or '/' && path[1] is '\\' or '/' ? 2 : -1; if (rootSegmentStart < 0) { return path; } var serverSeparator = FindSeparator(path, rootSegmentStart); if (serverSeparator < 0 || serverSeparator == path.Length - 1) { return path; } var shareSeparator = FindSeparator(path, serverSeparator + 1); var rootEnd = shareSeparator >= 0 ? shareSeparator : path.Length; var normalizedRoot = path[..rootEnd].ToUpperInvariant(); return normalizedRoot.Equals(path.AsSpan(0, rootEnd), StringComparison.Ordinal) ? path : string.Concat(normalizedRoot, path.AsSpan(rootEnd)); } private static int FindSeparator(string path, int startIndex) { for (var i = startIndex; i < path.Length; i++) { if (path[i] is '\\' or '/') { return i; } } return -1; } private static bool TryGetCanonicalPath(string path, out string canonicalPath) { canonicalPath = path; return PathNormalizer.TryResolveSymlinks(path, out var symlinkResolvedPath) && PathNormalizer.TryResolveToFilesystemPath(symlinkResolvedPath, out canonicalPath); } private static StringComparer Comparer => OperatingSystem.IsWindows() ? StringComparer.OrdinalIgnoreCase : StringComparer.Ordinal; }