diff --git a/SourceLink.sln b/SourceLink.sln
index 49104dbd28..2264fe97bf 100644
--- a/SourceLink.sln
+++ b/SourceLink.sln
@@ -68,8 +68,11 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.Gitea"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Microsoft.SourceLink.Gitea.UnitTests", "src\SourceLink.Gitea.UnitTests\Microsoft.SourceLink.Gitea.UnitTests.csproj", "{04C95AC8-E3A4-4A2B-94E6-4C62E910FD8A}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "dotnet-sourcelink", "src\dotnet-sourcelink\dotnet-sourcelink.csproj", "{4376B613-CD5B-4274-9071-30989769B0B2}"
+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
@@ -178,6 +181,10 @@ Global
{04C95AC8-E3A4-4A2B-94E6-4C62E910FD8A}.Debug|Any CPU.Build.0 = Debug|Any CPU
{04C95AC8-E3A4-4A2B-94E6-4C62E910FD8A}.Release|Any CPU.ActiveCfg = Release|Any CPU
{04C95AC8-E3A4-4A2B-94E6-4C62E910FD8A}.Release|Any CPU.Build.0 = Release|Any CPU
+ {4376B613-CD5B-4274-9071-30989769B0B2}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {4376B613-CD5B-4274-9071-30989769B0B2}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {4376B613-CD5B-4274-9071-30989769B0B2}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {4376B613-CD5B-4274-9071-30989769B0B2}.Release|Any CPU.Build.0 = Release|Any CPU
EndGlobalSection
GlobalSection(SolutionProperties) = preSolution
HideSolutionNode = FALSE
diff --git a/eng/Versions.props b/eng/Versions.props
index b6f079e418..635cfc0b1f 100644
--- a/eng/Versions.props
+++ b/eng/Versions.props
@@ -15,6 +15,9 @@
3.1.6
5.7.0
5.7.0
+ 2.0.0-beta1.20371.2
+ 0.3.0-alpha.20371.2
+ 1.8.1
4.5.0
4.7.2
diff --git a/src/dotnet-sourcelink/AuthenticationHeaderProvider.cs b/src/dotnet-sourcelink/AuthenticationHeaderProvider.cs
new file mode 100644
index 0000000000..9065b83276
--- /dev/null
+++ b/src/dotnet-sourcelink/AuthenticationHeaderProvider.cs
@@ -0,0 +1,30 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Net.Http.Headers;
+using System.Text;
+
+namespace Microsoft.SourceLink.Tools
+{
+ internal interface IAuthenticationHeaderValueProvider
+ {
+ AuthenticationHeaderValue GetValue();
+ }
+
+ internal sealed class BasicAuthenticationHeaderValueProvider : IAuthenticationHeaderValueProvider
+ {
+ private readonly string _username;
+ private readonly string _password;
+ private readonly Encoding _encoding;
+
+ public BasicAuthenticationHeaderValueProvider(string username, string password, Encoding encoding)
+ {
+ _username = username;
+ _password = password;
+ _encoding = encoding;
+ }
+
+ public AuthenticationHeaderValue GetValue()
+ => new("Basic", Convert.ToBase64String(_encoding.GetBytes($"{_username}:{_password}")));
+ }
+}
diff --git a/src/dotnet-sourcelink/HashAlgorithmGuids.cs b/src/dotnet-sourcelink/HashAlgorithmGuids.cs
new file mode 100644
index 0000000000..087cdca983
--- /dev/null
+++ b/src/dotnet-sourcelink/HashAlgorithmGuids.cs
@@ -0,0 +1,25 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Security.Cryptography;
+
+namespace Microsoft.SourceLink.Tools
+{
+ internal static class HashAlgorithmGuids
+ {
+ public static readonly Guid MD5 = new Guid("406ea660-64cf-4c82-b6f0-42d48172a799");
+ public static readonly Guid Sha1 = new("ff1816ec-aa5e-4d10-87f7-6f4963833460");
+ public static readonly Guid Sha256 = new("8829d00f-11b8-4213-878b-770e8597ac16");
+
+ public static HashAlgorithmName? TryGetName(Guid guid)
+ {
+ if (guid == MD5) return new HashAlgorithmName("MD5");
+ if (guid == Sha1) return new HashAlgorithmName("SHA1");
+ if (guid == Sha256) return new HashAlgorithmName("SHA256");
+ return null;
+ }
+
+ public static HashAlgorithmName GetName(Guid guid)
+ => TryGetName(guid) ?? throw new CryptographicException("unknown HashAlgorithm " + guid);
+ }
+}
diff --git a/src/dotnet-sourcelink/LanguageGuids.cs b/src/dotnet-sourcelink/LanguageGuids.cs
new file mode 100644
index 0000000000..cb66c92352
--- /dev/null
+++ b/src/dotnet-sourcelink/LanguageGuids.cs
@@ -0,0 +1,21 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+
+namespace Microsoft.SourceLink.Tools
+{
+ internal static class LanguageGuids
+ {
+ public static readonly Guid CSharp = new("3f5162f8-07c6-11d3-9053-00c04fa302a1");
+ public static readonly Guid FSharp = new("ab4f38c9-b6e6-43ba-be3b-58080b2ccce3");
+ public static readonly Guid VisualBasic = new("3a12d0b8-c26c-11d0-b442-00a0244a1dd2");
+
+ public static string GetName(Guid guid)
+ {
+ if (guid == CSharp) return "C#";
+ if (guid == FSharp) return "F#";
+ if (guid == VisualBasic) return "VB";
+ return guid.ToString();
+ }
+ }
+}
diff --git a/src/dotnet-sourcelink/Program.cs b/src/dotnet-sourcelink/Program.cs
new file mode 100644
index 0000000000..4a130f62d1
--- /dev/null
+++ b/src/dotnet-sourcelink/Program.cs
@@ -0,0 +1,533 @@
+// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
+
+using System;
+using System.Collections.Generic;
+using System.Collections.Immutable;
+using System.CommandLine;
+using System.CommandLine.Invocation;
+using System.CommandLine.Parsing;
+using System.IO;
+using System.Linq;
+using System.Net;
+using System.Net.Http;
+using System.Net.Http.Headers;
+using System.Reflection;
+using System.Reflection.Metadata;
+using System.Reflection.PortableExecutable;
+using System.Security.Cryptography;
+using System.Text;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace Microsoft.SourceLink.Tools
+{
+ internal sealed class Program
+ {
+ private static readonly Guid s_sourceLinkCustomDebugInformationId = new("CC110556-A091-4D38-9FEC-25AB9A351A6A");
+ private static readonly Guid s_embeddedSourceCustomDebugInformationId = new("0E8A571B-6926-466E-B4AD-8AB04611F5FE");
+ private static readonly byte[] s_crlfBytes = { (byte)'\r', (byte)'\n' };
+ private static readonly ProductInfoHeaderValue s_sourceLinkProductHeaderValue = new("SourceLink", GetSourceLinkVersion());
+
+ private static class AuthenticationMethod
+ {
+ public const string Basic = "basic";
+ }
+
+ private record DocumentInfo(
+ string ContainingFile,
+ string Name,
+ string? Uri,
+ bool IsEmbedded,
+ ImmutableArray Hash,
+ Guid HashAlgorithm);
+
+ private readonly IConsole _console;
+ private bool _errorReported;
+
+ public Program(IConsole console)
+ {
+ _console = console;
+ }
+
+ public static async Task Main(string[] args)
+ {
+ var rootCommand = GetRootCommand();
+ return await rootCommand.InvokeAsync(args);
+ }
+
+ private static string GetSourceLinkVersion()
+ {
+ var attribute = (AssemblyInformationalVersionAttribute)typeof(Program).Assembly.GetCustomAttributes(typeof(AssemblyInformationalVersionAttribute), false).Single();
+ return attribute.InformationalVersion.Split('+').First();
+ }
+
+ private static RootCommand GetRootCommand()
+ {
+ var authEncodingArg = new Argument(
+ name: "encoding-name",
+ parse: arg => Encoding.GetEncoding(arg.Tokens.Single().Value))
+ {
+ Arity = ArgumentArity.ExactlyOne
+ };
+
+ authEncodingArg.AddValidator(arg =>
+ {
+ var name = arg.Tokens.Single().Value;
+
+ try
+ {
+ _ = Encoding.GetEncoding(name);
+ return null;
+ }
+ catch
+ {
+ return $"Encoding '{name}' not supported";
+ }
+ });
+
+ var test = new Command("test", "TODO")
+ {
+ new Argument("path", "Path to an assembly or .pdb"),
+ new Option(new[] { "--auth", "-a" }, "Authentication method")
+ {
+ Argument = new Argument(name: "method", () => AuthenticationMethod.Basic) { Arity = ArgumentArity.ExactlyOne }.FromAmong(AuthenticationMethod.Basic)
+ },
+ new Option(new[] { "--auth-encoding", "-e" }, "Encoding to use for authentication value")
+ {
+ Argument = authEncodingArg,
+ },
+ new Option(new[] { "--user", "-u" }, "Username to use to authenticate")
+ {
+ Argument = new Argument(name: "user-name") { Arity = ArgumentArity.ExactlyOne }
+ },
+ new Option(new[] { "--password", "-p" }, "Password to use to authenticate")
+ {
+ Argument = new Argument() { Arity = ArgumentArity.ExactlyOne }
+ },
+ };
+ test.Handler = CommandHandler.Create(TestAsync);
+
+ var printJson = new Command("print-json", "Print Source Link JSON stored in the PDB")
+ {
+ new Argument("path", "Path to an assembly or .pdb"),
+ };
+ printJson.Handler = CommandHandler.Create(PrintJsonAsync);
+
+ var printDocuments = new Command("print-documents", "TODO")
+ {
+ new Argument("path", "Path to an assembly or .pdb"),
+ };
+ printDocuments.Handler = CommandHandler.Create(PrintDocumentsAsync);
+
+ var printUrls = new Command("print-urls", "TODO")
+ {
+ new Argument("path", "Path to an assembly or .pdb"),
+ };
+ printUrls.Handler = CommandHandler.Create(PrintUrlsAsync);
+
+ var root = new RootCommand()
+ {
+ test,
+ printJson,
+ printDocuments,
+ printUrls,
+ };
+
+ root.Description = "dotnet-sourcelink";
+
+ root.AddValidator(commandResult =>
+ {
+ if (commandResult.OptionResult("--auth") != null)
+ {
+ if (commandResult.OptionResult("--user") == null || commandResult.OptionResult("--password") == null)
+ {
+ return "Specify --user and --password options";
+ }
+ }
+
+ return null;
+ });
+
+ return root;
+ }
+
+ private void ReportError(string message)
+ {
+ _console.Error.Write(message);
+ _console.Error.Write(Environment.NewLine);
+ _errorReported = true;
+ }
+
+ private void WriteOutputLine(string message)
+ {
+ _console.Out.Write(message);
+ _console.Out.Write(Environment.NewLine);
+ }
+
+ private static async Task TestAsync(
+ string path,
+ string? authMethod,
+ Encoding? authEncoding,
+ string? user,
+ string? password,
+ IConsole console)
+ {
+ var authenticationHeader = (authMethod != null) ? GetAuthenticationHeader(authMethod, authEncoding ?? Encoding.ASCII, user!, password!) : null;
+
+ var cancellationSource = new CancellationTokenSource();
+ Console.CancelKeyPress += (sender, e) =>
+ {
+ e.Cancel = true;
+ cancellationSource.Cancel();
+ };
+
+ try
+ {
+ return await new Program(console).TestAsync(path, authenticationHeader, cancellationSource.Token).ConfigureAwait(false);
+ }
+ catch (OperationCanceledException)
+ {
+ console.Error.Write("Operation canceled.");
+ console.Error.Write(Environment.NewLine);
+ return -1;
+ }
+ }
+
+ private async Task TestAsync(string path, AuthenticationHeaderValue? authenticationHeader, CancellationToken cancellationToken)
+ {
+ var documents = new List();
+ ReadAndResolveDocuments(path, documents);
+
+ if (documents.Count == 0)
+ {
+ return _errorReported ? 1 : 0;
+ }
+
+ var handler = new HttpClientHandler();
+ if (handler.SupportsAutomaticDecompression)
+ handler.AutomaticDecompression = DecompressionMethods.GZip | DecompressionMethods.Deflate;
+
+ using var client = new HttpClient(handler);
+ client.DefaultRequestHeaders.UserAgent.Add(s_sourceLinkProductHeaderValue);
+ client.DefaultRequestHeaders.Authorization = authenticationHeader;
+
+ var outputLock = new object();
+
+ var errorReporter = new Action(message =>
+ {
+ lock (outputLock)
+ {
+ ReportError(message);
+ }
+ });
+
+ var tasks = documents.Where(document => document.Uri != null).Select(document => DownloadAndValidateDocumentAsync(client, document, errorReporter, cancellationToken));
+
+ _ = await Task.WhenAll(tasks).ConfigureAwait(false);
+
+ if (_errorReported)
+ {
+ return 1;
+ }
+
+ WriteOutputLine($"File '{path}' validated.");
+ return 0;
+ }
+
+ private static async Task DownloadAndValidateDocumentAsync(HttpClient client, DocumentInfo document, Action reportError, CancellationToken cancellationToken)
+ {
+ cancellationToken.ThrowIfCancellationRequested();
+
+ using var request = new HttpRequestMessage(HttpMethod.Get, document.Uri);
+ using var response = await client.SendAsync(request, cancellationToken).ConfigureAwait(false);
+
+ if (!response.IsSuccessStatusCode)
+ {
+ reportError($"Failed to download '{document.Uri}': {response.ReasonPhrase} ({response.StatusCode})");
+ return false;
+ }
+
+ var algorithmName = HashAlgorithmGuids.GetName(document.HashAlgorithm);
+
+ // TODO: consider reusing buffers and IncrementalHash instances
+
+ var content = await response.Content.ReadAsByteArrayAsync(cancellationToken).ConfigureAwait(false);
+
+ // When git core.autocrfl option is true git replaces LF with CRLF on checkout, but only if the file has consistent line endings.
+ // Line endings in files with mixed line endings are left unchanged.
+ // The checksums stored in the PDB reflect the content of the checked out file on a build server,
+ // hence they are calculated with the line endings changed.
+ // First, check if the raw file checksum matches the PDB then check if file with LF converted to CRLF matches.
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ using var incrementalHash = IncrementalHash.CreateHash(algorithmName);
+
+ incrementalHash.AppendData(content);
+ var rawHash = incrementalHash.GetHashAndReset();
+ if (document.Hash.SequenceEqual(rawHash))
+ {
+ return true;
+ }
+
+ cancellationToken.ThrowIfCancellationRequested();
+
+ var crlfHash = TryCalculateHashWithLineBreakSubstituted(content, incrementalHash);
+ if (crlfHash != null && document.Hash.SequenceEqual(crlfHash))
+ {
+ return true;
+ }
+
+ reportError($"Checksum validation failed for '{document.Uri}'.");
+ return false;
+ }
+
+ private static byte[]? TryCalculateHashWithLineBreakSubstituted(byte[] content, IncrementalHash incrementalHash)
+ {
+ int index = 0;
+ while (true)
+ {
+ int lf = Array.IndexOf(content, (byte)'\n', index);
+ if (lf < 0)
+ {
+ incrementalHash.AppendData(content, index, content.Length - index);
+ return incrementalHash.GetHashAndReset();
+ }
+
+ if (index - 1 >= 0 && content[index - 1] == (byte)'\r')
+ {
+ // The file either has CRLF line endings or mixed line endings.
+ // In either case there is no need to substitute LF to CRLF.
+ _ = incrementalHash.GetHashAndReset();
+ return null;
+ }
+
+ incrementalHash.AppendData(content, index, lf - index);
+ incrementalHash.AppendData(s_crlfBytes);
+ index = lf + 1;
+ }
+ }
+
+ private static Task PrintJsonAsync(string path, IConsole console)
+ => Task.FromResult(new Program(console).PrintJson(path));
+
+ private int PrintJson(string path)
+ {
+ ReadPdbMetadata(path, (filePath, metadataReader) =>
+ {
+ var sourceLink = ReadSourceLink(metadataReader);
+
+ if (sourceLink == null)
+ {
+ ReportError($"Source Link record not found in {filePath}.");
+ }
+ else
+ {
+ WriteOutputLine(sourceLink);
+ }
+ });
+
+ return _errorReported ? 1 : 0;
+ }
+
+ private static Task PrintDocumentsAsync(string path, IConsole console)
+ => Task.FromResult(new Program(console).PrintDocuments(path));
+
+ public static string ToHex(byte[] bytes)
+ => BitConverter.ToString(bytes).Replace("-", "").ToLowerInvariant();
+
+ private int PrintDocuments(string path)
+ {
+ ReadPdbMetadata(path, (_, metadataReader) =>
+ {
+ foreach (var documentHandle in metadataReader.Documents)
+ {
+ var document = metadataReader.GetDocument(documentHandle);
+ var hash = metadataReader.GetBlobBytes(document.Hash);
+ var hashAlgorithm = metadataReader.GetGuid(document.HashAlgorithm);
+ var language = metadataReader.GetGuid(document.Language);
+ var name = metadataReader.GetString(document.Name);
+
+ WriteOutputLine($"'{name}' {ToHex(hash)} {HashAlgorithmGuids.TryGetName(hashAlgorithm)?.Name ?? hashAlgorithm.ToString()} {LanguageGuids.GetName(language)}");
+ }
+ });
+
+ return _errorReported ? 1 : 0;
+ }
+
+ private static Task PrintUrlsAsync(string path,IConsole console)
+ => Task.FromResult(new Program(console).PrintUrls(path));
+
+ private int PrintUrls(string path)
+ {
+ var resolvedDocuments = new List();
+ ReadAndResolveDocuments(path, resolvedDocuments);
+
+ int unresolvedCount = 0;
+ foreach (var document in resolvedDocuments)
+ {
+ if (document.IsEmbedded)
+ {
+ WriteOutputLine($"'{document.Name}': embedded");
+ }
+ else if (document.Uri != null)
+ {
+ WriteOutputLine($"'{document.Name}': '{document.Uri}'");
+ }
+ else
+ {
+ unresolvedCount++;
+ }
+ }
+
+ if (unresolvedCount > 0)
+ {
+ ReportError($"Unable to resolve URL for {unresolvedCount} document(s):");
+ }
+
+ foreach (var document in resolvedDocuments)
+ {
+ if (!document.IsEmbedded && document.Uri == null)
+ {
+ WriteOutputLine(document.Name);
+ }
+ }
+
+ return _errorReported ? 1 : 0;
+ }
+
+ private bool ReadPdbMetadata(string path, Action reader)
+ {
+ var filePath = path;
+
+ try
+ {
+ if (string.Equals(Path.GetExtension(path), ".pdb", StringComparison.OrdinalIgnoreCase))
+ {
+ using var provider = MetadataReaderProvider.FromPortablePdbStream(File.OpenRead(path));
+ reader(filePath, provider.GetMetadataReader());
+ return true;
+ }
+
+ using var peReader = new PEReader(File.OpenRead(path));
+ if (peReader.TryOpenAssociatedPortablePdb(path, pdbFileStreamProvider: File.OpenRead, out var pdbReaderProvider, out filePath))
+ {
+ using (pdbReaderProvider)
+ {
+ reader(filePath ?? path, pdbReaderProvider!.GetMetadataReader());
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+ catch (Exception e)
+ {
+ ReportError($"Error reading '{filePath}': {e.Message}");
+ return false;
+ }
+ }
+
+ private void ReadAndResolveDocuments(string path, List resolvedDocuments)
+ {
+ if (!ReadPdbMetadata(path, (filePath, metadataReader) =>
+ {
+ var documents = new List<(string name, ImmutableArray hash, Guid hashAlgorithm, bool isEmbedded)>();
+ bool hasUnembeddedDocument = false;
+
+ foreach (var documentHandle in metadataReader.Documents)
+ {
+ var document = metadataReader.GetDocument(documentHandle);
+ var name = metadataReader.GetString(document.Name);
+ var isEmbedded = HasCustomDebugInformation(metadataReader, documentHandle, s_embeddedSourceCustomDebugInformationId);
+ var hash = metadataReader.GetBlobContent(document.Hash);
+ var hashAlgorithm = metadataReader.GetGuid(document.HashAlgorithm);
+
+ documents.Add((name, hash, hashAlgorithm, isEmbedded));
+
+ if (!isEmbedded)
+ {
+ hasUnembeddedDocument = true;
+ }
+ }
+
+ SourceLinkMap sourceLinkMap = default;
+ if (hasUnembeddedDocument)
+ {
+ var sourceLink = ReadSourceLink(metadataReader);
+ if (sourceLink == null)
+ {
+ ReportError($"Source Link record not found.");
+ return;
+ }
+
+ try
+ {
+ sourceLinkMap = SourceLinkMap.Parse(sourceLink);
+ }
+ catch (Exception e)
+ {
+ ReportError($"Error reading SourceLink: {e.Message}");
+ return;
+ }
+ }
+
+ foreach (var (name, hash, hashAlgorithm, isEmbedded) in documents)
+ {
+ string? uri = isEmbedded ? null : sourceLinkMap.TryGetUri(name, out var mappedUri) ? mappedUri : null;
+ resolvedDocuments.Add(new DocumentInfo(filePath, name, uri, isEmbedded, hash, hashAlgorithm));
+ }
+ }))
+ {
+ ReportError($"Symbol information not found for '{path}'.");
+ };
+ }
+
+ private static bool HasCustomDebugInformation(MetadataReader metadataReader, EntityHandle handle, Guid kind)
+ {
+ foreach (var cdiHandle in metadataReader.GetCustomDebugInformation(handle))
+ {
+ var cdi = metadataReader.GetCustomDebugInformation(cdiHandle);
+ if (metadataReader.GetGuid(cdi.Kind) == kind)
+ {
+ return true;
+ }
+ }
+
+ return false;
+ }
+
+ private static BlobReader GetCustomDebugInformationReader(MetadataReader metadataReader, EntityHandle handle, Guid kind)
+ {
+ foreach (var cdiHandle in metadataReader.GetCustomDebugInformation(handle))
+ {
+ var cdi = metadataReader.GetCustomDebugInformation(cdiHandle);
+ if (metadataReader.GetGuid(cdi.Kind) == kind)
+ {
+ return metadataReader.GetBlobReader(cdi.Value);
+ }
+ }
+
+ return default;
+ }
+
+ private static string? ReadSourceLink(MetadataReader metadataReader)
+ {
+ var blobReader = GetCustomDebugInformationReader(metadataReader, EntityHandle.ModuleDefinition, s_sourceLinkCustomDebugInformationId);
+ return blobReader.Length > 0 ? blobReader.ReadUTF8(blobReader.Length) : null;
+ }
+
+ private static AuthenticationHeaderValue GetAuthenticationHeader(string method, Encoding encoding, string username, string password)
+ {
+ return (method.ToLowerInvariant()) switch
+ {
+ AuthenticationMethod.Basic => new AuthenticationHeaderValue(
+ scheme: AuthenticationMethod.Basic,
+ parameter: Convert.ToBase64String(encoding.GetBytes($"{username}:{password}"))),
+
+ _ => throw new InvalidOperationException(),
+ };
+ }
+ }
+}
\ No newline at end of file
diff --git a/src/dotnet-sourcelink/dotnet-sourcelink.csproj b/src/dotnet-sourcelink/dotnet-sourcelink.csproj
new file mode 100644
index 0000000000..9a604a7098
--- /dev/null
+++ b/src/dotnet-sourcelink/dotnet-sourcelink.csproj
@@ -0,0 +1,21 @@
+
+
+ Exe
+ net7.0
+
+
+ true
+ True
+ sourcelink
+ Command line tool for SourceLink testing.
+ true
+ win-x64;win-x86;osx-x64
+
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/src/dotnet-sourcelink/runtimeconfig.template.json b/src/dotnet-sourcelink/runtimeconfig.template.json
new file mode 100644
index 0000000000..2c73f39890
--- /dev/null
+++ b/src/dotnet-sourcelink/runtimeconfig.template.json
@@ -0,0 +1,3 @@
+{
+ "rollForwardOnNoCandidateFx": 2
+}
\ No newline at end of file