diff --git a/Proposals/NNNN-random-uuid.md b/Proposals/NNNN-random-uuid.md new file mode 100644 index 000000000..4a17eb8d7 --- /dev/null +++ b/Proposals/NNNN-random-uuid.md @@ -0,0 +1,57 @@ +# Generating UUIDs using RandomNumberGenerators + +* Proposal: [SF-NNNN](NNNN-random-uuid.md) +* Authors: [FranzBusch](https://github.com/FranzBusch) +* Review Manager: TBD +* Status: **Awaiting review** +* Implementation: [swiftlang/swift-foundation#1271](https://github.com/swiftlang/swift-foundation/pull/1271) +* Review: ([pitch](https://forums.swift.org/...)) + +## Introduction + +UUIDs (Universally Unique IDentifiers) are 128 bits long and is intended to +guarantee uniqueness across space and time. This proposal adds APIs to generate +UUIDs from Swift's random number generators. + +## Motivation + +UUIDs often need to be randomly generated. This is currently possible by calling +the `UUID` initializer. However, this initializer doesn't allow providing a +custom source from which the `UUID` is generated. Swift's standard library +provides a common abstraction for random number generators through the +`RandomNumberGenerator` protocol. Providing methods to generate `UUID`s using a +`RandomNumberGenerator` allows developers to customize their source of randomness. + +An example where this is useful is where a system needs to generate UUIDs using a +deterministically seeded random number generator. + +## Proposed solution + +This proposal adds a new static method to the `UUID` type to generate new random `UUIDs` using a `RandomNumberGenerator`. + +```swift +/// Generates a new random UUID. +/// +/// - Parameter generator: The random number generator to use when creating the new random value. +/// - Returns: A random UUID. +@available(FoundationPreview 6.2, *) +public static func random( + using generator: inout some RandomNumberGenerator +) -> UUID +``` + +## Source compatibility + +The new API is purely additive and ha no impact on the existing API. + +## Implications on adoption + +This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source compatibility. + +## Alternatives considered + +### Initializer based random UUID generation + +The existing `UUID.init()` is already generating new random `UUID`s and a new +`UUID(using: &rng)` method would be a good alternative to the proposed static method. +However, the static `random` method has precedence on various types such as [Int.random](https://developer.apple.com/documentation/swift/int/random(in:)-9mjpw). diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index 23b69a115..7a29a75b7 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -76,6 +76,50 @@ public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable { hasher.combine(bytes: buffer) } } + + /// Generates a new random UUID. + /// + /// - Parameter generator: The random number generator to use when creating the new random value. + /// - Returns: A random UUID. + @available(FoundationPreview 6.2, *) + public static func random( + using generator: inout some RandomNumberGenerator + ) -> UUID { + let first = UInt64.random(in: .min ... .max, using: &generator) + let second = UInt64.random(in: .min ... .max, using: &generator) + + var firstBits = first + var secondBits = second + + // Set the version to 4 (0100 in binary) + firstBits &= 0xFFFFFFFFFFFF0FFF // Clear bits 48 through 51 + firstBits |= 0x0000000000004000 // Set the version bits to '0100' at the correct position + + // Set the variant to '10' (RFC9562 variant) + secondBits &= 0x3FFFFFFFFFFFFFFF // Clear the 2 most significant bits + secondBits |= 0x8000000000000000 // Set the two MSB to '10' + + let uuidBytes = ( + UInt8(truncatingIfNeeded: firstBits >> 56), + UInt8(truncatingIfNeeded: firstBits >> 48), + UInt8(truncatingIfNeeded: firstBits >> 40), + UInt8(truncatingIfNeeded: firstBits >> 32), + UInt8(truncatingIfNeeded: firstBits >> 24), + UInt8(truncatingIfNeeded: firstBits >> 16), + UInt8(truncatingIfNeeded: firstBits >> 8), + UInt8(truncatingIfNeeded: firstBits), + UInt8(truncatingIfNeeded: secondBits >> 56), + UInt8(truncatingIfNeeded: secondBits >> 48), + UInt8(truncatingIfNeeded: secondBits >> 40), + UInt8(truncatingIfNeeded: secondBits >> 32), + UInt8(truncatingIfNeeded: secondBits >> 24), + UInt8(truncatingIfNeeded: secondBits >> 16), + UInt8(truncatingIfNeeded: secondBits >> 8), + UInt8(truncatingIfNeeded: secondBits) + ) + + return UUID(uuid: uuidBytes) + } public var description: String { return uuidString diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index a21707f55..e2d4b6c14 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -115,4 +115,64 @@ final class UUIDTests : XCTestCase { XCTAssertFalse(uuid2 > uuid1) XCTAssertTrue(uuid2 == uuid1) } + + func testRandomVersionAndVariant() { + var generator = SystemRandomNumberGenerator() + for _ in 0..<10000 { + let uuid = UUID.random(using: &generator) + XCTAssertEqual(uuid.versionNumber, 0b0100) + XCTAssertEqual(uuid.varint, 0b10) + } + } + + func testDeterministicRandomGeneration() { + var generator = PCGRandomNumberGenerator(seed: 123456789) + + let firstUUID = UUID.random(using: &generator) + XCTAssertEqual(firstUUID, UUID(uuidString: "9492BAC4-F353-49E7-ACBB-A40941CA65DE")) + + let secondUUID = UUID.random(using: &generator) + XCTAssertEqual(secondUUID, UUID(uuidString: "392C44E5-EB3E-4455-85A7-AF9556722B9A")) + + let thirdUUID = UUID.random(using: &generator) + XCTAssertEqual(thirdUUID, UUID(uuidString: "9ABFCCE9-AA85-485C-9CBF-C62F0C8D1D1A")) + + let fourthUUID = UUID.random(using: &generator) + XCTAssertEqual(fourthUUID, UUID(uuidString: "2B29542E-F719-4D58-87B9-C6291ADD4541")) + } } + +extension UUID { + fileprivate var versionNumber: Int { + Int(self.uuid.6 >> 4) + } + + fileprivate var varint: Int { + Int(self.uuid.8 >> 6 & 0b11) + } +} + +fileprivate struct PCGRandomNumberGenerator: RandomNumberGenerator { + private static let multiplier: UInt128 = 47_026_247_687_942_121_848_144_207_491_837_523_525 + private static let increment: UInt128 = 117_397_592_171_526_113_268_558_934_119_004_209_487 + + private var state: UInt128 + + fileprivate init(seed: UInt64) { + self.state = UInt128(seed) + } + + fileprivate mutating func next() -> UInt64 { + self.state = self.state &* Self.multiplier &+ Self.increment + + return rotr64( + value: UInt64(truncatingIfNeeded: self.state &>> 64) ^ UInt64(truncatingIfNeeded: self.state), + rotation: UInt64(truncatingIfNeeded: self.state &>> 122) + ) + } + + private func rotr64(value: UInt64, rotation: UInt64) -> UInt64 { + (value &>> rotation) | value &<< ((~rotation &+ 1) & 63) + } +} +