From d9ffe2ff4b7a782cab7f65f57027c6b98109d66d Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Thu, 24 Apr 2025 20:57:09 +0200 Subject: [PATCH 1/3] Proposal to generate `UUID`s using `RandomNumberGenerator`s This PR adds a proposal to generate `UUID's` using `RandomNumberGenerator`s --- Proposals/NNNN-random-uuid.md | 57 +++++++++++++++++++ Sources/FoundationEssentials/UUID.swift | 44 ++++++++++++++ .../FoundationEssentialsTests/UUIDTests.swift | 19 +++++++ 3 files changed, 120 insertions(+) create mode 100644 Proposals/NNNN-random-uuid.md 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..30ee534f9 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 the last 12 bits + 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..d109ae3e6 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -115,4 +115,23 @@ 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) + } + } +} + +extension UUID { + fileprivate var versionNumber: Int { + Int(self.uuid.6 >> 4) + } + + fileprivate var varint: Int { + Int(self.uuid.8 >> 6 & 0b11) + } } From ffde843c54dfbe9b3650b81526519d931aa691fc Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 30 Apr 2025 11:05:00 +0200 Subject: [PATCH 2/3] Seeded deterministic UUID generation test --- .../FoundationEssentialsTests/UUIDTests.swift | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index d109ae3e6..e2d4b6c14 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -124,6 +124,22 @@ final class UUIDTests : XCTestCase { 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 { @@ -135,3 +151,28 @@ extension UUID { 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) + } +} + From 96cc29ce706b5f4d89bbff62b721e9a287eb436a Mon Sep 17 00:00:00 2001 From: Franz Busch Date: Wed, 30 Apr 2025 11:17:20 +0200 Subject: [PATCH 3/3] Fix code comment --- Sources/FoundationEssentials/UUID.swift | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index 30ee534f9..7a29a75b7 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -92,7 +92,7 @@ public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable { var secondBits = second // Set the version to 4 (0100 in binary) - firstBits &= 0xFFFFFFFFFFFF0FFF // Clear the last 12 bits + 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)