diff --git a/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift b/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift index fcc25aab81..273b70e6e5 100644 --- a/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift +++ b/Benchmarks/Benchmarks/Essentials/BenchmarkEssentials.swift @@ -26,7 +26,7 @@ let benchmarks = { Benchmark.defaultConfiguration.metrics = [.cpuTotal, .wallClock, .throughput] // MARK: UUID - + Benchmark("UUIDEqual", configuration: .init(scalingFactor: .mega)) { benchmark in let u1 = UUID() let u2 = UUID() @@ -34,4 +34,30 @@ let benchmarks = { assert(u1 != u2) } } + + Benchmark("UUIDCreate") { benchmark in + for _ in benchmark.scaledIterations { + blackHole(UUID()) + } + } + + Benchmark("UUIDCreateTimeOrdered") { benchmark in + for _ in benchmark.scaledIterations { + blackHole(UUID.version7()) + } + } + + Benchmark("UUIDString") { benchmark in + let uuid = UUID() + for _ in benchmark.scaledIterations { + blackHole(uuid.uuidString) + } + } + + Benchmark("UUIDStringLower") { benchmark in + let uuid = UUID() + for _ in benchmark.scaledIterations { + blackHole(uuid.lowercasedUUIDString) + } + } } diff --git a/Proposals/NNNN-uuid-versions.md b/Proposals/NNNN-uuid-versions.md new file mode 100644 index 0000000000..b63f0b84c9 --- /dev/null +++ b/Proposals/NNNN-uuid-versions.md @@ -0,0 +1,270 @@ +# UUID Version Support and Other Enhancements + +* Proposal: [SF-NNNN](NNNN-uuid-versions.md) +* Authors: [Tony Parker](https://github.com/parkera) +* Review Manager: TBD +* Status: **Awaiting review** +* Implementation: [swiftlang/swift-foundation#NNNNN](https://github.com/swiftlang/swift-foundation/pull/1836) +* Review: ([pitch](https://forums.swift.org/t/pitch-uuid-v7-other-improvements/85427)) + +## Introduction + +Foundation's `UUID` type currently generates only version 4 (random) UUIDs. [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562) defines several UUID versions, each suited to different use cases. This proposal adds support for creating UUIDs of version 7 (time-ordered) which has become widely adopted for database keys and distributed systems due to its monotonically increasing, sortable nature. + +In addition, `UUID` is in need of a few more additions for modern usage, including support for lowercase strings, access to the bytes using `Span`, and accessors for the commonly used `min` and `max` sentinel values. + +## Motivation + +UUID version 4 (random) is a good general-purpose identifier, but its randomness makes it poorly suited as a database primary key — inserts into B-tree indexes are scattered across the keyspace, leading to poor cache locality and increased write amplification. UUID version 7 addresses this by encoding a Unix timestamp in the most significant 48 bits, producing UUIDs that are monotonically increasing over time while retaining sufficient randomness for uniqueness. + +Today, developers who need time-ordered UUIDs usually construct the bytes manually using `UUID(uuid:)`, which is error-prone and requires understanding the RFC 9562 bit layout, or depend on another library. Foundation should provide a straightforward way to create version 7 UUIDs, and a general mechanism for version introspection that accommodates other UUID versions, even if we do not generate them in `UUID` itself. + +## Proposed solution + +Add a `version` property on `UUID` for introspection, a static factory method for creating version 7 UUIDs, and convenience properties for the nil (which we name `min` to avoid confusion in Swift) and max UUIDs. + +```swift +// Create a version 7 UUID +let id = UUID.version7() + +// Inspect the version of any UUID +switch id.version { +case 7: + print("v7 UUID, sortable by creation time") +case 4: + print("v4 UUID") +default: + print("other version") +} + +// The existing init() continues to create version 4 UUIDs +let randomID = UUID() +assert(randomID.version == 4) + +// Min and max UUIDs for sentinel values +let minID = UUID.min // 00000000-0000-0000-0000-000000000000 +let maxID = UUID.max // FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF + +// Access the raw bytes without copying +let uuid = UUID() +let span: Span = uuid.span // 16-element typed span +``` + +## Detailed design + +### Min and Max UUIDs + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// The minimum UUID, where all 128 bits are set to zero, as defined by + /// RFC 9562 Section 5.9. Can be used to represent the absence of a + /// UUID value. + public static let min: UUID + + /// The max UUID, where all 128 bits are set to one, as defined by + /// RFC 9562 Section 5.10. Can be used as a sentinel value, for example + /// to represent "the largest possible UUID" in a sorted range. + public static let max: UUID +} +``` + +The min UUID (`00000000-0000-0000-0000-000000000000`) and max UUID (`FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF`) are special forms defined by RFC 9562. They are useful as sentinel values — for example, representing "no UUID" or defining the bounds of a UUID range. Note that neither the min UUID nor the max UUID has a meaningful version or variant field; the `version` property returns `0` and `15` respectively. + +### Lowercase string representation + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// Returns a lowercase string created from the UUID, such as + /// "e621e1f8-c36c-495a-93fc-0c247a3e6e5f". + public var lowercasedUUIDString: String { get } +} +``` + +The existing `uuidString` property returns an uppercase representation. Many systems — including web APIs, databases, and URN formatting (RFC 4122 §3) — conventionally use lowercase UUIDs. `lowercasedUUIDString` avoids the need to call `uuidString.lowercased()`, which allocates an intermediate `String`. + +### `span` and `mutableSpan` properties + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// A `Span` view of the UUID's 16 bytes. + public var span: Span { get } + + /// A `MutableSpan` view of the UUID's 16 bytes. + public var mutableSpan: MutableSpan { mutating get } +} +``` + +These properties provide bounds-checked access to the UUID's bytes without the need for `withUnsafeBytes` or tuple element access. `span` provides read-only access; `mutableSpan` allows direct modification of the underlying bytes. Both are lifetime-dependent on the UUID value. + +### Initializing from a `Span` + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// Creates a UUID by copying exactly 16 bytes from a `Span`. + public init(copying span: Span) +} +``` + +This initializer copies the bytes from a `Span` into a new UUID. The span must contain exactly 16 bytes; otherwise, the initializer traps. + +### Initializing from an `OutputSpan` + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// Creates a UUID by filling its 16 bytes using a closure that + /// writes into an `OutputSpan`. + /// + /// The closure must write exactly 16 bytes into the output span. + public init( + initializingWith initializer: (inout OutputSpan) throws(E) -> () + ) throws(E) +} +``` + +This initializer provides a safe, typed-throw-compatible way to construct a UUID from bytes without going through `uuid_t`: + +```swift +let uuid = UUID { output in + // Note: It is up to the custom implementation here to create a valid UUID. + output.append(...) + output.append(...) +} +``` + +The closure receives an `OutputSpan` backed by the UUID's 16-byte storage. If the closure writes fewer or more than 16 bytes, the initializer traps. If the closure throws, the error is propagated with its original type. + +### `version` and `variant` properties + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// The variant of a UUID, as defined by RFC 9562 Section 4.1. + public enum Variant: Sendable, Hashable { + /// NCS backward compatibility (variant bits `0xx`). + case ncs + + /// The variant specified by RFC 9562 (variant bits `10x`). + case rfc9562 + + /// Microsoft backward compatibility (variant bits `110`). + case microsoft + + /// Reserved for future use (variant bits `111`). + case reserved + } + + /// The version of this UUID, derived from the version bits (bits 48–51) as defined by RFC 9562. + public var version: Int { get set } + + /// The variant of this UUID, derived from the variant bits (bits 64–65) as defined by RFC 9562. + public var variant: Variant { get } +} +``` + +The version value is encoded in bits 48–51 of the UUID (the high nibble of byte 6), per RFC 9562. The returned `Int` ranges from 0 to 15. Well-known versions include 1 (time-based), 3 (name-based MD5), 4 (random), 5 (name-based SHA-1), 6 (reordered time-based), 7 (time-ordered), and 8 (custom). The setter replaces only the version nibble, preserving all other bits. + +The variant value is encoded in the high bits of byte 8 (bits 64–65). UUIDs created by Foundation use the RFC 9562 variant (`.rfc9562`, binary `10`). The `Variant` enum covers all four variant values defined by RFC 9562. + +### Creating version 4 and version 7 UUIDs + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// Creates a new UUID with RFC 9562 version 4 (random) layout. This is equivalent to calling `UUID()`. + public static func version4() -> UUID + + /// Creates a new UUID with RFC 9562 version 7 layout: a Unix timestamp in milliseconds in the most significant 48 bits, followed by random bits. The variant and version fields are set per the RFC. + /// + /// Version 7 UUIDs sort in chronological order when compared using the standard `<` operator, making them well-suited as database primary keys. UUIDs generated within the same process are guaranteed to be monotonically increasing. + /// + /// - Parameter date: The date to encode in the timestamp field. If `nil`, the current time is used. When provided, the monotonicity guarantee does not apply. + /// - Parameter offset: A duration to add to the timestamp before encoding. Defaults to zero. If `date` is provided, it will be added to the value of that argument. + public static func version7(at date: Date? = nil, offset: Duration = .zero) -> UUID + + /// Creates a new UUID with RFC 9562 version 7 layout using the specified random number generator for the random bits. + /// + /// When called without an `at` argument, the timestamp portion is guaranteed to be monotonically increasing within the current process. + /// + /// - Parameter generator: The random number generator to use when creating the random portions of the UUID. + /// - Parameter date: The date to encode in the timestamp field. If `nil`, the current time is used. When provided, the monotonicity guarantee does not apply. + /// - Parameter offset: A duration to add to the timestamp before encoding. Defaults to zero. If `date` is provided, it will be added to the value of that argument. + /// - Returns: A version 7 UUID. + public static func version7( + using generator: inout some RandomNumberGenerator, + at date: Date? = nil, + offset: Duration = .zero + ) -> UUID +} +``` + +The most significant 48 bits contain a millisecond-precision Unix timestamp. The 12 bits following the version field (`rand_a`) encode sub-millisecond timestamp precision per RFC 9562 Section 6.2, Method 3. The remaining 62 bits (`rand_b`, after the variant field) are filled using `generator`. The `version7()` convenience delegates to `version7(using:)` with a `SystemRandomNumberGenerator`. + +When called without a `Date` argument, the combined timestamp (milliseconds + sub-millisecond precision) is guaranteed to be monotonically increasing within the current process. An atomic value tracks the last returned timestamp; if the system clock has not advanced since the previous call, the value is incremented by one sub-millisecond tick. This ensures strict ordering even under high-frequency generation or clock adjustments, following the same approach used by Go's `google/uuid` and PostgreSQL. When a caller provides an explicit `date`, the monotonicity guarantee does not apply. + +### Extracting the date + +```swift +@available(FoundationPreview 6.4, *) +extension UUID { + /// For version 7 UUIDs, returns the `Date` encoded in the most significant 48 bits. Returns `nil` for all other versions. + /// + /// The returned date has millisecond precision, as specified by RFC 9562. + /// + /// - Note: Even though this implementation, or others, may choose to encode more precision into other bytes of the `UUID`, this method may only return the portion of the timestamp stored in the RFC-specified bytes. + public var date: Date? { + get + } +} +``` + +## Source compatibility + +This proposal is purely additive. The existing `UUID()` initializer continues to create version 4 random UUIDs. The `random(using:)` static method is unaffected. No existing behavior changes. + +UUIDs created by `version7()` are fully valid UUIDs and interoperate with all existing APIs that accept `UUID` or `NSUUID`, including `Codable`, `Comparable`, bridging, and string serialization. + +## Implications on adoption + +This feature can be freely adopted and un-adopted in source code with no deployment constraints and without affecting source compatibility. + +## Future directions + +- **Version 5 (name-based SHA-1)**: A factory method like `UUID.nameBased(name:namespace:)` could be added in a future proposal for deterministic UUID generation. +- **Version 8 (custom)**: Could be exposed via an initializer that accepts the custom data bits while setting the version and variant fields automatically. For now, we do provide an initializer that allows for setting all of the bytes directly via `OutputSpan`. + +## Alternatives considered + +### Adding version as a parameter to `init()` + +Instead of `UUID.version7()`, we considered `UUID(version: 7)`. However, different versions require different parameters — version 5 needs a name and namespace, version 8 needs custom data — so a single initializer would either need to accept many optional parameters or use an associated-value enum. Static factory methods are clearer and allow each version to have its own natural parameter list. + +We also considered using an enumeration for each version with associated types for the different parameters. In practice, this doesn't look or act much differently than simply adding functions to `UUID` with the required arguments. + +### Supporting all UUID versions immediately + +We considered adding factory methods for all versions (1, 3, 5, 6, 7, 8), but the immediate need is version 7. Version 1 (time-based with MAC address) has privacy implications. Versions 3 and 5 require different parameters. Version 6 is a reordering of version 1 and shares its concerns. Version 8 is intentionally application-defined. Starting with version 7 keeps the proposal focused. + +### Different types for different versions + +We considered adding different types for each version of a UUID. Community feedback suggests that it is rare to need to restrict version at a _type_ level. If this functionality is needed, the `version` property can be checked dynamically at runtime. + +### Accepting a `Clock` parameter instead of `Date` + +We considered accepting a `Clock` argument to allow callers to inject a custom time source. However, RFC 9562 requires the timestamp to represent [Unix time](https://en.wikipedia.org/wiki/Unix_time) — specifically, the number of milliseconds since the Unix epoch (1 January 1970 UTC). This corresponds to what Swift would call a `UTCClock` (see the [UTCClock pitch](https://forums.swift.org/t/pitch-utcclock/78018)), not an arbitrary clock. A `SuspendingClock` or `ContinuousClock` measures elapsed time since boot, which would produce an incorrect UUID timestamp. Any clock that *does* produce correct results would necessarily be equivalent to `UTCClock`, making the generality unnecessary. Instead, we accept an optional `Date` for callers who need to embed a specific point in time. This matches the convention used across Foundation for representations of time since the Unix epoch. + +### Static function names + +Many contributors suggested the use of shorter names like `v7`. While this is unlikely to be confusing to readers, the short name feels overly informal. The Swift API guidelines also suggest avoiding abbreviations. + +We considered prefixing the function name with `make`, as the Swift naming guidelines suggest for some factory methods. However, this pattern is actually rare for similar Foundation API. Similar unprefixed API include `Date.now`, `RecurrenceRule` constructors like `.hourly(...)`, `.monthly(...)`, `CocoaError.error(...)`, `URL.temporaryDirectory`, `URL.homeDirectory(forUser: ...)` and more. + +### Deprecating `UUID()` + +We considered deprecating the no-argument initializer for `UUID`. We believe that this could be counter-productive in the long term, because it can create "deprecation fatigue." This may encourage callers to ignore warnings because they feel somewhat arbitrary, especially when existing code is correct and will continue to work in the future. + +For similar reasons, we cannot change the behavior of the current methods to change the case of the string or version of the result. For example, we expect there to be existing code that would break if we change the result of the `uuidString` to be lowercased. diff --git a/Sources/FoundationEssentials/UUID.swift b/Sources/FoundationEssentials/UUID.swift index 568e12b889..20d03289b5 100644 --- a/Sources/FoundationEssentials/UUID.swift +++ b/Sources/FoundationEssentials/UUID.swift @@ -9,7 +9,21 @@ // //===----------------------------------------------------------------------===// -internal import _FoundationCShims // uuid.h +#if canImport(Darwin) +import Darwin +#elseif canImport(Bionic) +@preconcurrency import Bionic +#elseif canImport(Glibc) +@preconcurrency import Glibc +#elseif canImport(Musl) +@preconcurrency import Musl +#elseif canImport(WinSDK) +import WinSDK +#elseif os(WASI) +@preconcurrency import WASILibc +#endif + +internal import Synchronization public typealias uuid_t = (UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8, UInt8) public typealias uuid_string_t = (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8) @@ -17,62 +31,135 @@ public typealias uuid_string_t = (Int8, Int8, Int8, Int8, Int8, Int8, Int8, Int8 /// Represents UUID strings, which can be used to uniquely identify types, interfaces, and other items. @available(macOS 10.8, iOS 6.0, tvOS 9.0, watchOS 2.0, *) public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable { - public private(set) var uuid: uuid_t = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) + internal var _storage = InlineArray<16, UInt8>(repeating: 0) - /* Create a new UUID with RFC 4122 version 4 random bytes */ - public init() { - withUnsafeMutablePointer(to: &uuid) { - $0.withMemoryRebound(to: UInt8.self, capacity: MemoryLayout.size) { - _foundation_uuid_generate_random($0) - } + /// The UUID bytes as a `uuid_t` tuple. + public var uuid: uuid_t { + get { + return unsafeBitCast(_storage, to: uuid_t.self) + } + set { + _storage = unsafeBitCast(newValue, to: InlineArray<16, UInt8>.self) } } - @inline(__always) - internal func withUUIDBytes(_ work: (UnsafeBufferPointer) throws -> R) rethrows -> R { - return try withExtendedLifetime(self) { - try withUnsafeBytes(of: uuid) { rawBuffer in - return try rawBuffer.withMemoryRebound(to: UInt8.self) { buffer in - return try work(buffer) - } - } - } + /// Create a new UUID with RFC 4122 version 4 random bytes. + public init() { + var generator = SystemRandomNumberGenerator() + self = UUID.random(using: &generator) } /// Create a UUID from a string such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F". /// /// Returns nil for invalid strings. public init?(uuidString string: __shared String) { - let res = withUnsafeMutablePointer(to: &uuid) { - $0.withMemoryRebound(to: UInt8.self, capacity: 16) { - return _foundation_uuid_parse(string, $0) - } - } - if res != 0 { + let utf8 = string.utf8Span + guard utf8.count == 36 else { return nil } + + var charIdx = 0 + var byteIdx = 0 + while charIdx < 36 { + switch charIdx { + case 8, 13, 18, 23: + guard utf8.span[charIdx] == UInt8(ascii: "-") else { + return nil + } + charIdx += 1 + default: + // from CodableUtilities.swift + guard let b1 = utf8.span[charIdx].hexDigitValue else { + return nil + } + guard let b2 = utf8.span[charIdx + 1].hexDigitValue else { + return nil + } + _storage[byteIdx] = b1 << 4 | b2 + byteIdx += 1 + charIdx += 2 + } + } } /// Create a UUID from a `uuid_t`. public init(uuid: uuid_t) { - self.uuid = uuid + self._storage = unsafeBitCast(uuid, to: InlineArray<16, UInt8>.self) } - /// Returns a string created from the UUID, such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" - public var uuidString: String { - var bytes: uuid_string_t = (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0) - return withUUIDBytes { valBuffer in - withUnsafeMutablePointer(to: &bytes) { strPtr in - strPtr.withMemoryRebound(to: CChar.self, capacity: MemoryLayout.size) { str in - _foundation_uuid_unparse_upper(valBuffer.baseAddress!, str) - return String(cString: str) + /// Creates a UUID by copying exactly 16 bytes from a `Span`. + /// + /// - Precondition: `span.count` must be exactly 16. + @available(FoundationPreview 6.4, *) + public init(copying span: Span) { + precondition(span.count == 16, "UUID requires exactly 16 bytes, but \(span.count) were provided") + self.init() + for i in 0..<16 { + _storage[i] = span[i] + } + } + + /// Creates a UUID by filling its 16 bytes using a closure that writes into an `OutputSpan`. + /// + /// The closure must write exactly 16 bytes into the output span. + @available(FoundationPreview 6.4, *) + public init( + initializingWith initializer: (inout OutputSpan) throws(E) -> () + ) throws(E) { + _storage = try InlineArray<16, UInt8>(initializingWith: { outputSpan throws(E) -> Void in + try initializer(&outputSpan) + let count = outputSpan.count + precondition(count == 16, "UUID requires exactly 16 bytes, but \(count) were provided") + }) + } + + // Hex lookup tables for UUID string formatting. Each byte is converted to two hex characters via table lookup. + private static let _upperHex: StaticString = "0123456789ABCDEF" + private static let _lowerHex: StaticString = "0123456789abcdef" + + /// Writes the UUID as a 36-character hex string into `buffer` using the given hex digit lookup table. Returns 36. + private func _unparse( + into buffer: UnsafeMutableBufferPointer, + hexTable: StaticString + ) -> Int { + hexTable.withUTF8Buffer { hex in + var o = 0 + for i in 0..<16 { + // Insert '-' after bytes 4, 6, 8, 10 + switch i { + case 4, 6, 8, 10: + buffer[o] = UInt8(ascii: "-") + o &+= 1 + default: + break } + let byte = _storage[i] + buffer[o] = hex[Int(byte &>> 4)] + buffer[o &+ 1] = hex[Int(byte & 0xF)] + o &+= 2 } + assert(o == 36) + } + return 36 + } + + /// Returns a string created from the UUID, such as "E621E1F8-C36C-495A-93FC-0C247A3E6E5F" + public var uuidString: String { + String(unsafeUninitializedCapacity: 36) { buffer in + _unparse(into: buffer, hexTable: UUID._upperHex) + } + } + + /// Returns a lowercase string created from the UUID, such as "e621e1f8-c36c-495a-93fc-0c247a3e6e5f" + @available(FoundationPreview 6.4, *) + public var lowercasedUUIDString: String { + String(unsafeUninitializedCapacity: 36) { buffer in + _unparse(into: buffer, hexTable: UUID._lowerHex) } } public func hash(into hasher: inout Hasher) { - withUnsafeBytes(of: uuid) { buffer in + withUnsafeBytes(of: _storage) { buffer in hasher.combine(bytes: buffer) } } @@ -130,8 +217,9 @@ public struct UUID : Hashable, Equatable, CustomStringConvertible, Sendable { } public static func ==(lhs: UUID, rhs: UUID) -> Bool { - withUnsafeBytes(of: lhs) { lhsPtr in - withUnsafeBytes(of: rhs) { rhsPtr in + // Implementation note: This operation is designed to avoid short-circuited early exits, so that comparison of any two UUID values is done in the same amount of time. + withUnsafeBytes(of: lhs._storage) { lhsPtr in + withUnsafeBytes(of: rhs._storage) { rhsPtr in let lhsTuple = lhsPtr.loadUnaligned(as: (UInt64, UInt64).self) let rhsTuple = rhsPtr.loadUnaligned(as: (UInt64, UInt64).self) return (lhsTuple.0 ^ rhsTuple.0) | (lhsTuple.1 ^ rhsTuple.1) == 0 @@ -169,17 +257,271 @@ extension UUID : Codable { } } +// MARK: - Nil and Max UUIDs + +@available(FoundationPreview 6.4, *) +extension UUID { + /// The `nil` (or minimum) UUID, where all bits are set to zero. + /// + /// As defined by [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#section-5.9), the nil UUID is a special form where all 128 bits are zero. It can be used to represent the absence of a UUID value. + public static let min = UUID(uuid: (0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0)) + + /// The max UUID, where all bits are set to one. + /// + /// As defined by [RFC 9562](https://www.rfc-editor.org/rfc/rfc9562#section-5.10), the max UUID is a special form where all 128 bits are one. It can be used as a sentinel value, for example to represent "the largest possible UUID" in a sorted range. + public static let max = UUID(uuid: (0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF)) +} + +// MARK: - Span + +@available(FoundationPreview 6.4, *) +extension UUID { + /// A `Span` view of the UUID's 16 bytes. + public var span: Span { + @_lifetime(borrow self) + borrowing get { + _storage.span + } + } + + /// A `MutableSpan` view of the UUID's 16 bytes. + public var mutableSpan: MutableSpan { + @_lifetime(&self) + mutating get { + _storage.mutableSpan + } + } +} + +// MARK: - UUID Version + +@available(FoundationPreview 6.4, *) +extension UUID { + /// The version of this UUID, derived from the version bits (bits 48–51) as defined by RFC 9562. + public var version: Int { + get { + Int(_storage[6] >> 4) + } + set { + _storage[6] = (_storage[6] & 0x0F) | (UInt8(newValue & 0x0F) << 4) + } + } + + /// The variant of a UUID, as defined by RFC 9562 Section 4.1. + public enum Variant: Sendable, Hashable { + /// NCS backward compatibility (variant bits `0xx`). + case ncs + + /// The variant specified by RFC 9562 (variant bits `10x`). + case rfc9562 + + /// Microsoft backward compatibility (variant bits `110`). + case microsoft + + /// Reserved for future use (variant bits `111`). + case reserved + } + + /// The variant of this UUID, derived from the variant bits (bits 64–65) as defined by RFC 9562. + public var variant: Variant { + let byte = _storage[8] + if byte & 0x80 == 0 { + return .ncs + } else if byte & 0xC0 == 0x80 { + return .rfc9562 + } else if byte & 0xE0 == 0xC0 { + return .microsoft + } else { + return .reserved + } + } + + /// Creates a new UUID with RFC 9562 version 4 (random) layout. This is equivalent to calling `UUID()`. + @export(implementation) + public static func version4() -> UUID { + UUID() + } + + /// Creates a new UUID with RFC 9562 version 7 layout: a Unix timestamp in milliseconds in the most significant 48 bits, followed by random bits. The variant and version fields are set per the RFC. + /// + /// Version 7 UUIDs sort in chronological order when compared using the standard `<` operator, making them well-suited as database primary keys. UUIDs generated within the same process are guaranteed to be monotonically increasing. + /// + /// - Parameter date: The date to encode in the timestamp field. If `nil`, the current time is used. When provided, the monotonicity guarantee does not apply. + /// - Parameter offset: A duration to add to the timestamp before encoding. Defaults to zero. If `date` is provided, it will be added to the value of that argument. + public static func version7(at date: Date? = nil, offset: Duration = .zero) -> UUID { + var generator = SystemRandomNumberGenerator() + return version7(using: &generator, at: date, offset: offset) + } + + /// Creates a new UUID with RFC 9562 version 7 layout using the specified random number generator for the random bits. + /// + /// When called without an `at` argument, the timestamp portion is guaranteed to be monotonically increasing within the current process. + /// + /// - Parameter generator: The random number generator to use when creating the random portions of the UUID. + /// - Parameter date: The date to encode in the timestamp field. If `nil`, the current time is used. When provided, the monotonicity guarantee does not apply. + /// - Parameter offset: A duration to add to the timestamp before encoding. Defaults to zero. If `date` is provided, it will be added to the value of that argument. + /// - Returns: A version 7 UUID. + public static func version7( + using generator: inout some RandomNumberGenerator, + at date: Date? = nil, + offset: Duration = .zero + ) -> UUID { + // The most significant 48 bits contain a millisecond-precision Unix timestamp. + // The 12 bits following the version field (`rand_a`) encode sub-millisecond timestamp precision per RFC 9562 Section 6.2, Method 3. + // The remaining 62 bits (`rand_b`, after the variant field) are filled using `generator`. + let combined: UInt64 + if let date { + // Caller-provided date (plus offset): convert to Duration, + // no monotonic guard + let elapsed = Duration.seconds(date.timeIntervalSince1970) + offset + let (ms, subMS) = elapsed._uuidTimestampComponents + combined = ms << 12 | UInt64(subMS) + } else { + // Current time (plus offset) with monotonic guarantee + combined = _nextMonotonicTimestamp(offset: offset) + } + + let ms = combined >> 12 + let subMS = UInt16(combined & 0x0FFF) + + var first: UInt64 = 0 + // Bits 0–47: millisecond timestamp + first |= ms << 16 + // Bits 48–51: version 7 (0111) + first |= 0x7000 + // Bits 52–63: sub-millisecond precision (12 bits) + first |= UInt64(subMS) + + // Bits 64–127: variant + random + var second = UInt64.random(in: .min ... .max, using: &generator) + // Set the variant to '10' in bits 64–65 + second &= 0x3FFF_FFFF_FFFF_FFFF + second |= 0x8000_0000_0000_0000 + + return UUID { span in + // TODO: when OutputSpan has OutputRawSpan, we can append two UInt64 directly instead of breaking it down into bytes. + span.append(UInt8(truncatingIfNeeded: first >> 56)) + span.append(UInt8(truncatingIfNeeded: first >> 48)) + span.append(UInt8(truncatingIfNeeded: first >> 40)) + span.append(UInt8(truncatingIfNeeded: first >> 32)) + span.append(UInt8(truncatingIfNeeded: first >> 24)) + span.append(UInt8(truncatingIfNeeded: first >> 16)) + span.append(UInt8(truncatingIfNeeded: first >> 8)) + span.append(UInt8(truncatingIfNeeded: first)) + span.append(UInt8(truncatingIfNeeded: second >> 56)) + span.append(UInt8(truncatingIfNeeded: second >> 48)) + span.append(UInt8(truncatingIfNeeded: second >> 40)) + span.append(UInt8(truncatingIfNeeded: second >> 32)) + span.append(UInt8(truncatingIfNeeded: second >> 24)) + span.append(UInt8(truncatingIfNeeded: second >> 16)) + span.append(UInt8(truncatingIfNeeded: second >> 8)) + span.append(UInt8(truncatingIfNeeded: second)) + } + } + + /// For version 7 UUIDs, returns the `Date` encoded in the most significant 48 bits. Returns `nil` for all other versions. + /// + /// The returned date has millisecond precision, as specified by RFC 9562. + /// + /// - Note: Even though this implementation, or others, may choose to encode more precision into other bytes of the `UUID`, this method may only return the portion of the timestamp stored in the RFC-specified bytes. + public var date: Date? { + guard version == 7 else { return nil } + let ms: UInt64 = UInt64(_storage[0]) << 40 | UInt64(_storage[1]) << 32 + | UInt64(_storage[2]) << 24 | UInt64(_storage[3]) << 16 + | UInt64(_storage[4]) << 8 | UInt64(_storage[5]) + return Date(timeIntervalSince1970: Double(ms) / 1000.0) + } + + // MARK: - Monotonic timestamp + + /// Tracks the last combined timestamp value to ensure monotonically increasing v7 UUIDs within a process. + private static let _lastTimestamp = Atomic(0) + + /// Returns a combined 60-bit timestamp, which is guaranteed to be strictly greater than any previously returned value. If the clock hasn't advanced since the last call, the previous value is incremented by 1. + private static func _nextMonotonicTimestamp(offset: Duration) -> UInt64 { + let elapsed = Duration.durationSince1970 + offset + let (ms, subMS) = elapsed._uuidTimestampComponents + + let current = ms << 12 | UInt64(subMS) + var old = _lastTimestamp.load(ordering: .relaxed) + + while true { + let next = Swift.max(current, old &+ 1) + let (exchanged, original) = _lastTimestamp.compareExchange( + expected: old, + desired: next, + ordering: .relaxed + ) + if exchanged { + return next + } + old = original + } + } +} + +extension Duration { + /// Attoseconds per millisecond (10^15). + private static let _attosPerMS: Int64 = 1_000_000_000_000_000 + + /// The current wall clock time as a `Duration` since the Unix epoch, using the highest precision time source available on the platform. + fileprivate static var durationSince1970: Duration { +#if canImport(WinSDK) + // FILETIME is 100-nanosecond intervals since January 1, 1601 (UTC). + // Subtract the 1601-to-1970 offset to get Unix epoch time. + var ft = FILETIME() + GetSystemTimePreciseAsFileTime(&ft) + var li = ULARGE_INTEGER() + li.LowPart = ft.dwLowDateTime + li.HighPart = ft.dwHighDateTime + // 100-ns ticks from 1601 to 1970 + let epochOffset: UInt64 = 116_444_736_000_000_000 + let ticks = li.QuadPart - epochOffset + let seconds = Int64(ticks / 10_000_000) + // Each tick is 100ns = 100_000_000_000 attoseconds + let remainingTicks = Int64(ticks % 10_000_000) + return Duration.seconds(seconds) + Duration(secondsComponent: 0, attosecondsComponent: remainingTicks * 100_000_000_000) +#else + var ts = timespec() + clock_gettime(CLOCK_REALTIME, &ts) + return Duration.seconds(ts.tv_sec) + Duration.nanoseconds(ts.tv_nsec) +#endif + } + + /// Extracts the 48-bit millisecond timestamp and 12-bit sub-millisecond fraction from this duration (interpreted as time since Unix epoch), using pure integer arithmetic. + /// + /// Returns `(ms, subMS)` where `ms` is clamped to 48 bits and `subMS` is 0–4095. + fileprivate var _uuidTimestampComponents: (ms: UInt64, subMS: UInt16) { + let (secs, attos) = self.components + + // Total milliseconds = seconds * 1000 + attoseconds / attosPerMS + let totalMS = Int64(secs) * 1000 + attos / Self._attosPerMS + + // Clamp to the 48-bit unsigned range (0 ... 0xFFFF_FFFF_FFFF) + let ms = UInt64(clamping: Swift.max(0, totalMS)) + & 0xFFFF_FFFF_FFFF + + // Sub-millisecond fraction: remaining attoseconds after + // removing whole milliseconds, scaled to 12 bits. + let remainingAttos = attos % Self._attosPerMS + let subMS = UInt16((remainingAttos * 4096) / Self._attosPerMS) + + return (ms, subMS) + } +} + @available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) extension UUID : Comparable { @available(macOS 14, iOS 17, tvOS 17, watchOS 10, *) public static func < (lhs: UUID, rhs: UUID) -> Bool { - var leftUUID = lhs.uuid - var rightUUID = rhs.uuid + // Implementation note: This operation is designed to avoid short-circuited early exits, so that comparison of any two UUID values is done in the same amount of time. + var leftStorage = lhs._storage + var rightStorage = rhs._storage var result: Int = 0 var diff: Int = 0 - withUnsafeBytes(of: &leftUUID) { leftPtr in - withUnsafeBytes(of: &rightUUID) { rightPtr in - for offset in (0 ..< MemoryLayout.size).reversed() { + withUnsafeBytes(of: &leftStorage) { leftPtr in + withUnsafeBytes(of: &rightStorage) { rightPtr in + for offset in (0 ..< 16).reversed() { diff = Int(leftPtr.load(fromByteOffset: offset, as: UInt8.self)) - Int(rightPtr.load(fromByteOffset: offset, as: UInt8.self)) // Constant time, no branching equivalent of diff --git a/Sources/FoundationEssentials/UUID_Wrappers.swift b/Sources/FoundationEssentials/UUID_Wrappers.swift index 8dd2aa9d63..c7d5b6c44b 100644 --- a/Sources/FoundationEssentials/UUID_Wrappers.swift +++ b/Sources/FoundationEssentials/UUID_Wrappers.swift @@ -119,7 +119,7 @@ internal class __NSConcreteUUID : _NSUUIDBridge, @unchecked Sendable { } override func encode(with coder: NSCoder) { - _storage.withUUIDBytes { buffer in + _storage.span.withUnsafeBufferPointer { buffer in coder.encodeBytes(buffer.baseAddress, length: buffer.count, forKey: "NS.uuidbytes") } } @@ -148,7 +148,7 @@ internal class __NSConcreteUUID : _NSUUIDBridge, @unchecked Sendable { } override open func getBytes(_ bytes: UnsafeMutablePointer) { - _storage.withUUIDBytes { buffer in + _storage.span.withUnsafeBufferPointer { buffer in bytes.initialize(from: buffer.baseAddress!, count: buffer.count) } } diff --git a/Sources/_FoundationCShims/CMakeLists.txt b/Sources/_FoundationCShims/CMakeLists.txt index d8e5d06a73..366663dfc4 100644 --- a/Sources/_FoundationCShims/CMakeLists.txt +++ b/Sources/_FoundationCShims/CMakeLists.txt @@ -14,8 +14,7 @@ add_library(_FoundationCShims STATIC platform_shims.c - string_shims.c - uuid.c) + string_shims.c) target_include_directories(_FoundationCShims PUBLIC include) diff --git a/Sources/_FoundationCShims/include/_FoundationCShims.h b/Sources/_FoundationCShims/include/_FoundationCShims.h index 33acfe14e9..dd8f17affd 100644 --- a/Sources/_FoundationCShims/include/_FoundationCShims.h +++ b/Sources/_FoundationCShims/include/_FoundationCShims.h @@ -22,7 +22,6 @@ #include "io_shims.h" #include "platform_shims.h" #include "filemanager_shims.h" -#include "uuid.h" #if FOUNDATION_FRAMEWORK && !TARGET_OS_EXCLAVEKIT #include "sandbox_shims.h" diff --git a/Sources/_FoundationCShims/include/uuid.h b/Sources/_FoundationCShims/include/uuid.h deleted file mode 100644 index d2e1c1560c..0000000000 --- a/Sources/_FoundationCShims/include/uuid.h +++ /dev/null @@ -1,81 +0,0 @@ -/* - * Copyright (c) 2004 Apple Computer, Inc. All rights reserved. - * - * %Begin-Header% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, and the entire permission notice in its entirety, - * including the disclaimer of warranties. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ALL OF - * WHICH ARE HEREBY DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT - * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR - * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE - * USE OF THIS SOFTWARE, EVEN IF NOT ADVISED OF THE POSSIBILITY OF SUCH - * DAMAGE. - * %End-Header% - */ - -#ifndef _CSHIMS_UUID_UUID_H -#define _CSHIMS_UUID_UUID_H - -#include "_CShimsTargetConditionals.h" -#include "_CShimsMacros.h" - -#if TARGET_OS_MAC -#include -#else -#include -typedef unsigned char __darwin_uuid_t[16]; -typedef char __darwin_uuid_string_t[37]; -#ifdef uuid_t -#undef uuid_t -#endif -typedef __darwin_uuid_t uuid_t; -typedef __darwin_uuid_string_t uuid_string_t; - -#define UUID_DEFINE(name,u0,u1,u2,u3,u4,u5,u6,u7,u8,u9,u10,u11,u12,u13,u14,u15) \ - static const uuid_t name __attribute__ ((unused)) = {u0,u1,u2,u3,u4,u5,u6,u7,u8,u9,u10,u11,u12,u13,u14,u15} -#endif - -#ifdef __cplusplus -extern "C" { -#endif - -INTERNAL void _foundation_uuid_clear(uuid_t uu); - -INTERNAL int _foundation_uuid_compare(const uuid_t uu1, const uuid_t uu2); - -INTERNAL void _foundation_uuid_copy(uuid_t dst, const uuid_t src); - -INTERNAL void _foundation_uuid_generate(uuid_t out); -INTERNAL void _foundation_uuid_generate_random(uuid_t out); -INTERNAL void _foundation_uuid_generate_time(uuid_t out); - -INTERNAL int _foundation_uuid_is_null(const uuid_t uu); - -INTERNAL int _foundation_uuid_parse(const uuid_string_t in, uuid_t uu); - -INTERNAL void _foundation_uuid_unparse(const uuid_t uu, uuid_string_t out); -INTERNAL void _foundation_uuid_unparse_lower(const uuid_t uu, uuid_string_t out); -INTERNAL void _foundation_uuid_unparse_upper(const uuid_t uu, uuid_string_t out); - -#ifdef __cplusplus -} -#endif - -#endif /* _CSHIMS_UUID_UUID_H */ diff --git a/Sources/_FoundationCShims/uuid.c b/Sources/_FoundationCShims/uuid.c deleted file mode 100644 index 1b382cd1fe..0000000000 --- a/Sources/_FoundationCShims/uuid.c +++ /dev/null @@ -1,278 +0,0 @@ -/* - * Copyright (c) 2004 Apple Computer, Inc. All rights reserved. - * - * %Begin-Header% - * Redistribution and use in source and binary forms, with or without - * modification, are permitted provided that the following conditions - * are met: - * 1. Redistributions of source code must retain the above copyright - * notice, and the entire permission notice in its entirety, - * including the disclaimer of warranties. - * 2. Redistributions in binary form must reproduce the above copyright - * notice, this list of conditions and the following disclaimer in the - * documentation and/or other materials provided with the distribution. - * 3. The name of the author may not be used to endorse or promote - * products derived from this software without specific prior - * written permission. - * - * THIS SOFTWARE IS PROVIDED ``AS IS'' AND ANY EXPRESS OR IMPLIED - * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES - * OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE, ALL OF - * WHICH ARE HEREBY DISCLAIMED. IN NO EVENT SHALL THE AUTHOR BE - * LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR - * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT - * OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR - * BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF - * LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE - * USE OF THIS SOFTWARE, EVEN IF NOT ADVISED OF THE POSSIBILITY OF SUCH - * DAMAGE. - * %End-Header% - */ - -#include "include/uuid.h" - -#if __has_include() -#include -#endif - -#if TARGET_OS_MAC - -INTERNAL void _foundation_uuid_clear(uuid_t uu) { - uuid_clear(uu); -} - -INTERNAL int _foundation_uuid_compare(const uuid_t uu1, const uuid_t uu2) { - return uuid_compare(uu1, uu2); -} - -INTERNAL void _foundation_uuid_copy(uuid_t dst, const uuid_t src) { - uuid_copy(dst, src); -} - -INTERNAL void _foundation_uuid_generate(uuid_t out) { - uuid_generate(out); -} - -INTERNAL void _foundation_uuid_generate_random(uuid_t out) { - uuid_generate_random(out); -} - -INTERNAL void _foundation_uuid_generate_time(uuid_t out) { - uuid_generate_time(out); -} - -INTERNAL int _foundation_uuid_is_null(const uuid_t uu) { - return uuid_is_null(uu); -} - -INTERNAL int _foundation_uuid_parse(const uuid_string_t in, uuid_t uu) { - return uuid_parse(in, uu); -} - -INTERNAL void _foundation_uuid_unparse(const uuid_t uu, uuid_string_t out) { - uuid_unparse(uu, out); -} - -INTERNAL void _foundation_uuid_unparse_lower(const uuid_t uu, uuid_string_t out) { - uuid_unparse_lower(uu, out); -} - -INTERNAL void _foundation_uuid_unparse_upper(const uuid_t uu, uuid_string_t out) { - uuid_unparse_upper(uu, out); -} - -#else - -#include -#include -#include -#if defined(__unix__) || (defined(__APPLE__) && defined(__MACH__)) -#include -#elif defined(_WIN32) -#include -#define WIN32_LEAN_AND_MEAN -#include -#include -#endif -#include - -#if TARGET_OS_LINUX || TARGET_OS_BSD || TARGET_OS_WASI -#include - -static inline void nanotime(struct timespec *tv) { - clock_gettime(CLOCK_MONOTONIC, tv); -} - -#elif TARGET_OS_WINDOWS -#include - -static inline void nanotime(struct timespec *tv) { - FILETIME ftTime; - - GetSystemTimePreciseAsFileTime(&ftTime); - - uint64_t Value = (((uint64_t)ftTime.dwHighDateTime << 32) | ftTime.dwLowDateTime); - - tv->tv_sec = Value / 1000000000; - tv->tv_nsec = Value - (tv->tv_sec * 1000000000); -} -#endif - -#if TARGET_OS_WINDOWS -static inline void read_random(void *buffer, unsigned numBytes) { - BCryptGenRandom(NULL, buffer, numBytes, - BCRYPT_RNG_USE_ENTROPY_IN_BUFFER | BCRYPT_USE_SYSTEM_PREFERRED_RNG); -} -#elif TARGET_OS_WASI -#include - -static inline void read_random(void *buffer, unsigned numBytes) { - getentropy(buffer, numBytes); -} -#else -static inline void read_random(void *buffer, unsigned numBytes) { - int fd = open("/dev/urandom", O_RDONLY); - read(fd, buffer, numBytes); - close(fd); -} -#endif - -UUID_DEFINE(UUID_NULL, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0); - -static void read_node(uint8_t *node) { -#if NETWORKING - struct ifnet *ifp; - struct ifaddr *ifa; - struct sockaddr_dl *sdl; - - ifnet_head_lock_shared(); - TAILQ_FOREACH(ifp, &ifnet_head, if_link) { - TAILQ_FOREACH(ifa, &ifp->if_addrhead, ifa_link) { - sdl = (struct sockaddr_dl *)ifa->ifa_addr; - if (sdl && sdl->sdl_family == AF_LINK && sdl->sdl_type == IFT_ETHER) { - memcpy(node, LLADDR(sdl), 6); - ifnet_head_done(); - return; - } - } - } - ifnet_head_done(); -#endif /* NETWORKING */ - - read_random(node, 6); - node[0] |= 0x01; -} - -static uint64_t read_time(void) { - struct timespec tv; - - nanotime(&tv); - - return (tv.tv_sec * 10000000ULL) + (tv.tv_nsec / 100ULL) + 0x01B21DD213814000ULL; -} - -void _foundation_uuid_clear(uuid_t uu) { - memset(uu, 0, sizeof(uuid_t)); -} - -int _foundation_uuid_compare(const uuid_t uu1, const uuid_t uu2) { - return memcmp(uu1, uu2, sizeof(uuid_t)); -} - -void _foundation_uuid_copy(uuid_t dst, const uuid_t src) { - memcpy(dst, src, sizeof(uuid_t)); -} - -void _foundation_uuid_generate_random(uuid_t out) { - read_random(out, sizeof(uuid_t)); - - out[6] = (out[6] & 0x0F) | 0x40; - out[8] = (out[8] & 0x3F) | 0x80; -} - -void _foundation_uuid_generate_time(uuid_t out) { - uint64_t time; - - read_node(&out[10]); - read_random(&out[8], 2); - - time = read_time(); - out[0] = (uint8_t)(time >> 24); - out[1] = (uint8_t)(time >> 16); - out[2] = (uint8_t)(time >> 8); - out[3] = (uint8_t)time; - out[4] = (uint8_t)(time >> 40); - out[5] = (uint8_t)(time >> 32); - out[6] = (uint8_t)(time >> 56); - out[7] = (uint8_t)(time >> 48); - - out[6] = (out[6] & 0x0F) | 0x10; - out[8] = (out[8] & 0x3F) | 0x80; -} - -void _foundation_uuid_generate(uuid_t out) -{ - _foundation_uuid_generate_random(out); -} - -int _foundation_uuid_is_null(const uuid_t uu) -{ - return !memcmp(uu, UUID_NULL, sizeof(uuid_t)); -} - -int _foundation_uuid_parse(const uuid_string_t in, uuid_t uu) -{ - int n = 0; - - sscanf(in, - "%2hhx%2hhx%2hhx%2hhx-" - "%2hhx%2hhx-" - "%2hhx%2hhx-" - "%2hhx%2hhx-" - "%2hhx%2hhx%2hhx%2hhx%2hhx%2hhx%n", - &uu[0], &uu[1], &uu[2], &uu[3], - &uu[4], &uu[5], - &uu[6], &uu[7], - &uu[8], &uu[9], - &uu[10], &uu[11], &uu[12], &uu[13], &uu[14], &uu[15], &n); - - return (n != 36 || in[n] != '\0' ? -1 : 0); -} - -void _foundation_uuid_unparse_lower(const uuid_t uu, uuid_string_t out) { - snprintf(out, - sizeof(uuid_string_t), - "%02x%02x%02x%02x-" - "%02x%02x-" - "%02x%02x-" - "%02x%02x-" - "%02x%02x%02x%02x%02x%02x", - uu[0], uu[1], uu[2], uu[3], - uu[4], uu[5], - uu[6], uu[7], - uu[8], uu[9], - uu[10], uu[11], uu[12], uu[13], uu[14], uu[15]); -} - -void _foundation_uuid_unparse_upper(const uuid_t uu, uuid_string_t out) { - snprintf(out, - sizeof(uuid_string_t), - "%02X%02X%02X%02X-" - "%02X%02X-" - "%02X%02X-" - "%02X%02X-" - "%02X%02X%02X%02X%02X%02X", - uu[0], uu[1], uu[2], uu[3], - uu[4], uu[5], - uu[6], uu[7], - uu[8], uu[9], - uu[10], uu[11], uu[12], uu[13], uu[14], uu[15]); -} - -void _foundation_uuid_unparse(const uuid_t uu, uuid_string_t out) { - _foundation_uuid_unparse_upper(uu, out); -} - -#endif - diff --git a/Tests/FoundationEssentialsTests/UUIDTests.swift b/Tests/FoundationEssentialsTests/UUIDTests.swift index 784ab658d8..7e1e68fabf 100644 --- a/Tests/FoundationEssentialsTests/UUIDTests.swift +++ b/Tests/FoundationEssentialsTests/UUIDTests.swift @@ -47,6 +47,24 @@ private struct UUIDTests { #expect(uuid.uuidString == "E621E1F8-C36C-495A-93FC-0C247A3E6E5F", "The uuidString representation must be uppercase.") } + @available(FoundationPreview 6.4, *) + @Test func lowercasedUUIDString() { + let uuid = UUID(uuid: (0xe6,0x21,0xe1,0xf8,0xc3,0x6c,0x49,0x5a,0x93,0xfc,0x0c,0x24,0x7a,0x3e,0x6e,0x5f)) + #expect(uuid.lowercasedUUIDString == "e621e1f8-c36c-495a-93fc-0c247a3e6e5f") + } + + @available(FoundationPreview 6.4, *) + @Test func lowercasedUUIDStringMatchesUpperCaseContent() { + let uuid = UUID() + #expect(uuid.lowercasedUUIDString == uuid.uuidString.lowercased()) + } + + @available(FoundationPreview 6.4, *) + @Test func lowercasedUUIDStringNilAndMax() { + #expect(UUID.min.lowercasedUUIDString == "00000000-0000-0000-0000-000000000000") + #expect(UUID.max.lowercasedUUIDString == "ffffffff-ffff-ffff-ffff-ffffffffffff") + } + @Test func description() { let uuid = UUID() let description: String = uuid.description @@ -56,8 +74,7 @@ private struct UUIDTests { @Test func hash() { let values: [UUID] = [ - // This list takes a UUID and tweaks every byte while - // leaving the version/variant intact. + // This list takes a UUID and tweaks every byte while leaving the version/variant intact. UUID(uuidString: "a53baa1c-b4f5-48db-9467-9786b76b256c")!, UUID(uuidString: "a63baa1c-b4f5-48db-9467-9786b76b256c")!, UUID(uuidString: "a53caa1c-b4f5-48db-9467-9786b76b256c")!, @@ -93,7 +110,6 @@ private struct UUIDTests { #expect(anyHashables[1] == anyHashables[2]) } - // rdar://71190003 (UUID has no customMirror) @Test func customMirror() throws { let uuid = try #require(UUID(uuidString: "89E90DC6-5EBA-41A8-A64D-81D3576EE46E")) #expect(String(reflecting: uuid) == "89E90DC6-5EBA-41A8-A64D-81D3576EE46E") @@ -130,11 +146,355 @@ private struct UUIDTests { var generator = SystemRandomNumberGenerator() for _ in 0..<10000 { let uuid = UUID.random(using: &generator) - #expect(uuid.versionNumber == 0b0100) - #expect(uuid.varint == 0b10) + #expect(uuid.version == 0b0100) + #expect(uuid.variant == .rfc9562) + } + } + + @available(FoundationPreview 6.4, *) + @Test func minUUID() { + let minUUID = UUID.min + #expect(minUUID.uuidString == "00000000-0000-0000-0000-000000000000") + let s = minUUID.span + for i in 0..<16 { + #expect(s[i] == 0) + } + } + + @available(FoundationPreview 6.4, *) + @Test func maxUUID() { + let maxUUID = UUID.max + #expect(maxUUID.uuidString == "FFFFFFFF-FFFF-FFFF-FFFF-FFFFFFFFFFFF") + let s = maxUUID.span + for i in 0..<16 { + #expect(s[i] == 0xFF) + } + } + + @available(FoundationPreview 6.4, *) + @Test func minAndMaxOrdering() { + let minUUID = UUID.min + let maxUUID = UUID.max + let randomUUID = UUID() + #expect(minUUID < randomUUID) + #expect(randomUUID < maxUUID) + #expect(minUUID < maxUUID) + } + + @available(FoundationPreview 6.4, *) + @Test func spanProperty() { + let uuid = UUID(uuid: (0xe6, 0x21, 0xe1, 0xf8, 0xc3, 0x6c, 0x49, 0x5a, 0x93, 0xfc, 0x0c, 0x24, 0x7a, 0x3e, 0x6e, 0x5f)) + let s = uuid.span + #expect(s.count == 16) + #expect(s[0] == 0xe6) + #expect(s[1] == 0x21) + #expect(s[6] == 0x49) + #expect(s[15] == 0x5f) + } + + @available(FoundationPreview 6.4, *) + @Test func spanMatchesUUIDBytes() { + let uuid = UUID() + let s = uuid.span + let t = uuid.uuid + #expect(s[0] == t.0) + #expect(s[1] == t.1) + #expect(s[6] == t.6) + #expect(s[8] == t.8) + #expect(s[15] == t.15) + } + + @available(FoundationPreview 6.4, *) + @Test func mutableSpan() { + var uuid = UUID.min + var span = uuid.mutableSpan + span[0] = 0xAB + span[15] = 0xCD + #expect(uuid.span[0] == 0xAB) + #expect(uuid.span[15] == 0xCD) + // Other bytes remain zero + for i in 1..<15 { + #expect(uuid.span[i] == 0) + } + } + + @available(FoundationPreview 6.4, *) + @Test func mutableSpanModifiesUUID() { + var uuid = UUID(uuid: (0xe6, 0x21, 0xe1, 0xf8, 0xc3, 0x6c, 0x49, 0x5a, 0x93, 0xfc, 0x0c, 0x24, 0x7a, 0x3e, 0x6e, 0x5f)) + // Overwrite version nibble to v7 + let previousValue = uuid.span[6] + var span = uuid.mutableSpan + span[6] = (previousValue & 0x0F) | 0x70 + #expect(uuid.version == 7) + } + + @available(FoundationPreview 6.4, *) + @Test func initializingWithOutputSpan() { + let uuid = UUID { (output: inout OutputSpan) in + for i: UInt8 in 0..<16 { + output.append(i) + } + } + let s = uuid.span + for i: UInt8 in 0..<16 { + #expect(s[Int(i)] == i) + } + } + + @available(FoundationPreview 6.4, *) + @Test func initializingWithOutputSpanMatchesUUIDInit() { + let expected = UUID(uuid: (0xe6, 0x21, 0xe1, 0xf8, 0xc3, 0x6c, 0x49, 0x5a, 0x93, 0xfc, 0x0c, 0x24, 0x7a, 0x3e, 0x6e, 0x5f)) + let bytes: [UInt8] = [0xe6, 0x21, 0xe1, 0xf8, 0xc3, 0x6c, 0x49, 0x5a, 0x93, 0xfc, 0x0c, 0x24, 0x7a, 0x3e, 0x6e, 0x5f] + let uuid = UUID { (output: inout OutputSpan) in + for b in bytes { + output.append(b) + } + } + #expect(uuid == expected) + } + + @available(FoundationPreview 6.4, *) + @Test func initFromSpan() { + let bytes: [UInt8] = [0xe6, 0x21, 0xe1, 0xf8, 0xc3, 0x6c, 0x49, 0x5a, 0x93, 0xfc, 0x0c, 0x24, 0x7a, 0x3e, 0x6e, 0x5f] + let span = bytes.span + let uuid = UUID(copying: span) + let expected = UUID(uuid: (0xe6, 0x21, 0xe1, 0xf8, 0xc3, 0x6c, 0x49, 0x5a, 0x93, 0xfc, 0x0c, 0x24, 0x7a, 0x3e, 0x6e, 0x5f)) + #expect(uuid == expected) + } + + @available(FoundationPreview 6.4, *) + @Test func versionProperty() { + // UUID() creates v4 + let defaultVersion = UUID() + #expect(defaultVersion.version == 4) + + // RFC 9562 Appendix A test vectors + // A.1: UUIDv1 + let v1 = UUID(uuidString: "C232AB00-9414-11EC-B3C8-9F6BDECED846")! + #expect(v1.version == 1) + + // A.2: UUIDv3 + let v3 = UUID(uuidString: "5df41881-3aed-3515-88a7-2f4a814cf09e")! + #expect(v3.version == 3) + + // A.3: UUIDv4 + let v4 = UUID(uuidString: "919108f7-52d1-4320-9bac-f847db4148a8")! + #expect(v4.version == 4) + + // A.4: UUIDv5 + let v5 = UUID(uuidString: "2ed6657d-e927-568b-95e1-2665a8aea6a2")! + #expect(v5.version == 5) + + // A.5: UUIDv6 + let v6 = UUID(uuidString: "1EC9414C-232A-6B00-B3C8-9F6BDECED846")! + #expect(v6.version == 6) + + // A.6: UUIDv7 + let v7 = UUID(uuidString: "017F22E2-79B0-7CC3-98C4-DC0C0C07398F")! + #expect(v7.version == 7) + + // B.1: UUIDv8 + let v8 = UUID(uuidString: "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0")! + #expect(v8.version == 8) + } + + @available(FoundationPreview 6.4, *) + @Test func version7VersionAndVariant() { + for _ in 0..<10000 { + let uuid = UUID.version7() + #expect(uuid.version == 7) + #expect(uuid.variant == .rfc9562) + } + } + + @available(FoundationPreview 6.4, *) + @Test func version7UsingGeneratorVersionAndVariant() { + var generator = SystemRandomNumberGenerator() + for _ in 0..<10000 { + let uuid = UUID.version7(using: &generator) + #expect(uuid.version == 7) + #expect(uuid.variant == .rfc9562) } } + @available(FoundationPreview 6.4, *) + @Test func version7UsingGeneratorTimestamp() throws { + var generator = SystemRandomNumberGenerator() + let date = Date(timeIntervalSince1970: 1700000000.123) + let uuid = UUID.version7(using: &generator, at: date) + let timestamp = try #require(uuid.date) + // We will lose some precision from the original date in the encoded date. + #expect(timestamp.timeIntervalSince1970.rounded(.down) == date.timeIntervalSince1970.rounded(.down)) + } + + @available(FoundationPreview 6.4, *) + @Test func version7UsingDeterministicGenerator() { + let fixedDate = Date(timeIntervalSince1970: 1645557742.0) // RFC 9562 A.6 timestamp + var gen1 = PCGRandomNumberGenerator(seed: 42) + var gen2 = PCGRandomNumberGenerator(seed: 42) + let uuid1 = UUID.version7(using: &gen1, at: fixedDate) + let uuid2 = UUID.version7(using: &gen2, at: fixedDate) + // Same seed and same date produces identical UUIDs + #expect(uuid1 == uuid2) + // Verify the timestamp round-trips + #expect(uuid1.date == fixedDate) + } + + @available(FoundationPreview 6.4, *) + @Test func version7DifferentSeedsSameDate() { + let fixedDate = Date(timeIntervalSince1970: 1645557742.0) + var gen1 = PCGRandomNumberGenerator(seed: 42) + var gen2 = PCGRandomNumberGenerator(seed: 99) + let uuid1 = UUID.version7(using: &gen1, at: fixedDate) + let uuid2 = UUID.version7(using: &gen2, at: fixedDate) + // Same date but different seeds produces different UUIDs + #expect(uuid1 != uuid2) + // Both should still have the same timestamp + #expect(uuid1.date == uuid2.date) + } + + @available(FoundationPreview 6.4, *) + @Test func version7AtSpecificDate() throws { + let date = Date(timeIntervalSince1970: 1000.0) + var generator = SystemRandomNumberGenerator() + let uuid = UUID.version7(using: &generator, at: date) + let timestamp = try #require(uuid.date) + #expect(timestamp == date) + #expect(uuid.version == 7) + } + + @available(FoundationPreview 6.4, *) + @Test func version7SubMillisecondPrecision() { + // RFC 9562 Section 6.2 Method 3: rand_a encodes sub-ms precision. + // Use a whole-millisecond date (no sub-ms fraction) to verify rand_a == 0 + let date = Date(timeIntervalSince1970: 1000.123) + var generator = SystemRandomNumberGenerator() + let uuid = UUID.version7(using: &generator, at: date) + // rand_a is the lower nibble of byte 6 and all of byte 7 + let randA = (UInt16(uuid.span[6]) & 0x0F) << 8 | UInt16(uuid.span[7]) + #expect(randA == 0) + // Verify the millisecond timestamp is correct + #expect(uuid.date == Date(timeIntervalSince1970: 1000.123)) + } + + @available(FoundationPreview 6.4, *) + @Test func version7WithOffsetFromDate() throws { + let base = Date(timeIntervalSince1970: 1000.0) + let offset = Duration.seconds(60) + var generator = SystemRandomNumberGenerator() + let uuid = UUID.version7(using: &generator, at: base, offset: offset) + let timestamp = try #require(uuid.date) + // Should encode base + 60s = 1060.0 + #expect(timestamp == Date(timeIntervalSince1970: 1060.0)) + } + + @available(FoundationPreview 6.4, *) + @Test func version7WithNegativeOffset() throws { + let base = Date(timeIntervalSince1970: 2000.0) + let offset = Duration.seconds(-500) + var generator = SystemRandomNumberGenerator() + let uuid = UUID.version7(using: &generator, at: base, offset: offset) + let timestamp = try #require(uuid.date) + // Should encode base - 500s = 1500.0 + #expect(timestamp == Date(timeIntervalSince1970: 1500.0)) + } + + @available(FoundationPreview 6.4, *) + @Test func version7WithOffsetFromCurrentTime() { + // Offset of +1 hour from current time should produce a UUID with a timestamp roughly 1 hour in the future + let before = Date().addingTimeInterval(3600.0 - 1.0) + var generator = SystemRandomNumberGenerator() + let uuid = UUID.version7(using: &generator, offset: .seconds(3600)) + let timestamp = uuid.date! + let after = Date().addingTimeInterval(3600.0 + 1.0) + #expect(timestamp >= before) + #expect(timestamp <= after) + } + + @available(FoundationPreview 6.4, *) + @Test func version7Monotonicity() { + // Generate many UUIDs in a tight loop without any delays. + // The monotonic guarantee ensures each is strictly greater than the previous, even within the same sub-millisecond. + var previous = UUID.version7() + for _ in 0..<10_000 { + let current = UUID.version7() + #expect(previous < current) + previous = current + } + } + + @available(FoundationPreview 6.4, *) + @Test func version7Date() throws { + let date = Date(timeIntervalSince1970: 1700000000.456) + var generator = SystemRandomNumberGenerator() + let uuid = UUID.version7(using: &generator, at: date) + let timestamp = try #require(uuid.date) + #expect(timestamp == date) + } + + // RFC 9562 Appendix A.6: UUIDv7 test vector with known timestamp + // Tuesday, February 22, 2022 2:22:22.00 PM GMT-05:00 = 1645557742000 ms + @available(FoundationPreview 6.4, *) + @Test func version7DateRFCVector() throws { + let v7 = UUID(uuidString: "017F22E2-79B0-7CC3-98C4-DC0C0C07398F")! + let timestamp = try #require(v7.date) + let expected = Date(timeIntervalSince1970: 1645557742.0) + #expect(timestamp == expected) + } + + @available(FoundationPreview 6.4, *) + @Test func version7DateNilForV4() { + let uuid = UUID() + #expect(uuid.date == nil) + } + + @available(FoundationPreview 6.4, *) + @Test func versionForArbitraryBytes() { + for v: UInt8 in 0..<16 { + // Construct a UUID with the version nibble set to `v` + let byte6 = v << 4 + let uuid = UUID(uuid: (0, 0, 0, 0, 0, 0, byte6, 0, 0x80, 0, 0, 0, 0, 0, 0, 0)) + #expect(uuid.version == Int(v)) + } + } + + @available(FoundationPreview 6.4, *) + @Test func versionSetter() { + var uuid = UUID() + #expect(uuid.version == 4) + uuid.version = 7 + #expect(uuid.version == 7) + // The lower nibble of byte 6 should be preserved + let original = UUID() + var modified = original + modified.version = 7 + #expect(modified.span[6] & 0x0F == original.span[6] & 0x0F) + #expect(modified.version == 7) + } + + @available(FoundationPreview 6.4, *) + @Test func variantProperty() { + // v4 UUIDs have RFC 9562 variant + let uuid = UUID() + #expect(uuid.variant == .rfc9562) + + // v7 UUIDs have RFC 9562 variant + let v7 = UUID.version7() + #expect(v7.variant == .rfc9562) + + // Manually constructed UUID with NCS variant (top bit 0) + let ncs = UUID(uuid: (0, 0, 0, 0, 0, 0, 0x40, 0, 0x00, 0, 0, 0, 0, 0, 0, 0)) + #expect(ncs.variant == .ncs) + + // Microsoft variant (top 3 bits 110) + let ms = UUID(uuid: (0, 0, 0, 0, 0, 0, 0x40, 0, 0xC0, 0, 0, 0, 0, 0, 0, 0)) + #expect(ms.variant == .microsoft) + + // Reserved variant (top 3 bits 111) + let res = UUID(uuid: (0, 0, 0, 0, 0, 0, 0x40, 0, 0xE0, 0, 0, 0, 0, 0, 0, 0)) + #expect(res.variant == .reserved) + } + @available(FoundationPreview 6.3, *) @Test func deterministicRandomGeneration() { var generator = PCGRandomNumberGenerator(seed: 123456789) @@ -153,16 +513,7 @@ private struct UUIDTests { } } -extension UUID { - fileprivate var versionNumber: Int { - Int(self.uuid.6 >> 4) - } - - fileprivate var varint: Int { - Int(self.uuid.8 >> 6 & 0b11) - } -} - +/// A seedable random number generator for deterministic testing. The same seed always produces the same sequence, allowing tests to verify exact UUID output. 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