diff --git a/src/shared/Core.Tests/InputArgumentsTests.cs b/src/shared/Core.Tests/InputArgumentsTests.cs index 37fe4c5f3..44d8bae44 100644 --- a/src/shared/Core.Tests/InputArgumentsTests.cs +++ b/src/shared/Core.Tests/InputArgumentsTests.cs @@ -9,19 +9,25 @@ public class InputArgumentsTests [Fact] public void InputArguments_Ctor_Null_ThrowsArgNullException() { - Assert.Throws(() => new InputArguments(null)); + Assert.Throws(() => new InputArguments((IDictionary)null)); + Assert.Throws(() => new InputArguments((IDictionary>)null)); } [Fact] public void InputArguments_CommonArguments_ValuePresent_ReturnsValues() { - var dict = new Dictionary + var dict = new Dictionary> { - ["protocol"] = "https", - ["host"] = "example.com", - ["path"] = "an/example/path", - ["username"] = "john.doe", - ["password"] = "password123" + ["protocol"] = new[] { "https" }, + ["host"] = new[] { "example.com" }, + ["path"] = new[] { "an/example/path" }, + ["username"] = new[] { "john.doe" }, + ["password"] = new[] { "password123" }, + ["wwwauth"] = new[] + { + "basic realm=\"example.com\"", + "bearer authorize_uri=https://id.example.com p=1 q=0" + } }; var inputArgs = new InputArguments(dict); @@ -31,10 +37,16 @@ public void InputArguments_CommonArguments_ValuePresent_ReturnsValues() Assert.Equal("an/example/path", inputArgs.Path); Assert.Equal("john.doe", inputArgs.UserName); Assert.Equal("password123", inputArgs.Password); + Assert.Equal(new[] + { + "basic realm=\"example.com\"", + "bearer authorize_uri=https://id.example.com p=1 q=0" + }, + inputArgs.WwwAuth); } [Fact] - public void InputArguments_CommonArguments_ValueMissing_ReturnsNull() + public void InputArguments_CommonArguments_ValueMissing_ReturnsNullOrEmptyCollection() { var dict = new Dictionary(); @@ -45,20 +57,23 @@ public void InputArguments_CommonArguments_ValueMissing_ReturnsNull() Assert.Null(inputArgs.Path); Assert.Null(inputArgs.UserName); Assert.Null(inputArgs.Password); + Assert.Empty(inputArgs.WwwAuth); } [Fact] public void InputArguments_OtherArguments() { - var dict = new Dictionary + var dict = new Dictionary> { - ["foo"] = "bar" + ["foo"] = new[] { "bar" }, + ["multi"] = new[] { "val1", "val2", "val3" }, }; var inputArgs = new InputArguments(dict); Assert.Equal("bar", inputArgs["foo"]); Assert.Equal("bar", inputArgs.GetArgumentOrDefault("foo")); + Assert.Equal(new[] { "val1", "val2", "val3" }, inputArgs.GetMultiArgumentOrDefault("multi")); } [Fact] diff --git a/src/shared/Core/Commands/GitCommandBase.cs b/src/shared/Core/Commands/GitCommandBase.cs index b186ce024..b277d1a75 100644 --- a/src/shared/Core/Commands/GitCommandBase.cs +++ b/src/shared/Core/Commands/GitCommandBase.cs @@ -33,7 +33,7 @@ internal async Task ExecuteAsync() // Parse standard input arguments // git-credential treats the keys as case-sensitive; so should we. - IDictionary inputDict = await Context.Streams.In.ReadDictionaryAsync(StringComparer.Ordinal); + IDictionary> inputDict = await Context.Streams.In.ReadMultiDictionaryAsync(StringComparer.Ordinal); var input = new InputArguments(inputDict); // Validate minimum arguments are present diff --git a/src/shared/Core/InputArguments.cs b/src/shared/Core/InputArguments.cs index 53aab181a..626fc805d 100644 --- a/src/shared/Core/InputArguments.cs +++ b/src/shared/Core/InputArguments.cs @@ -15,17 +15,24 @@ namespace GitCredentialManager /// public class InputArguments { - private readonly IReadOnlyDictionary _dict; + private readonly IReadOnlyDictionary> _dict; public InputArguments(IDictionary dict) { - if (dict == null) - { - throw new ArgumentNullException(nameof(dict)); - } + EnsureArgument.NotNull(dict, nameof(dict)); + + // Transform input from 1:1 to 1:n and store as readonly + _dict = new ReadOnlyDictionary>( + dict.ToDictionary(x => x.Key, x => (IList)new[] { x.Value }) + ); + } + + public InputArguments(IDictionary> dict) + { + EnsureArgument.NotNull(dict, nameof(dict)); // Wrap the dictionary internally as readonly - _dict = new ReadOnlyDictionary(dict); + _dict = new ReadOnlyDictionary>(dict); } #region Common Arguments @@ -35,6 +42,7 @@ public InputArguments(IDictionary dict) public string Path => GetArgumentOrDefault("path"); public string UserName => GetArgumentOrDefault("username"); public string Password => GetArgumentOrDefault("password"); + public IList WwwAuth => GetMultiArgumentOrDefault("wwwauth"); #endregion @@ -50,9 +58,33 @@ public string GetArgumentOrDefault(string key) return TryGetArgument(key, out string value) ? value : null; } + public IList GetMultiArgumentOrDefault(string key) + { + return TryGetMultiArgument(key, out IList values) ? values : Array.Empty(); + } + public bool TryGetArgument(string key, out string value) { - return _dict.TryGetValue(key, out value); + if (_dict.TryGetValue(key, out IList values)) + { + value = values.FirstOrDefault(); + return value != null; + } + + value = null; + return false; + } + + public bool TryGetMultiArgument(string key, out IList value) + { + if (_dict.TryGetValue(key, out IList values)) + { + value = values; + return true; + } + + value = null; + return false; } public bool TryGetHostAndPort(out string host, out int? port) diff --git a/src/shared/Core/Trace.cs b/src/shared/Core/Trace.cs index 86a1ec698..37f0de417 100644 --- a/src/shared/Core/Trace.cs +++ b/src/shared/Core/Trace.cs @@ -217,24 +217,25 @@ public void WriteDictionarySecrets( bool isSecretEntry = !(secretKeys is null) && secretKeys.Contains(entry.Key, keyComparer ?? EqualityComparer.Default); - void WriteSecretLine(object value) + void WriteSecretLine(string keySuffix, object value) { var message = isSecretEntry && !IsSecretTracingEnabled - ? $"\t{entry.Key}={SecretMask}" - : $"\t{entry.Key}={value}"; + ? $"\t{entry.Key}{keySuffix}={SecretMask}" + : $"\t{entry.Key}{keySuffix}={value}"; WriteLine(message, filePath, lineNumber, memberName); } if (entry.Value is IEnumerable values) { - foreach (string value in values) + List valueList = values.ToList(); + foreach (string value in valueList) { - WriteSecretLine(value); + WriteSecretLine(valueList.Count > 1 ? "[]" : string.Empty, value); } } else { - WriteSecretLine(entry.Value); + WriteSecretLine(string.Empty, entry.Value); } } }