From 922458cd0094773d463a42afe94b869aa47bf82e Mon Sep 17 00:00:00 2001 From: Ryland <41491307+ryalanms@users.noreply.github.com> Date: Thu, 17 Sep 2020 14:34:01 -0700 Subject: [PATCH] .NET Core WPF Build error on custom BaseIntermediateOutputPath #1718 (#3120) * Use more robust relative path calculation in markup code generation --- .../Internal/MarkupCompiler/MarkupCompiler.cs | 18 +- .../Internal/MarkupCompiler/PathInternal.cs | 382 ++++++++++++++++++ .../PresentationBuildTasks.csproj | 2 + 3 files changed, 389 insertions(+), 13 deletions(-) create mode 100644 src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/MS/Internal/MarkupCompiler/PathInternal.cs diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/MS/Internal/MarkupCompiler/MarkupCompiler.cs b/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/MS/Internal/MarkupCompiler/MarkupCompiler.cs index 8adf6cf0664..a2dce6f3559 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/MS/Internal/MarkupCompiler/MarkupCompiler.cs +++ b/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/MS/Internal/MarkupCompiler/MarkupCompiler.cs @@ -1575,19 +1575,11 @@ private string ParentFolderPrefix { get { - string parentFolderPrefix = string.Empty; - if (TargetPath.StartsWith(SourceFileInfo.SourcePath, StringComparison.OrdinalIgnoreCase)) - { - string relPath = TargetPath.Substring(SourceFileInfo.SourcePath.Length); - relPath += SourceFileInfo.RelativeSourceFilePath; - string[] dirs = relPath.Split(new Char[] { Path.DirectorySeparatorChar }); - for (int i = 1; i < dirs.Length; i++) - { - parentFolderPrefix += PARENTFOLDER; - } - } - - return parentFolderPrefix; +#if NETFX + return PathInternal.GetRelativePath(TargetPath, SourceFileInfo.SourcePath, StringComparison.OrdinalIgnoreCase); +#else + return Path.GetRelativePath(TargetPath, SourceFileInfo.SourcePath); +#endif } } diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/MS/Internal/MarkupCompiler/PathInternal.cs b/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/MS/Internal/MarkupCompiler/PathInternal.cs new file mode 100644 index 00000000000..661aa18714e --- /dev/null +++ b/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/MS/Internal/MarkupCompiler/PathInternal.cs @@ -0,0 +1,382 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +//--------------------------------------------------------------------------- +// +// Description: +// Returns a relative path from one path to another. +// +// Paths are resolved by calling the GetFullPath method before calculating +// the difference. The method uses the default file path comparison for the +// current platform (StringComparison.OrdinalIgnoreCase for Windows.) +// +//--------------------------------------------------------------------------- + +#pragma warning disable 1634, 1691 + +using System; +using System.Xml; +using System.IO; +using System.Text; +using System.Reflection; +using System.Globalization; +using System.ComponentModel; +using System.Security.Cryptography; + +using System.CodeDom; +using System.CodeDom.Compiler; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel.Design.Serialization; + +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +using System.Threading; +using MS.Internal.Markup; +using MS.Internal.Tasks; +using MS.Utility; // for SR +using Microsoft.Build.Utilities; +using Microsoft.Build.Tasks.Windows; +using System.Runtime.CompilerServices; + +namespace MS.Internal +{ + internal sealed class PathInternal + { + internal static string GetRelativePath(string relativeTo, string path, StringComparison comparisonType) + { + if (relativeTo == null) + throw new ArgumentNullException(nameof(relativeTo)); + + if (PathInternal.IsEffectivelyEmpty(relativeTo.AsSpan())) + throw new ArgumentException(nameof(relativeTo)); + + if (path == null) + throw new ArgumentNullException(nameof(path)); + + if (PathInternal.IsEffectivelyEmpty(path.AsSpan())) + throw new ArgumentException(nameof(path)); + + 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 (!PathInternal.AreRootsEqual(relativeTo, path, comparisonType)) + return path; + + int commonLength = PathInternal.GetCommonPathLength(relativeTo, path, ignoreCase: 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 + int relativeToLength = relativeTo.Length; + if (DoesEndInDirectorySeparator(relativeTo.AsSpan())) + relativeToLength--; + + bool pathEndsInSeparator = DoesEndInDirectorySeparator(path.AsSpan()); + int pathLength = path.Length; + if (pathEndsInSeparator) + pathLength--; + + // If we have effectively the same path, return "." + if (relativeToLength == pathLength && commonLength >= relativeToLength) return CurrentDir.ToString(); + + // 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(); + + // Add parent segments for segments past the common on the "from" path + if (commonLength < relativeToLength) + { + sb.Append(ParentDir); + + for (int i = commonLength + 1; i < relativeToLength; i++) + { + if (PathInternal.IsDirectorySeparator(relativeTo[i])) + { + sb.Append(Path.DirectorySeparatorChar); + sb.Append(ParentDir); + } + } + } + else if (PathInternal.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 + int 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(); + } + + /// + /// True if the given character is a directory separator. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + internal static bool IsDirectorySeparator(char c) + { + return c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; + } + + /// + /// Get the common path length from the start of the string. + /// + internal static int GetCommonPathLength(string first, string second, bool ignoreCase) + { + int 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; + } + + /// + /// Returns true if the two paths have the same root + /// + internal static bool AreRootsEqual(string first, string second, StringComparison comparisonType) + { + int firstRootLength = GetRootLength(first.AsSpan()); + int secondRootLength = GetRootLength(second.AsSpan()); + + return firstRootLength == secondRootLength + && string.Compare( + strA: first, + indexA: 0, + strB: second, + indexB: 0, + length: firstRootLength, + comparisonType: comparisonType) == 0; + } + + /// + /// Returns true if the path is effectively empty for the current OS. + /// For unix, this is empty or null. For Windows, this is empty, null, or + /// just spaces ((char)32). + /// + internal static bool IsEffectivelyEmpty(ReadOnlySpan path) + { + if (path.IsEmpty) + return true; + + foreach (char c in path) + { + if (c != ' ') + return false; + } + return true; + } + + + /// + /// Gets the length of the root of the path (drive, share, etc.). + /// + internal static int GetRootLength(ReadOnlySpan path) + { + int pathLength = path.Length; + int i = 0; + + bool deviceSyntax = IsDevice(path); + bool deviceUnc = deviceSyntax && IsDeviceUNC(path); + + if ((!deviceSyntax || deviceUnc) && pathLength > 0 && IsDirectorySeparator(path[0])) + { + // UNC or simple rooted path (e.g. "\foo", NOT "\\?\C:\foo") + if (deviceUnc || (pathLength > 1 && IsDirectorySeparator(path[1]))) + { + // UNC (\\?\UNC\ or \\), scan past server\share + + // Start past the prefix ("\\" or "\\?\UNC\") + i = deviceUnc ? UncExtendedPrefixLength : UncPrefixLength; + + // Skip two separators at most + int n = 2; + while (i < pathLength && (!IsDirectorySeparator(path[i]) || --n > 0)) + i++; + } + else + { + // Current drive rooted (e.g. "\foo") + i = 1; + } + } + else if (deviceSyntax) + { + // Device path (e.g. "\\?\.", "\\.\") + // Skip any characters following the prefix that aren't a separator + i = DevicePrefixLength; + while (i < pathLength && !IsDirectorySeparator(path[i])) + i++; + + // If there is another separator take it, as long as we have had at least one + // non-separator after the prefix (e.g. don't take "\\?\\", but take "\\?\a\") + if (i < pathLength && i > DevicePrefixLength && IsDirectorySeparator(path[i])) + i++; + } + else if (pathLength >= 2 + && path[1] == VolumeSeparatorChar + && IsValidDriveChar(path[0])) + { + // Valid drive specified path ("C:", "D:", etc.) + i = 2; + + // If the colon is followed by a directory separator, move past it (e.g "C:\") + if (pathLength > 2 && IsDirectorySeparator(path[2])) + i++; + } + + return i; + } + + /// + /// Gets the count of common characters from the left optionally ignoring case + /// + internal static unsafe int EqualStartingCharacterCount(string first, string second, bool ignoreCase) + { + if (string.IsNullOrEmpty(first) || string.IsNullOrEmpty(second)) return 0; + + int 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; + } + + /// + /// Returns true if the path uses any of the DOS device path syntaxes. ("\\.\", "\\?\", or "\??\") + /// + internal static bool IsDevice(ReadOnlySpan path) + { + // If the path begins with any two separators is will be recognized and normalized and prepped with + // "\??\" for internal usage correctly. "\??\" is recognized and handled, "/??/" is not. + return IsExtended(path) + || + ( + path.Length >= DevicePrefixLength + && IsDirectorySeparator(path[0]) + && IsDirectorySeparator(path[1]) + && (path[2] == '.' || path[2] == '?') + && IsDirectorySeparator(path[3]) + ); + } + + /// + /// Returns true if the path is a device UNC (\\?\UNC\, \\.\UNC\) + /// + internal static bool IsDeviceUNC(ReadOnlySpan path) + { + return path.Length >= UncExtendedPrefixLength + && IsDevice(path) + && IsDirectorySeparator(path[7]) + && path[4] == 'U' + && path[5] == 'N' + && path[6] == 'C'; + } + + /// + /// Returns true if the given character is a valid drive letter + /// + internal static bool IsValidDriveChar(char value) + { + return ((value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z')); + } + + /// + /// Returns true if the path uses the canonical form of extended syntax ("\\?\" or "\??\"). If the + /// path matches exactly (cannot use alternate directory separators) Windows will skip normalization + /// and path length checks. + /// + internal static bool IsExtended(ReadOnlySpan path) + { + // While paths like "//?/C:/" will work, they're treated the same as "\\.\" paths. + // Skipping of normalization will *only* occur if back slashes ('\') are used. + return path.Length >= DevicePrefixLength + && path[0] == '\\' + && (path[1] == '\\' || path[1] == '?') + && path[2] == '?' + && path[3] == '\\'; + } + + /// + /// Returns true if the path ends in a directory separator. + /// + public static bool DoesEndInDirectorySeparator(ReadOnlySpan path) + => path.Length > 0 && PathInternal.IsDirectorySeparator(path[path.Length - 1]); + + /// + /// Returns true if the path ends in a directory separator. + /// + public static bool DoesEndInDirectorySeparator(string path) + => path != null && path.Length > 0 && PathInternal.IsDirectorySeparator(path[path.Length - 1]); + + internal const char VolumeSeparatorChar = ':'; + // \\?\UNC\, \\.\UNC\ + internal const int UncExtendedPrefixLength = 8; + // \\?\, \\.\, \??\ + internal const int DevicePrefixLength = 4; + // \\ + internal const int UncPrefixLength = 2; + // ".." + internal const string ParentDir = ".."; + // '.' + internal const char CurrentDir = '.'; + } +} + diff --git a/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/PresentationBuildTasks.csproj b/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/PresentationBuildTasks.csproj index 9946655962a..38b33730d47 100644 --- a/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/PresentationBuildTasks.csproj +++ b/src/Microsoft.DotNet.Wpf/src/PresentationBuildTasks/PresentationBuildTasks.csproj @@ -27,6 +27,7 @@ true true AnyCPU;x64 + true @@ -278,6 +279,7 @@ +