Skip to content

Commit

Permalink
support the use of RandomNumberGenerator (#72)
Browse files Browse the repository at this point in the history
* support the use of RandomNumberGenerator

This commit makes the following changes:

- Each of the randomInteger static methods now has an overload that takes a RandomNumberGenerator argument.

- The existing randomInteger methods use SystemRandomNumberGenerator instead of a system-specific API.

* reserve capacity if array storage will be used

Co-authored-by: Rob Mayoff <[email protected]>
  • Loading branch information
mayoff and Rob Mayoff authored Aug 26, 2020
1 parent 355a12a commit a17fb58
Show file tree
Hide file tree
Showing 3 changed files with 109 additions and 44 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ BigInt deploys to macOS 10.10, iOS 9, watchOS 2 and tvOS 9.
It has been tested on the latest OS releases only---however, as the module uses very few platform-provided APIs,
there should be very few issues with earlier versions.

BigInt uses no APIs specific to Apple platforms except for `arc4random_buf` in `BigUInt Random.swift`, so
BigInt uses no APIs specific to Apple platforms, so
it should be easy to port it to other operating systems.

Setup instructions:
Expand Down
116 changes: 73 additions & 43 deletions Sources/Random.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,66 +6,96 @@
// Copyright © 2016-2017 Károly Lőrentey.
//

import Foundation
#if os(Linux) || os(FreeBSD)
import Glibc
#endif


extension BigUInt {
//MARK: Random Integers

/// Create a big integer consisting of `width` uniformly distributed random bits.
/// Create a big unsigned integer consisting of `width` uniformly distributed random bits.
///
/// - Returns: A big integer less than `1 << width`.
/// - Note: This function uses `arc4random_buf` to generate random bits.
public static func randomInteger(withMaximumWidth width: Int) -> BigUInt {
guard width > 0 else { return 0 }

let byteCount = (width + 7) / 8
assert(byteCount > 0)

let buffer = UnsafeMutablePointer<UInt8>.allocate(capacity: byteCount)
#if os(Linux) || os(FreeBSD)
let fd = open("/dev/urandom", O_RDONLY)
defer {
close(fd)
/// - Parameter width: The maximum number of one bits in the result.
/// - Parameter generator: The source of randomness.
/// - Returns: A big unsigned integer less than `1 << width`.
public static func randomInteger<RNG: RandomNumberGenerator>(withMaximumWidth width: Int, using generator: inout RNG) -> BigUInt {
var result = BigUInt.zero
var bitsLeft = width
var i = 0
let wordsNeeded = (width + Word.bitWidth - 1) / Word.bitWidth
if wordsNeeded > 2 {
result.reserveCapacity(wordsNeeded)
}
let _ = read(fd, buffer, MemoryLayout<UInt8>.size * byteCount)
#else
arc4random_buf(buffer, byteCount)
#endif
if width % 8 != 0 {
buffer[0] &= UInt8(1 << (width % 8) - 1)
while bitsLeft >= Word.bitWidth {
result[i] = generator.next()
i += 1
bitsLeft -= Word.bitWidth
}
defer {
buffer.deinitialize(count: byteCount)
buffer.deallocate()
if bitsLeft > 0 {
let mask: Word = (1 << bitsLeft) - 1
result[i] = (generator.next() as Word) & mask
}
return BigUInt(Data(bytesNoCopy: buffer, count: byteCount, deallocator: .none))
return result
}

/// Create a big integer consisting of `width-1` uniformly distributed random bits followed by a one bit.
/// Create a big unsigned integer consisting of `width` uniformly distributed random bits.
///
/// - Returns: A random big integer whose width is `width`.
/// - Note: This function uses `arc4random_buf` to generate random bits.
public static func randomInteger(withExactWidth width: Int) -> BigUInt {
/// - Note: I use a `SystemRandomGeneratorGenerator` as the source of randomness.
///
/// - Parameter width: The maximum number of one bits in the result.
/// - Returns: A big unsigned integer less than `1 << width`.
public static func randomInteger(withMaximumWidth width: Int) -> BigUInt {
var rng = SystemRandomNumberGenerator()
return randomInteger(withMaximumWidth: width, using: &rng)
}

/// Create a big unsigned integer consisting of `width-1` uniformly distributed random bits followed by a one bit.
///
/// - Note: If `width` is zero, the result is zero.
///
/// - Parameter width: The number of bits required to represent the answer.
/// - Parameter generator: The source of randomness.
/// - Returns: A random big unsigned integer whose width is `width`.
public static func randomInteger<RNG: RandomNumberGenerator>(withExactWidth width: Int, using generator: inout RNG) -> BigUInt {
// width == 0 -> return 0 because there is no room for a one bit.
// width == 1 -> return 1 because there is no room for any random bits.
guard width > 1 else { return BigUInt(width) }
var result = randomInteger(withMaximumWidth: width - 1)
var result = randomInteger(withMaximumWidth: width - 1, using: &generator)
result[(width - 1) / Word.bitWidth] |= 1 << Word((width - 1) % Word.bitWidth)
return result
}

/// Create a uniformly distributed random integer that's less than the specified limit.
/// Create a big unsigned integer consisting of `width-1` uniformly distributed random bits followed by a one bit.
///
/// - Returns: A random big integer that is less than `limit`.
/// - Note: This function uses `arc4random_buf` to generate random bits.
public static func randomInteger(lessThan limit: BigUInt) -> BigUInt {
/// - Note: If `width` is zero, the result is zero.
/// - Note: I use a `SystemRandomGeneratorGenerator` as the source of randomness.
///
/// - Returns: A random big unsigned integer whose width is `width`.
public static func randomInteger(withExactWidth width: Int) -> BigUInt {
var rng = SystemRandomNumberGenerator()
return randomInteger(withExactWidth: width, using: &rng)
}

/// Create a uniformly distributed random unsigned integer that's less than the specified limit.
///
/// - Precondition: `limit > 0`.
///
/// - Parameter limit: The upper bound on the result.
/// - Parameter generator: The source of randomness.
/// - Returns: A random big unsigned integer that is less than `limit`.
public static func randomInteger<RNG: RandomNumberGenerator>(lessThan limit: BigUInt, using generator: inout RNG) -> BigUInt {
precondition(limit > 0, "\(#function): 0 is not a valid limit")
let width = limit.bitWidth
var random = randomInteger(withMaximumWidth: width)
var random = randomInteger(withMaximumWidth: width, using: &generator)
while random >= limit {
random = randomInteger(withMaximumWidth: width)
random = randomInteger(withMaximumWidth: width, using: &generator)
}
return random
}

/// Create a uniformly distributed random unsigned integer that's less than the specified limit.
///
/// - Precondition: `limit > 0`.
/// - Note: I use a `SystemRandomGeneratorGenerator` as the source of randomness.
///
/// - Parameter limit: The upper bound on the result.
/// - Returns: A random big unsigned integer that is less than `limit`.
public static func randomInteger(lessThan limit: BigUInt) -> BigUInt {
var rng = SystemRandomNumberGenerator()
return randomInteger(lessThan: limit, using: &rng)
}
}
35 changes: 35 additions & 0 deletions Tests/BigIntTests/BigUIntTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -1444,6 +1444,40 @@ class BigUIntTests: XCTestCase {
XCTAssertEqual(zeroBits, [])
}

func testRandomFunctionsUseProvidedGenerator() {
// Here I verify that each of the randomInteger functions uses the provided RNG, and not SystemRandomNumberGenerator.
// This is important because all but BigUInt.randomInteger(withMaximumWidth:using:) are built on that base function, and it is easy to forget to pass along the provided generator and get a default SystemRandomNumberGenerator instead.

// Since SystemRandomNumberGenerator is seeded randomly, repeated uses should give varying results.
// So here I pass the same deterministic RNG repeatedly and verify that I get the same result each time.

struct CountingRNG: RandomNumberGenerator {
var i: UInt64 = 12345
mutating func next() -> UInt64 {
i += 1
return i
}
}

func gen(_ body: (inout CountingRNG) -> BigUInt) -> BigUInt {
var rng = CountingRNG()
return body(&rng)
}

func check(_ body: (inout CountingRNG) -> BigUInt) {
let expected = gen(body)
for _ in 0 ..< 100 {
let actual = gen(body)
XCTAssertEqual(expected, actual)
}
}

check { BigUInt.randomInteger(withMaximumWidth: 200, using: &$0) }
check { BigUInt.randomInteger(withExactWidth: 200, using: &$0) }
let limit = BigUInt(UInt64.max) * BigUInt(UInt64.max) * BigUInt(UInt64.max)
check { BigUInt.randomInteger(lessThan: limit, using: &$0) }
}

//
// you have to manually register linux tests here :-(
//
Expand Down Expand Up @@ -1497,5 +1531,6 @@ class BigUIntTests: XCTestCase {
("testRandomIntegerWithMaximumWidth", testRandomIntegerWithMaximumWidth),
("testRandomIntegerWithExactWidth", testRandomIntegerWithExactWidth),
("testRandomIntegerLessThan", testRandomIntegerLessThan),
("testRandomFunctionsUseProvidedGenerator", testRandomFunctionsUseProvidedGenerator),
]
}

0 comments on commit a17fb58

Please sign in to comment.