From ee97a204ed71ea4ccbf4e82d7670b4643bcdf069 Mon Sep 17 00:00:00 2001 From: Ashwin G <47941624+ashwingopalsamy@users.noreply.github.com> Date: Sat, 14 Dec 2024 13:29:00 +0530 Subject: [PATCH 1/5] feature: add UUID version8 support --- version8.go | 150 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 150 insertions(+) create mode 100644 version8.go diff --git a/version8.go b/version8.go new file mode 100644 index 0000000..429ebcf --- /dev/null +++ b/version8.go @@ -0,0 +1,150 @@ +// Copyright 2024 Google Inc. All rights reserved. +// Use of this source code is governed by a BSD-style +// license that can be found in the LICENSE file. + +package uuid + +import ( + "encoding/binary" + "io" + "time" +) + +var ( + lastTimestamp uint64 + sequence uint16 +) + +// NewV8 generates a version 8 UUID. +// It sets the version and variant fields and fills the rest with random data. +// It allows embedding of user-defined data while maintaining the UUID structure. +// +// The layout includes: +// - custom_a: 48 bits (user-defined) +// - ver: 4 bits (set to 0b1000 for version 8) +// - custom_b: 12 bits (user-defined) +// - var: 2 bits (set to 0b10 for RFC 4122 variant) +// - custom_c: 62 bits (user-defined) +// +// For details, see https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-8 +// +// NewV8 generates a UUID version 8 with completely random user-defined fields. +// It uses the randomness pool if enabled or falls back to a secure random source. +// On error, NewV8 returns Nil and an error. +func NewV8() (UUID, error) { + uuid, err := NewRandom() + if err != nil { + return uuid, err + } + uuid[6] = (uuid[6] & 0x0F) | 0x80 // Set version to 8 + uuid[8] = (uuid[8] & 0x3F) | 0x80 // Set variant to RFC 4122 + return uuid, nil +} + +// NewV8FromReader generates a version 8 UUID with user-defined custom_a and custom_b. +// It uses random bits for custom_c if no random reader is provided. +// On error, NewV8FromReader returns Nil and an error. +func NewV8FromReader(customA, customB uint64, random io.Reader) (UUID, error) { + var uuid UUID + + // Encode custom_a (48 bits) + binary.BigEndian.PutUint64(uuid[:8], customA) + copy(uuid[:6], uuid[2:8]) // Retain only the lower 48 bits + + // Set version and custom_b (12 bits) + uuid[6] = (uuid[6] & 0x0F) | 0x80 + binary.BigEndian.PutUint16(uuid[6:8], uint16(customB)|0x8000) + + // Fill custom_c (62 bits) + if random == nil { + random = rander + } + if _, err := io.ReadFull(random, uuid[8:]); err != nil { + return Nil, err + } + uuid[8] = (uuid[8] & 0x3F) | 0x80 + + return uuid, nil +} + +// NewV8TimeBased generates a version 8 UUID with a time-based custom_a field. +// It ensures uniqueness using a sequence number for UUIDs created in the same nanosecond. +func NewV8TimeBased(random io.Reader) (UUID, error) { + var uuid UUID + timestamp := uint64(time.Now().UnixNano()) + + timeMu.Lock() + defer timeMu.Unlock() + + if timestamp == lastTimestamp { + sequence++ + } else { + lastTimestamp = timestamp + sequence = 0 + } + + // Encode timestamp into custom_a (48 bits) + binary.BigEndian.PutUint64(uuid[:8], timestamp) + copy(uuid[:6], uuid[2:8]) + + // Set version and variant + uuid[6] = (uuid[6] & 0x0F) | 0x80 + uuid[8] = (uuid[8] & 0x3F) | 0x80 + + // Add sequence to custom_c (16 bits) + binary.BigEndian.PutUint16(uuid[8:], sequence) + + // Fill the rest with custom_c + if random == nil { + for i := 10; i < 16; i++ { + uuid[i] = 0 + } + } else if _, err := io.ReadFull(random, uuid[10:]); err != nil { + return Nil, err + } + + return uuid, nil +} + +// makeV8 generates a version 8 UUID using user-provided or random data for custom_a, custom_b, and custom_c. +func makeV8(uuid []byte, customA, customB, customC []byte) { + /* + Layout of UUIDv8: + 0 1 2 3 + 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | custom_a | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | custom_a | ver | custom_b | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + |var| custom_c | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + | custom_c | + +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ + */ + + _ = uuid[15] // Bounds check + + // Fill custom_a (48 bits) + if customA != nil { + copy(uuid[0:6], customA) + } else { + randomBits(uuid[0:6]) + } + + // Set version and fill custom_b (12 bits) + uuid[6] = (uuid[6] & 0x0F) | 0x80 + if customB != nil { + copy(uuid[6:8], customB) + } else { + randomBits(uuid[6:8]) + } + uuid[8] = (uuid[8] & 0x3F) | 0x80 + + // Fill custom_c (62 bits) + if customC != nil { + copy(uuid[8:], customC) + } else { + randomBits(uuid[8:]) + } +} From a6b2edcf3463b304f9df4bff7b06fb2cc71a9090 Mon Sep 17 00:00:00 2001 From: Ashwin G <47941624+ashwingopalsamy@users.noreply.github.com> Date: Sat, 14 Dec 2024 13:29:12 +0530 Subject: [PATCH 2/5] tests: add UUID version8 tests --- uuid_test.go | 97 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/uuid_test.go b/uuid_test.go index 906ecbe..b890eae 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -6,6 +6,7 @@ package uuid import ( "bytes" + "encoding/binary" "errors" "fmt" "os" @@ -928,3 +929,99 @@ func TestVersion7MonotonicityStrict(t *testing.T) { u1 = u2 } } + +func TestVersion8(t *testing.T) { + m := make(map[string]bool) + for i := 0; i < 128; i++ { + uuid, err := NewV8() + if err != nil { + t.Fatalf("NewV8 failed: %v", err) + } + + s := uuid.String() + if m[s] { + t.Errorf("Duplicate UUID generated: %s", s) + } + m[s] = true + + if uuid.Version() != 8 { + t.Errorf("Expected version 8, got %d", uuid.Version()) + } + if uuid.Variant() != RFC4122 { + t.Errorf("Expected variant RFC4122, got %d", uuid.Variant()) + } + } +} + +func TestVersion8FromReader(t *testing.T) { + customA := uint64(0x123456789ABC) + customB := uint64(0xDEF) + r := bytes.NewReader(make([]byte, 16)) // Simulated random reader + + uuid, err := NewV8FromReader(customA, customB, r) + if err != nil { + t.Fatalf("NewV8FromReader failed: %v", err) + } + + if uuid.Version() != 8 { + t.Errorf("Expected version 8, got %d", uuid.Version()) + } + if uuid.Variant() != RFC4122 { + t.Errorf("Expected variant RFC4122, got %d", uuid.Variant()) + } + + gotCustomA := binary.BigEndian.Uint64(append([]byte{0, 0}, uuid[:6]...)) & 0xFFFFFFFFFFFF + if gotCustomA != customA { + t.Errorf("Expected custom_a %x, got %x", customA, gotCustomA) + } + + gotCustomB := binary.BigEndian.Uint16(uuid[6:8]) & 0xFFF + if gotCustomB != uint16(customB) { + t.Errorf("Expected custom_b %x, got %x", customB, gotCustomB) + } +} + +func TestVersion8Uniqueness(t *testing.T) { + m := make(map[string]bool) + for i := 0; i < 10000; i++ { + uuid, err := NewV8() + if err != nil { + t.Fatalf("NewV8 failed: %v", err) + } + + s := uuid.String() + if m[s] { + t.Fatalf("Duplicate UUID found: %s", s) + } + m[s] = true + } +} + +func TestVersion8Monotonicity(t *testing.T) { + u1 := Must(NewV8TimeBased(nil)).String() + for i := 0; i < 10000; i++ { + u2 := Must(NewV8TimeBased(nil)).String() + if u2 <= u1 { + t.Errorf("Monotonicity failed at iteration %d: %s < %s", i, u2, u1) + break + } + u1 = u2 + } +} + +func TestVersion8WithNilRandomReader(t *testing.T) { + customA := uint64(0x123456789ABC) + customB := uint64(0xDEF) + + uuid, err := NewV8FromReader(customA, customB, nil) + if err != nil { + t.Fatalf("NewV8FromReader failed with nil random reader: %v", err) + } + + if uuid.Version() != 8 { + t.Errorf("Expected version 8, got %d", uuid.Version()) + } + if uuid.Variant() != RFC4122 { + t.Errorf("Expected variant RFC4122, got %d", uuid.Variant()) + } +} From bbbda9c259b8415ae9902e4f39507e473c861862 Mon Sep 17 00:00:00 2001 From: Ashwin Gopalsamy <47941624+ashwingopalsamy@users.noreply.github.com> Date: Wed, 19 Feb 2025 19:08:22 +0530 Subject: [PATCH 3/5] fix: address code review comments remove redundant function (dead code) and place the `sync.Mutex` lock after the `if` condition --- version8.go | 46 ++-------------------------------------------- 1 file changed, 2 insertions(+), 44 deletions(-) diff --git a/version8.go b/version8.go index 429ebcf..dbf596a 100644 --- a/version8.go +++ b/version8.go @@ -74,7 +74,6 @@ func NewV8TimeBased(random io.Reader) (UUID, error) { timestamp := uint64(time.Now().UnixNano()) timeMu.Lock() - defer timeMu.Unlock() if timestamp == lastTimestamp { sequence++ @@ -83,6 +82,8 @@ func NewV8TimeBased(random io.Reader) (UUID, error) { sequence = 0 } + defer timeMu.Unlock() + // Encode timestamp into custom_a (48 bits) binary.BigEndian.PutUint64(uuid[:8], timestamp) copy(uuid[:6], uuid[2:8]) @@ -105,46 +106,3 @@ func NewV8TimeBased(random io.Reader) (UUID, error) { return uuid, nil } - -// makeV8 generates a version 8 UUID using user-provided or random data for custom_a, custom_b, and custom_c. -func makeV8(uuid []byte, customA, customB, customC []byte) { - /* - Layout of UUIDv8: - 0 1 2 3 - 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | custom_a | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | custom_a | ver | custom_b | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - |var| custom_c | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - | custom_c | - +-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ - */ - - _ = uuid[15] // Bounds check - - // Fill custom_a (48 bits) - if customA != nil { - copy(uuid[0:6], customA) - } else { - randomBits(uuid[0:6]) - } - - // Set version and fill custom_b (12 bits) - uuid[6] = (uuid[6] & 0x0F) | 0x80 - if customB != nil { - copy(uuid[6:8], customB) - } else { - randomBits(uuid[6:8]) - } - uuid[8] = (uuid[8] & 0x3F) | 0x80 - - // Fill custom_c (62 bits) - if customC != nil { - copy(uuid[8:], customC) - } else { - randomBits(uuid[8:]) - } -} From f36b58ed27a065308a993db888279bc3139e5367 Mon Sep 17 00:00:00 2001 From: Ashwin Gopalsamy <47941624+ashwingopalsamy@users.noreply.github.com> Date: Wed, 19 Feb 2025 19:39:01 +0530 Subject: [PATCH 4/5] fix: remove mu.Unlock --- version8.go | 2 -- 1 file changed, 2 deletions(-) diff --git a/version8.go b/version8.go index dbf596a..8ab69c6 100644 --- a/version8.go +++ b/version8.go @@ -82,8 +82,6 @@ func NewV8TimeBased(random io.Reader) (UUID, error) { sequence = 0 } - defer timeMu.Unlock() - // Encode timestamp into custom_a (48 bits) binary.BigEndian.PutUint64(uuid[:8], timestamp) copy(uuid[:6], uuid[2:8]) From 9d9f0cbe750eee48be08cc7b515a644a765a9783 Mon Sep 17 00:00:00 2001 From: Ashwin Gopalsamy <47941624+ashwingopalsamy@users.noreply.github.com> Date: Fri, 11 Apr 2025 12:04:31 +0530 Subject: [PATCH 5/5] Update version8.go --- version8.go | 2 ++ 1 file changed, 2 insertions(+) diff --git a/version8.go b/version8.go index 8ab69c6..27de2a5 100644 --- a/version8.go +++ b/version8.go @@ -82,6 +82,8 @@ func NewV8TimeBased(random io.Reader) (UUID, error) { sequence = 0 } + timeMu.Unlock() + // Encode timestamp into custom_a (48 bits) binary.BigEndian.PutUint64(uuid[:8], timestamp) copy(uuid[:6], uuid[2:8])