From ae6ff3573ba1aedff3cbd5cd38a4d2a5233f308f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:19:52 +0000 Subject: [PATCH 01/13] Initial plan From 22441780ddb8b09c9bb5c43ed8e117b1ff61f13a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:27:32 +0000 Subject: [PATCH 02/13] Add RandomNumberGenerator polyfills for Net60, Net80, and Net90 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .../Net60/RandomNumberGeneratorTests.cs | 86 +++++++++++++++++++ .../Net80/RandomNumberGeneratorTests.cs | 53 ++++++++++++ .../Net90/RandomNumberGeneratorTests.cs | 64 ++++++++++++++ PolyShim/Net60/RandomNumberGenerator.cs | 80 +++++++++++++++++ PolyShim/Net80/RandomNumberGenerator.cs | 48 +++++++++++ PolyShim/Net90/RandomNumberGenerator.cs | 51 +++++++++++ PolyShim/Signatures.md | 11 ++- 7 files changed, 391 insertions(+), 2 deletions(-) create mode 100644 PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs create mode 100644 PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs create mode 100644 PolyShim.Tests/Net90/RandomNumberGeneratorTests.cs create mode 100644 PolyShim/Net60/RandomNumberGenerator.cs create mode 100644 PolyShim/Net80/RandomNumberGenerator.cs create mode 100644 PolyShim/Net90/RandomNumberGenerator.cs diff --git a/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs b/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs new file mode 100644 index 00000000..2221f336 --- /dev/null +++ b/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs @@ -0,0 +1,86 @@ +using System; +using System.Security.Cryptography; +using FluentAssertions; +using Xunit; + +namespace PolyShim.Tests.Net60; + +public class RandomNumberGeneratorTests +{ + [Fact] + public void GetBytes_PartialArray_Test() + { + // Arrange + var rng = RandomNumberGenerator.Create(); + var data = new byte[20]; + + // Act + rng.GetBytes(data, 5, 10); + + // Assert + // First 5 bytes should be zero + for (var i = 0; i < 5; i++) + { + data[i].Should().Be(0); + } + + // Middle 10 bytes should have at least some non-zero values + var middleSection = new byte[10]; + Array.Copy(data, 5, middleSection, 0, 10); + middleSection.Should().Contain(b => b != 0); + + // Last 5 bytes should be zero + for (var i = 15; i < 20; i++) + { + data[i].Should().Be(0); + } + + ((IDisposable)rng).Dispose(); + } + + [Fact] + public void GetBytes_EmptyCount_Test() + { + // Arrange + var rng = RandomNumberGenerator.Create(); + var data = new byte[10]; + + // Act & Assert + // Should not throw and should leave array unchanged + rng.GetBytes(data, 0, 0); + data.Should().AllBeEquivalentTo(0); + + ((IDisposable)rng).Dispose(); + } + + [Fact] + public void GetInt32_MaxValue_Test() + { + // Act & assert + for (var i = 0; i < 100; i++) + { + var value = RandomNumberGenerator.GetInt32(10); + value.Should().BeInRange(0, 9); + } + } + + [Fact] + public void GetInt32_Range_Test() + { + // Act & assert + for (var i = 0; i < 100; i++) + { + var value = RandomNumberGenerator.GetInt32(10, 20); + value.Should().BeInRange(10, 19); + } + } + + [Fact] + public void GetInt32_InvalidRange_Test() + { + // Act & assert + Assert.Throws(() => RandomNumberGenerator.GetInt32(20, 10)); + Assert.Throws(() => RandomNumberGenerator.GetInt32(0)); + Assert.Throws(() => RandomNumberGenerator.GetInt32(-1)); + } +} diff --git a/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs b/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs new file mode 100644 index 00000000..99896722 --- /dev/null +++ b/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs @@ -0,0 +1,53 @@ +using System; +using System.Security.Cryptography; +using FluentAssertions; +using Xunit; + +namespace PolyShim.Tests.Net80; + +public class RandomNumberGeneratorTests +{ + [Fact] + public void GetItems_Test() + { + // Arrange + var choices = new[] { 1, 2, 3, 4, 5 }; + + // Act + for (var i = 0; i < 100; i++) + { + var items = RandomNumberGenerator.GetItems(choices, 3); + + // Assert + items.Should().HaveCount(3); + + foreach (var item in items) + { + choices.Should().Contain(item); + } + } + } + + [Fact] + public void Shuffle_Test() + { + // Arrange + var originalItems = new[] { 1, 2, 3, 4, 5 }; + + // Act + for (var i = 0; i < 100; i++) + { + var items = (int[])originalItems.Clone(); + RandomNumberGenerator.Shuffle(items); + + // Assert + items.Should().HaveCount(originalItems.Length); + items.Should().BeEquivalentTo(originalItems); + + foreach (var item in items) + { + originalItems.Should().Contain(item); + } + } + } +} diff --git a/PolyShim.Tests/Net90/RandomNumberGeneratorTests.cs b/PolyShim.Tests/Net90/RandomNumberGeneratorTests.cs new file mode 100644 index 00000000..01f82734 --- /dev/null +++ b/PolyShim.Tests/Net90/RandomNumberGeneratorTests.cs @@ -0,0 +1,64 @@ +using System; +using System.Security.Cryptography; +using FluentAssertions; +using Xunit; + +namespace PolyShim.Tests.Net90; + +public class RandomNumberGeneratorTests +{ + [Fact] + public void GetHexString_Test() + { + // Act & assert + for (var i = 0; i < 100; i++) + { + RandomNumberGenerator.GetHexString(16).Should().MatchRegex("^[0-9A-F]{16}$"); + RandomNumberGenerator.GetHexString(16, true).Should().MatchRegex("^[0-9a-f]{16}$"); + RandomNumberGenerator.GetHexString(15).Should().MatchRegex("^[0-9A-F]{15}$"); + } + } + + [Fact] + public void GetString_Array_Test() + { + // Arrange + var choices = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".ToCharArray(); + + // Act + for (var i = 0; i < 100; i++) + { + var str = RandomNumberGenerator.GetString(choices, 16); + + // Assert + str.Length.Should().Be(16); + + foreach (var ch in str) + { + choices.Should().Contain(ch); + } + } + } + + [Fact] + public void GetString_Span_Test() + { + // Arrange + var choices = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".AsSpan(); + + // Act + for (var i = 0; i < 100; i++) + { + var str = RandomNumberGenerator.GetString(choices, 16); + + // Assert + str.Length.Should().Be(16); + + foreach (var ch in str) + { + choices.ToArray().Should().Contain(ch); + } + } + } +} diff --git a/PolyShim/Net60/RandomNumberGenerator.cs b/PolyShim/Net60/RandomNumberGenerator.cs new file mode 100644 index 00000000..50a30b3d --- /dev/null +++ b/PolyShim/Net60/RandomNumberGenerator.cs @@ -0,0 +1,80 @@ +#if (NETCOREAPP && !NET6_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD) +#nullable enable +// ReSharper disable RedundantUsingDirective +// ReSharper disable CheckNamespace +// ReSharper disable InconsistentNaming +// ReSharper disable PartialTypeWithSinglePart + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; + +[ExcludeFromCodeCoverage] +internal static class MemberPolyfills_Net60_RandomNumberGenerator +{ + extension(RandomNumberGenerator rng) + { + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-byte()-system-int32-system-int32) + public void GetBytes(byte[] data, int offset, int count) + { + if (data is null) + throw new ArgumentNullException(nameof(data)); + if (offset < 0) + throw new ArgumentOutOfRangeException(nameof(offset)); + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count)); + if (offset + count > data.Length) + throw new ArgumentException("offset and count exceed the size of data"); + + if (count == 0) + return; + + var buffer = new byte[count]; + rng.GetBytes(buffer); + Array.Copy(buffer, 0, data, offset, count); + } + } + + extension(RandomNumberGenerator) + { + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32) + public static int GetInt32(int toExclusive) + { + if (toExclusive <= 0) + throw new ArgumentOutOfRangeException(nameof(toExclusive)); + + return RandomNumberGenerator.GetInt32(0, toExclusive); + } + + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32-system-int32) + public static int GetInt32(int fromInclusive, int toExclusive) + { + if (fromInclusive >= toExclusive) + { + throw new ArgumentException("fromInclusive must be less than toExclusive"); + } + + var range = (uint)(toExclusive - fromInclusive); + var rng = RandomNumberGenerator.Create(); + try + { + uint result; + do + { + var buffer = new byte[4]; + rng.GetBytes(buffer); + result = BitConverter.ToUInt32(buffer, 0); + } while (result > uint.MaxValue - (uint.MaxValue % range + 1) % range); + + return (int)(result % range) + fromInclusive; + } + finally + { + // Explicit cast needed for .NET Framework 3.5 where RandomNumberGenerator + // doesn't properly expose IDisposable for using statements. + ((IDisposable)rng).Dispose(); + } + } + } +} +#endif diff --git a/PolyShim/Net80/RandomNumberGenerator.cs b/PolyShim/Net80/RandomNumberGenerator.cs new file mode 100644 index 00000000..55e7f8c7 --- /dev/null +++ b/PolyShim/Net80/RandomNumberGenerator.cs @@ -0,0 +1,48 @@ +#if (NETCOREAPP && !NET8_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD) +#nullable enable +// ReSharper disable RedundantUsingDirective +// ReSharper disable CheckNamespace +// ReSharper disable InconsistentNaming +// ReSharper disable PartialTypeWithSinglePart + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; + +[ExcludeFromCodeCoverage] +internal static class MemberPolyfills_Net80_RandomNumberGenerator +{ + extension(RandomNumberGenerator) + { + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(-0()-system-int32) + public static T[] GetItems(T[] choices, int length) + { + if (choices is null) + throw new ArgumentNullException(nameof(choices)); + if (length < 0) + throw new ArgumentOutOfRangeException(nameof(length)); + + var result = new T[length]; + for (var i = 0; i < length; i++) + { + var index = RandomNumberGenerator.GetInt32(choices.Length); + result[i] = choices[index]; + } + return result; + } + + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.shuffle#system-security-cryptography-randomnumbergenerator-shuffle-1(-0()) + public static void Shuffle(T[] items) + { + if (items is null) + throw new ArgumentNullException(nameof(items)); + + for (var i = items.Length - 1; i > 0; i--) + { + var j = RandomNumberGenerator.GetInt32(i + 1); + (items[i], items[j]) = (items[j], items[i]); + } + } + } +} +#endif diff --git a/PolyShim/Net90/RandomNumberGenerator.cs b/PolyShim/Net90/RandomNumberGenerator.cs new file mode 100644 index 00000000..7187d932 --- /dev/null +++ b/PolyShim/Net90/RandomNumberGenerator.cs @@ -0,0 +1,51 @@ +#if (NETCOREAPP && !NET9_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD) +#nullable enable +// ReSharper disable RedundantUsingDirective +// ReSharper disable CheckNamespace +// ReSharper disable InconsistentNaming +// ReSharper disable PartialTypeWithSinglePart + +using System; +using System.Text; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; + +[ExcludeFromCodeCoverage] +internal static class MemberPolyfills_Net90_RandomNumberGenerator +{ + extension(RandomNumberGenerator) + { + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.gethexstring#system-security-cryptography-randomnumbergenerator-gethexstring(system-int32-system-boolean) + public static string GetHexString(int stringLength, bool lowercase = false) + { + if (stringLength < 0) + throw new ArgumentOutOfRangeException(nameof(stringLength)); + + var bytes = new byte[(stringLength + 1) / 2]; + RandomNumberGenerator.Fill(bytes); + + var hex = lowercase ? Convert.ToHexStringLower(bytes) : Convert.ToHexString(bytes); + return hex.Substring(0, stringLength); + } + + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getstring + public static string GetString(ReadOnlySpan choices, int length) + { + if (choices.Length == 0) + throw new ArgumentException("choices must not be empty", nameof(choices)); + if (length < 0) + throw new ArgumentOutOfRangeException(nameof(length)); + + var buffer = new StringBuilder(length); + + for (var i = 0; i < length; i++) + { + var index = RandomNumberGenerator.GetInt32(choices.Length); + buffer.Append(choices[index]); + } + + return buffer.ToString(); + } + } +} +#endif diff --git a/PolyShim/Signatures.md b/PolyShim/Signatures.md index 4eee34af..dbbe0ec0 100644 --- a/PolyShim/Signatures.md +++ b/PolyShim/Signatures.md @@ -1,8 +1,8 @@ # Signatures -- **Total:** 343 +- **Total:** 350 - **Types:** 76 -- **Members:** 267 +- **Members:** 274 ___ @@ -279,7 +279,14 @@ ___ - [`void Shuffle(T[])`](https://learn.microsoft.com/dotnet/api/system.random.shuffle#system-random-shuffle-1(-0())) .NET 8.0 - `RandomNumberGenerator` - **[class]** .NET Core 1.0 + - [`int GetInt32(int, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32-system-int32)) .NET 6.0 + - [`int GetInt32(int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32)) .NET 6.0 + - [`string GetHexString(int, bool)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.gethexstring#system-security-cryptography-randomnumbergenerator-gethexstring(system-int32-system-boolean)) .NET 9.0 + - [`string GetString(ReadOnlySpan, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getstring) .NET 9.0 + - [`T[] GetItems(T[], int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(-0()-system-int32)) .NET 8.0 - [`void Fill(Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.fill) .NET Core 2.1 + - [`void GetBytes(byte[], int, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-byte()-system-int32-system-int32)) .NET 6.0 + - [`void Shuffle(T[])`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.shuffle#system-security-cryptography-randomnumbergenerator-shuffle-1(-0())) .NET 8.0 - `Range` - [**[struct]**](https://learn.microsoft.com/dotnet/api/system.range) .NET Core 3.0 - `ReadOnlyMemory` From 2fe7ebefd9a5d281c1732fcd42219f1a8fbe31e9 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:28:51 +0000 Subject: [PATCH 03/13] Add documentation URL to RandomNumberGenerator type polyfill Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/NetCore10/RandomNumberGenerator.cs | 1 + PolyShim/Signatures.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/PolyShim/NetCore10/RandomNumberGenerator.cs b/PolyShim/NetCore10/RandomNumberGenerator.cs index a788faa5..14a43745 100644 --- a/PolyShim/NetCore10/RandomNumberGenerator.cs +++ b/PolyShim/NetCore10/RandomNumberGenerator.cs @@ -13,6 +13,7 @@ namespace System.Security.Cryptography; // Polyfill for RandomNumberGenerator class on .NET Standard < 1.3 // Note: This uses Random instead of cryptographically secure RNG as System.Security.Cryptography // types are not available on these older platforms. +// https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator [ExcludeFromCodeCoverage] internal abstract class RandomNumberGenerator : IDisposable { diff --git a/PolyShim/Signatures.md b/PolyShim/Signatures.md index dbbe0ec0..ae922813 100644 --- a/PolyShim/Signatures.md +++ b/PolyShim/Signatures.md @@ -278,7 +278,7 @@ ___ - [`void NextBytes(Span)`](https://learn.microsoft.com/dotnet/api/system.random.nextbytes#system-random-nextbytes(system-span((system-byte)))) .NET Core 2.1 - [`void Shuffle(T[])`](https://learn.microsoft.com/dotnet/api/system.random.shuffle#system-random-shuffle-1(-0())) .NET 8.0 - `RandomNumberGenerator` - - **[class]** .NET Core 1.0 + - [**[class]**](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator) .NET Core 1.0 - [`int GetInt32(int, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32-system-int32)) .NET 6.0 - [`int GetInt32(int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32)) .NET 6.0 - [`string GetHexString(int, bool)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.gethexstring#system-security-cryptography-randomnumbergenerator-gethexstring(system-int32-system-boolean)) .NET 9.0 From 606a1ab062a867c803de5d30a49b9cf29eeb819c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:30:17 +0000 Subject: [PATCH 04/13] Address code review feedback: improve error messages and add clarity to bias rejection logic Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/Net60/RandomNumberGenerator.cs | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/PolyShim/Net60/RandomNumberGenerator.cs b/PolyShim/Net60/RandomNumberGenerator.cs index 50a30b3d..16fdd4ce 100644 --- a/PolyShim/Net60/RandomNumberGenerator.cs +++ b/PolyShim/Net60/RandomNumberGenerator.cs @@ -24,7 +24,9 @@ public void GetBytes(byte[] data, int offset, int count) if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); if (offset + count > data.Length) - throw new ArgumentException("offset and count exceed the size of data"); + throw new ArgumentException( + "The sum of offset and count exceeds the length of data." + ); if (count == 0) return; @@ -58,13 +60,18 @@ public static int GetInt32(int fromInclusive, int toExclusive) var rng = RandomNumberGenerator.Create(); try { + // Reject values that would cause bias in the distribution. + // This ensures uniform distribution by rejecting values in the + // incomplete final bucket. + var rejectionThreshold = uint.MaxValue - (uint.MaxValue % range + 1) % range; + uint result; do { var buffer = new byte[4]; rng.GetBytes(buffer); result = BitConverter.ToUInt32(buffer, 0); - } while (result > uint.MaxValue - (uint.MaxValue % range + 1) % range); + } while (result > rejectionThreshold); return (int)(result % range) + fromInclusive; } From bc91ceeebea4a25c18fa94129019c5afa56c92fb Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:39:49 +0000 Subject: [PATCH 05/13] Fix TFM boundaries: move GetHexString and GetString to Net80 Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .../Net80/RandomNumberGeneratorTests.cs | 55 ++++++++++++++++ .../Net90/RandomNumberGeneratorTests.cs | 64 ------------------- PolyShim/Net80/RandomNumberGenerator.cs | 33 ++++++++++ PolyShim/Net90/RandomNumberGenerator.cs | 51 --------------- PolyShim/Signatures.md | 4 +- 5 files changed, 90 insertions(+), 117 deletions(-) delete mode 100644 PolyShim.Tests/Net90/RandomNumberGeneratorTests.cs delete mode 100644 PolyShim/Net90/RandomNumberGenerator.cs diff --git a/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs b/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs index 99896722..a87d18e7 100644 --- a/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs @@ -50,4 +50,59 @@ public void Shuffle_Test() } } } + + [Fact] + public void GetHexString_Test() + { + // Act & assert + for (var i = 0; i < 100; i++) + { + RandomNumberGenerator.GetHexString(16).Should().MatchRegex("^[0-9A-F]{16}$"); + RandomNumberGenerator.GetHexString(16, true).Should().MatchRegex("^[0-9a-f]{16}$"); + RandomNumberGenerator.GetHexString(15).Should().MatchRegex("^[0-9A-F]{15}$"); + } + } + + [Fact] + public void GetString_Array_Test() + { + // Arrange + var choices = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".ToCharArray(); + + // Act + for (var i = 0; i < 100; i++) + { + var str = RandomNumberGenerator.GetString(choices, 16); + + // Assert + str.Length.Should().Be(16); + + foreach (var ch in str) + { + choices.Should().Contain(ch); + } + } + } + + [Fact] + public void GetString_Span_Test() + { + // Arrange + var choices = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".AsSpan(); + + // Act + for (var i = 0; i < 100; i++) + { + var str = RandomNumberGenerator.GetString(choices, 16); + + // Assert + str.Length.Should().Be(16); + + foreach (var ch in str) + { + choices.ToArray().Should().Contain(ch); + } + } + } } diff --git a/PolyShim.Tests/Net90/RandomNumberGeneratorTests.cs b/PolyShim.Tests/Net90/RandomNumberGeneratorTests.cs deleted file mode 100644 index 01f82734..00000000 --- a/PolyShim.Tests/Net90/RandomNumberGeneratorTests.cs +++ /dev/null @@ -1,64 +0,0 @@ -using System; -using System.Security.Cryptography; -using FluentAssertions; -using Xunit; - -namespace PolyShim.Tests.Net90; - -public class RandomNumberGeneratorTests -{ - [Fact] - public void GetHexString_Test() - { - // Act & assert - for (var i = 0; i < 100; i++) - { - RandomNumberGenerator.GetHexString(16).Should().MatchRegex("^[0-9A-F]{16}$"); - RandomNumberGenerator.GetHexString(16, true).Should().MatchRegex("^[0-9a-f]{16}$"); - RandomNumberGenerator.GetHexString(15).Should().MatchRegex("^[0-9A-F]{15}$"); - } - } - - [Fact] - public void GetString_Array_Test() - { - // Arrange - var choices = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".ToCharArray(); - - // Act - for (var i = 0; i < 100; i++) - { - var str = RandomNumberGenerator.GetString(choices, 16); - - // Assert - str.Length.Should().Be(16); - - foreach (var ch in str) - { - choices.Should().Contain(ch); - } - } - } - - [Fact] - public void GetString_Span_Test() - { - // Arrange - var choices = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".AsSpan(); - - // Act - for (var i = 0; i < 100; i++) - { - var str = RandomNumberGenerator.GetString(choices, 16); - - // Assert - str.Length.Should().Be(16); - - foreach (var ch in str) - { - choices.ToArray().Should().Contain(ch); - } - } - } -} diff --git a/PolyShim/Net80/RandomNumberGenerator.cs b/PolyShim/Net80/RandomNumberGenerator.cs index 55e7f8c7..9f5350c6 100644 --- a/PolyShim/Net80/RandomNumberGenerator.cs +++ b/PolyShim/Net80/RandomNumberGenerator.cs @@ -6,6 +6,7 @@ // ReSharper disable PartialTypeWithSinglePart using System; +using System.Text; using System.Diagnostics.CodeAnalysis; using System.Security.Cryptography; @@ -43,6 +44,38 @@ public static void Shuffle(T[] items) (items[i], items[j]) = (items[j], items[i]); } } + + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.gethexstring#system-security-cryptography-randomnumbergenerator-gethexstring(system-int32-system-boolean) + public static string GetHexString(int stringLength, bool lowercase = false) + { + if (stringLength < 0) + throw new ArgumentOutOfRangeException(nameof(stringLength)); + + var bytes = new byte[(stringLength + 1) / 2]; + RandomNumberGenerator.Fill(bytes); + + var hex = lowercase ? Convert.ToHexStringLower(bytes) : Convert.ToHexString(bytes); + return hex.Substring(0, stringLength); + } + + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getstring + public static string GetString(ReadOnlySpan choices, int length) + { + if (choices.Length == 0) + throw new ArgumentException("choices must not be empty", nameof(choices)); + if (length < 0) + throw new ArgumentOutOfRangeException(nameof(length)); + + var buffer = new StringBuilder(length); + + for (var i = 0; i < length; i++) + { + var index = RandomNumberGenerator.GetInt32(choices.Length); + buffer.Append(choices[index]); + } + + return buffer.ToString(); + } } } #endif diff --git a/PolyShim/Net90/RandomNumberGenerator.cs b/PolyShim/Net90/RandomNumberGenerator.cs deleted file mode 100644 index 7187d932..00000000 --- a/PolyShim/Net90/RandomNumberGenerator.cs +++ /dev/null @@ -1,51 +0,0 @@ -#if (NETCOREAPP && !NET9_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD) -#nullable enable -// ReSharper disable RedundantUsingDirective -// ReSharper disable CheckNamespace -// ReSharper disable InconsistentNaming -// ReSharper disable PartialTypeWithSinglePart - -using System; -using System.Text; -using System.Diagnostics.CodeAnalysis; -using System.Security.Cryptography; - -[ExcludeFromCodeCoverage] -internal static class MemberPolyfills_Net90_RandomNumberGenerator -{ - extension(RandomNumberGenerator) - { - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.gethexstring#system-security-cryptography-randomnumbergenerator-gethexstring(system-int32-system-boolean) - public static string GetHexString(int stringLength, bool lowercase = false) - { - if (stringLength < 0) - throw new ArgumentOutOfRangeException(nameof(stringLength)); - - var bytes = new byte[(stringLength + 1) / 2]; - RandomNumberGenerator.Fill(bytes); - - var hex = lowercase ? Convert.ToHexStringLower(bytes) : Convert.ToHexString(bytes); - return hex.Substring(0, stringLength); - } - - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getstring - public static string GetString(ReadOnlySpan choices, int length) - { - if (choices.Length == 0) - throw new ArgumentException("choices must not be empty", nameof(choices)); - if (length < 0) - throw new ArgumentOutOfRangeException(nameof(length)); - - var buffer = new StringBuilder(length); - - for (var i = 0; i < length; i++) - { - var index = RandomNumberGenerator.GetInt32(choices.Length); - buffer.Append(choices[index]); - } - - return buffer.ToString(); - } - } -} -#endif diff --git a/PolyShim/Signatures.md b/PolyShim/Signatures.md index ae922813..d9d3a270 100644 --- a/PolyShim/Signatures.md +++ b/PolyShim/Signatures.md @@ -281,8 +281,8 @@ ___ - [**[class]**](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator) .NET Core 1.0 - [`int GetInt32(int, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32-system-int32)) .NET 6.0 - [`int GetInt32(int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32)) .NET 6.0 - - [`string GetHexString(int, bool)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.gethexstring#system-security-cryptography-randomnumbergenerator-gethexstring(system-int32-system-boolean)) .NET 9.0 - - [`string GetString(ReadOnlySpan, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getstring) .NET 9.0 + - [`string GetHexString(int, bool)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.gethexstring#system-security-cryptography-randomnumbergenerator-gethexstring(system-int32-system-boolean)) .NET 8.0 + - [`string GetString(ReadOnlySpan, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getstring) .NET 8.0 - [`T[] GetItems(T[], int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(-0()-system-int32)) .NET 8.0 - [`void Fill(Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.fill) .NET Core 2.1 - [`void GetBytes(byte[], int, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-byte()-system-int32-system-int32)) .NET 6.0 From 67aa26f35cd1a211b5c53d4a5a1152af440f67a7 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:46:37 +0000 Subject: [PATCH 06/13] Add additional RandomNumberGenerator polyfills: GetBytes(int), GetNonZeroBytes, GetItems/Shuffle span overloads Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .../Net60/RandomNumberGeneratorTests.cs | 43 ++++++++++++ .../Net80/RandomNumberGeneratorTests.cs | 66 +++++++++++++++++++ PolyShim/Net60/RandomNumberGenerator.cs | 46 +++++++++++++ PolyShim/Net80/RandomNumberGenerator.cs | 40 +++++++++++ PolyShim/Signatures.md | 10 ++- 5 files changed, 203 insertions(+), 2 deletions(-) diff --git a/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs b/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs index 2221f336..c3baaa3b 100644 --- a/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs @@ -83,4 +83,47 @@ public void GetInt32_InvalidRange_Test() Assert.Throws(() => RandomNumberGenerator.GetInt32(0)); Assert.Throws(() => RandomNumberGenerator.GetInt32(-1)); } + + [Fact] + public void GetBytes_Static_Test() + { + // Act + var data = RandomNumberGenerator.GetBytes(16); + + // Assert + data.Should().HaveCount(16); + data.Should().Contain(b => b != 0); + } + + [Fact] + public void GetNonZeroBytes_Array_Test() + { + // Arrange + var rng = RandomNumberGenerator.Create(); + var data = new byte[10]; + + // Act + rng.GetNonZeroBytes(data); + + // Assert + data.Should().NotContain((byte)0); + + ((IDisposable)rng).Dispose(); + } + + [Fact] + public void GetNonZeroBytes_Span_Test() + { + // Arrange + var rng = RandomNumberGenerator.Create(); + var data = new Span(new byte[10]); + + // Act + rng.GetNonZeroBytes(data); + + // Assert + data.ToArray().Should().NotContain((byte)0); + + ((IDisposable)rng).Dispose(); + } } diff --git a/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs b/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs index a87d18e7..eafad29d 100644 --- a/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs @@ -105,4 +105,70 @@ public void GetString_Span_Test() } } } + + [Fact] + public void GetItems_ReadOnlySpan_Test() + { + // Arrange + var choices = new[] { 1, 2, 3, 4, 5 }.AsSpan(); + + // Act + for (var i = 0; i < 100; i++) + { + var items = RandomNumberGenerator.GetItems(choices, 3); + + // Assert + items.Should().HaveCount(3); + + foreach (var item in items) + { + choices.ToArray().Should().Contain(item); + } + } + } + + [Fact] + public void GetItems_Span_Test() + { + // Arrange + var choices = new[] { 1, 2, 3, 4, 5 }.AsSpan(); + var destination = new Span(new int[3]); + + // Act + for (var i = 0; i < 100; i++) + { + RandomNumberGenerator.GetItems(choices, destination); + + // Assert + destination.Length.Should().Be(3); + + foreach (var item in destination) + { + choices.ToArray().Should().Contain(item); + } + } + } + + [Fact] + public void Shuffle_Span_Test() + { + // Arrange + var originalItems = new[] { 1, 2, 3, 4, 5 }; + + // Act + for (var i = 0; i < 100; i++) + { + var items = new Span((int[])originalItems.Clone()); + RandomNumberGenerator.Shuffle(items); + + // Assert + items.Length.Should().Be(originalItems.Length); + items.ToArray().Should().BeEquivalentTo(originalItems); + + foreach (var item in items) + { + originalItems.Should().Contain(item); + } + } + } } diff --git a/PolyShim/Net60/RandomNumberGenerator.cs b/PolyShim/Net60/RandomNumberGenerator.cs index 16fdd4ce..cea61120 100644 --- a/PolyShim/Net60/RandomNumberGenerator.cs +++ b/PolyShim/Net60/RandomNumberGenerator.cs @@ -35,10 +35,56 @@ public void GetBytes(byte[] data, int offset, int count) rng.GetBytes(buffer); Array.Copy(buffer, 0, data, offset, count); } + + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-byte()) + public void GetNonZeroBytes(byte[] data) + { + if (data is null) + throw new ArgumentNullException(nameof(data)); + + for (var i = 0; i < data.Length; i++) + { + byte value; + do + { + var buffer = new byte[1]; + rng.GetBytes(buffer); + value = buffer[0]; + } while (value == 0); + data[i] = value; + } + } + + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-span((system-byte))) + public void GetNonZeroBytes(Span data) + { + for (var i = 0; i < data.Length; i++) + { + byte value; + do + { + var buffer = new byte[1]; + rng.GetBytes(buffer); + value = buffer[0]; + } while (value == 0); + data[i] = value; + } + } } extension(RandomNumberGenerator) { + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-int32) + public static byte[] GetBytes(int count) + { + if (count < 0) + throw new ArgumentOutOfRangeException(nameof(count)); + + var data = new byte[count]; + RandomNumberGenerator.Fill(data); + return data; + } + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32) public static int GetInt32(int toExclusive) { diff --git a/PolyShim/Net80/RandomNumberGenerator.cs b/PolyShim/Net80/RandomNumberGenerator.cs index 9f5350c6..8f35dd9e 100644 --- a/PolyShim/Net80/RandomNumberGenerator.cs +++ b/PolyShim/Net80/RandomNumberGenerator.cs @@ -32,6 +32,36 @@ public static T[] GetItems(T[] choices, int length) return result; } + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(system-readonlyspan((-0))-system-int32) + public static T[] GetItems(ReadOnlySpan choices, int length) + { + if (choices.Length == 0) + throw new ArgumentException("choices must not be empty", nameof(choices)); + if (length < 0) + throw new ArgumentOutOfRangeException(nameof(length)); + + var result = new T[length]; + for (var i = 0; i < length; i++) + { + var index = RandomNumberGenerator.GetInt32(choices.Length); + result[i] = choices[index]; + } + return result; + } + + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(system-readonlyspan((-0))-system-span((-0))) + public static void GetItems(ReadOnlySpan choices, Span destination) + { + if (choices.Length == 0) + throw new ArgumentException("choices must not be empty", nameof(choices)); + + for (var i = 0; i < destination.Length; i++) + { + var index = RandomNumberGenerator.GetInt32(choices.Length); + destination[i] = choices[index]; + } + } + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.shuffle#system-security-cryptography-randomnumbergenerator-shuffle-1(-0()) public static void Shuffle(T[] items) { @@ -45,6 +75,16 @@ public static void Shuffle(T[] items) } } + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.shuffle#system-security-cryptography-randomnumbergenerator-shuffle-1(system-span((-0))) + public static void Shuffle(Span items) + { + for (var i = items.Length - 1; i > 0; i--) + { + var j = RandomNumberGenerator.GetInt32(i + 1); + (items[i], items[j]) = (items[j], items[i]); + } + } + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.gethexstring#system-security-cryptography-randomnumbergenerator-gethexstring(system-int32-system-boolean) public static string GetHexString(int stringLength, bool lowercase = false) { diff --git a/PolyShim/Signatures.md b/PolyShim/Signatures.md index d9d3a270..4f5afbef 100644 --- a/PolyShim/Signatures.md +++ b/PolyShim/Signatures.md @@ -1,8 +1,8 @@ # Signatures -- **Total:** 350 +- **Total:** 356 - **Types:** 76 -- **Members:** 274 +- **Members:** 280 ___ @@ -279,13 +279,19 @@ ___ - [`void Shuffle(T[])`](https://learn.microsoft.com/dotnet/api/system.random.shuffle#system-random-shuffle-1(-0())) .NET 8.0 - `RandomNumberGenerator` - [**[class]**](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator) .NET Core 1.0 + - [`byte[] GetBytes(int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-int32)) .NET 6.0 - [`int GetInt32(int, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32-system-int32)) .NET 6.0 - [`int GetInt32(int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32)) .NET 6.0 - [`string GetHexString(int, bool)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.gethexstring#system-security-cryptography-randomnumbergenerator-gethexstring(system-int32-system-boolean)) .NET 8.0 - [`string GetString(ReadOnlySpan, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getstring) .NET 8.0 + - [`T[] GetItems(ReadOnlySpan, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(system-readonlyspan((-0))-system-int32)) .NET 8.0 - [`T[] GetItems(T[], int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(-0()-system-int32)) .NET 8.0 - [`void Fill(Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.fill) .NET Core 2.1 - [`void GetBytes(byte[], int, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-byte()-system-int32-system-int32)) .NET 6.0 + - [`void GetItems(ReadOnlySpan, Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(system-readonlyspan((-0))-system-span((-0)))) .NET 8.0 + - [`void GetNonZeroBytes(byte[])`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-byte())) .NET 6.0 + - [`void GetNonZeroBytes(Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-span((system-byte)))) .NET 6.0 + - [`void Shuffle(Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.shuffle#system-security-cryptography-randomnumbergenerator-shuffle-1(system-span((-0)))) .NET 8.0 - [`void Shuffle(T[])`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.shuffle#system-security-cryptography-randomnumbergenerator-shuffle-1(-0())) .NET 8.0 - `Range` - [**[struct]**](https://learn.microsoft.com/dotnet/api/system.range) .NET Core 3.0 From 71746716f12b58a8a8c603c00a42768b8a6deb0e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 16:51:57 +0000 Subject: [PATCH 07/13] Add GetBytes(Span) instance method to RandomNumberGenerator Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .../NetCore21/RandomNumberGeneratorTests.cs | 30 +++++++++++++++++++ PolyShim/NetCore21/RandomNumberGenerator.cs | 14 +++++++++ PolyShim/Signatures.md | 5 ++-- 3 files changed, 47 insertions(+), 2 deletions(-) diff --git a/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs b/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs index f3c79aea..c4a75cda 100644 --- a/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs @@ -33,4 +33,34 @@ public void Fill_EmptySpan_Test() // Should not throw and should return immediately RandomNumberGenerator.Fill(span); } + + [Fact] + public void GetBytes_Span_Test() + { + // Arrange + var rng = RandomNumberGenerator.Create(); + var span = new Span(new byte[16]); + + // Act + rng.GetBytes(span); + + // Assert + span.ToArray().Should().Contain(b => b != 0); + + ((IDisposable)rng).Dispose(); + } + + [Fact] + public void GetBytes_EmptySpan_Test() + { + // Arrange + var rng = RandomNumberGenerator.Create(); + var span = new Span(); + + // Act & Assert + // Should not throw and should return immediately + rng.GetBytes(span); + + ((IDisposable)rng).Dispose(); + } } diff --git a/PolyShim/NetCore21/RandomNumberGenerator.cs b/PolyShim/NetCore21/RandomNumberGenerator.cs index bbce1b77..7a4fc38d 100644 --- a/PolyShim/NetCore21/RandomNumberGenerator.cs +++ b/PolyShim/NetCore21/RandomNumberGenerator.cs @@ -12,6 +12,20 @@ [ExcludeFromCodeCoverage] internal static class MemberPolyfills_NetCore21_RandomNumberGenerator { + extension(RandomNumberGenerator rng) + { + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-span((system-byte))) + public void GetBytes(Span data) + { + if (data.Length == 0) + return; + + var buffer = new byte[data.Length]; + rng.GetBytes(buffer); + buffer.CopyTo(data); + } + } + extension(RandomNumberGenerator) { // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.fill diff --git a/PolyShim/Signatures.md b/PolyShim/Signatures.md index 4f5afbef..0a27e60e 100644 --- a/PolyShim/Signatures.md +++ b/PolyShim/Signatures.md @@ -1,8 +1,8 @@ # Signatures -- **Total:** 356 +- **Total:** 357 - **Types:** 76 -- **Members:** 280 +- **Members:** 281 ___ @@ -288,6 +288,7 @@ ___ - [`T[] GetItems(T[], int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(-0()-system-int32)) .NET 8.0 - [`void Fill(Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.fill) .NET Core 2.1 - [`void GetBytes(byte[], int, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-byte()-system-int32-system-int32)) .NET 6.0 + - [`void GetBytes(Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-span((system-byte)))) .NET Core 2.1 - [`void GetItems(ReadOnlySpan, Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(system-readonlyspan((-0))-system-span((-0)))) .NET 8.0 - [`void GetNonZeroBytes(byte[])`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-byte())) .NET 6.0 - [`void GetNonZeroBytes(Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-span((system-byte)))) .NET 6.0 From eb2fbd9932dd0a2cecbc8292ce38e329a401c0f1 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:06:06 +0000 Subject: [PATCH 08/13] Address code review feedback: fix GC pressure, overflow checks, and exception types Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim/Net60/RandomNumberGenerator.cs | 15 +++++++++++---- PolyShim/Net80/RandomNumberGenerator.cs | 2 ++ 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/PolyShim/Net60/RandomNumberGenerator.cs b/PolyShim/Net60/RandomNumberGenerator.cs index cea61120..b2158a24 100644 --- a/PolyShim/Net60/RandomNumberGenerator.cs +++ b/PolyShim/Net60/RandomNumberGenerator.cs @@ -23,7 +23,11 @@ public void GetBytes(byte[] data, int offset, int count) throw new ArgumentOutOfRangeException(nameof(offset)); if (count < 0) throw new ArgumentOutOfRangeException(nameof(count)); - if (offset + count > data.Length) + if (offset > data.Length) + throw new ArgumentException( + "The sum of offset and count exceeds the length of data." + ); + if (count > data.Length - offset) throw new ArgumentException( "The sum of offset and count exceeds the length of data." ); @@ -42,12 +46,12 @@ public void GetNonZeroBytes(byte[] data) if (data is null) throw new ArgumentNullException(nameof(data)); + var buffer = new byte[1]; for (var i = 0; i < data.Length; i++) { byte value; do { - var buffer = new byte[1]; rng.GetBytes(buffer); value = buffer[0]; } while (value == 0); @@ -58,12 +62,12 @@ public void GetNonZeroBytes(byte[] data) // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-span((system-byte))) public void GetNonZeroBytes(Span data) { + var buffer = new byte[1]; for (var i = 0; i < data.Length; i++) { byte value; do { - var buffer = new byte[1]; rng.GetBytes(buffer); value = buffer[0]; } while (value == 0); @@ -99,7 +103,10 @@ public static int GetInt32(int fromInclusive, int toExclusive) { if (fromInclusive >= toExclusive) { - throw new ArgumentException("fromInclusive must be less than toExclusive"); + throw new ArgumentException( + "fromInclusive must be less than toExclusive", + nameof(fromInclusive) + ); } var range = (uint)(toExclusive - fromInclusive); diff --git a/PolyShim/Net80/RandomNumberGenerator.cs b/PolyShim/Net80/RandomNumberGenerator.cs index 8f35dd9e..36db63d7 100644 --- a/PolyShim/Net80/RandomNumberGenerator.cs +++ b/PolyShim/Net80/RandomNumberGenerator.cs @@ -20,6 +20,8 @@ public static T[] GetItems(T[] choices, int length) { if (choices is null) throw new ArgumentNullException(nameof(choices)); + if (choices.Length == 0) + throw new ArgumentException("choices must not be empty", nameof(choices)); if (length < 0) throw new ArgumentOutOfRangeException(nameof(length)); From 608b56ec45c71d724d63839f7efd629c34245c63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:12:04 +0000 Subject: [PATCH 09/13] Reorganize RandomNumberGenerator polyfills by correct TFM and remove guards Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .../Net60/RandomNumberGeneratorTests.cs | 110 ----------------- .../Net80/RandomNumberGeneratorTests.cs | 66 ---------- .../NetCore20/RandomNumberGeneratorTests.cs | 71 +++++++++++ .../NetCore21/RandomNumberGeneratorTests.cs | 16 +++ .../NetCore30/RandomNumberGeneratorTests.cs | 30 +++++ PolyShim/Net60/RandomNumberGenerator.cs | 114 ------------------ PolyShim/Net80/RandomNumberGenerator.cs | 48 -------- PolyShim/NetCore20/RandomNumberGenerator.cs | 45 +++++++ PolyShim/NetCore21/RandomNumberGenerator.cs | 20 ++- PolyShim/NetCore30/RandomNumberGenerator.cs | 54 +++++++++ PolyShim/Signatures.md | 16 ++- 11 files changed, 240 insertions(+), 350 deletions(-) create mode 100644 PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs create mode 100644 PolyShim.Tests/NetCore30/RandomNumberGeneratorTests.cs create mode 100644 PolyShim/NetCore20/RandomNumberGenerator.cs create mode 100644 PolyShim/NetCore30/RandomNumberGenerator.cs diff --git a/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs b/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs index c3baaa3b..2315b47e 100644 --- a/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs @@ -1,4 +1,3 @@ -using System; using System.Security.Cryptography; using FluentAssertions; using Xunit; @@ -7,83 +6,6 @@ namespace PolyShim.Tests.Net60; public class RandomNumberGeneratorTests { - [Fact] - public void GetBytes_PartialArray_Test() - { - // Arrange - var rng = RandomNumberGenerator.Create(); - var data = new byte[20]; - - // Act - rng.GetBytes(data, 5, 10); - - // Assert - // First 5 bytes should be zero - for (var i = 0; i < 5; i++) - { - data[i].Should().Be(0); - } - - // Middle 10 bytes should have at least some non-zero values - var middleSection = new byte[10]; - Array.Copy(data, 5, middleSection, 0, 10); - middleSection.Should().Contain(b => b != 0); - - // Last 5 bytes should be zero - for (var i = 15; i < 20; i++) - { - data[i].Should().Be(0); - } - - ((IDisposable)rng).Dispose(); - } - - [Fact] - public void GetBytes_EmptyCount_Test() - { - // Arrange - var rng = RandomNumberGenerator.Create(); - var data = new byte[10]; - - // Act & Assert - // Should not throw and should leave array unchanged - rng.GetBytes(data, 0, 0); - data.Should().AllBeEquivalentTo(0); - - ((IDisposable)rng).Dispose(); - } - - [Fact] - public void GetInt32_MaxValue_Test() - { - // Act & assert - for (var i = 0; i < 100; i++) - { - var value = RandomNumberGenerator.GetInt32(10); - value.Should().BeInRange(0, 9); - } - } - - [Fact] - public void GetInt32_Range_Test() - { - // Act & assert - for (var i = 0; i < 100; i++) - { - var value = RandomNumberGenerator.GetInt32(10, 20); - value.Should().BeInRange(10, 19); - } - } - - [Fact] - public void GetInt32_InvalidRange_Test() - { - // Act & assert - Assert.Throws(() => RandomNumberGenerator.GetInt32(20, 10)); - Assert.Throws(() => RandomNumberGenerator.GetInt32(0)); - Assert.Throws(() => RandomNumberGenerator.GetInt32(-1)); - } - [Fact] public void GetBytes_Static_Test() { @@ -94,36 +16,4 @@ public void GetBytes_Static_Test() data.Should().HaveCount(16); data.Should().Contain(b => b != 0); } - - [Fact] - public void GetNonZeroBytes_Array_Test() - { - // Arrange - var rng = RandomNumberGenerator.Create(); - var data = new byte[10]; - - // Act - rng.GetNonZeroBytes(data); - - // Assert - data.Should().NotContain((byte)0); - - ((IDisposable)rng).Dispose(); - } - - [Fact] - public void GetNonZeroBytes_Span_Test() - { - // Arrange - var rng = RandomNumberGenerator.Create(); - var data = new Span(new byte[10]); - - // Act - rng.GetNonZeroBytes(data); - - // Assert - data.ToArray().Should().NotContain((byte)0); - - ((IDisposable)rng).Dispose(); - } } diff --git a/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs b/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs index eafad29d..b70544ef 100644 --- a/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs @@ -7,50 +7,6 @@ namespace PolyShim.Tests.Net80; public class RandomNumberGeneratorTests { - [Fact] - public void GetItems_Test() - { - // Arrange - var choices = new[] { 1, 2, 3, 4, 5 }; - - // Act - for (var i = 0; i < 100; i++) - { - var items = RandomNumberGenerator.GetItems(choices, 3); - - // Assert - items.Should().HaveCount(3); - - foreach (var item in items) - { - choices.Should().Contain(item); - } - } - } - - [Fact] - public void Shuffle_Test() - { - // Arrange - var originalItems = new[] { 1, 2, 3, 4, 5 }; - - // Act - for (var i = 0; i < 100; i++) - { - var items = (int[])originalItems.Clone(); - RandomNumberGenerator.Shuffle(items); - - // Assert - items.Should().HaveCount(originalItems.Length); - items.Should().BeEquivalentTo(originalItems); - - foreach (var item in items) - { - originalItems.Should().Contain(item); - } - } - } - [Fact] public void GetHexString_Test() { @@ -63,28 +19,6 @@ public void GetHexString_Test() } } - [Fact] - public void GetString_Array_Test() - { - // Arrange - var choices = - "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".ToCharArray(); - - // Act - for (var i = 0; i < 100; i++) - { - var str = RandomNumberGenerator.GetString(choices, 16); - - // Assert - str.Length.Should().Be(16); - - foreach (var ch in str) - { - choices.Should().Contain(ch); - } - } - } - [Fact] public void GetString_Span_Test() { diff --git a/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs b/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs new file mode 100644 index 00000000..c9ffe2f9 --- /dev/null +++ b/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs @@ -0,0 +1,71 @@ +using System; +using System.Security.Cryptography; +using FluentAssertions; +using Xunit; + +namespace PolyShim.Tests.NetCore20; + +public class RandomNumberGeneratorTests +{ + [Fact] + public void GetBytes_PartialArray_Test() + { + // Arrange + var rng = RandomNumberGenerator.Create(); + var data = new byte[20]; + + // Act + rng.GetBytes(data, 5, 10); + + // Assert + // First 5 bytes should be zero + for (var i = 0; i < 5; i++) + { + data[i].Should().Be(0); + } + + // Middle 10 bytes should have at least some non-zero values + var middleSection = new byte[10]; + Array.Copy(data, 5, middleSection, 0, 10); + middleSection.Should().Contain(b => b != 0); + + // Last 5 bytes should be zero + for (var i = 15; i < 20; i++) + { + data[i].Should().Be(0); + } + + ((IDisposable)rng).Dispose(); + } + + [Fact] + public void GetBytes_EmptyCount_Test() + { + // Arrange + var rng = RandomNumberGenerator.Create(); + var data = new byte[10]; + + // Act & Assert + // Should not throw and should leave array unchanged + rng.GetBytes(data, 0, 0); + data.Should().AllBeEquivalentTo(0); + + ((IDisposable)rng).Dispose(); + } + + [Fact] + public void GetNonZeroBytes_Array_Test() + { + // Arrange + var rng = RandomNumberGenerator.Create(); + var data = new byte[10]; + + // Act + rng.GetNonZeroBytes(data); + + // Assert + data.Should().NotContain((byte)0); + + ((IDisposable)rng).Dispose(); + } +} diff --git a/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs b/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs index c4a75cda..4598621f 100644 --- a/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs @@ -63,4 +63,20 @@ public void GetBytes_EmptySpan_Test() ((IDisposable)rng).Dispose(); } + + [Fact] + public void GetNonZeroBytes_Span_Test() + { + // Arrange + var rng = RandomNumberGenerator.Create(); + var data = new Span(new byte[10]); + + // Act + rng.GetNonZeroBytes(data); + + // Assert + data.ToArray().Should().NotContain((byte)0); + + ((IDisposable)rng).Dispose(); + } } diff --git a/PolyShim.Tests/NetCore30/RandomNumberGeneratorTests.cs b/PolyShim.Tests/NetCore30/RandomNumberGeneratorTests.cs new file mode 100644 index 00000000..47251460 --- /dev/null +++ b/PolyShim.Tests/NetCore30/RandomNumberGeneratorTests.cs @@ -0,0 +1,30 @@ +using System.Security.Cryptography; +using FluentAssertions; +using Xunit; + +namespace PolyShim.Tests.NetCore30; + +public class RandomNumberGeneratorTests +{ + [Fact] + public void GetInt32_MaxValue_Test() + { + // Act & assert + for (var i = 0; i < 100; i++) + { + var value = RandomNumberGenerator.GetInt32(10); + value.Should().BeInRange(0, 9); + } + } + + [Fact] + public void GetInt32_Range_Test() + { + // Act & assert + for (var i = 0; i < 100; i++) + { + var value = RandomNumberGenerator.GetInt32(10, 20); + value.Should().BeInRange(10, 19); + } + } +} diff --git a/PolyShim/Net60/RandomNumberGenerator.cs b/PolyShim/Net60/RandomNumberGenerator.cs index b2158a24..e0375962 100644 --- a/PolyShim/Net60/RandomNumberGenerator.cs +++ b/PolyShim/Net60/RandomNumberGenerator.cs @@ -12,129 +12,15 @@ [ExcludeFromCodeCoverage] internal static class MemberPolyfills_Net60_RandomNumberGenerator { - extension(RandomNumberGenerator rng) - { - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-byte()-system-int32-system-int32) - public void GetBytes(byte[] data, int offset, int count) - { - if (data is null) - throw new ArgumentNullException(nameof(data)); - if (offset < 0) - throw new ArgumentOutOfRangeException(nameof(offset)); - if (count < 0) - throw new ArgumentOutOfRangeException(nameof(count)); - if (offset > data.Length) - throw new ArgumentException( - "The sum of offset and count exceeds the length of data." - ); - if (count > data.Length - offset) - throw new ArgumentException( - "The sum of offset and count exceeds the length of data." - ); - - if (count == 0) - return; - - var buffer = new byte[count]; - rng.GetBytes(buffer); - Array.Copy(buffer, 0, data, offset, count); - } - - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-byte()) - public void GetNonZeroBytes(byte[] data) - { - if (data is null) - throw new ArgumentNullException(nameof(data)); - - var buffer = new byte[1]; - for (var i = 0; i < data.Length; i++) - { - byte value; - do - { - rng.GetBytes(buffer); - value = buffer[0]; - } while (value == 0); - data[i] = value; - } - } - - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-span((system-byte))) - public void GetNonZeroBytes(Span data) - { - var buffer = new byte[1]; - for (var i = 0; i < data.Length; i++) - { - byte value; - do - { - rng.GetBytes(buffer); - value = buffer[0]; - } while (value == 0); - data[i] = value; - } - } - } - extension(RandomNumberGenerator) { // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-int32) public static byte[] GetBytes(int count) { - if (count < 0) - throw new ArgumentOutOfRangeException(nameof(count)); - var data = new byte[count]; RandomNumberGenerator.Fill(data); return data; } - - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32) - public static int GetInt32(int toExclusive) - { - if (toExclusive <= 0) - throw new ArgumentOutOfRangeException(nameof(toExclusive)); - - return RandomNumberGenerator.GetInt32(0, toExclusive); - } - - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32-system-int32) - public static int GetInt32(int fromInclusive, int toExclusive) - { - if (fromInclusive >= toExclusive) - { - throw new ArgumentException( - "fromInclusive must be less than toExclusive", - nameof(fromInclusive) - ); - } - - var range = (uint)(toExclusive - fromInclusive); - var rng = RandomNumberGenerator.Create(); - try - { - // Reject values that would cause bias in the distribution. - // This ensures uniform distribution by rejecting values in the - // incomplete final bucket. - var rejectionThreshold = uint.MaxValue - (uint.MaxValue % range + 1) % range; - - uint result; - do - { - var buffer = new byte[4]; - rng.GetBytes(buffer); - result = BitConverter.ToUInt32(buffer, 0); - } while (result > rejectionThreshold); - - return (int)(result % range) + fromInclusive; - } - finally - { - // Explicit cast needed for .NET Framework 3.5 where RandomNumberGenerator - // doesn't properly expose IDisposable for using statements. - ((IDisposable)rng).Dispose(); - } - } } } #endif diff --git a/PolyShim/Net80/RandomNumberGenerator.cs b/PolyShim/Net80/RandomNumberGenerator.cs index 36db63d7..38b9ba46 100644 --- a/PolyShim/Net80/RandomNumberGenerator.cs +++ b/PolyShim/Net80/RandomNumberGenerator.cs @@ -15,33 +15,9 @@ internal static class MemberPolyfills_Net80_RandomNumberGenerator { extension(RandomNumberGenerator) { - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(-0()-system-int32) - public static T[] GetItems(T[] choices, int length) - { - if (choices is null) - throw new ArgumentNullException(nameof(choices)); - if (choices.Length == 0) - throw new ArgumentException("choices must not be empty", nameof(choices)); - if (length < 0) - throw new ArgumentOutOfRangeException(nameof(length)); - - var result = new T[length]; - for (var i = 0; i < length; i++) - { - var index = RandomNumberGenerator.GetInt32(choices.Length); - result[i] = choices[index]; - } - return result; - } - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(system-readonlyspan((-0))-system-int32) public static T[] GetItems(ReadOnlySpan choices, int length) { - if (choices.Length == 0) - throw new ArgumentException("choices must not be empty", nameof(choices)); - if (length < 0) - throw new ArgumentOutOfRangeException(nameof(length)); - var result = new T[length]; for (var i = 0; i < length; i++) { @@ -54,9 +30,6 @@ public static T[] GetItems(ReadOnlySpan choices, int length) // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(system-readonlyspan((-0))-system-span((-0))) public static void GetItems(ReadOnlySpan choices, Span destination) { - if (choices.Length == 0) - throw new ArgumentException("choices must not be empty", nameof(choices)); - for (var i = 0; i < destination.Length; i++) { var index = RandomNumberGenerator.GetInt32(choices.Length); @@ -64,19 +37,6 @@ public static void GetItems(ReadOnlySpan choices, Span destination) } } - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.shuffle#system-security-cryptography-randomnumbergenerator-shuffle-1(-0()) - public static void Shuffle(T[] items) - { - if (items is null) - throw new ArgumentNullException(nameof(items)); - - for (var i = items.Length - 1; i > 0; i--) - { - var j = RandomNumberGenerator.GetInt32(i + 1); - (items[i], items[j]) = (items[j], items[i]); - } - } - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.shuffle#system-security-cryptography-randomnumbergenerator-shuffle-1(system-span((-0))) public static void Shuffle(Span items) { @@ -90,9 +50,6 @@ public static void Shuffle(Span items) // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.gethexstring#system-security-cryptography-randomnumbergenerator-gethexstring(system-int32-system-boolean) public static string GetHexString(int stringLength, bool lowercase = false) { - if (stringLength < 0) - throw new ArgumentOutOfRangeException(nameof(stringLength)); - var bytes = new byte[(stringLength + 1) / 2]; RandomNumberGenerator.Fill(bytes); @@ -103,11 +60,6 @@ public static string GetHexString(int stringLength, bool lowercase = false) // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getstring public static string GetString(ReadOnlySpan choices, int length) { - if (choices.Length == 0) - throw new ArgumentException("choices must not be empty", nameof(choices)); - if (length < 0) - throw new ArgumentOutOfRangeException(nameof(length)); - var buffer = new StringBuilder(length); for (var i = 0; i < length; i++) diff --git a/PolyShim/NetCore20/RandomNumberGenerator.cs b/PolyShim/NetCore20/RandomNumberGenerator.cs new file mode 100644 index 00000000..9c649287 --- /dev/null +++ b/PolyShim/NetCore20/RandomNumberGenerator.cs @@ -0,0 +1,45 @@ +#if (NETCOREAPP && !NETCOREAPP2_0_OR_GREATER) || (NETFRAMEWORK && !NET46_OR_GREATER) || (NETSTANDARD && !NETSTANDARD2_0_OR_GREATER) +#nullable enable +// ReSharper disable RedundantUsingDirective +// ReSharper disable CheckNamespace +// ReSharper disable InconsistentNaming +// ReSharper disable PartialTypeWithSinglePart + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; + +[ExcludeFromCodeCoverage] +internal static class MemberPolyfills_NetCore20_RandomNumberGenerator +{ + extension(RandomNumberGenerator rng) + { + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-byte()-system-int32-system-int32) + public void GetBytes(byte[] data, int offset, int count) + { + if (count == 0) + return; + + var buffer = new byte[count]; + rng.GetBytes(buffer); + Array.Copy(buffer, 0, data, offset, count); + } + + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-byte()) + public void GetNonZeroBytes(byte[] data) + { + var buffer = new byte[1]; + for (var i = 0; i < data.Length; i++) + { + byte value; + do + { + rng.GetBytes(buffer); + value = buffer[0]; + } while (value == 0); + data[i] = value; + } + } + } +} +#endif diff --git a/PolyShim/NetCore21/RandomNumberGenerator.cs b/PolyShim/NetCore21/RandomNumberGenerator.cs index 7a4fc38d..5d47dab2 100644 --- a/PolyShim/NetCore21/RandomNumberGenerator.cs +++ b/PolyShim/NetCore21/RandomNumberGenerator.cs @@ -24,6 +24,22 @@ public void GetBytes(Span data) rng.GetBytes(buffer); buffer.CopyTo(data); } + + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-span((system-byte))) + public void GetNonZeroBytes(Span data) + { + var buffer = new byte[1]; + for (var i = 0; i < data.Length; i++) + { + byte value; + do + { + rng.GetBytes(buffer); + value = buffer[0]; + } while (value == 0); + data[i] = value; + } + } } extension(RandomNumberGenerator) @@ -37,9 +53,7 @@ public static void Fill(Span data) var rng = RandomNumberGenerator.Create(); try { - var buffer = new byte[data.Length]; - rng.GetBytes(buffer); - buffer.CopyTo(data); + rng.GetBytes(data); } finally { diff --git a/PolyShim/NetCore30/RandomNumberGenerator.cs b/PolyShim/NetCore30/RandomNumberGenerator.cs new file mode 100644 index 00000000..fee9db85 --- /dev/null +++ b/PolyShim/NetCore30/RandomNumberGenerator.cs @@ -0,0 +1,54 @@ +#if (NETCOREAPP && !NETCOREAPP3_0_OR_GREATER) || (NETFRAMEWORK) || (NETSTANDARD && !NETSTANDARD2_1_OR_GREATER) +#nullable enable +// ReSharper disable RedundantUsingDirective +// ReSharper disable CheckNamespace +// ReSharper disable InconsistentNaming +// ReSharper disable PartialTypeWithSinglePart + +using System; +using System.Diagnostics.CodeAnalysis; +using System.Security.Cryptography; + +[ExcludeFromCodeCoverage] +internal static class MemberPolyfills_NetCore30_RandomNumberGenerator +{ + extension(RandomNumberGenerator) + { + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32) + public static int GetInt32(int toExclusive) + { + return RandomNumberGenerator.GetInt32(0, toExclusive); + } + + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32-system-int32) + public static int GetInt32(int fromInclusive, int toExclusive) + { + var range = (uint)(toExclusive - fromInclusive); + var rng = RandomNumberGenerator.Create(); + try + { + // Reject values that would cause bias in the distribution. + // This ensures uniform distribution by rejecting values in the + // incomplete final bucket. + var rejectionThreshold = uint.MaxValue - (uint.MaxValue % range + 1) % range; + + uint result; + do + { + var buffer = new byte[4]; + rng.GetBytes(buffer); + result = BitConverter.ToUInt32(buffer, 0); + } while (result > rejectionThreshold); + + return (int)(result % range) + fromInclusive; + } + finally + { + // Explicit cast needed for .NET Framework 3.5 where RandomNumberGenerator + // doesn't properly expose IDisposable for using statements. + ((IDisposable)rng).Dispose(); + } + } + } +} +#endif diff --git a/PolyShim/Signatures.md b/PolyShim/Signatures.md index 0a27e60e..d5c8212a 100644 --- a/PolyShim/Signatures.md +++ b/PolyShim/Signatures.md @@ -1,8 +1,8 @@ # Signatures -- **Total:** 357 +- **Total:** 355 - **Types:** 76 -- **Members:** 281 +- **Members:** 279 ___ @@ -280,20 +280,18 @@ ___ - `RandomNumberGenerator` - [**[class]**](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator) .NET Core 1.0 - [`byte[] GetBytes(int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-int32)) .NET 6.0 - - [`int GetInt32(int, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32-system-int32)) .NET 6.0 - - [`int GetInt32(int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32)) .NET 6.0 + - [`int GetInt32(int, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32-system-int32)) .NET Core 3.0 + - [`int GetInt32(int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32)) .NET Core 3.0 - [`string GetHexString(int, bool)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.gethexstring#system-security-cryptography-randomnumbergenerator-gethexstring(system-int32-system-boolean)) .NET 8.0 - [`string GetString(ReadOnlySpan, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getstring) .NET 8.0 - [`T[] GetItems(ReadOnlySpan, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(system-readonlyspan((-0))-system-int32)) .NET 8.0 - - [`T[] GetItems(T[], int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(-0()-system-int32)) .NET 8.0 - [`void Fill(Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.fill) .NET Core 2.1 - - [`void GetBytes(byte[], int, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-byte()-system-int32-system-int32)) .NET 6.0 + - [`void GetBytes(byte[], int, int)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-byte()-system-int32-system-int32)) .NET Core 2.0 - [`void GetBytes(Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getbytes#system-security-cryptography-randomnumbergenerator-getbytes(system-span((system-byte)))) .NET Core 2.1 - [`void GetItems(ReadOnlySpan, Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(system-readonlyspan((-0))-system-span((-0)))) .NET 8.0 - - [`void GetNonZeroBytes(byte[])`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-byte())) .NET 6.0 - - [`void GetNonZeroBytes(Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-span((system-byte)))) .NET 6.0 + - [`void GetNonZeroBytes(byte[])`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-byte())) .NET Core 2.0 + - [`void GetNonZeroBytes(Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-span((system-byte)))) .NET Core 2.1 - [`void Shuffle(Span)`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.shuffle#system-security-cryptography-randomnumbergenerator-shuffle-1(system-span((-0)))) .NET 8.0 - - [`void Shuffle(T[])`](https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.shuffle#system-security-cryptography-randomnumbergenerator-shuffle-1(-0())) .NET 8.0 - `Range` - [**[struct]**](https://learn.microsoft.com/dotnet/api/system.range) .NET Core 3.0 - `ReadOnlyMemory` From 41d1ed5ebb64ad3bde24c6eddbe14e0154dd74ef Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:27:37 +0000 Subject: [PATCH 10/13] Address code review: simplify test names, use Linq/using statements, use arrow methods Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .../Net80/RandomNumberGeneratorTests.cs | 6 +++--- .../NetCore20/RandomNumberGeneratorTests.cs | 17 +++++------------ .../NetCore21/RandomNumberGeneratorTests.cs | 12 +++--------- PolyShim/NetCore21/RandomNumberGenerator.cs | 2 -- PolyShim/NetCore30/RandomNumberGenerator.cs | 8 ++------ 5 files changed, 13 insertions(+), 32 deletions(-) diff --git a/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs b/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs index b70544ef..2961422c 100644 --- a/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs @@ -20,7 +20,7 @@ public void GetHexString_Test() } [Fact] - public void GetString_Span_Test() + public void GetString_Test() { // Arrange var choices = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".AsSpan(); @@ -41,7 +41,7 @@ public void GetString_Span_Test() } [Fact] - public void GetItems_ReadOnlySpan_Test() + public void GetItems_Test() { // Arrange var choices = new[] { 1, 2, 3, 4, 5 }.AsSpan(); @@ -84,7 +84,7 @@ public void GetItems_Span_Test() } [Fact] - public void Shuffle_Span_Test() + public void Shuffle_Test() { // Arrange var originalItems = new[] { 1, 2, 3, 4, 5 }; diff --git a/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs b/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs index c9ffe2f9..cbf4cb86 100644 --- a/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs @@ -1,4 +1,5 @@ using System; +using System.Linq; using System.Security.Cryptography; using FluentAssertions; using Xunit; @@ -11,7 +12,7 @@ public class RandomNumberGeneratorTests public void GetBytes_PartialArray_Test() { // Arrange - var rng = RandomNumberGenerator.Create(); + using var rng = RandomNumberGenerator.Create(); var data = new byte[20]; // Act @@ -25,39 +26,33 @@ public void GetBytes_PartialArray_Test() } // Middle 10 bytes should have at least some non-zero values - var middleSection = new byte[10]; - Array.Copy(data, 5, middleSection, 0, 10); - middleSection.Should().Contain(b => b != 0); + data.Skip(5).Take(10).Should().Contain(b => b != 0); // Last 5 bytes should be zero for (var i = 15; i < 20; i++) { data[i].Should().Be(0); } - - ((IDisposable)rng).Dispose(); } [Fact] public void GetBytes_EmptyCount_Test() { // Arrange - var rng = RandomNumberGenerator.Create(); + using var rng = RandomNumberGenerator.Create(); var data = new byte[10]; // Act & Assert // Should not throw and should leave array unchanged rng.GetBytes(data, 0, 0); data.Should().AllBeEquivalentTo(0); - - ((IDisposable)rng).Dispose(); } [Fact] public void GetNonZeroBytes_Array_Test() { // Arrange - var rng = RandomNumberGenerator.Create(); + using var rng = RandomNumberGenerator.Create(); var data = new byte[10]; // Act @@ -65,7 +60,5 @@ public void GetNonZeroBytes_Array_Test() // Assert data.Should().NotContain((byte)0); - - ((IDisposable)rng).Dispose(); } } diff --git a/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs b/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs index 4598621f..678da86e 100644 --- a/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs @@ -38,7 +38,7 @@ public void Fill_EmptySpan_Test() public void GetBytes_Span_Test() { // Arrange - var rng = RandomNumberGenerator.Create(); + using var rng = RandomNumberGenerator.Create(); var span = new Span(new byte[16]); // Act @@ -46,29 +46,25 @@ public void GetBytes_Span_Test() // Assert span.ToArray().Should().Contain(b => b != 0); - - ((IDisposable)rng).Dispose(); } [Fact] public void GetBytes_EmptySpan_Test() { // Arrange - var rng = RandomNumberGenerator.Create(); + using var rng = RandomNumberGenerator.Create(); var span = new Span(); // Act & Assert // Should not throw and should return immediately rng.GetBytes(span); - - ((IDisposable)rng).Dispose(); } [Fact] public void GetNonZeroBytes_Span_Test() { // Arrange - var rng = RandomNumberGenerator.Create(); + using var rng = RandomNumberGenerator.Create(); var data = new Span(new byte[10]); // Act @@ -76,7 +72,5 @@ public void GetNonZeroBytes_Span_Test() // Assert data.ToArray().Should().NotContain((byte)0); - - ((IDisposable)rng).Dispose(); } } diff --git a/PolyShim/NetCore21/RandomNumberGenerator.cs b/PolyShim/NetCore21/RandomNumberGenerator.cs index 5d47dab2..b531abec 100644 --- a/PolyShim/NetCore21/RandomNumberGenerator.cs +++ b/PolyShim/NetCore21/RandomNumberGenerator.cs @@ -57,8 +57,6 @@ public static void Fill(Span data) } finally { - // Explicit cast needed for .NET Framework 3.5 where RandomNumberGenerator - // doesn't properly expose IDisposable for using statements. ((IDisposable)rng).Dispose(); } } diff --git a/PolyShim/NetCore30/RandomNumberGenerator.cs b/PolyShim/NetCore30/RandomNumberGenerator.cs index fee9db85..7c034a6d 100644 --- a/PolyShim/NetCore30/RandomNumberGenerator.cs +++ b/PolyShim/NetCore30/RandomNumberGenerator.cs @@ -15,10 +15,8 @@ internal static class MemberPolyfills_NetCore30_RandomNumberGenerator extension(RandomNumberGenerator) { // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32) - public static int GetInt32(int toExclusive) - { - return RandomNumberGenerator.GetInt32(0, toExclusive); - } + public static int GetInt32(int toExclusive) => + RandomNumberGenerator.GetInt32(0, toExclusive); // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32-system-int32) public static int GetInt32(int fromInclusive, int toExclusive) @@ -44,8 +42,6 @@ public static int GetInt32(int fromInclusive, int toExclusive) } finally { - // Explicit cast needed for .NET Framework 3.5 where RandomNumberGenerator - // doesn't properly expose IDisposable for using statements. ((IDisposable)rng).Dispose(); } } From 8d27fa85486c541d18f61c4f940890f4ec6a350c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:39:33 +0000 Subject: [PATCH 11/13] Add 0-100 loops to random tests, use Linq for all array checks, restore comment Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .../Net60/RandomNumberGeneratorTests.cs | 12 +++-- .../NetCore20/RandomNumberGeneratorTests.cs | 25 ++++------ .../NetCore21/RandomNumberGeneratorTests.cs | 49 ++++++++++--------- PolyShim/NetCore21/RandomNumberGenerator.cs | 2 + 4 files changed, 45 insertions(+), 43 deletions(-) diff --git a/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs b/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs index 2315b47e..4b10f42b 100644 --- a/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs @@ -9,11 +9,13 @@ public class RandomNumberGeneratorTests [Fact] public void GetBytes_Static_Test() { - // Act - var data = RandomNumberGenerator.GetBytes(16); + // Act & assert + for (var i = 0; i < 100; i++) + { + var data = RandomNumberGenerator.GetBytes(16); - // Assert - data.Should().HaveCount(16); - data.Should().Contain(b => b != 0); + data.Should().HaveCount(16); + data.Should().Contain(b => b != 0); + } } } diff --git a/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs b/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs index cbf4cb86..9096762f 100644 --- a/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs @@ -20,19 +20,13 @@ public void GetBytes_PartialArray_Test() // Assert // First 5 bytes should be zero - for (var i = 0; i < 5; i++) - { - data[i].Should().Be(0); - } + data.Take(5).Should().AllBeEquivalentTo(0); // Middle 10 bytes should have at least some non-zero values data.Skip(5).Take(10).Should().Contain(b => b != 0); // Last 5 bytes should be zero - for (var i = 15; i < 20; i++) - { - data[i].Should().Be(0); - } + data.Skip(15).Should().AllBeEquivalentTo(0); } [Fact] @@ -51,14 +45,15 @@ public void GetBytes_EmptyCount_Test() [Fact] public void GetNonZeroBytes_Array_Test() { - // Arrange - using var rng = RandomNumberGenerator.Create(); - var data = new byte[10]; + // Act & assert + for (var i = 0; i < 100; i++) + { + using var rng = RandomNumberGenerator.Create(); + var data = new byte[10]; - // Act - rng.GetNonZeroBytes(data); + rng.GetNonZeroBytes(data); - // Assert - data.Should().NotContain((byte)0); + data.Should().NotContain((byte)0); + } } } diff --git a/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs b/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs index 678da86e..1fad5b00 100644 --- a/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs @@ -10,17 +10,18 @@ public class RandomNumberGeneratorTests [Fact] public void Fill_Span_Test() { - // Arrange - var span = new Span(new byte[16]); + // Act & assert + for (var i = 0; i < 100; i++) + { + var span = new Span(new byte[16]); - // Act - RandomNumberGenerator.Fill(span); + RandomNumberGenerator.Fill(span); - // Assert - // Verify that not all bytes are zero (cryptographic RNG should produce non-zero data). - // Note: Theoretically, a crypto RNG could produce all zeros (probability: 2^-128), - // but this is astronomically unlikely in practice. - span.ToArray().Should().Contain(b => b != 0); + // Verify that not all bytes are zero (cryptographic RNG should produce non-zero data). + // Note: Theoretically, a crypto RNG could produce all zeros (probability: 2^-128), + // but this is astronomically unlikely in practice. + span.ToArray().Should().Contain(b => b != 0); + } } [Fact] @@ -37,15 +38,16 @@ public void Fill_EmptySpan_Test() [Fact] public void GetBytes_Span_Test() { - // Arrange - using var rng = RandomNumberGenerator.Create(); - var span = new Span(new byte[16]); + // Act & assert + for (var i = 0; i < 100; i++) + { + using var rng = RandomNumberGenerator.Create(); + var span = new Span(new byte[16]); - // Act - rng.GetBytes(span); + rng.GetBytes(span); - // Assert - span.ToArray().Should().Contain(b => b != 0); + span.ToArray().Should().Contain(b => b != 0); + } } [Fact] @@ -63,14 +65,15 @@ public void GetBytes_EmptySpan_Test() [Fact] public void GetNonZeroBytes_Span_Test() { - // Arrange - using var rng = RandomNumberGenerator.Create(); - var data = new Span(new byte[10]); + // Act & assert + for (var i = 0; i < 100; i++) + { + using var rng = RandomNumberGenerator.Create(); + var data = new Span(new byte[10]); - // Act - rng.GetNonZeroBytes(data); + rng.GetNonZeroBytes(data); - // Assert - data.ToArray().Should().NotContain((byte)0); + data.ToArray().Should().NotContain((byte)0); + } } } diff --git a/PolyShim/NetCore21/RandomNumberGenerator.cs b/PolyShim/NetCore21/RandomNumberGenerator.cs index b531abec..5d47dab2 100644 --- a/PolyShim/NetCore21/RandomNumberGenerator.cs +++ b/PolyShim/NetCore21/RandomNumberGenerator.cs @@ -57,6 +57,8 @@ public static void Fill(Span data) } finally { + // Explicit cast needed for .NET Framework 3.5 where RandomNumberGenerator + // doesn't properly expose IDisposable for using statements. ((IDisposable)rng).Dispose(); } } From cb920a699d44dcd0fdca104a555c8513aaeaf1e3 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 17:51:13 +0000 Subject: [PATCH 12/13] Refactor polyfills: add loop to partial array test, use OnlyContain, reuse GetItems, use GetInt32 for GetNonZeroBytes, reorder methods Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- .../NetCore20/RandomNumberGeneratorTests.cs | 27 ++++++++----------- PolyShim/Net80/RandomNumberGenerator.cs | 20 ++++++-------- PolyShim/NetCore20/RandomNumberGenerator.cs | 9 +------ PolyShim/NetCore30/RandomNumberGenerator.cs | 8 +++--- 4 files changed, 24 insertions(+), 40 deletions(-) diff --git a/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs b/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs index 9096762f..7326b228 100644 --- a/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs @@ -11,22 +11,18 @@ public class RandomNumberGeneratorTests [Fact] public void GetBytes_PartialArray_Test() { - // Arrange - using var rng = RandomNumberGenerator.Create(); - var data = new byte[20]; - - // Act - rng.GetBytes(data, 5, 10); - - // Assert - // First 5 bytes should be zero - data.Take(5).Should().AllBeEquivalentTo(0); + // Act & assert + for (var i = 0; i < 100; i++) + { + using var rng = RandomNumberGenerator.Create(); + var data = new byte[20]; - // Middle 10 bytes should have at least some non-zero values - data.Skip(5).Take(10).Should().Contain(b => b != 0); + rng.GetBytes(data, 5, 10); - // Last 5 bytes should be zero - data.Skip(15).Should().AllBeEquivalentTo(0); + data.Take(5).Should().OnlyContain(b => b == 0); + data.Skip(5).Take(10).Should().Contain(b => b != 0); + data.Skip(15).Should().OnlyContain(b => b == 0); + } } [Fact] @@ -37,9 +33,8 @@ public void GetBytes_EmptyCount_Test() var data = new byte[10]; // Act & Assert - // Should not throw and should leave array unchanged rng.GetBytes(data, 0, 0); - data.Should().AllBeEquivalentTo(0); + data.Should().OnlyContain(b => b == 0); } [Fact] diff --git a/PolyShim/Net80/RandomNumberGenerator.cs b/PolyShim/Net80/RandomNumberGenerator.cs index 38b9ba46..1cee1d32 100644 --- a/PolyShim/Net80/RandomNumberGenerator.cs +++ b/PolyShim/Net80/RandomNumberGenerator.cs @@ -15,18 +15,6 @@ internal static class MemberPolyfills_Net80_RandomNumberGenerator { extension(RandomNumberGenerator) { - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(system-readonlyspan((-0))-system-int32) - public static T[] GetItems(ReadOnlySpan choices, int length) - { - var result = new T[length]; - for (var i = 0; i < length; i++) - { - var index = RandomNumberGenerator.GetInt32(choices.Length); - result[i] = choices[index]; - } - return result; - } - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(system-readonlyspan((-0))-system-span((-0))) public static void GetItems(ReadOnlySpan choices, Span destination) { @@ -37,6 +25,14 @@ public static void GetItems(ReadOnlySpan choices, Span destination) } } + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getitems#system-security-cryptography-randomnumbergenerator-getitems-1(system-readonlyspan((-0))-system-int32) + public static T[] GetItems(ReadOnlySpan choices, int length) + { + var result = new T[length]; + RandomNumberGenerator.GetItems(choices, result.AsSpan()); + return result; + } + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.shuffle#system-security-cryptography-randomnumbergenerator-shuffle-1(system-span((-0))) public static void Shuffle(Span items) { diff --git a/PolyShim/NetCore20/RandomNumberGenerator.cs b/PolyShim/NetCore20/RandomNumberGenerator.cs index 9c649287..d96eb438 100644 --- a/PolyShim/NetCore20/RandomNumberGenerator.cs +++ b/PolyShim/NetCore20/RandomNumberGenerator.cs @@ -28,16 +28,9 @@ public void GetBytes(byte[] data, int offset, int count) // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-byte()) public void GetNonZeroBytes(byte[] data) { - var buffer = new byte[1]; for (var i = 0; i < data.Length; i++) { - byte value; - do - { - rng.GetBytes(buffer); - value = buffer[0]; - } while (value == 0); - data[i] = value; + data[i] = (byte)RandomNumberGenerator.GetInt32(1, 256); } } } diff --git a/PolyShim/NetCore30/RandomNumberGenerator.cs b/PolyShim/NetCore30/RandomNumberGenerator.cs index 7c034a6d..c000087f 100644 --- a/PolyShim/NetCore30/RandomNumberGenerator.cs +++ b/PolyShim/NetCore30/RandomNumberGenerator.cs @@ -14,10 +14,6 @@ internal static class MemberPolyfills_NetCore30_RandomNumberGenerator { extension(RandomNumberGenerator) { - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32) - public static int GetInt32(int toExclusive) => - RandomNumberGenerator.GetInt32(0, toExclusive); - // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32-system-int32) public static int GetInt32(int fromInclusive, int toExclusive) { @@ -45,6 +41,10 @@ public static int GetInt32(int fromInclusive, int toExclusive) ((IDisposable)rng).Dispose(); } } + + // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getint32#system-security-cryptography-randomnumbergenerator-getint32(system-int32) + public static int GetInt32(int toExclusive) => + RandomNumberGenerator.GetInt32(0, toExclusive); } } #endif From 031dc8a16d257fa59095ca13327efe9dec464c9a Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Tue, 17 Feb 2026 18:04:55 +0000 Subject: [PATCH 13/13] Fix test AAA pattern: add blank lines between Arrange-Act-Assert sections, use GetInt32 in GetNonZeroBytes Span Co-authored-by: Tyrrrz <1935960+Tyrrrz@users.noreply.github.com> --- PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs | 3 ++- PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs | 8 ++++---- .../NetCore20/RandomNumberGeneratorTests.cs | 12 +++++++++--- .../NetCore21/RandomNumberGeneratorTests.cs | 12 +++++++++--- .../NetCore30/RandomNumberGeneratorTests.cs | 8 ++++++-- PolyShim/NetCore21/RandomNumberGenerator.cs | 9 +-------- 6 files changed, 31 insertions(+), 21 deletions(-) diff --git a/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs b/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs index 4b10f42b..0701cc0d 100644 --- a/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/Net60/RandomNumberGeneratorTests.cs @@ -9,11 +9,12 @@ public class RandomNumberGeneratorTests [Fact] public void GetBytes_Static_Test() { - // Act & assert for (var i = 0; i < 100; i++) { + // Act var data = RandomNumberGenerator.GetBytes(16); + // Assert data.Should().HaveCount(16); data.Should().Contain(b => b != 0); } diff --git a/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs b/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs index 2961422c..18f5c459 100644 --- a/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/Net80/RandomNumberGeneratorTests.cs @@ -25,9 +25,9 @@ public void GetString_Test() // Arrange var choices = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789".AsSpan(); - // Act for (var i = 0; i < 100; i++) { + // Act var str = RandomNumberGenerator.GetString(choices, 16); // Assert @@ -46,9 +46,9 @@ public void GetItems_Test() // Arrange var choices = new[] { 1, 2, 3, 4, 5 }.AsSpan(); - // Act for (var i = 0; i < 100; i++) { + // Act var items = RandomNumberGenerator.GetItems(choices, 3); // Assert @@ -68,9 +68,9 @@ public void GetItems_Span_Test() var choices = new[] { 1, 2, 3, 4, 5 }.AsSpan(); var destination = new Span(new int[3]); - // Act for (var i = 0; i < 100; i++) { + // Act RandomNumberGenerator.GetItems(choices, destination); // Assert @@ -89,9 +89,9 @@ public void Shuffle_Test() // Arrange var originalItems = new[] { 1, 2, 3, 4, 5 }; - // Act for (var i = 0; i < 100; i++) { + // Act var items = new Span((int[])originalItems.Clone()); RandomNumberGenerator.Shuffle(items); diff --git a/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs b/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs index 7326b228..b83d00c0 100644 --- a/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/NetCore20/RandomNumberGeneratorTests.cs @@ -11,14 +11,16 @@ public class RandomNumberGeneratorTests [Fact] public void GetBytes_PartialArray_Test() { - // Act & assert for (var i = 0; i < 100; i++) { + // Arrange using var rng = RandomNumberGenerator.Create(); var data = new byte[20]; + // Act rng.GetBytes(data, 5, 10); + // Assert data.Take(5).Should().OnlyContain(b => b == 0); data.Skip(5).Take(10).Should().Contain(b => b != 0); data.Skip(15).Should().OnlyContain(b => b == 0); @@ -32,22 +34,26 @@ public void GetBytes_EmptyCount_Test() using var rng = RandomNumberGenerator.Create(); var data = new byte[10]; - // Act & Assert + // Act rng.GetBytes(data, 0, 0); + + // Assert data.Should().OnlyContain(b => b == 0); } [Fact] public void GetNonZeroBytes_Array_Test() { - // Act & assert for (var i = 0; i < 100; i++) { + // Arrange using var rng = RandomNumberGenerator.Create(); var data = new byte[10]; + // Act rng.GetNonZeroBytes(data); + // Assert data.Should().NotContain((byte)0); } } diff --git a/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs b/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs index 1fad5b00..b834d018 100644 --- a/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/NetCore21/RandomNumberGeneratorTests.cs @@ -10,13 +10,15 @@ public class RandomNumberGeneratorTests [Fact] public void Fill_Span_Test() { - // Act & assert for (var i = 0; i < 100; i++) { + // Arrange var span = new Span(new byte[16]); + // Act RandomNumberGenerator.Fill(span); + // Assert // Verify that not all bytes are zero (cryptographic RNG should produce non-zero data). // Note: Theoretically, a crypto RNG could produce all zeros (probability: 2^-128), // but this is astronomically unlikely in practice. @@ -38,14 +40,16 @@ public void Fill_EmptySpan_Test() [Fact] public void GetBytes_Span_Test() { - // Act & assert for (var i = 0; i < 100; i++) { + // Arrange using var rng = RandomNumberGenerator.Create(); var span = new Span(new byte[16]); + // Act rng.GetBytes(span); + // Assert span.ToArray().Should().Contain(b => b != 0); } } @@ -65,14 +69,16 @@ public void GetBytes_EmptySpan_Test() [Fact] public void GetNonZeroBytes_Span_Test() { - // Act & assert for (var i = 0; i < 100; i++) { + // Arrange using var rng = RandomNumberGenerator.Create(); var data = new Span(new byte[10]); + // Act rng.GetNonZeroBytes(data); + // Assert data.ToArray().Should().NotContain((byte)0); } } diff --git a/PolyShim.Tests/NetCore30/RandomNumberGeneratorTests.cs b/PolyShim.Tests/NetCore30/RandomNumberGeneratorTests.cs index 47251460..6acc6a01 100644 --- a/PolyShim.Tests/NetCore30/RandomNumberGeneratorTests.cs +++ b/PolyShim.Tests/NetCore30/RandomNumberGeneratorTests.cs @@ -9,10 +9,12 @@ public class RandomNumberGeneratorTests [Fact] public void GetInt32_MaxValue_Test() { - // Act & assert for (var i = 0; i < 100; i++) { + // Act var value = RandomNumberGenerator.GetInt32(10); + + // Assert value.Should().BeInRange(0, 9); } } @@ -20,10 +22,12 @@ public void GetInt32_MaxValue_Test() [Fact] public void GetInt32_Range_Test() { - // Act & assert for (var i = 0; i < 100; i++) { + // Act var value = RandomNumberGenerator.GetInt32(10, 20); + + // Assert value.Should().BeInRange(10, 19); } } diff --git a/PolyShim/NetCore21/RandomNumberGenerator.cs b/PolyShim/NetCore21/RandomNumberGenerator.cs index 5d47dab2..3e308491 100644 --- a/PolyShim/NetCore21/RandomNumberGenerator.cs +++ b/PolyShim/NetCore21/RandomNumberGenerator.cs @@ -28,16 +28,9 @@ public void GetBytes(Span data) // https://learn.microsoft.com/dotnet/api/system.security.cryptography.randomnumbergenerator.getnonzerobytes#system-security-cryptography-randomnumbergenerator-getnonzerobytes(system-span((system-byte))) public void GetNonZeroBytes(Span data) { - var buffer = new byte[1]; for (var i = 0; i < data.Length; i++) { - byte value; - do - { - rng.GetBytes(buffer); - value = buffer[0]; - } while (value == 0); - data[i] = value; + data[i] = (byte)RandomNumberGenerator.GetInt32(1, 256); } } }