diff --git a/src/Renci.SshNet/Common/Extensions.cs b/src/Renci.SshNet/Common/Extensions.cs index c46368240..00dc27b90 100644 --- a/src/Renci.SshNet/Common/Extensions.cs +++ b/src/Renci.SshNet/Common/Extensions.cs @@ -358,5 +358,30 @@ internal static string Join(this IEnumerable values, string separator) // which is not available on all targets. return string.Join(separator, values); } + +#if NETFRAMEWORK || NETSTANDARD2_0 + internal static bool TryAdd(this Dictionary dictionary, TKey key, TValue value) + { + if (!dictionary.ContainsKey(key)) + { + dictionary.Add(key, value); + return true; + } + + return false; + } + + internal static bool Remove(this Dictionary dictionary, TKey key, out TValue value) + { + if (dictionary.TryGetValue(key, out value)) + { + _ = dictionary.Remove(key); + return true; + } + + value = default; + return false; + } +#endif } } diff --git a/src/Renci.SshNet/ConnectionInfo.cs b/src/Renci.SshNet/ConnectionInfo.cs index 7f36c3406..f22e98816 100644 --- a/src/Renci.SshNet/ConnectionInfo.cs +++ b/src/Renci.SshNet/ConnectionInfo.cs @@ -50,34 +50,32 @@ public class ConnectionInfo : IConnectionInfoInternal /// /// Gets supported key exchange algorithms for this connection. /// - public IDictionary> KeyExchangeAlgorithms { get; private set; } + public IOrderedDictionary> KeyExchangeAlgorithms { get; } /// /// Gets supported encryptions for this connection. /// -#pragma warning disable CA1859 // Use concrete types when possible for improved performance - public IDictionary Encryptions { get; private set; } -#pragma warning restore CA1859 // Use concrete types when possible for improved performance + public IOrderedDictionary Encryptions { get; } /// /// Gets supported hash algorithms for this connection. /// - public IDictionary HmacAlgorithms { get; private set; } + public IOrderedDictionary HmacAlgorithms { get; } /// /// Gets supported host key algorithms for this connection. /// - public IDictionary> HostKeyAlgorithms { get; private set; } + public IOrderedDictionary> HostKeyAlgorithms { get; } /// /// Gets supported authentication methods for this connection. /// - public IList AuthenticationMethods { get; private set; } + public IList AuthenticationMethods { get; } /// /// Gets supported compression algorithms for this connection. /// - public IDictionary> CompressionAlgorithms { get; private set; } + public IOrderedDictionary> CompressionAlgorithms { get; } /// /// Gets the supported channel requests for this connection. @@ -85,7 +83,7 @@ public class ConnectionInfo : IConnectionInfoInternal /// /// The supported channel requests for this connection. /// - public IDictionary ChannelRequests { get; private set; } + public IDictionary ChannelRequests { get; } /// /// Gets a value indicating whether connection is authenticated. @@ -101,7 +99,7 @@ public class ConnectionInfo : IConnectionInfoInternal /// /// The connection host. /// - public string Host { get; private set; } + public string Host { get; } /// /// Gets connection port. @@ -109,12 +107,12 @@ public class ConnectionInfo : IConnectionInfoInternal /// /// The connection port. The default value is 22. /// - public int Port { get; private set; } + public int Port { get; } /// /// Gets connection username. /// - public string Username { get; private set; } + public string Username { get; } /// /// Gets proxy type. @@ -122,27 +120,27 @@ public class ConnectionInfo : IConnectionInfoInternal /// /// The type of the proxy. /// - public ProxyTypes ProxyType { get; private set; } + public ProxyTypes ProxyType { get; } /// /// Gets proxy connection host. /// - public string ProxyHost { get; private set; } + public string ProxyHost { get; } /// /// Gets proxy connection port. /// - public int ProxyPort { get; private set; } + public int ProxyPort { get; } /// /// Gets proxy connection username. /// - public string ProxyUsername { get; private set; } + public string ProxyUsername { get; } /// /// Gets proxy connection password. /// - public string ProxyPassword { get; private set; } + public string ProxyPassword { get; } /// /// Gets or sets connection timeout. @@ -347,7 +345,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy MaxSessions = 10; Encoding = Encoding.UTF8; - KeyExchangeAlgorithms = new Dictionary> + KeyExchangeAlgorithms = new OrderedDictionary> { { "mlkem768x25519-sha256", () => new KeyExchangeMLKem768X25519Sha256() }, { "sntrup761x25519-sha512", () => new KeyExchangeSNtruP761X25519Sha512() }, @@ -365,7 +363,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy { "diffie-hellman-group1-sha1", () => new KeyExchangeDiffieHellmanGroup1Sha1() }, }; - Encryptions = new Dictionary + Encryptions = new OrderedDictionary { { "aes128-ctr", new CipherInfo(128, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) }, { "aes192-ctr", new CipherInfo(192, (key, iv) => new AesCipher(key, iv, AesCipherMode.CTR, pkcs7Padding: false)) }, @@ -379,7 +377,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy { "3des-cbc", new CipherInfo(192, (key, iv) => new TripleDesCipher(key, iv, CipherMode.CBC, pkcs7Padding: false)) }, }; - HmacAlgorithms = new Dictionary + HmacAlgorithms = new OrderedDictionary { /* Encrypt-and-MAC (encrypt-and-authenticate) variants */ { "hmac-sha2-256", new HashInfo(32*8, key => new HMACSHA256(key)) }, @@ -392,7 +390,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy }; #pragma warning disable SA1107 // Code should not contain multiple statements on one line - var hostAlgs = new Dictionary>(); + var hostAlgs = new OrderedDictionary>(); hostAlgs.Add("ssh-ed25519-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ssh-ed25519-cert-v01@openssh.com", cert, hostAlgs); }); hostAlgs.Add("ecdsa-sha2-nistp256-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ecdsa-sha2-nistp256-cert-v01@openssh.com", cert, hostAlgs); }); hostAlgs.Add("ecdsa-sha2-nistp384-cert-v01@openssh.com", data => { var cert = new Certificate(data); return new CertificateHostAlgorithm("ecdsa-sha2-nistp384-cert-v01@openssh.com", cert, hostAlgs); }); @@ -411,7 +409,7 @@ public ConnectionInfo(string host, int port, string username, ProxyTypes proxyTy #pragma warning restore SA1107 // Code should not contain multiple statements on one line HostKeyAlgorithms = hostAlgs; - CompressionAlgorithms = new Dictionary> + CompressionAlgorithms = new OrderedDictionary> { { "none", null }, { "zlib@openssh.com", () => new ZlibOpenSsh() }, diff --git a/src/Renci.SshNet/IOrderedDictionary`2.cs b/src/Renci.SshNet/IOrderedDictionary`2.cs new file mode 100644 index 000000000..23ee59b91 --- /dev/null +++ b/src/Renci.SshNet/IOrderedDictionary`2.cs @@ -0,0 +1,149 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics.CodeAnalysis; + +namespace Renci.SshNet +{ + /// + /// Represents a collection of key/value pairs that are accessible by the key or index. + /// + /// The type of the keys in the dictionary. + /// The type of the values in the dictionary. + public interface IOrderedDictionary : + IDictionary, IReadOnlyDictionary + where TKey : notnull + { + // Some members are redefined with 'new' to resolve ambiguities. + + /// Gets or sets the value associated with the specified key. + /// The key of the value to get or set. + /// The value associated with the specified key. If the specified key is not found, a get operation throws a , and a set operation creates a new element with the specified key. + /// is . + /// The property is retrieved and does not exist in the collection. + /// Setting the value of an existing key does not impact its order in the collection. + new TValue this[TKey key] { get; set; } + + /// Gets a collection containing the keys in the . + new ICollection Keys { get; } + + /// Gets a collection containing the values in the . + new ICollection Values { get; } + + /// Gets the number of key/value pairs contained in the . + new int Count { get; } + + /// Determines whether the contains the specified key. + /// The key to locate in the . + /// if the contains an element with the specified key; otherwise, . + /// is . + new bool ContainsKey(TKey key); + + /// Determines whether the contains a specific value. + /// The value to locate in the . The value can be null for reference types. + /// if the contains an element with the specified value; otherwise, . + bool ContainsValue(TValue value); + + /// Gets the key/value pair at the specified index. + /// The zero-based index of the pair to get. + /// The element at the specified index. + /// is less than 0 or greater than or equal to . + KeyValuePair GetAt(int index); + + /// Determines the index of a specific key in the . + /// The key to locate. + /// The index of if found; otherwise, -1. + /// is . + int IndexOf(TKey key); + + /// Inserts an item into the collection at the specified index. + /// The zero-based index at which item should be inserted. + /// The key to insert. + /// The value to insert. + /// is . + /// An element with the same key already exists in the . + /// is less than 0 or greater than . + void Insert(int index, TKey key, TValue value); + + /// Removes the value with the specified key from the and copies the element to the value parameter. + /// The key of the element to remove. + /// The removed element. + /// if the element is successfully found and removed; otherwise, . + /// is . + bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value); + + /// Removes the key/value pair at the specified index. + /// The zero-based index of the item to remove. + /// is less than 0 or greater than or equal to . + void RemoveAt(int index); + + /// Sets the key/value pair at the specified index. + /// The zero-based index at which to set the key/value pair. + /// The key to store at the specified index. + /// The value to store at the specified index. + /// is . + /// An element with the same key already exists at an index different to . + /// is less than 0 or greater than or equal to . + void SetAt(int index, TKey key, TValue value); + + /// Sets the value for the key at the specified index. + /// The zero-based index at which to set the key/value pair. + /// The value to store at the specified index. + /// is less than 0 or greater than or equal to . + void SetAt(int index, TValue value); + + /// + /// Moves an existing key/value pair to the specified index in the collection. + /// + /// The current zero-based index of the key/value pair to move. + /// The zero-based index at which to set the key/value pair. + /// + /// or are less than 0 or greater than or equal to . + /// + void SetPosition(int index, int newIndex); + + /// + /// Moves an existing key/value pair to the specified index in the collection. + /// + /// The key to move. + /// The zero-based index at which to set the key/value pair. + /// The specified key does not exist in the collection. + /// is less than 0 or greater than or equal to . + void SetPosition(TKey key, int newIndex); + + /// Adds the specified key and value to the dictionary if the key doesn't already exist. + /// The key of the element to add. + /// The value of the element to add. The value can be for reference types. + /// if the key didn't exist and the key and value were added to the dictionary; otherwise, . + /// is . + bool TryAdd(TKey key, TValue value); + + /// Adds the specified key and value to the dictionary if the key doesn't already exist. + /// The key of the element to add. + /// The value of the element to add. The value can be for reference types. + /// The index of the added or existing . This is always a valid index into the dictionary. + /// if the key didn't exist and the key and value were added to the dictionary; otherwise, . + /// is . + bool TryAdd(TKey key, TValue value, out int index); + + /// Gets the value associated with the specified key. + /// The key of the value to get. + /// + /// When this method returns, contains the value associated with the specified key, if the key is found; + /// otherwise, the default value for the type of the value parameter. + /// + /// if the contains an element with the specified key; otherwise, . + /// is . + new bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value); + + /// Gets the value associated with the specified key. + /// The key of the value to get. + /// + /// When this method returns, contains the value associated with the specified key, if the key is found; + /// otherwise, the default value for the type of the value parameter. + /// + /// The index of if found; otherwise, -1. + /// if the contains an element with the specified key; otherwise, . + /// is . + bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value, out int index); + } +} diff --git a/src/Renci.SshNet/OrderedDictionary.net9.cs b/src/Renci.SshNet/OrderedDictionary.net9.cs new file mode 100644 index 000000000..6bc37d025 --- /dev/null +++ b/src/Renci.SshNet/OrderedDictionary.net9.cs @@ -0,0 +1,245 @@ +#if NET9_0_OR_GREATER +#nullable enable +#pragma warning disable SA1649 // File name should match first type name + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; + +namespace Renci.SshNet +{ + internal sealed class OrderedDictionary : IOrderedDictionary + where TKey : notnull + { + private readonly System.Collections.Generic.OrderedDictionary _impl; + + public OrderedDictionary(EqualityComparer? comparer = null) + { + _impl = new System.Collections.Generic.OrderedDictionary(comparer); + } + + public TValue this[TKey key] + { + get + { + return _impl[key]; + } + set + { + _impl[key] = value; + } + } + + public ICollection Keys + { + get + { + return _impl.Keys; + } + } + + IEnumerable IReadOnlyDictionary.Keys + { + get + { + return ((IReadOnlyDictionary)_impl).Keys; + } + } + + public ICollection Values + { + get + { + return _impl.Values; + } + } + + IEnumerable IReadOnlyDictionary.Values + { + get + { + return ((IReadOnlyDictionary)_impl).Values; + } + } + + public int Count + { + get + { + return _impl.Count; + } + } + + bool ICollection>.IsReadOnly + { + get + { + return ((ICollection>)_impl).IsReadOnly; + } + } + + public void Add(TKey key, TValue value) + { + _impl.Add(key, value); + } + + void ICollection>.Add(KeyValuePair item) + { + ((ICollection>)_impl).Add(item); + } + + public void Clear() + { + _impl.Clear(); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return ((ICollection>)_impl).Contains(item); + } + + public bool ContainsKey(TKey key) + { + return _impl.ContainsKey(key); + } + + public bool ContainsValue(TValue value) + { + return _impl.ContainsValue(value); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + ((ICollection>)_impl).CopyTo(array, arrayIndex); + } + + public KeyValuePair GetAt(int index) + { + return _impl.GetAt(index); + } + + public IEnumerator> GetEnumerator() + { + return _impl.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public int IndexOf(TKey key) + { + return _impl.IndexOf(key); + } + + public void Insert(int index, TKey key, TValue value) + { + _impl.Insert(index, key, value); + } + + public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value) + { + return _impl.Remove(key, out value); + } + + public bool Remove(TKey key) + { + return _impl.Remove(key); + } + + bool ICollection>.Remove(KeyValuePair item) + { + return ((ICollection>)_impl).Remove(item); + } + + public void RemoveAt(int index) + { + _impl.RemoveAt(index); + } + + public void SetAt(int index, TKey key, TValue value) + { + _impl.SetAt(index, key, value); + } + + public void SetAt(int index, TValue value) + { + _impl.SetAt(index, value); + } + + public void SetPosition(int index, int newIndex) + { + if ((uint)newIndex >= Count) + { + throw new ArgumentOutOfRangeException(nameof(newIndex)); + } + + var kvp = _impl.GetAt(index); + + _impl.RemoveAt(index); + + _impl.Insert(newIndex, kvp.Key, kvp.Value); + } + + public void SetPosition(TKey key, int newIndex) + { + if ((uint)newIndex >= Count) + { + throw new ArgumentOutOfRangeException(nameof(newIndex)); + } + + if (!_impl.Remove(key, out var value)) + { + // Please throw a nicely formatted, localised exception. + _ = _impl[key]; + + Debug.Fail("Previous line should throw KeyNotFoundException."); + } + + _impl.Insert(newIndex, key, value); + } + + public bool TryAdd(TKey key, TValue value) + { + return _impl.TryAdd(key, value); + } + + public bool TryAdd(TKey key, TValue value, out int index) + { +#if NET10_0_OR_GREATER + return _impl.TryAdd(key, value, out index); +#else + var success = _impl.TryAdd(key, value); + + index = _impl.IndexOf(key); + + return success; +#endif + } + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) + { + return _impl.TryGetValue(key, out value); + } + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value, out int index) + { +#if NET10_0_OR_GREATER + return _impl.TryGetValue(key, out value, out index); +#else + if (_impl.TryGetValue(key, out value)) + { + index = _impl.IndexOf(key); + return true; + } + + index = -1; + return false; +#endif + } + } +} +#endif diff --git a/src/Renci.SshNet/OrderedDictionary.netstandard.cs b/src/Renci.SshNet/OrderedDictionary.netstandard.cs new file mode 100644 index 000000000..a69b3200d --- /dev/null +++ b/src/Renci.SshNet/OrderedDictionary.netstandard.cs @@ -0,0 +1,514 @@ +#if !NET9_0_OR_GREATER +#nullable enable +#pragma warning disable SA1649 // File name should match first type name + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Diagnostics; +using System.Diagnostics.CodeAnalysis; +using System.Linq; + +using Renci.SshNet.Common; + +namespace Renci.SshNet +{ + internal sealed class OrderedDictionary : IOrderedDictionary + where TKey : notnull + { + private readonly Dictionary _dictionary; + private readonly List> _list; + + private KeyCollection? _keys; + private ValueCollection? _values; + + public OrderedDictionary(EqualityComparer? comparer = null) + { + _dictionary = new Dictionary(comparer); + _list = new List>(); + } + + public TValue this[TKey key] + { + get + { + return _dictionary[key]; + } + set + { + if (_dictionary.TryAdd(key, value)) + { + _list.Add(new KeyValuePair(key, value)); + } + else + { + _dictionary[key] = value; + _list[IndexOf(key)] = new KeyValuePair(key, value); + } + + AssertConsistency(); + } + } + + [Conditional("DEBUG")] + private void AssertConsistency() + { + Debug.Assert(_list.Count == _dictionary.Count); + + foreach (var kvp in _list) + { + Debug.Assert(_dictionary.TryGetValue(kvp.Key, out var value)); + Debug.Assert(EqualityComparer.Default.Equals(kvp.Value, value)); + } + + foreach (var kvp in _dictionary) + { + var index = EnumeratingIndexOf(kvp.Key); + Debug.Assert(index >= 0); + Debug.Assert(EqualityComparer.Default.Equals(kvp.Value, _list[index].Value)); + } + } + + public ICollection Keys + { + get + { + return _keys ??= new KeyCollection(this); + } + } + + IEnumerable IReadOnlyDictionary.Keys + { + get + { + return Keys; + } + } + + public ICollection Values + { + get + { + return _values ??= new ValueCollection(this); + } + } + + IEnumerable IReadOnlyDictionary.Values + { + get + { + return Values; + } + } + + public int Count + { + get + { + Debug.Assert(_list.Count == _dictionary.Count); + return _list.Count; + } + } + + bool ICollection>.IsReadOnly + { + get + { + return false; + } + } + + public void Add(TKey key, TValue value) + { + _dictionary.Add(key, value); + _list.Add(new KeyValuePair(key, value)); + + AssertConsistency(); + } + + void ICollection>.Add(KeyValuePair item) + { + Add(item.Key, item.Value); + } + + public void Clear() + { + _dictionary.Clear(); + _list.Clear(); + } + + bool ICollection>.Contains(KeyValuePair item) + { + return ((ICollection>)_dictionary).Contains(item); + } + + public bool ContainsKey(TKey key) + { + return _dictionary.ContainsKey(key); + } + + public bool ContainsValue(TValue value) + { + return _dictionary.ContainsValue(value); + } + + void ICollection>.CopyTo(KeyValuePair[] array, int arrayIndex) + { + _list.CopyTo(array, arrayIndex); + } + + public KeyValuePair GetAt(int index) + { + return _list[index]; + } + + public IEnumerator> GetEnumerator() + { + return _list.GetEnumerator(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + + public int IndexOf(TKey key) + { + // Fast lookup. + if (!_dictionary.ContainsKey(key)) + { + Debug.Assert(EnumeratingIndexOf(key) == -1); + return -1; + } + + var index = EnumeratingIndexOf(key); + + Debug.Assert(index >= 0); + + return index; + } + + private int EnumeratingIndexOf(TKey key) + { + Debug.Assert(key is not null); + + var i = -1; + + foreach (var kvp in _list) + { + i++; + + if (_dictionary.Comparer.Equals(key, kvp.Key)) + { + return i; + } + } + + return -1; + } + + public void Insert(int index, TKey key, TValue value) + { + // This validation is also done by _list.Insert but we must + // do it before _dictionary.Add to avoid corrupting the state. + if ((uint)index > Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + _dictionary.Add(key, value); + _list.Insert(index, new KeyValuePair(key, value)); + + AssertConsistency(); + } + + public bool Remove(TKey key, [MaybeNullWhen(false)] out TValue value) + { + if (_dictionary.Remove(key, out value)) + { + _list.RemoveAt(EnumeratingIndexOf(key)); + AssertConsistency(); + return true; + } + + AssertConsistency(); + value = default!; + return false; + } + + public bool Remove(TKey key) + { + if (_dictionary.Remove(key)) + { + _list.RemoveAt(EnumeratingIndexOf(key)); + AssertConsistency(); + return true; + } + + AssertConsistency(); + return false; + } + + bool ICollection>.Remove(KeyValuePair item) + { + if (((ICollection>)_dictionary).Remove(item)) + { + _list.RemoveAt(EnumeratingIndexOf(item.Key)); + AssertConsistency(); + return true; + } + + AssertConsistency(); + return false; + } + + public void RemoveAt(int index) + { + var key = _list[index].Key; + + _list.RemoveAt(index); + + var success = _dictionary.Remove(key); + Debug.Assert(success); + + AssertConsistency(); + } + + public void SetAt(int index, TKey key, TValue value) + { + if ((uint)index >= Count) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } + + if (TryGetValue(key, out _, out var existingIndex)) + { + if (index != existingIndex) + { + throw new ArgumentException("An item with the same key has already been added", nameof(key)); + } + } + else + { + var oldKeyRemoved = _dictionary.Remove(_list[index].Key); + + Debug.Assert(oldKeyRemoved); + } + + _dictionary[key] = value; + _list[index] = new KeyValuePair(key, value); + + AssertConsistency(); + } + + public void SetAt(int index, TValue value) + { + var key = _list[index].Key; + + _list[index] = new KeyValuePair(key, value); + _dictionary[key] = value; + + AssertConsistency(); + } + + public void SetPosition(int index, int newIndex) + { + if ((uint)newIndex >= Count) + { + throw new ArgumentOutOfRangeException(nameof(newIndex)); + } + + var kvp = _list[index]; + + _list.RemoveAt(index); + _list.Insert(newIndex, kvp); + + AssertConsistency(); + } + + public void SetPosition(TKey key, int newIndex) + { + // This performs the same lookup that IndexOf would + // but throws a nicely formatted KeyNotFoundException + // if the key does not exist in the collection. + _ = _dictionary[key]; + + Debug.Assert(key is not null); + + var oldIndex = EnumeratingIndexOf(key); + + Debug.Assert(oldIndex >= 0); + + SetPosition(oldIndex, newIndex); + } + + public bool TryAdd(TKey key, TValue value) + { + if (_dictionary.TryAdd(key, value)) + { + _list.Add(new KeyValuePair(key, value)); + AssertConsistency(); + return true; + } + + AssertConsistency(); + return false; + } + + public bool TryAdd(TKey key, TValue value, out int index) + { + if (_dictionary.TryAdd(key, value)) + { + _list.Add(new KeyValuePair(key, value)); + index = _list.Count - 1; + AssertConsistency(); + return true; + } + + index = EnumeratingIndexOf(key); + AssertConsistency(); + return false; + } + +#if NET + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value) +#else + public bool TryGetValue(TKey key, out TValue value) +#endif + { + return _dictionary.TryGetValue(key, out value); + } + + public bool TryGetValue(TKey key, [MaybeNullWhen(false)] out TValue value, out int index) + { + if (_dictionary.TryGetValue(key, out value)) + { + index = EnumeratingIndexOf(key); + return true; + } + + index = -1; + return false; + } + + private sealed class KeyCollection : KeyOrValueCollection + { + public KeyCollection(OrderedDictionary orderedDictionary) + : base(orderedDictionary) + { + } + + public override bool Contains(TKey item) + { + return OrderedDictionary._dictionary.ContainsKey(item); + } + + public override void CopyTo(TKey[] array, int arrayIndex) + { + base.CopyTo(array, arrayIndex); // Validation + + foreach (var kvp in OrderedDictionary._list) + { + array[arrayIndex++] = kvp.Key; + } + } + + public override IEnumerator GetEnumerator() + { + return OrderedDictionary._list.Select(kvp => kvp.Key).GetEnumerator(); + } + } + + private sealed class ValueCollection : KeyOrValueCollection + { + public ValueCollection(OrderedDictionary orderedDictionary) + : base(orderedDictionary) + { + } + + public override bool Contains(TValue item) + { + return OrderedDictionary._dictionary.ContainsValue(item); + } + + public override void CopyTo(TValue[] array, int arrayIndex) + { + base.CopyTo(array, arrayIndex); // Validation + + foreach (var kvp in OrderedDictionary._list) + { + array[arrayIndex++] = kvp.Value; + } + } + + public override IEnumerator GetEnumerator() + { + return OrderedDictionary._list.Select(kvp => kvp.Value).GetEnumerator(); + } + } + + private abstract class KeyOrValueCollection : ICollection + { + protected OrderedDictionary OrderedDictionary { get; } + + protected KeyOrValueCollection(OrderedDictionary orderedDictionary) + { + OrderedDictionary = orderedDictionary; + } + + public int Count + { + get + { + return OrderedDictionary.Count; + } + } + + public bool IsReadOnly + { + get + { + return true; + } + } + + public void Add(T item) + { + throw new NotSupportedException(); + } + + public void Clear() + { + throw new NotSupportedException(); + } + + public abstract bool Contains(T item); + + public virtual void CopyTo(T[] array, int arrayIndex) + { + ThrowHelper.ThrowIfNull(array); + ThrowHelper.ThrowIfNegative(arrayIndex); + + if (array.Length - arrayIndex < Count) + { + throw new ArgumentException( + "Destination array was not long enough. Check the destination index, length, and the array's lower bounds.", + nameof(array)); + } + } + + public abstract IEnumerator GetEnumerator(); + + public bool Remove(T item) + { + throw new NotSupportedException(); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return GetEnumerator(); + } + } + } +} +#endif diff --git a/test/Renci.SshNet.Tests/.editorconfig b/test/Renci.SshNet.Tests/.editorconfig index ae7f49219..e3ef92c0c 100644 --- a/test/Renci.SshNet.Tests/.editorconfig +++ b/test/Renci.SshNet.Tests/.editorconfig @@ -309,6 +309,10 @@ dotnet_diagnostic.MA0026.severity = silent # https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0042.md dotnet_diagnostic.MA0042.severity = silent +# MA0160: Use ContainsKey instead of TryGetValue +# https://github.com/meziantou/Meziantou.Analyzer/blob/main/docs/Rules/MA0160.md +dotnet_diagnostic.MA0160.severity = silent + #### .NET Compiler Platform analysers rules #### # CA1031: Do not catch general exception types @@ -343,6 +347,10 @@ dotnet_diagnostic.CA1822.severity = silent # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1825 dotnet_diagnostic.CA1825.severity = silent +# CA1841: Prefer Dictionary Contains methods +# https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1841 +dotnet_diagnostic.CA1841.severity = silent + # CA1859: Use concrete types when possible for improved performance # https://learn.microsoft.com/en-us/dotnet/fundamentals/code-analysis/quality-rules/ca1859 dotnet_diagnostic.CA1859.severity = silent diff --git a/test/Renci.SshNet.Tests/Classes/OrderedDictionaryTest.cs b/test/Renci.SshNet.Tests/Classes/OrderedDictionaryTest.cs new file mode 100644 index 000000000..d5c55a081 --- /dev/null +++ b/test/Renci.SshNet.Tests/Classes/OrderedDictionaryTest.cs @@ -0,0 +1,469 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Renci.SshNet.Tests.Classes +{ + [TestClass] + public class OrderedDictionaryTest + { + private static void AssertEqual(List> expected, OrderedDictionary o) + { + Assert.AreEqual(expected.Count, o.Count); + + CollectionAssert.AreEqual(expected, ToList(o)); // Test the enumerator + + for (int i = 0; i < expected.Count; i++) + { + Assert.AreEqual(expected[i], o.GetAt(i)); + + Assert.AreEqual(expected[i].Value, o[expected[i].Key]); + + Assert.IsTrue(o.TryGetValue(expected[i].Key, out TValue value)); + Assert.AreEqual(expected[i].Value, value); + + Assert.IsTrue(o.TryGetValue(expected[i].Key, out value, out int index)); + Assert.AreEqual(expected[i].Value, value); + Assert.AreEqual(i, index); + + Assert.IsTrue(((ICollection>)o).Contains(expected[i])); + Assert.IsTrue(o.ContainsKey(expected[i].Key)); + Assert.IsTrue(o.ContainsValue(expected[i].Value)); + Assert.IsTrue(o.Keys.Contains(expected[i].Key)); + Assert.IsTrue(o.Values.Contains(expected[i].Value)); + + Assert.AreEqual(i, o.IndexOf(expected[i].Key)); + + Assert.IsFalse(o.TryAdd(expected[i].Key, default)); + Assert.IsFalse(o.TryAdd(expected[i].Key, default, out index)); + Assert.AreEqual(i, index); + } + + Assert.AreEqual(expected.Count, o.Keys.Count); + CollectionAssert.AreEqual(expected.Select(kvp => kvp.Key).ToList(), ToList(o.Keys)); + CollectionAssert.AreEqual(ToList(o.Keys), ToList(((IReadOnlyDictionary)o).Keys)); + + Assert.AreEqual(expected.Count, o.Values.Count); + CollectionAssert.AreEqual(expected.Select(kvp => kvp.Value).ToList(), ToList(o.Values)); + CollectionAssert.AreEqual(ToList(o.Values), ToList(((IReadOnlyDictionary)o).Values)); + + // Test CopyTo + var kvpArray = new KeyValuePair[1 + expected.Count + 1]; + ((ICollection>)o).CopyTo(kvpArray, 1); + CollectionAssert.AreEqual( + (List>)[default, .. expected, default], + kvpArray); + + var keysArray = new TKey[1 + expected.Count + 1]; + o.Keys.CopyTo(keysArray, 1); + CollectionAssert.AreEqual( + (List)[default, .. expected.Select(kvp => kvp.Key), default], + keysArray); + + var valuesArray = new TValue[1 + expected.Count + 1]; + o.Values.CopyTo(valuesArray, 1); + CollectionAssert.AreEqual( + (List)[default, .. expected.Select(kvp => kvp.Value), default], + valuesArray); + + // Creates a List via enumeration, avoiding the ICollection.CopyTo + // optimisation in the List constructor. + static List ToList(IEnumerable values) + { + List list = new(); + foreach (T t in values) + { + list.Add(t); + } + return list; + } + } + + [TestMethod] + public void NullKey_ThrowsArgumentNull() + { + OrderedDictionary o = new() { { "a", 4 } }; + + Assert.ThrowsException(() => o[null]); + Assert.ThrowsException(() => o.Add(null, 1)); + Assert.ThrowsException(() => ((ICollection>)o).Add(new KeyValuePair(null, 1))); + Assert.ThrowsException(() => ((ICollection>)o).Contains(new KeyValuePair(null, 1))); + Assert.ThrowsException(() => o.ContainsKey(null)); + Assert.ThrowsException(() => o.IndexOf(null)); + Assert.ThrowsException(() => o.Insert(0, null, 1)); + Assert.ThrowsException(() => o.Remove(null, out _)); + Assert.ThrowsException(() => o.Remove(null)); + Assert.ThrowsException(() => ((ICollection>)o).Remove(new KeyValuePair(null, 1))); + Assert.ThrowsException(() => o.SetAt(0, null, 1)); + Assert.ThrowsException(() => o.SetPosition(null, 0)); + Assert.ThrowsException(() => o.TryAdd(null, 1)); + Assert.ThrowsException(() => o.TryAdd(null, 1, out _)); + Assert.ThrowsException(() => o.TryGetValue(null, out _)); + Assert.ThrowsException(() => o.TryGetValue(null, out _, out _)); + } + + [TestMethod] + public void Indexer_Match_GetterReturnsValue() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 } }; + + Assert.AreEqual(8, o["b"]); + } + + [TestMethod] + public void Indexer_Match_SetterChangesValue() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 } }; + + o["a"] = 5; + + AssertEqual([new("a", 5), new("b", 8)], o); + } + + [TestMethod] + public void Indexer_NoMatch_GetterThrowsKeyNotFound() + { + OrderedDictionary o = new() { { "a", 4 } }; + + Assert.ThrowsException(() => o["b"]); + } + + [TestMethod] + public void Indexer_NoMatch_SetterAddsItem() + { + OrderedDictionary o = new() { { "a", 4 } }; + + o["b"] = 8; + + AssertEqual([new("a", 4), new("b", 8)], o); + } + + [TestMethod] + public void Add_Match() + { + OrderedDictionary o = new() { { "a", 4 } }; + + Assert.ThrowsException(() => o.Add("a", 8)); + Assert.ThrowsException(() => ((ICollection>)o).Add(new KeyValuePair("a", 8))); + } + + [TestMethod] + public void Clear() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 } }; + + AssertEqual([new("a", 4), new("b", 8)], o); + o.Clear(); + AssertEqual([], o); + } + + [TestMethod] + public void CopyTo() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 } }; + + Assert.ThrowsException(() => ((ICollection>)o).CopyTo(null, 0)); + Assert.ThrowsException(() => ((ICollection>)o).CopyTo(new KeyValuePair[3], -1)); + Assert.ThrowsException(() => ((ICollection>)o).CopyTo(new KeyValuePair[3], 3)); + Assert.ThrowsException(() => ((ICollection>)o).CopyTo(new KeyValuePair[3], 2)); + + Assert.ThrowsException(() => o.Keys.CopyTo(null, 0)); + Assert.ThrowsException(() => o.Keys.CopyTo(new string[3], -1)); + Assert.ThrowsException(() => o.Keys.CopyTo(new string[3], 3)); + Assert.ThrowsException(() => o.Keys.CopyTo(new string[3], 2)); + + Assert.ThrowsException(() => o.Values.CopyTo(null, 0)); + Assert.ThrowsException(() => o.Values.CopyTo(new int[3], -1)); + Assert.ThrowsException(() => o.Values.CopyTo(new int[3], 3)); + Assert.ThrowsException(() => o.Values.CopyTo(new int[3], 2)); + } + + [TestMethod] + public void ContainsKvp_ChecksKeyAndValue() + { + OrderedDictionary o = new() { { "a", 4 } }; + + Assert.IsFalse(((ICollection>)o).Contains(new KeyValuePair("a", 8))); + Assert.IsTrue(((ICollection>)o).Contains(new KeyValuePair("a", 4))); + } + + [TestMethod] + public void NullValues_Permitted() + { + OrderedDictionary o = new() { { "a", "1" } }; + + Assert.IsFalse(o.ContainsValue(null)); + + o.Add("b", null); + + AssertEqual([new("a", "1"), new("b", null)], o); + } + + [TestMethod] + public void GetAt_OutOfRange() + { + OrderedDictionary o = new() { { "a", "1" } }; + + Assert.ThrowsException(() => o.GetAt(-2)); + Assert.ThrowsException(() => o.GetAt(-1)); + Assert.ThrowsException(() => o.GetAt(1)); + } + + [TestMethod] + public void RemoveKvp_ChecksKeyAndValue() + { + OrderedDictionary o = new() { { "a", 4 } }; + + Assert.IsFalse(((ICollection>)o).Remove(new KeyValuePair("a", 8))); + AssertEqual([new("a", 4)], o); + + Assert.IsTrue(((ICollection>)o).Remove(new KeyValuePair("a", 4))); + AssertEqual([], o); + } + + [TestMethod] + public void SetAt() + { + OrderedDictionary o = new(); + + Assert.ThrowsException(() => o.SetAt(-2, 1.1)); + Assert.ThrowsException(() => o.SetAt(-1, 1.1)); + Assert.ThrowsException(() => o.SetAt(0, 1.1)); + Assert.ThrowsException(() => o.SetAt(1, 1.1)); + + o.Add("a", 4); + + Assert.ThrowsException(() => o.SetAt(-2, 1.1)); + Assert.ThrowsException(() => o.SetAt(-1, 1.1)); + + o.SetAt(0, 1.1); + + AssertEqual([new("a", 1.1)], o); + + Assert.ThrowsException(() => o.SetAt(1, 5.5)); + } + + [TestMethod] + public void SetAt3Params_OutOfRange() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 }, { "c", 12 } }; + + Assert.ThrowsException(() => o.SetAt(-1, "d", 16)); + Assert.ThrowsException(() => o.SetAt(3, "d", 16)); + } + + [TestMethod] + public void SetAt3Params_ExistingKeyCorrectIndex_PermitsChangingValue() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 }, { "c", 12 } }; + + o.SetAt(2, "c", 16); + + AssertEqual([new("a", 4), new("b", 8), new("c", 16)], o); + } + + [TestMethod] + public void SetAt3Params_ExistingKeyDifferentIndex_Throws() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 }, { "c", 12 } }; + + Assert.ThrowsException(() => o.SetAt(1, "c", 16)); + } + + [TestMethod] + public void SetAt3Params_PermitsChangingToNewKey() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 }, { "c", 12 } }; + + o.SetAt(1, "d", 16); + + AssertEqual([new("a", 4), new("d", 16), new("c", 12)], o); + } + + [TestMethod] + public void Get_NonExistent() + { + OrderedDictionary o = new() { { "a", 4 } }; + + Assert.ThrowsException(() => o["doesn't exist"]); + Assert.IsFalse(((ICollection>)o).Contains(new KeyValuePair("doesn't exist", 1))); + Assert.IsFalse(o.ContainsKey("doesn't exist")); + Assert.IsFalse(o.ContainsValue(999)); + Assert.AreEqual(-1, o.IndexOf("doesn't exist")); + + Assert.IsFalse(o.Remove("doesn't exist", out float value)); + Assert.AreEqual(default, value); + + Assert.IsFalse(o.Remove("doesn't exist")); + + Assert.IsFalse(((ICollection>)o).Remove(new KeyValuePair("doesn't exist", 1))); + + Assert.IsFalse(o.TryGetValue("doesn't exist", out value)); + Assert.AreEqual(default, value); + + Assert.IsFalse(o.TryGetValue("doesn't exist", out value, out int index)); + Assert.AreEqual(default, value); + Assert.AreEqual(-1, index); + + AssertEqual([new("a", 4)], o); + } + + [TestMethod] + public void Insert() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 } }; + + Assert.ThrowsException(() => o.Insert(-1, "c", 12)); + + o.Insert(0, "c", 12); // Start + AssertEqual([new("c", 12), new("a", 4), new("b", 8)], o); + + o.Insert(2, "d", 12); // Middle + AssertEqual([new("c", 12), new("a", 4), new("d", 12), new("b", 8)], o); + + o.Insert(o.Count, "e", 16); // End + AssertEqual([new("c", 12), new("a", 4), new("d", 12), new("b", 8), new("e", 16)], o); + + Assert.ThrowsException(() => o.Insert(o.Count + 1, "f", 16)); + + // Existing key + Assert.ThrowsException(() => o.Insert(0, "a", 12)); + } + + [TestMethod] + public void Remove_Success() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 }, { "c", 12 } }; + + Assert.IsTrue(o.Remove("b")); + AssertEqual([new("a", 4), new("c", 12)], o); + + Assert.IsTrue(o.Remove("a", out float value)); + Assert.AreEqual(4, value); + AssertEqual([new("c", 12)], o); + + // ICollection.Remove must match Key and Value + Assert.IsFalse(((ICollection>)o).Remove(new KeyValuePair("c", -1))); + AssertEqual([new("c", 12)], o); + + Assert.IsTrue(((ICollection>)o).Remove(new KeyValuePair("c", 12))); + AssertEqual([], o); + } + + [TestMethod] + public void RemoveAt() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 }, { "c", 12 }, { "d", 16 } }; + + Assert.ThrowsException(() => o.RemoveAt(-2)); + Assert.ThrowsException(() => o.RemoveAt(-1)); + + o.RemoveAt(0); // Start + AssertEqual([new("b", 8), new("c", 12), new("d", 16)], o); + + o.RemoveAt(1); // Middle + AssertEqual([new("b", 8), new("d", 16)], o); + + o.RemoveAt(1); // End + AssertEqual([new("b", 8)], o); + + Assert.ThrowsException(() => o.RemoveAt(1)); + } + + [TestMethod] + public void SetPosition_ByIndex() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 }, { "c", 12 }, { "d", 16 } }; + + ArgumentOutOfRangeException ex; + + ex = Assert.ThrowsException(() => o.SetPosition(-1, 0)); + Assert.AreEqual("index", ex.ParamName); + + ex = Assert.ThrowsException(() => o.SetPosition(0, -1)); + Assert.AreEqual("newIndex", ex.ParamName); + + ex = Assert.ThrowsException(() => o.SetPosition(0, 4)); + Assert.AreEqual("newIndex", ex.ParamName); + + ex = Assert.ThrowsException(() => o.SetPosition(4, 0)); + Assert.AreEqual("index", ex.ParamName); + + o.SetPosition(1, 0); + AssertEqual([new("b", 8), new("a", 4), new("c", 12), new("d", 16)], o); + + o.SetPosition(0, 1); + AssertEqual([new("a", 4), new("b", 8), new("c", 12), new("d", 16)], o); + + o.SetPosition(1, 2); + AssertEqual([new("a", 4), new("c", 12), new("b", 8), new("d", 16)], o); + + o.SetPosition(2, 1); + AssertEqual([new("a", 4), new("b", 8), new("c", 12), new("d", 16)], o); + + o.SetPosition(0, 3); + AssertEqual([new("b", 8), new("c", 12), new("d", 16), new("a", 4)], o); + + o.SetPosition(3, 1); + AssertEqual([new("b", 8), new("a", 4), new("c", 12), new("d", 16)], o); + + o.SetPosition(1, 1); // No-op + AssertEqual([new("b", 8), new("a", 4), new("c", 12), new("d", 16)], o); + } + + [TestMethod] + public void SetPosition_ByKey() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 }, { "c", 12 }, { "d", 16 } }; + + Assert.ThrowsException(() => o.SetPosition("a", -1)); + Assert.ThrowsException(() => o.SetPosition("a", 4)); + Assert.ThrowsException(() => o.SetPosition("e", 0)); + + o.SetPosition("b", 0); + AssertEqual([new("b", 8), new("a", 4), new("c", 12), new("d", 16)], o); + + o.SetPosition("b", 1); + AssertEqual([new("a", 4), new("b", 8), new("c", 12), new("d", 16)], o); + + o.SetPosition("a", 3); + AssertEqual([new("b", 8), new("c", 12), new("d", 16), new("a", 4)], o); + + o.SetPosition("d", 2); // No-op + AssertEqual([new("b", 8), new("c", 12), new("d", 16), new("a", 4)], o); + } + + [TestMethod] + public void TryAdd_Success() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 } }; + + Assert.IsTrue(o.TryAdd("c", 12)); + + AssertEqual([new("a", 4), new("b", 8), new("c", 12)], o); + + Assert.IsTrue(o.TryAdd("d", 16, out int index)); + Assert.AreEqual(3, index); + + AssertEqual([new("a", 4), new("b", 8), new("c", 12), new("d", 16)], o); + } + + [TestMethod] + public void KeysAndValuesAreReadOnly() + { + OrderedDictionary o = new() { { "a", 4 }, { "b", 8 } }; + + Assert.IsTrue(o.Keys.IsReadOnly); + Assert.ThrowsException(() => o.Keys.Add("c")); + Assert.ThrowsException(o.Keys.Clear); + Assert.ThrowsException(() => o.Keys.Remove("a")); + + Assert.IsTrue(o.Values.IsReadOnly); + Assert.ThrowsException(() => o.Values.Add(12)); + Assert.ThrowsException(o.Values.Clear); + Assert.ThrowsException(() => o.Values.Remove(4)); + } + } +}