diff --git a/SourceLink.sln b/SourceLink.sln index 3f21885f6..ddd10ae9d 100644 --- a/SourceLink.sln +++ b/SourceLink.sln @@ -1,7 +1,7 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio Version 16 -VisualStudioVersion = 16.0.29011.400 +# Visual Studio Version 17 +VisualStudioVersion = 17.9.34322.126 MinimumVisualStudioVersion = 10.0.40219.1 Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.Build.Tasks.Git", "src\Microsoft.Build.Tasks.Git\Microsoft.Build.Tasks.Git.csproj", "{A86F9DC3-9595-44AC-ACC6-025FB74813E6}" EndProject @@ -33,19 +33,6 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.GitLab EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.GitLab.UnitTests", "src\SourceLink.GitLab.UnitTests\Microsoft.SourceLink.GitLab.UnitTests.csproj", "{46C6BD7C-ABB7-4444-B095-C63868FACC41}" EndProject -Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{7621150D-714B-4C87-8834-199A0495B92F}" - ProjectSection(SolutionItems) = preProject - src\Common\CommonResources.resx = src\Common\CommonResources.resx - src\Common\GetSourceLinkUrlGitTask.cs = src\Common\GetSourceLinkUrlGitTask.cs - src\Common\Names.cs = src\Common\Names.cs - src\Common\NullableAttributes.cs = src\Common\NullableAttributes.cs - src\Common\PathUtilities.cs = src\Common\PathUtilities.cs - src\Common\SequenceComparer.cs = src\Common\SequenceComparer.cs - src\Common\TeamFoundationUrlParser.cs = src\Common\TeamFoundationUrlParser.cs - src\Common\TranslateRepositoryUrlGitTask.cs = src\Common\TranslateRepositoryUrlGitTask.cs - src\Common\UriUtilities.cs = src\Common\UriUtilities.cs - EndProjectSection -EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.Bitbucket.Git", "src\SourceLink.Bitbucket.Git\Microsoft.SourceLink.Bitbucket.Git.csproj", "{9138BCAB-41BC-4DED-8A3F-7EB4177CDCE1}" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.Bitbucket.Git.UnitTests", "src\SourceLink.Bitbucket.Git.UnitTests\Microsoft.SourceLink.Bitbucket.Git.UnitTests.csproj", "{97AD20FC-9526-4579-8E95-47890FE12783}" @@ -74,12 +61,39 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.Gitee" EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.Gitee.UnitTests", "src\SourceLink.Gitee.UnitTests\Microsoft.SourceLink.Gitee.UnitTests.csproj", "{D647294C-40C0-4624-A76B-C4C276233609}" EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Common", "Common", "{4BB63B07-D1DD-48CF-845F-34B3BBB0F596}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "AzureDevOps", "AzureDevOps", "{8A4D5938-D99A-4BEC-8C02-4BF69E70D720}" + ProjectSection(SolutionItems) = preProject + src\Common\AzureDevOps\AzureDevOpsUrlParser.cs = src\Common\AzureDevOps\AzureDevOpsUrlParser.cs + src\Common\AzureDevOps\Items.props = src\Common\AzureDevOps\Items.props + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "GitProvider", "GitProvider", "{59A2C2E8-AE72-46C4-B51C-4244D6451597}" + ProjectSection(SolutionItems) = preProject + src\Common\GitProvider\CommonResources.resx = src\Common\GitProvider\CommonResources.resx + src\Common\GitProvider\GetSourceLinkUrlGitTask.cs = src\Common\GitProvider\GetSourceLinkUrlGitTask.cs + src\Common\GitProvider\Items.props = src\Common\GitProvider\Items.props + src\Common\GitProvider\TranslateRepositoryUrlGitTask.cs = src\Common\GitProvider\TranslateRepositoryUrlGitTask.cs + EndProjectSection +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Utilities", "Utilities", "{F351EFBE-DAEE-437A-A492-4F1434489693}" + ProjectSection(SolutionItems) = preProject + src\Common\Utilities\Hash.cs = src\Common\Utilities\Hash.cs + src\Common\Utilities\Index.cs = src\Common\Utilities\Index.cs + src\Common\Utilities\IsExternalInit.cs = src\Common\Utilities\IsExternalInit.cs + src\Common\Utilities\Names.cs = src\Common\Utilities\Names.cs + src\Common\Utilities\NullableAttributes.cs = src\Common\Utilities\NullableAttributes.cs + src\Common\Utilities\PathUtilities.cs = src\Common\Utilities\PathUtilities.cs + src\Common\Utilities\Range.cs = src\Common\Utilities\Range.cs + src\Common\Utilities\RequiredMemberAttribute.cs = src\Common\Utilities\RequiredMemberAttribute.cs + src\Common\Utilities\SequenceComparer.cs = src\Common\Utilities\SequenceComparer.cs + src\Common\Utilities\SetsRequiredMembersAttribute.cs = src\Common\Utilities\SetsRequiredMembersAttribute.cs + src\Common\Utilities\UriUtilities.cs = src\Common\Utilities\UriUtilities.cs + src\Common\Utilities\ValueTuple.cs = src\Common\Utilities\ValueTuple.cs + EndProjectSection +EndProject Global - GlobalSection(SharedMSBuildProjectFiles) = preSolution - src\SourceLink.Tools\Microsoft.SourceLink.Tools.projitems*{4376b613-cd5b-4274-9071-30989769b0b2}*SharedItemsImports = 5 - src\SourceLink.Tools\Microsoft.SourceLink.Tools.projitems*{5df76cc2-5f0e-45a6-ad56-6bbbccbc1a78}*SharedItemsImports = 13 - src\SourceLink.Tools\Microsoft.SourceLink.Tools.projitems*{99d113a9-24ec-471d-9f74-d2ac2f16220b}*SharedItemsImports = 5 - EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -201,7 +215,17 @@ Global GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE EndGlobalSection + GlobalSection(NestedProjects) = preSolution + {8A4D5938-D99A-4BEC-8C02-4BF69E70D720} = {4BB63B07-D1DD-48CF-845F-34B3BBB0F596} + {59A2C2E8-AE72-46C4-B51C-4244D6451597} = {4BB63B07-D1DD-48CF-845F-34B3BBB0F596} + {F351EFBE-DAEE-437A-A492-4F1434489693} = {4BB63B07-D1DD-48CF-845F-34B3BBB0F596} + EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {0E6D4D3C-C44D-4A03-B5C0-10A36F51E272} EndGlobalSection + GlobalSection(SharedMSBuildProjectFiles) = preSolution + src\SourceLink.Tools\Microsoft.SourceLink.Tools.projitems*{4376b613-cd5b-4274-9071-30989769b0b2}*SharedItemsImports = 5 + src\SourceLink.Tools\Microsoft.SourceLink.Tools.projitems*{5df76cc2-5f0e-45a6-ad56-6bbbccbc1a78}*SharedItemsImports = 13 + src\SourceLink.Tools\Microsoft.SourceLink.Tools.projitems*{99d113a9-24ec-471d-9f74-d2ac2f16220b}*SharedItemsImports = 5 + EndGlobalSection EndGlobal diff --git a/src/Common/AzureDevOpsUrlParser.cs b/src/Common/AzureDevOps/AzureDevOpsUrlParser.cs similarity index 100% rename from src/Common/AzureDevOpsUrlParser.cs rename to src/Common/AzureDevOps/AzureDevOpsUrlParser.cs diff --git a/src/Common/AzureDevOps/Items.props b/src/Common/AzureDevOps/Items.props new file mode 100644 index 000000000..a6cd01678 --- /dev/null +++ b/src/Common/AzureDevOps/Items.props @@ -0,0 +1,6 @@ + + + + + + \ No newline at end of file diff --git a/src/Common/CommonResources.resx b/src/Common/GitProvider/CommonResources.resx similarity index 100% rename from src/Common/CommonResources.resx rename to src/Common/GitProvider/CommonResources.resx diff --git a/src/Common/GetSourceLinkUrlGitTask.cs b/src/Common/GitProvider/GetSourceLinkUrlGitTask.cs similarity index 100% rename from src/Common/GetSourceLinkUrlGitTask.cs rename to src/Common/GitProvider/GetSourceLinkUrlGitTask.cs diff --git a/src/Common/GitProvider/Items.props b/src/Common/GitProvider/Items.props new file mode 100644 index 000000000..7d6bb0a17 --- /dev/null +++ b/src/Common/GitProvider/Items.props @@ -0,0 +1,16 @@ + + + + + + + + Microsoft.Build.Tasks.SourceControl + true + + + + + + + \ No newline at end of file diff --git a/src/Common/TranslateRepositoryUrlGitTask.cs b/src/Common/GitProvider/TranslateRepositoryUrlGitTask.cs similarity index 100% rename from src/Common/TranslateRepositoryUrlGitTask.cs rename to src/Common/GitProvider/TranslateRepositoryUrlGitTask.cs diff --git a/src/Common/xlf/CommonResources.cs.xlf b/src/Common/GitProvider/xlf/CommonResources.cs.xlf similarity index 100% rename from src/Common/xlf/CommonResources.cs.xlf rename to src/Common/GitProvider/xlf/CommonResources.cs.xlf diff --git a/src/Common/xlf/CommonResources.de.xlf b/src/Common/GitProvider/xlf/CommonResources.de.xlf similarity index 100% rename from src/Common/xlf/CommonResources.de.xlf rename to src/Common/GitProvider/xlf/CommonResources.de.xlf diff --git a/src/Common/xlf/CommonResources.es.xlf b/src/Common/GitProvider/xlf/CommonResources.es.xlf similarity index 100% rename from src/Common/xlf/CommonResources.es.xlf rename to src/Common/GitProvider/xlf/CommonResources.es.xlf diff --git a/src/Common/xlf/CommonResources.fr.xlf b/src/Common/GitProvider/xlf/CommonResources.fr.xlf similarity index 100% rename from src/Common/xlf/CommonResources.fr.xlf rename to src/Common/GitProvider/xlf/CommonResources.fr.xlf diff --git a/src/Common/xlf/CommonResources.it.xlf b/src/Common/GitProvider/xlf/CommonResources.it.xlf similarity index 100% rename from src/Common/xlf/CommonResources.it.xlf rename to src/Common/GitProvider/xlf/CommonResources.it.xlf diff --git a/src/Common/xlf/CommonResources.ja.xlf b/src/Common/GitProvider/xlf/CommonResources.ja.xlf similarity index 100% rename from src/Common/xlf/CommonResources.ja.xlf rename to src/Common/GitProvider/xlf/CommonResources.ja.xlf diff --git a/src/Common/xlf/CommonResources.ko.xlf b/src/Common/GitProvider/xlf/CommonResources.ko.xlf similarity index 100% rename from src/Common/xlf/CommonResources.ko.xlf rename to src/Common/GitProvider/xlf/CommonResources.ko.xlf diff --git a/src/Common/xlf/CommonResources.pl.xlf b/src/Common/GitProvider/xlf/CommonResources.pl.xlf similarity index 100% rename from src/Common/xlf/CommonResources.pl.xlf rename to src/Common/GitProvider/xlf/CommonResources.pl.xlf diff --git a/src/Common/xlf/CommonResources.pt-BR.xlf b/src/Common/GitProvider/xlf/CommonResources.pt-BR.xlf similarity index 100% rename from src/Common/xlf/CommonResources.pt-BR.xlf rename to src/Common/GitProvider/xlf/CommonResources.pt-BR.xlf diff --git a/src/Common/xlf/CommonResources.ru.xlf b/src/Common/GitProvider/xlf/CommonResources.ru.xlf similarity index 100% rename from src/Common/xlf/CommonResources.ru.xlf rename to src/Common/GitProvider/xlf/CommonResources.ru.xlf diff --git a/src/Common/xlf/CommonResources.tr.xlf b/src/Common/GitProvider/xlf/CommonResources.tr.xlf similarity index 100% rename from src/Common/xlf/CommonResources.tr.xlf rename to src/Common/GitProvider/xlf/CommonResources.tr.xlf diff --git a/src/Common/xlf/CommonResources.zh-Hans.xlf b/src/Common/GitProvider/xlf/CommonResources.zh-Hans.xlf similarity index 100% rename from src/Common/xlf/CommonResources.zh-Hans.xlf rename to src/Common/GitProvider/xlf/CommonResources.zh-Hans.xlf diff --git a/src/Common/xlf/CommonResources.zh-Hant.xlf b/src/Common/GitProvider/xlf/CommonResources.zh-Hant.xlf similarity index 100% rename from src/Common/xlf/CommonResources.zh-Hant.xlf rename to src/Common/GitProvider/xlf/CommonResources.zh-Hant.xlf diff --git a/src/Common/GitUrlMappingTask.cs b/src/Common/GitUrlMappingTask.cs deleted file mode 100644 index a31b6ad98..000000000 --- a/src/Common/GitUrlMappingTask.cs +++ /dev/null @@ -1,224 +0,0 @@ -// 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.txt file in the project root for more information. - -using System; -using System.Collections.Generic; -using System.Diagnostics; -using System.Linq; -using Microsoft.Build.Framework; -using Microsoft.Build.Utilities; - -namespace Microsoft.Build.Tasks.SourceControl -{ - public abstract class GitUrlMappingTask : Task - { - private const string SourceControlName = "git"; - private const string NotApplicableValue = "N/A"; - private const string ContentUrlMetadataName = "ContentUrl"; - - [Required] - public ITaskItem SourceRoot { get; set; } - - /// - /// List of additional repository hosts for which the task produces SourceLink URLs. - /// Each item maps a domain of a repository host (stored in the item identity) to a URL of the server that provides source file content (stored in ContentUrl metadata). - /// ContentUrl is optional. If not specified it defaults to "https://{domain}/raw". - /// - public ITaskItem[] Hosts { get; set; } - - public string ImplicitHost { get; set; } - - [Output] - public string SourceLinkUrl { get; set; } - - internal GitUrlMappingTask() { } - - protected abstract string ProviderDisplayName { get; } - protected abstract string HostsItemGroupName { get; } - protected abstract Uri GetDefaultContentUri(Uri uri); - protected abstract string BuildSourceLinkUrl(string contentUrl, string relativeUrl, string revisionId); - - public override bool Execute() - { - ExecuteImpl(); - return !Log.HasLoggedErrors; - } - - private void ExecuteImpl() - { - // skip SourceRoot that already has SourceLinkUrl set, or its SourceControl is not "git": - if (!string.IsNullOrEmpty(SourceRoot.GetMetadata(Names.SourceRoot.SourceLinkUrl)) || - !string.Equals(SourceRoot.GetMetadata(Names.SourceRoot.SourceControl), SourceControlName, StringComparison.OrdinalIgnoreCase)) - { - SourceLinkUrl = NotApplicableValue; - return; - } - - var repoUrl = SourceRoot.GetMetadata(Names.SourceRoot.RepositoryUrl); - if (!Uri.TryCreate(repoUrl, UriKind.Absolute, out var repoUri)) - { - Log.LogError(CommonResources.ValueOfWithIdentityIsInvalid, Names.SourceRoot.RepositoryUrlFullName, SourceRoot.ItemSpec, repoUrl); - return; - } - - var mappings = GetUrlMappings().ToArray(); - if (Log.HasLoggedErrors) - { - return; - } - - if (mappings.Length == 0) - { - Log.LogError(CommonResources.AtLeastOneRepositoryHostIsRequired, HostsItemGroupName, ProviderDisplayName); - return; - } - - var contentUri = GetMatchingContentUri(mappings, repoUri); - if (contentUri == null) - { - SourceLinkUrl = NotApplicableValue; - return; - } - - bool IsHexDigit(char c) - => c >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F'; - - string revisionId = SourceRoot.GetMetadata(Names.SourceRoot.RevisionId); - if (revisionId == null || revisionId.Length != 40 || !revisionId.All(IsHexDigit)) - { - Log.LogError(CommonResources.ValueOfWithIdentityIsNotValidCommitHash, Names.SourceRoot.RevisionIdFullName, SourceRoot.ItemSpec, revisionId); - return; - } - - var relativeUrl = repoUri.LocalPath.TrimEnd('/'); - - // The URL may or may not end with '.git' (case-sensitive), but content URLs do not include '.git' suffix: - const string gitUrlSuffix = ".git"; - if (relativeUrl.EndsWith(gitUrlSuffix, StringComparison.Ordinal)) - { - relativeUrl = relativeUrl.Substring(0, relativeUrl.Length - gitUrlSuffix.Length); - } - - SourceLinkUrl = BuildSourceLinkUrl(contentUri.ToString(), relativeUrl, revisionId); - } - - private struct UrlMapping - { - public readonly Uri Host; - public readonly Uri ContentUri; - public readonly bool HasDefaultContentUri; - - public UrlMapping(Uri host, Uri contentUri, bool hasDefaultContentUri) - { - Host = host; - ContentUri = contentUri; - HasDefaultContentUri = hasDefaultContentUri; - } - } - - private IEnumerable GetUrlMappings() - { - bool isValidContentUri(Uri uri) - => uri.Query == "" && uri.UserInfo == ""; - - bool tryParseAuthority(string value, out Uri uri) - => Uri.TryCreate("unknown://" + value, UriKind.Absolute, out uri) && IsAuthorityUri(uri); - - Uri getDefaultUri(string authority) - => GetDefaultContentUri(new Uri("https://" + authority, UriKind.Absolute)); - - if (Hosts != null) - { - foreach (var item in Hosts) - { - string authority = item.ItemSpec; - - if (!tryParseAuthority(authority, out var authorityUri)) - { - Log.LogError(CommonResources.ValuePassedToTaskParameterNotValidDomainName, nameof(Hosts), item.ItemSpec); - continue; - } - - Uri contentUri; - string contentUrl = item.GetMetadata(ContentUrlMetadataName); - bool hasDefaultContentUri = string.IsNullOrEmpty(contentUrl); - if (hasDefaultContentUri) - { - contentUri = getDefaultUri(authority); - } - else if (!Uri.TryCreate(contentUrl, UriKind.Absolute, out contentUri) || !isValidContentUri(contentUri)) - { - Log.LogError(CommonResources.ValuePassedToTaskParameterNotValidHostUri, nameof(Hosts), contentUrl); - continue; - } - - yield return new UrlMapping(authorityUri, contentUri, hasDefaultContentUri); - } - } - - // Add implicit host last, so that matching prefers explicitly listed hosts over the implicit one. - if (!string.IsNullOrEmpty(ImplicitHost)) - { - if (tryParseAuthority(ImplicitHost, out var authorityUri)) - { - yield return new UrlMapping(authorityUri, getDefaultUri(ImplicitHost), hasDefaultContentUri: true); - } - else - { - Log.LogError(CommonResources.ValuePassedToTaskParameterNotValidDomainName, nameof(ImplicitHost), ImplicitHost); - } - } - } - - private static Uri GetMatchingContentUri(UrlMapping[] mappings, Uri repoUri) - { - UrlMapping? findMatch(bool exactHost) - { - UrlMapping? candidate = null; - - foreach (var mapping in mappings) - { - var host = mapping.Host.Host; - var port = mapping.Host.Port; - var contentUri = mapping.ContentUri; - - if (exactHost && repoUri.Host.Equals(host, StringComparison.OrdinalIgnoreCase) || - !exactHost && repoUri.Host.EndsWith("." + host, StringComparison.OrdinalIgnoreCase)) - { - // Port matches exactly: - if (repoUri.Port == port) - { - return mapping; - } - - // Port not specified: - if (candidate == null && port == -1) - { - candidate = mapping; - } - } - } - - return candidate; - } - - var result = findMatch(exactHost: true) ?? findMatch(exactHost: false); - if (result == null) - { - return null; - } - - var value = result.Value; - - // If the mapping did not specify ContentUrl and did not specify port, - // use the port from the RepositoryUrl, if a non-default is specified. - if (value.HasDefaultContentUri && value.Host.Port == -1 && !repoUri.IsDefaultPort && value.ContentUri.Port != repoUri.Port) - { - return new Uri($"{value.ContentUri.Scheme}://{value.ContentUri.Host}:{repoUri.Port}{value.ContentUri.PathAndQuery}"); - } - - return value.ContentUri; - } - } -} diff --git a/src/Common/Utilities/Hash.cs b/src/Common/Utilities/Hash.cs new file mode 100644 index 000000000..ef351ed05 --- /dev/null +++ b/src/Common/Utilities/Hash.cs @@ -0,0 +1,419 @@ +// 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. + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using Microsoft.CodeAnalysis; + +namespace Roslyn.Utilities +{ + internal static class Hash + { + /// + /// This is how VB Anonymous Types combine hash values for fields. + /// + internal static int Combine(int newKey, int currentKey) + { + return unchecked((currentKey * (int)0xA5555529) + newKey); + } + + internal static int Combine(bool newKeyPart, int currentKey) + { + return Combine(currentKey, newKeyPart ? 1 : 0); + } + + /// + /// This is how VB Anonymous Types combine hash values for fields. + /// PERF: Do not use with enum types because that involves multiple + /// unnecessary boxing operations. Unfortunately, we can't constrain + /// T to "non-enum", so we'll use a more restrictive constraint. + /// + internal static int Combine(T newKeyPart, int currentKey) where T : class? + { + int hash = unchecked(currentKey * (int)0xA5555529); + + if (newKeyPart != null) + { + return unchecked(hash + newKeyPart.GetHashCode()); + } + + return hash; + } + + internal static int CombineValues(IEnumerable? values, int maxItemsToHash = int.MaxValue) + { + if (values == null) + { + return 0; + } + + var hashCode = 0; + var count = 0; + foreach (var value in values) + { + if (count++ >= maxItemsToHash) + { + break; + } + + // Should end up with a constrained virtual call to object.GetHashCode (i.e. avoid boxing where possible). + if (value != null) + { + hashCode = Hash.Combine(value.GetHashCode(), hashCode); + } + } + + return hashCode; + } + + internal static int CombineValues(ImmutableDictionary values, int maxItemsToHash = int.MaxValue) + where TKey : notnull + { + if (values == null) + return 0; + + var hashCode = 0; + var count = 0; + foreach (var value in values) + { + if (count++ >= maxItemsToHash) + break; + + hashCode = Hash.Combine(value.GetHashCode(), hashCode); + } + + return hashCode; + } + + internal static int CombineValues(T[]? values, int maxItemsToHash = int.MaxValue) + { + if (values == null) + { + return 0; + } + + var maxSize = Math.Min(maxItemsToHash, values.Length); + var hashCode = 0; + + for (int i = 0; i < maxSize; i++) + { + T value = values[i]; + + // Should end up with a constrained virtual call to object.GetHashCode (i.e. avoid boxing where possible). + if (value != null) + { + hashCode = Hash.Combine(value.GetHashCode(), hashCode); + } + } + + return hashCode; + } + + internal static int CombineValues(ImmutableArray values, int maxItemsToHash = int.MaxValue) + { + if (values.IsDefaultOrEmpty) + { + return 0; + } + + var hashCode = 0; + var count = 0; + foreach (var value in values) + { + if (count++ >= maxItemsToHash) + { + break; + } + + // Should end up with a constrained virtual call to object.GetHashCode (i.e. avoid boxing where possible). + if (value != null) + { + hashCode = Hash.Combine(value.GetHashCode(), hashCode); + } + } + + return hashCode; + } + + internal static int CombineValues(IEnumerable? values, StringComparer stringComparer, int maxItemsToHash = int.MaxValue) + { + if (values == null) + { + return 0; + } + + var hashCode = 0; + var count = 0; + foreach (var value in values) + { + if (count++ >= maxItemsToHash) + { + break; + } + + if (value != null) + { + hashCode = Hash.Combine(stringComparer.GetHashCode(value), hashCode); + } + } + + return hashCode; + } + + internal static int CombineValues(ImmutableArray values, StringComparer stringComparer, int maxItemsToHash = int.MaxValue) + { + if (values == null) + return 0; + + var hashCode = 0; + var count = 0; + foreach (var value in values) + { + if (count++ >= maxItemsToHash) + break; + + if (value != null) + hashCode = Hash.Combine(stringComparer.GetHashCode(value), hashCode); + } + + return hashCode; + } + + /// + /// The offset bias value used in the FNV-1a algorithm + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + internal const int FnvOffsetBias = unchecked((int)2166136261); + + /// + /// The generative factor used in the FNV-1a algorithm + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + internal const int FnvPrime = 16777619; + + /// + /// Compute the FNV-1a hash of a sequence of bytes + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The sequence of bytes + /// The FNV-1a hash of + internal static int GetFNVHashCode(byte[] data) + { + int hashCode = Hash.FnvOffsetBias; + + for (int i = 0; i < data.Length; i++) + { + hashCode = unchecked((hashCode ^ data[i]) * Hash.FnvPrime); + } + + return hashCode; + } + + /// + /// Compute the FNV-1a hash of a sequence of bytes and determines if the byte + /// sequence is valid ASCII and hence the hash code matches a char sequence + /// encoding the same text. + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The sequence of bytes that are likely to be ASCII text. + /// True if the sequence contains only characters in the ASCII range. + /// The FNV-1a hash of + internal static int GetFNVHashCode(ReadOnlySpan data, out bool isAscii) + { + int hashCode = Hash.FnvOffsetBias; + + byte asciiMask = 0; + + for (int i = 0; i < data.Length; i++) + { + byte b = data[i]; + asciiMask |= b; + hashCode = unchecked((hashCode ^ b) * Hash.FnvPrime); + } + + isAscii = (asciiMask & 0x80) == 0; + return hashCode; + } + + /// + /// Compute the FNV-1a hash of a sequence of bytes + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The sequence of bytes + /// The FNV-1a hash of + internal static int GetFNVHashCode(ImmutableArray data) + { + int hashCode = Hash.FnvOffsetBias; + + for (int i = 0; i < data.Length; i++) + { + hashCode = unchecked((hashCode ^ data[i]) * Hash.FnvPrime); + } + + return hashCode; + } + + /// + /// Compute the hashcode of a sub-string using FNV-1a + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// Note: FNV-1a was developed and tuned for 8-bit sequences. We're using it here + /// for 16-bit Unicode chars on the understanding that the majority of chars will + /// fit into 8-bits and, therefore, the algorithm will retain its desirable traits + /// for generating hash codes. + /// + internal static int GetFNVHashCode(ReadOnlySpan data) + { + return CombineFNVHash(Hash.FnvOffsetBias, data); + } + + /// + /// Compute the hashcode of a sub-string using FNV-1a + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// Note: FNV-1a was developed and tuned for 8-bit sequences. We're using it here + /// for 16-bit Unicode chars on the understanding that the majority of chars will + /// fit into 8-bits and, therefore, the algorithm will retain its desirable traits + /// for generating hash codes. + /// + /// The input string + /// The start index of the first character to hash + /// The number of characters, beginning with to hash + /// The FNV-1a hash code of the substring beginning at and ending after characters. + internal static int GetFNVHashCode(string text, int start, int length) + => GetFNVHashCode(text.AsSpan(start, length)); + + /// + /// Compute the hashcode of a sub-string using FNV-1a + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The input string + /// The start index of the first character to hash + /// The FNV-1a hash code of the substring beginning at and ending at the end of the string. + internal static int GetFNVHashCode(string text, int start) + { + return GetFNVHashCode(text, start, length: text.Length - start); + } + + /// + /// Compute the hashcode of a string using FNV-1a + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The input string + /// The FNV-1a hash code of + internal static int GetFNVHashCode(string text) + { + return CombineFNVHash(Hash.FnvOffsetBias, text); + } + + /// + /// Compute the hashcode of a string using FNV-1a + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The input string + /// The FNV-1a hash code of + internal static int GetFNVHashCode(System.Text.StringBuilder text) + { + int hashCode = Hash.FnvOffsetBias; + +#if NETCOREAPP3_1_OR_GREATER + foreach (var chunk in text.GetChunks()) + { + hashCode = CombineFNVHash(hashCode, chunk.Span); + } +#else + // StringBuilder.GetChunks is not available in this target framework. Since there is no other direct access + // to the underlying storage spans of StringBuilder, we fall back to using slower per-character operations. + int end = text.Length; + + for (int i = 0; i < end; i++) + { + hashCode = unchecked((hashCode ^ text[i]) * Hash.FnvPrime); + } +#endif + + return hashCode; + } + + /// + /// Compute the hashcode of a sub string using FNV-1a + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The input string as a char array + /// The start index of the first character to hash + /// The number of characters, beginning with to hash + /// The FNV-1a hash code of the substring beginning at and ending after characters. + internal static int GetFNVHashCode(char[] text, int start, int length) + { + int hashCode = Hash.FnvOffsetBias; + int end = start + length; + + for (int i = start; i < end; i++) + { + hashCode = unchecked((hashCode ^ text[i]) * Hash.FnvPrime); + } + + return hashCode; + } + + /// + /// Compute the hashcode of a single character using the FNV-1a algorithm + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// Note: In general, this isn't any more useful than "char.GetHashCode". However, + /// it may be needed if you need to generate the same hash code as a string or + /// substring with just a single character. + /// + /// The character to hash + /// The FNV-1a hash code of the character. + internal static int GetFNVHashCode(char ch) + { + return Hash.CombineFNVHash(Hash.FnvOffsetBias, ch); + } + + /// + /// Combine a string with an existing FNV-1a hash code + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The accumulated hash code + /// The string to combine + /// The result of combining with using the FNV-1a algorithm + internal static int CombineFNVHash(int hashCode, string text) + { + foreach (char ch in text) + { + hashCode = unchecked((hashCode ^ ch) * Hash.FnvPrime); + } + + return hashCode; + } + + /// + /// Combine a char with an existing FNV-1a hash code + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The accumulated hash code + /// The new character to combine + /// The result of combining with using the FNV-1a algorithm + internal static int CombineFNVHash(int hashCode, char ch) + { + return unchecked((hashCode ^ ch) * Hash.FnvPrime); + } + + /// + /// Combine a string with an existing FNV-1a hash code + /// See http://en.wikipedia.org/wiki/Fowler%E2%80%93Noll%E2%80%93Vo_hash_function + /// + /// The accumulated hash code + /// The string to combine + /// The result of combining with using the FNV-1a algorithm + internal static int CombineFNVHash(int hashCode, ReadOnlySpan data) + { + for (int i = 0; i < data.Length; i++) + { + hashCode = unchecked((hashCode ^ data[i]) * Hash.FnvPrime); + } + + return hashCode; + } + } +} diff --git a/src/Common/Utilities/Index.cs b/src/Common/Utilities/Index.cs new file mode 100644 index 000000000..2dff6f0bb --- /dev/null +++ b/src/Common/Utilities/Index.cs @@ -0,0 +1,141 @@ +// 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. + +#if !NETCOREAPP +using System.Runtime.CompilerServices; +namespace System +{ + /// Represent a type can be used to index a collection either from the start or the end. + /// + /// Index is used by the C# compiler to support the new index syntax + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 } ; + /// int lastElement = someArray[^1]; // lastElement = 5 + /// + /// + internal readonly struct Index : IEquatable + { + private readonly int _value; + + /// Construct an Index using a value and indicating if the index is from the start or from the end. + /// The index value. it has to be zero or positive number. + /// Indicating if the index is from the start or from the end. + /// + /// If the Index constructed from the end, index value 1 means pointing at the last element and index value 0 means pointing at beyond last element. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public Index(int value, bool fromEnd = false) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "Non-negative number required."); + } + + if (fromEnd) + _value = ~value; + else + _value = value; + } + + // The following private constructors mainly created for perf reason to avoid the checks + private Index(int value) + { + _value = value; + } + + /// Create an Index pointing at first element. + public static Index Start => new Index(0); + + /// Create an Index pointing at beyond last element. + public static Index End => new Index(~0); + + /// Create an Index from the start at the position indicated by the value. + /// The index value from the start. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromStart(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "Non-negative number required."); + } + + return new Index(value); + } + + /// Create an Index from the end at the position indicated by the value. + /// The index value from the end. + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public static Index FromEnd(int value) + { + if (value < 0) + { + throw new ArgumentOutOfRangeException(nameof(value), value, "Non-negative number required."); + } + + return new Index(~value); + } + + /// Returns the index value. + public int Value + { + get + { + if (_value < 0) + return ~_value; + else + return _value; + } + } + + /// Indicates whether the index is from the start or the end. + public bool IsFromEnd => _value < 0; + + /// Calculate the offset from the start using the giving collection length. + /// The length of the collection that the Index will be used with. length has to be a positive value + /// + /// For performance reason, we don't validate the input length parameter and the returned offset value against negative values. + /// we don't validate either the returned offset is greater than the input length. + /// It is expected Index will be used with collections which always have non negative length/count. If the returned offset is negative and + /// then used to index a collection will get out of range exception which will be same affect as the validation. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public int GetOffset(int length) + { + int offset = _value; + if (IsFromEnd) + { + // offset = length - (~value) + // offset = length + (~(~value) + 1) + // offset = length + value + 1 + + offset += length + 1; + } + return offset; + } + + /// Indicates whether the current Index object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => value is Index && _value == ((Index)value)._value; + + /// Indicates whether the current Index object is equal to another Index object. + /// An object to compare with this object + public bool Equals(Index other) => _value == other._value; + + /// Returns the hash code for this instance. + public override int GetHashCode() => _value; + + /// Converts integer number to an Index. + public static implicit operator Index(int value) => FromStart(value); + + /// Converts the value of the current Index object to its equivalent string representation. + public override string ToString() + { + if (IsFromEnd) + return $"^{((uint)Value).ToString()}"; + + return ((uint)Value).ToString(); + } + } +} +#endif diff --git a/src/Common/Utilities/IsExternalInit.cs b/src/Common/Utilities/IsExternalInit.cs new file mode 100644 index 000000000..2e63533ac --- /dev/null +++ b/src/Common/Utilities/IsExternalInit.cs @@ -0,0 +1,32 @@ +// 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. + +// Copied from: +// https://github.com/dotnet/runtime/blob/218ef0f7776c2c20f6c594e3475b80f1fe2d7d08/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/IsExternalInit.cs + +#if NET6_0_OR_GREATER + +using System.Runtime.CompilerServices; + +#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API) +[assembly: TypeForwardedTo(typeof(IsExternalInit))] +#pragma warning restore RS0016 // Add public types and members to the declared API + +#else + +using System.ComponentModel; + +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} + +#endif diff --git a/src/Common/Names.cs b/src/Common/Utilities/Names.cs similarity index 100% rename from src/Common/Names.cs rename to src/Common/Utilities/Names.cs diff --git a/src/Common/NullableAttributes.cs b/src/Common/Utilities/NullableAttributes.cs similarity index 100% rename from src/Common/NullableAttributes.cs rename to src/Common/Utilities/NullableAttributes.cs diff --git a/src/Common/PathUtilities.cs b/src/Common/Utilities/PathUtilities.cs similarity index 53% rename from src/Common/PathUtilities.cs rename to src/Common/Utilities/PathUtilities.cs index aa20eb1f3..c9f863bcd 100644 --- a/src/Common/PathUtilities.cs +++ b/src/Common/Utilities/PathUtilities.cs @@ -5,12 +5,15 @@ using System; using System.Collections.Generic; using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; using System.IO; namespace Microsoft.Build.Tasks.SourceControl { internal static class PathUtilities { + internal static bool IsUnixLikePlatform => Path.DirectorySeparatorChar == '/'; + private static readonly char[] s_directorySeparators = { Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar }; private const string UncPrefix = @"\\"; private const string UnixRoot = "/"; @@ -49,5 +52,49 @@ public static string EndWithSeparator(this string path) public static string EndWithSeparator(this string path, char separator) => path.EndsWithSeparator() ? path : path + separator; + + /// + /// True if the path is an absolute path (rooted to drive or network share) + /// + public static bool IsAbsolute([NotNullWhen(true)] string? path) + { + if (path is not { Length: >0 }) + { + return false; + } + + if (IsUnixLikePlatform) + { + return path[0] == Path.DirectorySeparatorChar; + } + + // "C:\" + if (IsDriveRootedAbsolutePath(path)) + { + // Including invalid paths (e.g. "*:\") + return true; + } + + // "\\machine\share" + // Including invalid/incomplete UNC paths (e.g. "\\goo") + return path.Length >= 2 && + IsDirectorySeparator(path[0]) && + IsDirectorySeparator(path[1]); + } + + /// + /// True if the character is the platform directory separator character or the alternate directory separator. + /// + public static bool IsDirectorySeparator(char c) + => c == Path.DirectorySeparatorChar || c == Path.AltDirectorySeparatorChar; + + /// + /// Returns true if given path is absolute and starts with a drive specification ("C:\"). + /// + private static bool IsDriveRootedAbsolutePath(string path) + { + Debug.Assert(!IsUnixLikePlatform); + return path.Length >= 3 && path[1] == Path.VolumeSeparatorChar && IsDirectorySeparator(path[2]); + } } } diff --git a/src/Common/Utilities/Range.cs b/src/Common/Utilities/Range.cs new file mode 100644 index 000000000..84d1afdf4 --- /dev/null +++ b/src/Common/Utilities/Range.cs @@ -0,0 +1,104 @@ +// 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. + +#if !NETCOREAPP +using System.Runtime.CompilerServices; +using Roslyn.Utilities; +namespace System +{ + /// Represent a range has start and end indexes. + /// + /// Range is used by the C# compiler to support the range syntax. + /// + /// int[] someArray = new int[5] { 1, 2, 3, 4, 5 }; + /// int[] subArray1 = someArray[0..2]; // { 1, 2 } + /// int[] subArray2 = someArray[1..^0]; // { 2, 3, 4, 5 } + /// + /// + internal readonly struct Range : IEquatable + { + /// Represent the inclusive start index of the Range. + public Index Start { get; } + + /// Represent the exclusive end index of the Range. + public Index End { get; } + + /// Construct a Range object using the start and end indexes. + /// Represent the inclusive start index of the range. + /// Represent the exclusive end index of the range. + public Range(Index start, Index end) + { + Start = start; + End = end; + } + + /// Indicates whether the current Range object is equal to another object of the same type. + /// An object to compare with this object + public override bool Equals(object? value) => + value is Range r && + r.Start.Equals(Start) && + r.End.Equals(End); + + /// Indicates whether the current Range object is equal to another Range object. + /// An object to compare with this object + public bool Equals(Range other) => other.Start.Equals(Start) && other.End.Equals(End); + + /// Returns the hash code for this instance. + public override int GetHashCode() + { + return Hash.Combine(Start.GetHashCode(), End.GetHashCode()); + } + + /// Converts the value of the current Range object to its equivalent string representation. + public override string ToString() + { + return $"{getFromEndSpecifier(Start)}{toString(Start)}..{getFromEndSpecifier(End)}{toString(End)}"; + + static string getFromEndSpecifier(Index index) => index.IsFromEnd ? "^" : string.Empty; + static string toString(Index index) => ((uint)index.Value).ToString(); + } + + /// Create a Range object starting from start index to the end of the collection. + public static Range StartAt(Index start) => new Range(start, Index.End); + + /// Create a Range object starting from first element in the collection to the end Index. + public static Range EndAt(Index end) => new Range(Index.Start, end); + + /// Create a Range object starting from first element to the end. + public static Range All => new Range(Index.Start, Index.End); + + /// Calculate the start offset and length of range object using a collection length. + /// The length of the collection that the range will be used with. length has to be a positive value. + /// + /// For performance reason, we don't validate the input length parameter against negative values. + /// It is expected Range will be used with collections which always have non negative length/count. + /// We validate the range is inside the length scope though. + /// + [MethodImpl(MethodImplOptions.AggressiveInlining)] + public (int Offset, int Length) GetOffsetAndLength(int length) + { + int start; + Index startIndex = Start; + if (startIndex.IsFromEnd) + start = length - startIndex.Value; + else + start = startIndex.Value; + + int end; + Index endIndex = End; + if (endIndex.IsFromEnd) + end = length - endIndex.Value; + else + end = endIndex.Value; + + if ((uint)end > (uint)length || (uint)start > (uint)end) + { + throw new ArgumentOutOfRangeException(nameof(length)); + } + + return (start, end - start); + } + } +} +#endif diff --git a/src/Common/Utilities/RequiredMemberAttribute.cs b/src/Common/Utilities/RequiredMemberAttribute.cs new file mode 100644 index 000000000..fd59b3e51 --- /dev/null +++ b/src/Common/Utilities/RequiredMemberAttribute.cs @@ -0,0 +1,27 @@ +// 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. + +// Copied from: +// https://github.com/dotnet/runtime/blob/fdd104ec5e1d0d2aa24a6723995a98d0124f724b/src/libraries/System.Private.CoreLib/src/System/Runtime/CompilerServices/RequiredMemberAttribute.cs + +#if NET7_0_OR_GREATER + +using System.Runtime.CompilerServices; + +#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API) +[assembly: TypeForwardedTo(typeof(RequiredMemberAttribute))] +#pragma warning restore RS0016 // Add public types and members to the declared API + +#else + +namespace System.Runtime.CompilerServices +{ + /// Specifies that a type has required members or that a member is required. + [AttributeUsage(AttributeTargets.Class | AttributeTargets.Struct | AttributeTargets.Field | AttributeTargets.Property, AllowMultiple = false, Inherited = false)] + internal sealed class RequiredMemberAttribute : Attribute + { + } +} + +#endif diff --git a/src/Common/SequenceComparer.cs b/src/Common/Utilities/SequenceComparer.cs similarity index 100% rename from src/Common/SequenceComparer.cs rename to src/Common/Utilities/SequenceComparer.cs diff --git a/src/Common/Utilities/SetsRequiredMembersAttribute.cs b/src/Common/Utilities/SetsRequiredMembersAttribute.cs new file mode 100644 index 000000000..60536a6cc --- /dev/null +++ b/src/Common/Utilities/SetsRequiredMembersAttribute.cs @@ -0,0 +1,31 @@ +// 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. + +// Copied from: +// https://github.com/dotnet/runtime/blob/fdd104ec5e1d0d2aa24a6723995a98d0124f724b/src/libraries/System.Private.CoreLib/src/System/Diagnostics/CodeAnalysis/SetsRequiredMembersAttribute.cs + +#if NET7_0_OR_GREATER + +using System.Runtime.CompilerServices; +using System.Diagnostics.CodeAnalysis; + +#pragma warning disable RS0016 // Add public types and members to the declared API (this is a supporting forwarder for an internal polyfill API) +[assembly: TypeForwardedTo(typeof(SetsRequiredMembersAttribute))] +#pragma warning restore RS0016 // Add public types and members to the declared API + +#else + +namespace System.Diagnostics.CodeAnalysis +{ + /// + /// Specifies that this constructor sets all required members for the current type, and callers + /// do not need to set any required members themselves. + /// + [AttributeUsage(AttributeTargets.Constructor, AllowMultiple = false, Inherited = false)] + internal sealed class SetsRequiredMembersAttribute : Attribute + { + } +} + +#endif diff --git a/src/Common/UriUtilities.cs b/src/Common/Utilities/UriUtilities.cs similarity index 52% rename from src/Common/UriUtilities.cs rename to src/Common/Utilities/UriUtilities.cs index 981c9c13a..bdcf4ac37 100644 --- a/src/Common/UriUtilities.cs +++ b/src/Common/Utilities/UriUtilities.cs @@ -5,6 +5,7 @@ using System; using System.Linq; using System.Diagnostics.CodeAnalysis; +using System.IO; namespace Microsoft.Build.Tasks.SourceControl { @@ -80,5 +81,77 @@ public static string GetPath(this Uri uri) public static string GetPathAndQuery(this Uri uri) => uri.GetComponents(UriComponents.PathAndQuery, UriFormat.SafeUnescaped); + + /// + /// Converts an absolute local file path or an absolute URL string to . + /// + /// + /// The can't be represented as . + /// For example, UNC paths with invalid characters in server name. + /// + public static Uri CreateAbsoluteUri(string absolutePath) + { + var uriString = IsAscii(absolutePath) ? absolutePath : GetAbsoluteUriString(absolutePath); + try + { +#pragma warning disable RS0030 // Do not use banned APIs + return new(uriString, UriKind.Absolute); +#pragma warning restore + + } + catch (UriFormatException e) + { + // The standard URI format exception does not include the failing path, however + // in pretty much all cases we need to know the URI string (and original string) in order to fix the issue. + throw new UriFormatException($"Failed create URI from '{uriString}'; original string: '{absolutePath}'", e); + } + } + + // Implements workaround for https://github.com/dotnet/runtime/issues/89538: + internal static string GetAbsoluteUriString(string absolutePath) + { + if (!PathUtilities.IsAbsolute(absolutePath)) + { + return absolutePath; + } + + var parts = absolutePath.Split([Path.DirectorySeparatorChar, Path.AltDirectorySeparatorChar]); + + if (PathUtilities.IsUnixLikePlatform) + { + // Unix path: first part is empty, all parts should be escaped + return "file://" + string.Join("/", parts.Select(EscapeUriPart)); + } + + if (parts is ["", "", var serverName, ..]) + { + // UNC path: first non-empty part is server name and shouldn't be escaped + return "file://" + serverName + "/" + string.Join("/", parts.Skip(3).Select(EscapeUriPart)); + } + + // Drive-rooted path: first part is "C:" and shouldn't be escaped + return "file:///" + parts[0] + "/" + string.Join("/", parts.Skip(1).Select(EscapeUriPart)); + +#pragma warning disable SYSLIB0013 // Type or member is obsolete + static string EscapeUriPart(string stringToEscape) + => Uri.EscapeUriString(stringToEscape).Replace("#", "%23"); +#pragma warning restore + } + + private static bool IsAscii(char c) + => (uint)c <= '\x007f'; + + private static bool IsAscii(string filePath) + { + for (var i = 0; i < filePath.Length; i++) + { + if (!IsAscii(filePath[i])) + { + return false; + } + } + + return true; + } } } diff --git a/src/Common/ValueTuple.cs b/src/Common/Utilities/ValueTuple.cs similarity index 100% rename from src/Common/ValueTuple.cs rename to src/Common/Utilities/ValueTuple.cs diff --git a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj index 2e0715a20..76e1ca219 100644 --- a/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj +++ b/src/Microsoft.Build.Tasks.Git/Microsoft.Build.Tasks.Git.csproj @@ -18,11 +18,7 @@ - - - - - + diff --git a/src/Microsoft.Build.Tasks.Tfvc/Microsoft.Build.Tasks.Tfvc.csproj b/src/Microsoft.Build.Tasks.Tfvc/Microsoft.Build.Tasks.Tfvc.csproj index 3509bace4..0f6ef2c3e 100644 --- a/src/Microsoft.Build.Tasks.Tfvc/Microsoft.Build.Tasks.Tfvc.csproj +++ b/src/Microsoft.Build.Tasks.Tfvc/Microsoft.Build.Tasks.Tfvc.csproj @@ -20,8 +20,6 @@ - - - + diff --git a/src/SourceLink.AzureDevOpsServer.Git/Microsoft.SourceLink.AzureDevOpsServer.Git.csproj b/src/SourceLink.AzureDevOpsServer.Git/Microsoft.SourceLink.AzureDevOpsServer.Git.csproj index 6e643422c..5d6c260f1 100644 --- a/src/SourceLink.AzureDevOpsServer.Git/Microsoft.SourceLink.AzureDevOpsServer.Git.csproj +++ b/src/SourceLink.AzureDevOpsServer.Git/Microsoft.SourceLink.AzureDevOpsServer.Git.csproj @@ -12,22 +12,7 @@ MSBuild Tasks Azure DepOps Server TFS Git source link true - - - - - - - - - Microsoft.Build.Tasks.SourceControl - true - - - - - - + diff --git a/src/SourceLink.AzureRepos.Git/Microsoft.SourceLink.AzureRepos.Git.csproj b/src/SourceLink.AzureRepos.Git/Microsoft.SourceLink.AzureRepos.Git.csproj index e8ef360d5..7e68aacbd 100644 --- a/src/SourceLink.AzureRepos.Git/Microsoft.SourceLink.AzureRepos.Git.csproj +++ b/src/SourceLink.AzureRepos.Git/Microsoft.SourceLink.AzureRepos.Git.csproj @@ -12,22 +12,7 @@ MSBuild Tasks Azure DevOps Repos VSTS Git source link true - - - - - - - - - Microsoft.Build.Tasks.SourceControl - true - - - - - - + diff --git a/src/SourceLink.AzureRepos.Tfvc/Microsoft.SourceLink.AzureRepos.Tfvc.csproj b/src/SourceLink.AzureRepos.Tfvc/Microsoft.SourceLink.AzureRepos.Tfvc.csproj index 5fdde5e16..ebfd18965 100644 --- a/src/SourceLink.AzureRepos.Tfvc/Microsoft.SourceLink.AzureRepos.Tfvc.csproj +++ b/src/SourceLink.AzureRepos.Tfvc/Microsoft.SourceLink.AzureRepos.Tfvc.csproj @@ -14,7 +14,7 @@ true - + diff --git a/src/SourceLink.Bitbucket.Git/Microsoft.SourceLink.Bitbucket.Git.csproj b/src/SourceLink.Bitbucket.Git/Microsoft.SourceLink.Bitbucket.Git.csproj index 95b89fd1f..990fc770b 100644 --- a/src/SourceLink.Bitbucket.Git/Microsoft.SourceLink.Bitbucket.Git.csproj +++ b/src/SourceLink.Bitbucket.Git/Microsoft.SourceLink.Bitbucket.Git.csproj @@ -12,21 +12,7 @@ MSBuild Tasks Bitbucket source link true - - - - - - - - Microsoft.Build.Tasks.SourceControl - true - - - - - - + diff --git a/src/SourceLink.Common.UnitTests/Microsoft.SourceLink.Common.UnitTests.csproj b/src/SourceLink.Common.UnitTests/Microsoft.SourceLink.Common.UnitTests.csproj index a75eb43e2..9cfd728cd 100644 --- a/src/SourceLink.Common.UnitTests/Microsoft.SourceLink.Common.UnitTests.csproj +++ b/src/SourceLink.Common.UnitTests/Microsoft.SourceLink.Common.UnitTests.csproj @@ -3,10 +3,11 @@ net472;$(NetCurrent) - - - - + + + + + Microsoft.Build.Tasks.SourceControl true diff --git a/src/SourceLink.Common/Microsoft.SourceLink.Common.csproj b/src/SourceLink.Common/Microsoft.SourceLink.Common.csproj index 64ebcbd01..503af03be 100644 --- a/src/SourceLink.Common/Microsoft.SourceLink.Common.csproj +++ b/src/SourceLink.Common/Microsoft.SourceLink.Common.csproj @@ -18,9 +18,7 @@ - - - + diff --git a/src/SourceLink.GitHub/Microsoft.SourceLink.GitHub.csproj b/src/SourceLink.GitHub/Microsoft.SourceLink.GitHub.csproj index 7130e1f7e..0948b1df3 100644 --- a/src/SourceLink.GitHub/Microsoft.SourceLink.GitHub.csproj +++ b/src/SourceLink.GitHub/Microsoft.SourceLink.GitHub.csproj @@ -12,21 +12,7 @@ MSBuild Tasks GitHub source link true - - - - - - - - Microsoft.Build.Tasks.SourceControl - true - - - - - - + diff --git a/src/SourceLink.GitLab/Microsoft.SourceLink.GitLab.csproj b/src/SourceLink.GitLab/Microsoft.SourceLink.GitLab.csproj index 3e9373d5f..b7ea016ca 100644 --- a/src/SourceLink.GitLab/Microsoft.SourceLink.GitLab.csproj +++ b/src/SourceLink.GitLab/Microsoft.SourceLink.GitLab.csproj @@ -12,21 +12,7 @@ MSBuild Tasks GitLab source link true - - - - - - - - Microsoft.Build.Tasks.SourceControl - true - - - - - - + diff --git a/src/SourceLink.GitWeb/Microsoft.SourceLink.GitWeb.csproj b/src/SourceLink.GitWeb/Microsoft.SourceLink.GitWeb.csproj index c322f662b..e2dfe3743 100644 --- a/src/SourceLink.GitWeb/Microsoft.SourceLink.GitWeb.csproj +++ b/src/SourceLink.GitWeb/Microsoft.SourceLink.GitWeb.csproj @@ -12,22 +12,7 @@ MSBuild Tasks GitWeb source link true - - - - - - - - Microsoft.Build.Tasks.SourceControl - true - - - - - - - + diff --git a/src/SourceLink.Gitea/Microsoft.SourceLink.Gitea.csproj b/src/SourceLink.Gitea/Microsoft.SourceLink.Gitea.csproj index 8904abb28..44c70588d 100644 --- a/src/SourceLink.Gitea/Microsoft.SourceLink.Gitea.csproj +++ b/src/SourceLink.Gitea/Microsoft.SourceLink.Gitea.csproj @@ -12,21 +12,7 @@ MSBuild Tasks Gitea source link true - - - - - - - - Microsoft.Build.Tasks.SourceControl - true - - - - - - + diff --git a/src/SourceLink.Gitee/Microsoft.SourceLink.Gitee.csproj b/src/SourceLink.Gitee/Microsoft.SourceLink.Gitee.csproj index 61d7dac95..38ae8d222 100644 --- a/src/SourceLink.Gitee/Microsoft.SourceLink.Gitee.csproj +++ b/src/SourceLink.Gitee/Microsoft.SourceLink.Gitee.csproj @@ -12,21 +12,7 @@ MSBuild Tasks Gitee source link true - - - - - - - - Microsoft.Build.Tasks.SourceControl - true - - - - - - + diff --git a/src/TestUtilities/TestUtilities.csproj b/src/TestUtilities/TestUtilities.csproj index 2154ed099..2e7008c6c 100644 --- a/src/TestUtilities/TestUtilities.csproj +++ b/src/TestUtilities/TestUtilities.csproj @@ -21,7 +21,7 @@ - +