Add version 7 UUID generation

This commit is contained in:
Feoramund
2024-06-21 12:04:45 -04:00
parent fee81985b4
commit 4cfbd83b10
4 changed files with 151 additions and 1 deletions

View File

@@ -8,6 +8,11 @@ EXPECTED_LENGTH :: 8 + 4 + 4 + 4 + 12 + 4
VERSION_BYTE_INDEX :: 6
VARIANT_BYTE_INDEX :: 8
VERSION_7_TIME_MASK :: 0xffffffff_ffff0000_00000000_00000000
VERSION_7_TIME_SHIFT :: 80
VERSION_7_COUNTER_MASK :: 0x00000000_00000fff_00000000_00000000
VERSION_7_COUNTER_SHIFT :: 64
Read_Error :: enum {
None,
Invalid_Length,

View File

@@ -4,6 +4,7 @@ import "core:crypto/legacy/md5"
import "core:crypto/legacy/sha1"
import "core:math/rand"
import "core:mem"
import "core:time"
/*
Generate a version 3 UUID.
@@ -158,3 +159,86 @@ generate_v5 :: proc {
generate_v5_bytes,
generate_v5_string,
}
/*
Generate a version 7 UUID.
This UUID will be pseudorandom, save for 6 pre-determined version and variant
bits and a 48 bit timestamp.
It is designed with time-based sorting in mind, such as for database usage, as
the highest bits are allocated from the timestamp of when it is created.
Returns:
- result: The generated UUID.
*/
generate_v7 :: proc() -> (result: Identifier) {
unix_time_in_milliseconds := time.to_unix_nanoseconds(time.now()) / 1e6
temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT
bytes_generated := rand.read(result[6:])
assert(bytes_generated == 10, "RNG failed to generate 10 bytes for UUID v7.")
result |= transmute(Identifier)temporary
result[VERSION_BYTE_INDEX] &= 0x0F
result[VERSION_BYTE_INDEX] |= 0x70
result[VARIANT_BYTE_INDEX] &= 0x3F
result[VARIANT_BYTE_INDEX] |= 0x80
return
}
/*
Generate a version 7 UUID with an incremented counter.
This UUID will be pseudorandom, save for 6 pre-determined version and variant
bits, a 48 bit timestamp, and 12 bits of counter state.
It is designed with time-based sorting in mind, such as for database usage, as
the highest bits are allocated from the timestamp of when it is created.
This procedure is preferable if you are generating hundreds or thousands of
UUIDs as a batch within the span of a millisecond. Do note that the counter
only has 12 bits of state, thus `counter` cannot exceed the number 4,095.
Example:
import "core:uuid"
// Create a batch of UUIDs all at once.
batch: [dynamic]uuid.Identifier
for i: u16 = 0; i < 1000; i += 1 {
my_uuid := uuid.generate_v7_counter(i)
append(&batch, my_uuid)
}
Inputs:
- counter: A 12-bit value, incremented each time a UUID is generated in a batch.
Returns:
- result: The generated UUID.
*/
generate_v7_counter :: proc(counter: u16) -> (result: Identifier) {
assert(counter <= 0x0fff, "This implementation of the version 7 UUID does not support counters in excess of 12 bits (4,095).")
unix_time_in_milliseconds := time.to_unix_nanoseconds(time.now()) / 1e6
temporary := cast(u128be)unix_time_in_milliseconds << VERSION_7_TIME_SHIFT
temporary |= cast(u128be)counter << VERSION_7_COUNTER_SHIFT
bytes_generated := rand.read(result[8:])
assert(bytes_generated == 8, "RNG failed to generate 8 bytes for UUID v7.")
result |= transmute(Identifier)temporary
result[VERSION_BYTE_INDEX] &= 0x0F
result[VERSION_BYTE_INDEX] |= 0x70
result[VARIANT_BYTE_INDEX] &= 0x3F
result[VARIANT_BYTE_INDEX] |= 0x80
return
}

View File

@@ -95,3 +95,34 @@ variant :: proc "contextless" (id: Identifier) -> (variant: Variant_Type) #no_bo
return .Unknown
}
}
/*
Get the timestamp of a version 7 UUID.
Inputs:
- id: The identifier.
Returns:
- timestamp: The timestamp, in milliseconds since the UNIX epoch.
*/
time_v7 :: proc "contextless" (id: Identifier) -> (timestamp: u64) {
time_bits := transmute(u128be)id & VERSION_7_TIME_MASK
return cast(u64)(time_bits >> VERSION_7_TIME_SHIFT)
}
/*
Get the 12-bit counter value of a version 7 UUID.
The UUID must have been generated with a counter, otherwise this procedure will
return random bits.
Inputs:
- id: The identifier.
Returns:
- counter: The 12-bit counter value.
*/
counter_v7 :: proc "contextless" (id: Identifier) -> (counter: u16) {
counter_bits := transmute(u128be)id & VERSION_7_COUNTER_MASK
return cast(u16)(counter_bits >> VERSION_7_COUNTER_SHIFT)
}

View File

@@ -1,13 +1,16 @@
package test_core_uuid
import "core:testing"
import "core:encoding/uuid"
import "core:log"
import "core:testing"
import "core:time"
@(test)
test_version_and_variant :: proc(t: ^testing.T) {
v3 := uuid.generate_v3(uuid.Namespace_DNS, "")
v4 := uuid.generate_v4()
v5 := uuid.generate_v5(uuid.Namespace_DNS, "")
v7 := uuid.generate_v7()
testing.expect_value(t, uuid.version(v3), 3)
testing.expect_value(t, uuid.variant(v3), uuid.Variant_Type.RFC_4122)
@@ -15,6 +18,8 @@ test_version_and_variant :: proc(t: ^testing.T) {
testing.expect_value(t, uuid.variant(v4), uuid.Variant_Type.RFC_4122)
testing.expect_value(t, uuid.version(v5), 5)
testing.expect_value(t, uuid.variant(v5), uuid.Variant_Type.RFC_4122)
testing.expect_value(t, uuid.version(v7), 7)
testing.expect_value(t, uuid.variant(v7), uuid.Variant_Type.RFC_4122)
}
@(test)
@@ -48,6 +53,31 @@ test_namespaced_uuids :: proc(t: ^testing.T) {
}
}
@(test)
test_v7 :: proc(t: ^testing.T) {
v7_a := uuid.generate_v7()
time.sleep(10 * time.Millisecond)
v7_b := uuid.generate_v7()
time.sleep(10 * time.Millisecond)
v7_c := uuid.generate_v7()
time_bits_a := uuid.time_v7(v7_a)
time_bits_b := uuid.time_v7(v7_b)
time_bits_c := uuid.time_v7(v7_c)
log.debugf("A: %02x, %i", v7_a, time_bits_a)
log.debugf("B: %02x, %i", v7_b, time_bits_b)
log.debugf("C: %02x, %i", v7_c, time_bits_c)
testing.expect(t, time_bits_b > time_bits_a, "The time bits on the later-generated v7 UUID are lesser than the earlier UUID.")
testing.expect(t, time_bits_c > time_bits_b, "The time bits on the later-generated v7 UUID are lesser than the earlier UUID.")
testing.expect(t, time_bits_c > time_bits_a, "The time bits on the later-generated v7 UUID are lesser than the earlier UUID.")
v7_with_counter := uuid.generate_v7_counter(0x555)
log.debugf("D: %02x", v7_with_counter)
testing.expect_value(t, uuid.counter_v7(v7_with_counter), 0x555)
}
@(test)
test_writing :: proc(t: ^testing.T) {
id: uuid.Identifier