Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added GetInt32 to RandomNumberGenerator. #31243

Merged
merged 1 commit into from Jul 25, 2018
Merged
Changes from all commits
Commits
File filter...
Filter file types
Jump to…
Jump to file or symbol
Failed to load files and symbols.

Always

Just for now

@@ -43,6 +43,8 @@ public abstract partial class RandomNumberGenerator : System.IDisposable
public static void Fill(Span<byte> data) => throw null;
public virtual void GetBytes(System.Span<byte> data) { }
public virtual void GetNonZeroBytes(System.Span<byte> data) { }
public static int GetInt32(int fromInclusive, int toExclusive) { throw null; }
public static int GetInt32(int toExclusive) { throw null; }
}
public abstract partial class RSA : System.Security.Cryptography.AsymmetricAlgorithm
{
@@ -64,6 +64,9 @@
<data name="ArgumentOutOfRange_NeedPosNum" xml:space="preserve">
<value>Positive number required.</value>
</data>
<data name="Argument_InvalidRandomRange" xml:space="preserve">
<value>Range of random number does not contain at least one possibility.</value>
</data>
<data name="Argument_InvalidOffLen" xml:space="preserve">
<value>Offset and length were out of bounds for the array or count is greater than the number of elements from index to the end of the source collection.</value>
</data>
@@ -3,6 +3,7 @@
// See the LICENSE file in the project root for more information.

using System.Buffers;
using System.Runtime.InteropServices;

namespace System.Security.Cryptography
{
@@ -96,12 +97,62 @@ public static void Fill(Span<byte> data)
RandomNumberGeneratorImplementation.FillSpan(data);
}

public static int GetInt32(int fromInclusive, int toExclusive)
{
if (fromInclusive >= toExclusive)
throw new ArgumentException(SR.Argument_InvalidRandomRange);

// The total possible range is [0, 4,294,967,295).
// Subtract one to account for zero being an actual possibility.
uint range = (uint)toExclusive - (uint)fromInclusive - 1;

// If there is only one possible choice, nothing random will actually happen, so return
// the only possibility.
if (range == 0)
{
return fromInclusive;
}

// Create a mask for the bits that we care about for the range. The other bits will be
// masked away.
uint mask = range;
mask |= mask >> 1;
mask |= mask >> 2;
mask |= mask >> 4;
mask |= mask >> 8;
mask |= mask >> 16;

Span<uint> resultSpan = stackalloc uint[1];
uint result;

do
{
RandomNumberGeneratorImplementation.FillSpan(MemoryMarshal.AsBytes(resultSpan));
result = mask & resultSpan[0];
}
while (result > range);

return (int)result + fromInclusive;
}

public static int GetInt32(int toExclusive)
{
if (toExclusive <= 0)
throw new ArgumentOutOfRangeException(nameof(toExclusive), SR.ArgumentOutOfRange_NeedPosNum);

return GetInt32(0, toExclusive);
}

internal void VerifyGetBytes(byte[] data, int offset, int count)
{
if (data == null) throw new ArgumentNullException(nameof(data));
if (offset < 0) throw new ArgumentOutOfRangeException(nameof(offset), SR.ArgumentOutOfRange_NeedNonNegNum);
if (count < 0) throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_NeedNonNegNum);
if (count > data.Length - offset) throw new ArgumentException(SR.Argument_InvalidOffLen);
if (data == null)
throw new ArgumentNullException(nameof(data));
if (offset < 0)
throw new ArgumentOutOfRangeException(nameof(offset), SR.ArgumentOutOfRange_NeedNonNegNum);
if (count < 0)
throw new ArgumentOutOfRangeException(nameof(count), SR.ArgumentOutOfRange_NeedNonNegNum);
if (count > data.Length - offset)
throw new ArgumentException(SR.Argument_InvalidOffLen);
}
}
}
@@ -2,6 +2,8 @@
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.

using System.Buffers.Binary;
using System.Collections.Generic;
using Xunit;

namespace System.Security.Cryptography.RNG.Tests
@@ -92,5 +94,193 @@ public static void Fill_RandomDistribution()

RandomDataGenerator.VerifyRandomDistribution(random);
}

[Theory]
[InlineData(10, 10)]
[InlineData(10, 9)]
[InlineData(-10, -10)]
[InlineData(-10, -11)]
public static void GetInt32_LowerAndUpper_InvalidRange(int fromInclusive, int toExclusive)
{
Assert.Throws<ArgumentException>(() => RandomNumberGenerator.GetInt32(fromInclusive, toExclusive));
}

[Theory]
[InlineData(0)]
[InlineData(-10)]
public static void GetInt32_Upper_InvalidRange(int toExclusive)
{
Assert.Throws<ArgumentOutOfRangeException>(() => RandomNumberGenerator.GetInt32(toExclusive));
}

[Theory]
[InlineData(1 << 1)]
[InlineData(1 << 4)]
[InlineData(1 << 16)]
[InlineData(1 << 24)]
public static void GetInt32_PowersOfTwo(int toExclusive)
{
for (int i = 0; i < 10; i++)
{
int result = RandomNumberGenerator.GetInt32(toExclusive);
Assert.InRange(result, 0, toExclusive - 1);
}
}

[Theory]
[InlineData((1 << 1) + 1)]
[InlineData((1 << 4) + 1)]
[InlineData((1 << 16) + 1)]
[InlineData((1 << 24) + 1)]
public static void GetInt32_PowersOfTwoPlusOne(int toExclusive)
{
for (int i = 0; i < 10; i++)
{
int result = RandomNumberGenerator.GetInt32(toExclusive);
Assert.InRange(result, 0, toExclusive - 1);
}
}

[Fact]
public static void GetInt32_FullRange()
{
int result = RandomNumberGenerator.GetInt32(int.MinValue, int.MaxValue);
Assert.NotEqual(int.MaxValue, result);
}

[Fact]
public static void GetInt32_DoesNotProduceSameNumbers()
{
int result1 = RandomNumberGenerator.GetInt32(int.MinValue, int.MaxValue);
int result2 = RandomNumberGenerator.GetInt32(int.MinValue, int.MaxValue);
int result3 = RandomNumberGenerator.GetInt32(int.MinValue, int.MaxValue);

// The changes of this happening are (2^32 - 1) * 3.
Assert.False(result1 == result2 && result2 == result3, "Generated the same number three times in a row.");
}

[Fact]
public static void GetInt32_FullRange_DistributesBitsEvenly()
{
// This test should work since we are selecting random numbers that are a
// Power of two minus one so no bit should favored.
int numberToGenerate = 256;
byte[] bytes = new byte[numberToGenerate * 4];
Span<byte> bytesSpan = bytes.AsSpan();
for (int i = 0, j = 0; i < numberToGenerate; i++, j += 4)
{
int result = RandomNumberGenerator.GetInt32(int.MinValue, int.MaxValue);
Span<byte> slice = bytesSpan.Slice(j, 4);
BinaryPrimitives.WriteInt32LittleEndian(slice, result);
}
RandomDataGenerator.VerifyRandomDistribution(bytes);
}

[Fact]
public static void GetInt32_CoinFlipLowByte()
{
int numberToGenerate = 1024;
Span<int> generated = stackalloc int[numberToGenerate];

for (int i = 0; i < numberToGenerate; i++)
{
generated[i] = RandomNumberGenerator.GetInt32(0, 2);
}
VerifyAllInRange(generated, 0, 2);
VerifyDistribution(generated, 0.5);
}


[Fact]
public static void GetInt32_CoinFlipOverByteBoundary()
{
int numberToGenerate = 1024;
Span<int> generated = stackalloc int[numberToGenerate];

for (int i = 0; i < numberToGenerate; i++)
{
generated[i] = RandomNumberGenerator.GetInt32(255, 257);
}
VerifyAllInRange(generated, 255, 257);
VerifyDistribution(generated, 0.5);
}

[Fact]
public static void GetInt32_NegativeBounds1000d20()
{
int numberToGenerate = 1000;
Span<int> generated = stackalloc int[numberToGenerate];

for (int i = 0; i < numberToGenerate; i++)
{
generated[i] = RandomNumberGenerator.GetInt32(-4000, -3979);
}
VerifyAllInRange(generated, -4000, -3979);
VerifyDistribution(generated, 0.05);
}

[Fact]
public static void GetInt32_1000d6()
{
int numberToGenerate = 1000;
Span<int> generated = stackalloc int[numberToGenerate];

for (int i = 0; i < numberToGenerate; i++)
{
generated[i] = RandomNumberGenerator.GetInt32(1, 7);
}
VerifyAllInRange(generated, 1, 7);
VerifyDistribution(generated, 0.16);
}

[Theory]
[InlineData(int.MinValue, int.MinValue + 3)]
[InlineData(-257, -129)]
[InlineData(-100, 5)]
[InlineData(254, 512)]
[InlineData(-1_073_741_909, - 1_073_741_825)]
[InlineData(65_534, 65_539)]
[InlineData(16_777_214, 16_777_217)]
public static void GetInt32_MaskRangeCorrect(int fromInclusive, int toExclusive)
{
int numberToGenerate = 1000;
Span<int> generated = stackalloc int[numberToGenerate];

for (int i = 0; i < numberToGenerate; i++)
{
generated[i] = RandomNumberGenerator.GetInt32(fromInclusive, toExclusive);
}

double expectedDistribution = 1d / (toExclusive - fromInclusive);
VerifyAllInRange(generated, fromInclusive, toExclusive);
VerifyDistribution(generated, expectedDistribution);
}

private static void VerifyAllInRange(ReadOnlySpan<int> numbers, int fromInclusive, int toExclusive)
{
for (int i = 0; i < numbers.Length; i++)
{
Assert.InRange(numbers[i], fromInclusive, toExclusive - 1);
}
}

private static void VerifyDistribution(ReadOnlySpan<int> numbers, double expected)
{
var observedNumbers = new Dictionary<int, int>(numbers.Length);
for (int i = 0; i < numbers.Length; i++)
{
int number = numbers[i];
if (!observedNumbers.TryAdd(number, 1))
{
observedNumbers[number]++;
}
}
const double tolerance = 0.07;
foreach ((_, int occurences) in observedNumbers)
{
double percentage = occurences / (double)numbers.Length;
Assert.True(Math.Abs(expected - percentage) < tolerance, "Occurred number of times within threshold.");
}
}
}
}
ProTip! Use n and p to navigate between commits in a pull request.
You can’t perform that action at this time.