-
Notifications
You must be signed in to change notification settings - Fork 43
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
chore: migrate UUID from v4 to v7 (#145)
- Loading branch information
1 parent
dd74634
commit 080bacd
Showing
9 changed files
with
228 additions
and
9 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,86 @@ | ||
// | ||
// TimeBasedEpochGenerator.swift | ||
// PostHog | ||
// | ||
// Created by Manoel Aranda Neto on 17.06.24. | ||
// | ||
|
||
import Foundation | ||
|
||
class TimeBasedEpochGenerator { | ||
static let shared = TimeBasedEpochGenerator() | ||
|
||
// Private initializer to prevent multiple instances | ||
private init() {} | ||
|
||
private var lastEntropy = [UInt8](repeating: 0, count: 10) | ||
private var lastTimestamp: UInt64 = 0 | ||
|
||
private let lock = NSLock() | ||
|
||
func v7() -> UUID { | ||
var uuid: UUID? | ||
|
||
lock.withLock { | ||
uuid = generateUUID() | ||
} | ||
|
||
// or fallback to UUID v4 | ||
return uuid ?? UUID() | ||
} | ||
|
||
private func generateUUID() -> UUID? { | ||
let timestamp = Date().timeIntervalSince1970 | ||
let unixTimeMilliseconds = UInt64(timestamp * 1000) | ||
|
||
var uuidBytes = [UInt8]() | ||
|
||
let timeBytes = unixTimeMilliseconds.bigEndianData.suffix(6) // First 6 bytes for the timestamp | ||
uuidBytes.append(contentsOf: timeBytes) | ||
|
||
if unixTimeMilliseconds == lastTimestamp { | ||
var check = true | ||
for index in (0 ..< 10).reversed() where check { | ||
var temp = lastEntropy[index] | ||
temp = temp &+ 0x01 | ||
check = lastEntropy[index] == 0xFF | ||
lastEntropy[index] = temp | ||
} | ||
} else { | ||
lastTimestamp = unixTimeMilliseconds | ||
|
||
// Prepare the random part (10 bytes to complete the UUID) | ||
let status = SecRandomCopyBytes(kSecRandomDefault, lastEntropy.count, &lastEntropy) | ||
// If we can't generate secure random bytes, use a fallback | ||
if status != errSecSuccess { | ||
let randomData = (0 ..< 10).map { _ in UInt8.random(in: 0 ... 255) } | ||
lastEntropy = randomData | ||
} | ||
} | ||
uuidBytes.append(contentsOf: lastEntropy) | ||
|
||
// Set version (7) in the version byte | ||
uuidBytes[6] = (uuidBytes[6] & 0x0F) | 0x70 | ||
|
||
// Set the UUID variant (10xx for standard UUIDs) | ||
uuidBytes[8] = (uuidBytes[8] & 0x3F) | 0x80 | ||
|
||
// Ensure we have a total of 16 bytes | ||
if uuidBytes.count == 16 { | ||
return UUID(uuid: (uuidBytes[0], uuidBytes[1], uuidBytes[2], uuidBytes[3], | ||
uuidBytes[4], uuidBytes[5], uuidBytes[6], uuidBytes[7], | ||
uuidBytes[8], uuidBytes[9], uuidBytes[10], uuidBytes[11], | ||
uuidBytes[12], uuidBytes[13], uuidBytes[14], uuidBytes[15])) | ||
} | ||
|
||
return nil | ||
} | ||
} | ||
|
||
extension UInt64 { | ||
// Correctly generate Data representation in big endian format | ||
var bigEndianData: Data { | ||
var bigEndianValue = bigEndian | ||
return Data(bytes: &bigEndianValue, count: MemoryLayout<UInt64>.size) | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,17 @@ | ||
// | ||
// UUIDUtils.swift | ||
// PostHog | ||
// | ||
// Created by Manoel Aranda Neto on 17.06.24. | ||
// | ||
|
||
// Inspired and adapted from https://github.com/nthState/UUIDV7/blob/main/Sources/UUIDV7/UUIDV7.swift | ||
// but using SecRandomCopyBytes | ||
|
||
import Foundation | ||
|
||
public extension UUID { | ||
static func v7() -> Self { | ||
TimeBasedEpochGenerator.shared.v7() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,101 @@ | ||
// | ||
// UUIDTest.swift | ||
// PostHogTests | ||
// | ||
// Created by Manoel Aranda Neto on 17.06.24. | ||
// | ||
|
||
import Foundation | ||
|
||
import Foundation | ||
import Nimble | ||
@testable import PostHog | ||
import Quick | ||
|
||
class UUIDTest: QuickSpec { | ||
private func compareULongs(_ l1: UInt64, _ l2: UInt64) -> Int { | ||
let high1 = Int32(bitPattern: UInt32((l1 >> 32) & 0xFFFF_FFFF)) | ||
let high2 = Int32(bitPattern: UInt32((l2 >> 32) & 0xFFFF_FFFF)) | ||
var diff = compareUInts(high1, high2) | ||
if diff == 0 { | ||
let low1 = Int32(bitPattern: UInt32(l1 & 0xFFFF_FFFF)) | ||
let low2 = Int32(bitPattern: UInt32(l2 & 0xFFFF_FFFF)) | ||
diff = compareUInts(low1, low2) | ||
} | ||
return diff | ||
} | ||
|
||
private func compareUInts(_ i1: Int32, _ i2: Int32) -> Int { | ||
if i1 < 0 { | ||
return Int(i2 < 0 ? (i1 - i2) : 1) | ||
} | ||
return Int(i2 < 0 ? -1 : (i1 - i2)) | ||
} | ||
|
||
override func spec() { | ||
it("mostSignificantBits") { | ||
let uuid = UUID(uuidString: "019025e6-b135-7e40-97df-ae0cebef184c")! | ||
expect(uuid.mostSignificantBits) == 112_631_663_430_041_152 | ||
} | ||
|
||
it("leastSignificantBits") { | ||
let uuid = UUID(uuidString: "019025e6-b135-7e40-97df-ae0cebef184c")! | ||
expect(uuid.leastSignificantBits) == -7_503_087_083_654_801_332 | ||
} | ||
|
||
it("test sorted and duplicated") { | ||
let count = 10000 | ||
|
||
var created: [UUID] = [] | ||
for _ in 0 ..< count { | ||
created.append(UUID.v7()) | ||
} | ||
|
||
let sortedUUIDs = created.sorted { uuid1, uuid2 in | ||
if uuid1.mostSignificantBits != uuid2.mostSignificantBits { | ||
return uuid1.mostSignificantBits < uuid2.mostSignificantBits | ||
} | ||
return uuid1.leastSignificantBits < uuid2.leastSignificantBits | ||
} | ||
|
||
var unique: Set<UUID> = Set(minimumCapacity: count) | ||
|
||
for i in 0 ..< created.count { | ||
expect(sortedUUIDs[i]) == created[i] | ||
if !unique.insert(created[i]).inserted { | ||
fatalError("Duplicate at index \(i)") | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
extension UUID { | ||
var mostSignificantBits: Int64 { | ||
let uuidBytes = withUnsafePointer(to: uuid) { | ||
$0.withMemoryRebound(to: UInt8.self, capacity: 16) { | ||
Array(UnsafeBufferPointer(start: $0, count: 16)) | ||
} | ||
} | ||
|
||
var mostSignificantBits: UInt64 = 0 | ||
for i in 0 ..< 8 { | ||
mostSignificantBits = (mostSignificantBits << 8) | UInt64(uuidBytes[i]) | ||
} | ||
return Int64(bitPattern: mostSignificantBits) | ||
} | ||
|
||
var leastSignificantBits: Int64 { | ||
let uuidBytes = withUnsafePointer(to: uuid) { | ||
$0.withMemoryRebound(to: UInt8.self, capacity: 16) { | ||
Array(UnsafeBufferPointer(start: $0, count: 16)) | ||
} | ||
} | ||
|
||
var leastSignificantBits: UInt64 = 0 | ||
for i in 8 ..< 16 { | ||
leastSignificantBits = (leastSignificantBits << 8) | UInt64(uuidBytes[i]) | ||
} | ||
return Int64(bitPattern: leastSignificantBits) | ||
} | ||
} |