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 @@
-
+