diff --git a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs index 393c08923a6e3d..5e329e0b9aa5e5 100644 --- a/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs +++ b/src/libraries/System.Net.Primitives/ref/System.Net.Primitives.cs @@ -287,6 +287,32 @@ public IPEndPoint(System.Net.IPAddress address, int port) { } public static bool TryParse(System.ReadOnlySpan s, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.IPEndPoint? result) { throw null; } public static bool TryParse(string s, [System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] out System.Net.IPEndPoint? result) { throw null; } } + public readonly partial struct IPNetwork : System.IEquatable, System.IFormattable, System.IParsable, System.ISpanFormattable, System.ISpanParsable + { + private readonly object _dummy; + private readonly int _dummyPrimitive; + public IPNetwork(System.Net.IPAddress baseAddress, int prefixLength) { throw null; } + public System.Net.IPAddress BaseAddress { get { throw null; } } + public int PrefixLength { get { throw null; } } + public bool Contains(System.Net.IPAddress address) { throw null; } + public bool Equals(System.Net.IPNetwork other) { throw null; } + public override bool Equals([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] object? obj) { throw null; } + public override int GetHashCode() { throw null; } + public static bool operator ==(System.Net.IPNetwork left, System.Net.IPNetwork right) { throw null; } + public static bool operator !=(System.Net.IPNetwork left, System.Net.IPNetwork right) { throw null; } + public static System.Net.IPNetwork Parse(System.ReadOnlySpan s) { throw null; } + public static System.Net.IPNetwork Parse(string s) { throw null; } + string System.IFormattable.ToString(string? format, System.IFormatProvider? provider) { throw null; } + static System.Net.IPNetwork System.IParsable.Parse([System.Diagnostics.CodeAnalysis.NotNullAttribute] string s, System.IFormatProvider? provider) { throw null; } + static bool System.IParsable.TryParse([System.Diagnostics.CodeAnalysis.NotNullWhenAttribute(true)] string? s, System.IFormatProvider? provider, out System.Net.IPNetwork result) { throw null; } + bool System.ISpanFormattable.TryFormat(System.Span destination, out int charsWritten, System.ReadOnlySpan format, System.IFormatProvider? provider) { throw null; } + static System.Net.IPNetwork System.ISpanParsable.Parse(System.ReadOnlySpan s, System.IFormatProvider? provider) { throw null; } + static bool System.ISpanParsable.TryParse(System.ReadOnlySpan s, System.IFormatProvider? provider, out System.Net.IPNetwork result) { throw null; } + public override string ToString() { throw null; } + public bool TryFormat(System.Span destination, out int charsWritten) { throw null; } + public static bool TryParse(System.ReadOnlySpan s, out System.Net.IPNetwork result) { throw null; } + public static bool TryParse(string? s, out System.Net.IPNetwork result) { throw null; } + } public partial interface IWebProxy { System.Net.ICredentials? Credentials { get; set; } diff --git a/src/libraries/System.Net.Primitives/src/Resources/Strings.resx b/src/libraries/System.Net.Primitives/src/Resources/Strings.resx index 5a998979f8548e..958a0e2e269f99 100644 --- a/src/libraries/System.Net.Primitives/src/Resources/Strings.resx +++ b/src/libraries/System.Net.Primitives/src/Resources/Strings.resx @@ -78,6 +78,12 @@ An invalid IP address was specified. + + An invalid IP network was specified. + + + The specified baseAddress has non-zero bits after the network prefix. + An error occurred when adding a cookie to the container. diff --git a/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj b/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj index 46fd9cc48db7fb..47385e4c229894 100644 --- a/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj +++ b/src/libraries/System.Net.Primitives/src/System.Net.Primitives.csproj @@ -28,6 +28,7 @@ + + /// Represents an IP network with an containing the network prefix and an defining the prefix length. + /// + /// + /// This type disallows arbitrary IP-address/prefix-length CIDR pairs. must be defined so that all bits after the network prefix are set to zero. + /// In other words, is always the first usable address of the network. + /// The constructor and the parsing methods will throw in case there are non-zero bits after the prefix. + /// + public readonly struct IPNetwork : IEquatable, ISpanFormattable, ISpanParsable + { + private readonly IPAddress? _baseAddress; + + /// + /// Gets the that represents the prefix of the network. + /// + public IPAddress BaseAddress => _baseAddress ?? IPAddress.Any; + + /// + /// Gets the length of the network prefix in bits. + /// + public int PrefixLength { get; } + + /// + /// Initializes a new instance of the class with the specified and prefix length. + /// + /// The that represents the prefix of the network. + /// The length of the prefix in bits. + /// The specified is . + /// The specified is smaller than `0` or longer than maximum length of 's . + /// The specified has non-zero bits after the network prefix. + public IPNetwork(IPAddress baseAddress, int prefixLength) + { + ArgumentNullException.ThrowIfNull(baseAddress); + + if (prefixLength < 0 || prefixLength > GetMaxPrefixLength(baseAddress)) + { + ThrowArgumentOutOfRangeException(); + } + + if (HasNonZeroBitsAfterNetworkPrefix(baseAddress, prefixLength)) + { + ThrowInvalidBaseAddressException(); + } + + _baseAddress = baseAddress; + PrefixLength = prefixLength; + + [DoesNotReturn] + static void ThrowArgumentOutOfRangeException() => throw new ArgumentOutOfRangeException(nameof(prefixLength)); + + [DoesNotReturn] + static void ThrowInvalidBaseAddressException() => throw new ArgumentException(SR.net_bad_ip_network_invalid_baseaddress, nameof(baseAddress)); + } + + // Non-validating ctor + private IPNetwork(IPAddress baseAddress, int prefixLength, bool _) + { + _baseAddress = baseAddress; + PrefixLength = prefixLength; + } + + /// + /// Determines whether a given is part of the network. + /// + /// The to check. + /// if the is part of the network; otherwise, . + /// The specified is . + public bool Contains(IPAddress address) + { + ArgumentNullException.ThrowIfNull(address); + + if (address.AddressFamily != BaseAddress.AddressFamily) + { + return false; + } + + // This prevents the 'uint.MaxValue << 32' and the 'UInt128.MaxValue << 128' special cases in the code below. + if (PrefixLength == 0) + { + return true; + } + + if (address.AddressFamily == AddressFamily.InterNetwork) + { + uint mask = uint.MaxValue << (32 - PrefixLength); + if (BitConverter.IsLittleEndian) + { + mask = BinaryPrimitives.ReverseEndianness(mask); + } + + return BaseAddress.PrivateAddress == (address.PrivateAddress & mask); + } + else + { + UInt128 baseAddressValue = default; + UInt128 otherAddressValue = default; + + BaseAddress.TryWriteBytes(MemoryMarshal.AsBytes(new Span(ref baseAddressValue)), out int bytesWritten); + Debug.Assert(bytesWritten == IPAddressParserStatics.IPv6AddressBytes); + address.TryWriteBytes(MemoryMarshal.AsBytes(new Span(ref otherAddressValue)), out bytesWritten); + Debug.Assert(bytesWritten == IPAddressParserStatics.IPv6AddressBytes); + + UInt128 mask = UInt128.MaxValue << (128 - PrefixLength); + if (BitConverter.IsLittleEndian) + { + mask = BinaryPrimitives.ReverseEndianness(mask); + } + + return baseAddressValue == (otherAddressValue & mask); + } + } + + /// + /// Converts a CIDR to an instance. + /// + /// A that defines an IP network in CIDR notation. + /// An instance. + /// The specified string is . + /// is not a valid CIDR network string, or the address contains non-zero bits after the network prefix. + public static IPNetwork Parse(string s) + { + ArgumentNullException.ThrowIfNull(s); + return Parse(s.AsSpan()); + } + + /// + /// Converts a CIDR character span to an instance. + /// + /// A character span that defines an IP network in CIDR notation. + /// An instance. + /// is not a valid CIDR network string, or the address contains non-zero bits after the network prefix. + public static IPNetwork Parse(ReadOnlySpan s) + { + if (!TryParse(s, out IPNetwork result)) + { + throw new FormatException(SR.net_bad_ip_network); + } + + return result; + } + + /// + /// Converts the specified CIDR string to an instance and returns a value indicating whether the conversion succeeded. + /// + /// A that defines an IP network in CIDR notation. + /// When the method returns, contains an instance if the conversion succeeds. + /// if the conversion was succesful; otherwise, . + public static bool TryParse(string? s, out IPNetwork result) + { + if (s == null) + { + result = default; + return false; + } + + return TryParse(s.AsSpan(), out result); + } + + /// + /// Converts the specified CIDR character span to an instance and returns a value indicating whether the conversion succeeded. + /// + /// A that defines an IP network in CIDR notation. + /// When the method returns, contains an instance if the conversion succeeds. + /// if the conversion was succesful; otherwise, . + public static bool TryParse(ReadOnlySpan s, out IPNetwork result) + { + int separatorIndex = s.LastIndexOf('/'); + if (separatorIndex >= 0) + { + ReadOnlySpan ipAddressSpan = s.Slice(0, separatorIndex); + ReadOnlySpan prefixLengthSpan = s.Slice(separatorIndex + 1); + + if (IPAddress.TryParse(ipAddressSpan, out IPAddress? address) && + int.TryParse(prefixLengthSpan, NumberStyles.None, CultureInfo.InvariantCulture, out int prefixLength) && + prefixLength <= GetMaxPrefixLength(address) && + !HasNonZeroBitsAfterNetworkPrefix(address, prefixLength)) + { + Debug.Assert(prefixLength >= 0); // Parsing with NumberStyles.None should ensure that prefixLength is always non-negative. + result = new IPNetwork(address, prefixLength, false); + return true; + } + } + + result = default; + return false; + } + + private static int GetMaxPrefixLength(IPAddress baseAddress) => baseAddress.AddressFamily == AddressFamily.InterNetwork ? 32 : 128; + + private static bool HasNonZeroBitsAfterNetworkPrefix(IPAddress baseAddress, int prefixLength) + { + if (baseAddress.AddressFamily == AddressFamily.InterNetwork) + { + // The cast to long ensures that the mask becomes 0 for the case where 'prefixLength == 0'. + uint mask = (uint)((long)uint.MaxValue << (32 - prefixLength)); + if (BitConverter.IsLittleEndian) + { + mask = BinaryPrimitives.ReverseEndianness(mask); + } + + return (baseAddress.PrivateAddress & mask) != baseAddress.PrivateAddress; + } + else + { + UInt128 value = default; + baseAddress.TryWriteBytes(MemoryMarshal.AsBytes(new Span(ref value)), out int bytesWritten); + Debug.Assert(bytesWritten == IPAddressParserStatics.IPv6AddressBytes); + if (prefixLength == 0) + { + return value != UInt128.Zero; + } + + UInt128 mask = UInt128.MaxValue << (128 - prefixLength); + if (BitConverter.IsLittleEndian) + { + mask = BinaryPrimitives.ReverseEndianness(mask); + } + + return (value & mask) != value; + } + } + + /// + /// Converts the instance to a string containing the 's CIDR notation. + /// + /// The containing the 's CIDR notation. + public override string ToString() => + string.Create(CultureInfo.InvariantCulture, stackalloc char[128], $"{BaseAddress}/{(uint)PrefixLength}"); + + /// + /// Attempts to write the 's CIDR notation to the given span and returns a value indicating whether the operation succeeded. + /// + /// The destination span of characters. + /// When this method returns, contains the number of characters that were written to . + /// if the formatting was succesful; otherwise . + public bool TryFormat(Span destination, out int charsWritten) => + destination.TryWrite(CultureInfo.InvariantCulture, $"{BaseAddress}/{(uint)PrefixLength}", out charsWritten); + + /// + /// Determines whether two instances are equal. + /// + /// The instance to compare to this instance. + /// if the networks are equal; otherwise . + /// Uninitialized instance. + public bool Equals(IPNetwork other) => + PrefixLength == other.PrefixLength && + BaseAddress.Equals(other.BaseAddress); + + /// + /// Determines whether two instances are equal. + /// + /// The instance to compare to this instance. + /// if is an instance and the networks are equal; otherwise . + /// Uninitialized instance. + public override bool Equals([NotNullWhen(true)] object? obj) => + obj is IPNetwork other && + Equals(other); + + /// + /// Determines whether the specified instances of are equal. + /// + /// + /// + /// if the networks are equal; otherwise . + public static bool operator ==(IPNetwork left, IPNetwork right) => left.Equals(right); + + /// + /// Determines whether the specified instances of are not equal. + /// + /// + /// + /// if the networks are not equal; otherwise . + public static bool operator !=(IPNetwork left, IPNetwork right) => !(left == right); + + /// + /// Returns the hash code for this instance. + /// + /// An integer hash value. + public override int GetHashCode() => HashCode.Combine(BaseAddress, PrefixLength); + + /// + string IFormattable.ToString(string? format, IFormatProvider? provider) => ToString(); + + /// + bool ISpanFormattable.TryFormat(Span destination, out int charsWritten, ReadOnlySpan format, IFormatProvider? provider) => + TryFormat(destination, out charsWritten); + + /// + static IPNetwork IParsable.Parse([NotNull] string s, IFormatProvider? provider) => Parse(s); + + /// + static bool IParsable.TryParse([NotNullWhen(true)] string? s, IFormatProvider? provider, out IPNetwork result) => TryParse(s, out result); + + /// + static IPNetwork ISpanParsable.Parse(ReadOnlySpan s, IFormatProvider? provider) => Parse(s); + + /// + static bool ISpanParsable.TryParse(ReadOnlySpan s, IFormatProvider? provider, out IPNetwork result) => TryParse(s, out result); + } +} diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPNetworkTest.cs b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPNetworkTest.cs new file mode 100644 index 00000000000000..56434733cef049 --- /dev/null +++ b/src/libraries/System.Net.Primitives/tests/FunctionalTests/IPNetworkTest.cs @@ -0,0 +1,276 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. + +using Xunit; + +namespace System.Net.Primitives.Functional.Tests +{ + public class IPNetworkTest + { + public static TheoryData IncorrectFormatData = new TheoryData() + { + { "127.0.0.1" }, + { "A.B.C.D/24" }, + { "127.0.0.1/AB" }, + { "127.0.0.1/-1" }, + { "127.0.0.1/+1" }, + { "2a01:110:8012::/f" }, + { "" }, + }; + + public static TheoryData InvalidNetworkNotationData = new TheoryData() + { + { "127.0.0.1/33" }, // PrefixLength max is 32 for IPv4 + { "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/129" }, // PrefixLength max is 128 for IPv6 + { "127.0.0.1/31" }, // Bits exceed the prefix length of 31 (32nd bit is on) + { "198.51.255.0/23" }, // Bits exceed the prefix length of 23 + { "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/127" }, // Bits exceed the prefix length of 31 + { "2a01:110:8012::/45" }, // Bits exceed the prefix length of 45 (47th bit is on) + }; + + public static TheoryData ValidIPNetworkData = new TheoryData() + { + { "0.0.0.0/32" }, // the whole IPv4 space + { "0.0.0.0/0" }, + { "128.0.0.0/1" }, + { "::/128" }, // the whole IPv6 space + { "255.255.255.255/32" }, + { "198.51.254.0/23" }, + { "42.42.128.0/17" }, + { "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128" }, + { "2a01:110:8012::/47" }, + { "2a01:110:8012::/100" }, + }; + + [Theory] + [MemberData(nameof(ValidIPNetworkData))] + public void Constructor_Valid_Succeeds(string input) + { + string[] splitInput = input.Split('/'); + IPAddress address = IPAddress.Parse(splitInput[0]); + int prefixLegth = int.Parse(splitInput[1]); + + IPNetwork network = new IPNetwork(address, prefixLegth); + + Assert.Equal(address, network.BaseAddress); + Assert.Equal(prefixLegth, network.PrefixLength); + } + + [Fact] + public void Constructor_NullIPAddress_ThrowsArgumentNullException() + { + Assert.Throws(() => new IPNetwork(null, 1)); + } + + [Theory] + [InlineData("192.168.0.1", -1)] + [InlineData("192.168.0.1", 33)] + [InlineData("::", -1)] + [InlineData("ffff::", 129)] + public void Constructor_PrefixLenghtOutOfRange_ThrowsArgumentOutOfRangeException(string ipStr, int prefixLength) + { + IPAddress address = IPAddress.Parse(ipStr); + Assert.Throws(() => new IPNetwork(address, prefixLength)); + } + + [Theory] + [InlineData("192.168.0.1", 31)] + [InlineData("42.42.192.0", 17)] + [InlineData("128.0.0.0", 0)] + [InlineData("2a01:110:8012::", 46)] + public void Constructor_NonZeroBitsAfterNetworkPrefix_ThrowsArgumentException(string ipStr, int prefixLength) + { + IPAddress address = IPAddress.Parse(ipStr); + Assert.Throws(() => new IPNetwork(address, prefixLength)); + } + + [Theory] + [MemberData(nameof(IncorrectFormatData))] + public void Parse_IncorrectFormat_ThrowsFormatException(string input) + { + Assert.Throws(() => IPNetwork.Parse(input)); + } + + [Theory] + [MemberData(nameof(IncorrectFormatData))] + public void TryParse_IncorrectFormat_ReturnsFalse(string input) + { + Assert.False(IPNetwork.TryParse(input, out _)); + } + + [Theory] + [MemberData(nameof(InvalidNetworkNotationData))] + public void Parse_InvalidNetworkNotation_ThrowsFormatException(string input) + { + Assert.Throws(() => IPNetwork.Parse(input)); + } + + [Theory] + [MemberData(nameof(InvalidNetworkNotationData))] + public void TryParse_InvalidNetworkNotation_ReturnsFalse(string input) + { + Assert.False(IPNetwork.TryParse(input, out _)); + } + + [Theory] + [MemberData(nameof(ValidIPNetworkData))] + public void Parse_ValidNetworkNotation_Succeeds(string input) + { + var network = IPNetwork.Parse(input); + Assert.Equal(input, network.ToString()); + } + + [Theory] + [MemberData(nameof(ValidIPNetworkData))] + public void TryParse_ValidNetworkNotation_Succeeds(string input) + { + Assert.True(IPNetwork.TryParse(input, out IPNetwork network)); + Assert.Equal(input, network.ToString()); + } + + [Fact] + public void Contains_Null_ThrowsArgumentNullException() + { + IPNetwork v4 = IPNetwork.Parse("127.0.0.0/8"); + IPNetwork v6 = IPNetwork.Parse("::1/128"); + + Assert.Throws(() => v4.Contains(null)); + Assert.Throws(() => v6.Contains(null)); + } + + [Fact] + public void Contains_DifferentAddressFamily_ReturnsFalse() + { + IPNetwork network = IPNetwork.Parse("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128"); + Assert.False(network.Contains(IPAddress.Loopback)); + } + + [Theory] + [InlineData("0.0.0.0/0", "0.0.0.0", "127.127.127.127", "255.255.255.255")] // the whole IPv4 space + [InlineData("::/0", "::", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] // the whole IPv6 space + [InlineData("255.255.255.255/32", "255.255.255.255")] // single IPv4 address + [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] // single IPv6 address + [InlineData("255.255.255.0/24", "255.255.255.0", "255.255.255.255")] + [InlineData("198.51.248.0/22", "198.51.248.0", "198.51.250.42", "198.51.251.255")] + [InlineData("255.255.255.128/25", "255.255.255.128", "255.255.255.129", "255.255.255.255")] + [InlineData("2a00::/13", "2a00::", "2a00::1", "2a01::", "2a07::", "2a07:ffff:ffff:ffff:ffff:ffff:ffff:ffff")] + [InlineData("2a01:110:8012::/47", "2a01:110:8012::", "2a01:110:8012:42::", "2a01:110:8013::", "2a01:110:8013:ffff:ffff:ffff:ffff:ffff")] + [InlineData("2a01:110:8012:1012:314f:2a00::/87", "2a01:110:8012:1012:314f:2a00::", "2a01:110:8012:1012:314f:2a00::1", "2a01:110:8012:1012:314f:2a00:abcd:4242", "2a01:110:8012:1012:314f:2bff:ffff:ffff")] + [InlineData("2a01:110:8012:1010:914e:2451:1700:0/105", "2a01:110:8012:1010:914e:2451:1700:0", "2a01:110:8012:1010:914e:2451:1742:4242", "2a01:110:8012:1010:914e:2451:177f:ffff")] + public void Contains_WhenInNework_ReturnsTrue(string networkString, params string[] addresses) + { + var network = IPNetwork.Parse(networkString); + + foreach (string address in addresses) + { + Assert.True(network.Contains(IPAddress.Parse(address))); + } + } + + [Theory] + [InlineData("255.255.255.255/32", "255.255.255.254")] // single IPv4 address + [InlineData("ffff:ffff:ffff:ffff:ffff:ffff:ffff:ffff/128", "ffff:ffff:ffff:ffff:ffff:ffff:ffff:fffe")] // single IPv6 address + [InlineData("255.255.255.0/24", "255.255.254.0")] + [InlineData("198.51.248.0/22", "198.50.248.1", "198.52.248.1", "198.51.247.1", "198.51.252.1")] + [InlineData("255.255.255.128/25", "255.255.255.127")] + [InlineData("2a00::/13", "2900:ffff:ffff:ffff:ffff:ffff:ffff:ffff", "2a08::", "2a10::", "3a00::", "2b00::")] + [InlineData("2a01:110:8012::/47", "2a01:110:8011:1::", "2a01:110:8014::", "2a00:110:8012::1", "2a01:111:8012::")] + [InlineData("2a01:110:8012:1012:314f:2a00::/87", "2a01:110:8012:1012:314f:2c00::", "2a01:110:8012:1012:314f:2900::", "2a01:110:8012:1012:324f:2aff:ffff:ffff")] + [InlineData("2a01:110:8012:1010:914e:2451:1700:0/105", "2a01:110:8012:1010:914e:2451:16ff:ffff", "2a01:110:8012:1010:914e:2451:1780:0", "2a01:110:8013:1010:914e:2451:1700:0")] + public void Contains_WhenNotInNetwork_ReturnsFalse(string networkString, params string[] addresses) + { + var network = IPNetwork.Parse(networkString); + + foreach (string address in addresses) + { + Assert.False(network.Contains(IPAddress.Parse(address))); + } + } + + [Theory] + [InlineData(false)] + [InlineData(true)] + public void Equals_WhenDifferent_ReturnsFalse(bool testOperator) + { + var network = IPNetwork.Parse("127.0.0.0/24"); + + var rangeWithDifferentPrefix = IPNetwork.Parse("127.0.1.0/24"); + var rangeWithDifferentPrefixLength = IPNetwork.Parse("127.0.0.0/25"); + + if (testOperator) + { + Assert.False(network == rangeWithDifferentPrefix); + Assert.False(network == rangeWithDifferentPrefixLength); + Assert.True(network != rangeWithDifferentPrefix); + Assert.True(network != rangeWithDifferentPrefixLength); + } + else + { + Assert.False(network.Equals(rangeWithDifferentPrefix)); + Assert.False(network.Equals(rangeWithDifferentPrefixLength)); + + Assert.False(network.Equals((object)rangeWithDifferentPrefix)); + Assert.False(network.Equals((object)rangeWithDifferentPrefixLength)); + } + } + + [Theory] + [InlineData("127.0.0.0/24")] + [InlineData("2a01:110:8012::/47")] + public void EqualiyMethods_WhenEqual(string input) + { + var a = IPNetwork.Parse(input); + var b = IPNetwork.Parse(input); + + Assert.True(a.Equals(b)); + Assert.True(a.Equals((object)b)); + Assert.True(a == b); + Assert.False(a != b); + Assert.Equal(a.GetHashCode(), b.GetHashCode()); + } + + [Fact] + public void Equals_WhenNull_ReturnsFalse() + { + var network = IPNetwork.Parse("127.0.0.0/24"); + + Assert.False(network.Equals(null)); + } + + [Fact] + public void TryFormatSpan_EnoughLength_Succeeds() + { + var input = "127.0.0.0/24"; + var network = IPNetwork.Parse(input); + + Span span = stackalloc char[15]; // IPAddress.TryFormat requires a size of 15 + + Assert.True(network.TryFormat(span, out int charsWritten)); + Assert.Equal(input.Length, charsWritten); + Assert.Equal(input, span.Slice(0, charsWritten).ToString()); + } + + [Theory] + [InlineData("127.127.127.127/32", 15)] + [InlineData("127.127.127.127/32", 0)] + [InlineData("127.127.127.127/32", 1)] + public void TryFormatSpan_NotEnoughLength_ReturnsFalse(string input, int spanLengthToTest) + { + var network = IPNetwork.Parse(input); + + Span span = stackalloc char[spanLengthToTest]; + + Assert.False(network.TryFormat(span, out int charsWritten)); + } + + [Fact] + public void DefaultInstance_IsValid() + { + IPNetwork network = default; + Assert.Equal(IPAddress.Any, network.BaseAddress); + Assert.Equal(default, network); + Assert.NotEqual(IPNetwork.Parse("10.20.30.0/24"), network); + Assert.True(network.Contains(IPAddress.Parse("10.11.12.13"))); + } + } +} diff --git a/src/libraries/System.Net.Primitives/tests/FunctionalTests/System.Net.Primitives.Functional.Tests.csproj b/src/libraries/System.Net.Primitives/tests/FunctionalTests/System.Net.Primitives.Functional.Tests.csproj index 628da5bd9ca22a..86fc2553bd3797 100644 --- a/src/libraries/System.Net.Primitives/tests/FunctionalTests/System.Net.Primitives.Functional.Tests.csproj +++ b/src/libraries/System.Net.Primitives/tests/FunctionalTests/System.Net.Primitives.Functional.Tests.csproj @@ -17,6 +17,7 @@ +