From 4bfaf87aae8905778fbcd1867498b03dc2331311 Mon Sep 17 00:00:00 2001 From: Robert Hague Date: Sat, 1 Mar 2025 16:13:28 +0100 Subject: [PATCH 1/2] Add an OrderedDictionary implementation for algorithm priorities During the key exchange, the algorithms to be used are chosen based on the order that the client sends: first algorithm is most desirable. Currently, the algorithm collections in ConnectionInfo are defined as IDictionary<,> and backed by Dictionary<,>, which does not have any guarantees on the order of enumeration (in practice, when only adding and not removing items it does enumerate in the order that items were added as an implementation detail, but it's not great to rely on it). This change adds IOrderedDictionary<,> and uses it in ConnectionInfo. On .NET 9, this is backed by System.Collections.Generic.OrderedDictionary<,> and on lower targets, it uses a relatively simple implementation backed by a List and a Dictionary. --- src/Renci.SshNet/Common/Extensions.cs | 25 + src/Renci.SshNet/ConnectionInfo.cs | 42 +- src/Renci.SshNet/IOrderedDictionary`2.cs | 149 +++++ src/Renci.SshNet/OrderedDictionary.net9.cs | 245 +++++++++ .../OrderedDictionary.netstandard.cs | 520 ++++++++++++++++++ test/Renci.SshNet.Tests/.editorconfig | 8 + .../Classes/OrderedDictionaryTest.cs | 469 ++++++++++++++++ 7 files changed, 1436 insertions(+), 22 deletions(-) create mode 100644 src/Renci.SshNet/IOrderedDictionary`2.cs create mode 100644 src/Renci.SshNet/OrderedDictionary.net9.cs create mode 100644 src/Renci.SshNet/OrderedDictionary.netstandard.cs create mode 100644 test/Renci.SshNet.Tests/Classes/OrderedDictionaryTest.cs 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..9a741d349 --- /dev/null +++ b/src/Renci.SshNet/OrderedDictionary.netstandard.cs @@ -0,0 +1,520 @@ +#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); +#if NET + ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex); +#else + if (arrayIndex < 0) + { + throw new ArgumentOutOfRangeException(nameof(arrayIndex)); + } +#endif + 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)); + } + } +} From 05ec8d9c8048185d3903daa5b63d86d5dcd7d718 Mon Sep 17 00:00:00 2001 From: Robert Hague Date: Tue, 25 Mar 2025 12:48:08 +0100 Subject: [PATCH 2/2] use ThrowIfNegative --- src/Renci.SshNet/OrderedDictionary.netstandard.cs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/Renci.SshNet/OrderedDictionary.netstandard.cs b/src/Renci.SshNet/OrderedDictionary.netstandard.cs index 9a741d349..a69b3200d 100644 --- a/src/Renci.SshNet/OrderedDictionary.netstandard.cs +++ b/src/Renci.SshNet/OrderedDictionary.netstandard.cs @@ -487,14 +487,8 @@ public void Clear() public virtual void CopyTo(T[] array, int arrayIndex) { ThrowHelper.ThrowIfNull(array); -#if NET - ArgumentOutOfRangeException.ThrowIfNegative(arrayIndex); -#else - if (arrayIndex < 0) - { - throw new ArgumentOutOfRangeException(nameof(arrayIndex)); - } -#endif + ThrowHelper.ThrowIfNegative(arrayIndex); + if (array.Length - arrayIndex < Count) { throw new ArgumentException(