Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 7 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,13 @@ and parsing of UUIDs in different formats.

This package supports the following UUID versions:

- Version 1, based on timestamp and MAC address
- Version 3, based on MD5 hashing of a named value
- Version 4, based on random numbers
- Version 5, based on SHA-1 hashing of a named value
- Version 6, a k-sortable id based on timestamp, and field-compatible with v1
- Version 7, a k-sortable id based on timestamp
* Version 1, based on timestamp and MAC address
* Version 3, based on MD5 hashing of a named value
* Version 4, based on random numbers
* Version 5, based on SHA-1 hashing of a named value
* Version 6, a k-sortable id based on timestamp, and field-compatible with v1
* Version 7, a k-sortable id based on timestamp
* Version 8, for custom UUID implementations

## Project History

Expand Down
3 changes: 3 additions & 0 deletions error.go
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,9 @@ const (

// ErrInvalidVersion indicates an unsupported or invalid UUID version.
ErrInvalidVersion = Error("uuid:")

// ErrV8FieldLength indicates a V8 custom field has incorrect length.
ErrV8FieldLength = Error("uuid: V8 field has incorrect length")
)

// Wrapped errors for backward compatibility. These wrap ErrIncorrectFormatInString
Expand Down
59 changes: 59 additions & 0 deletions generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"crypto/rand"
"crypto/sha1"
"encoding/binary"
"fmt"
"io"
"net"
"sync"
Expand Down Expand Up @@ -98,6 +99,18 @@ func NewV7AtTime(atTime time.Time) (UUID, error) {
return DefaultGenerator.NewV7AtTime(atTime)
}

// NewV8 returns a custom UUID based on user-provided data as specified in RFC 9562.
// The UUID is constructed from three fields:
// - customA: exactly 6 bytes (48 bits) - occupies bits 0-47
// - customB: exactly 2 bytes (only lower 12 bits used) - occupies bits 52-63
// - customC: exactly 8 bytes (only lower 62 bits used) - occupies bits 66-127
//
// Version (4 bits) and variant (2 bits) are set automatically.
// Returns ErrV8FieldLength if any field is not exactly the required length.
func NewV8(customA []byte, customB []byte, customC []byte) (UUID, error) {
return DefaultGenerator.NewV8(customA, customB, customC)
}

// Generator provides an interface for generating UUIDs.
type Generator interface {
NewV1() (UUID, error)
Expand All @@ -109,6 +122,7 @@ type Generator interface {
NewV6AtTime(time.Time) (UUID, error)
NewV7() (UUID, error)
NewV7AtTime(time.Time) (UUID, error)
NewV8([]byte, []byte, []byte) (UUID, error)
}

// Gen is a reference UUID generator based on the specifications laid out in
Expand Down Expand Up @@ -403,6 +417,51 @@ func (g *Gen) NewV7AtTime(atTime time.Time) (UUID, error) {
return u, nil
}

// NewV8 returns a UUID based on user-provided data as specified in RFC 9562.
// See the package-level NewV8 function for documentation.
func (g *Gen) NewV8(customA []byte, customB []byte, customC []byte) (UUID, error) {
var u UUID
/* https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-8
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 |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */

// Validate input lengths
if len(customA) != 6 {
return Nil, fmt.Errorf("%w: customA must be exactly 6 bytes (48 bits), got %d", ErrV8FieldLength, len(customA))
}
if len(customB) != 2 {
return Nil, fmt.Errorf("%w: customB must be exactly 2 bytes (16 bits, where 12 bits are used per RFC9562), got %d", ErrV8FieldLength, len(customB))
}
if len(customC) != 8 {
return Nil, fmt.Errorf("%w: customC must be exactly 8 bytes (64 bits, where 62 bits are used per RFC9562), got %d", ErrV8FieldLength, len(customC))
}

// Copy customA (48 bits = 6 bytes) into u[0:6]
copy(u[0:6], customA)

// Copy customB (16 bits from 2 bytes) into u[6:8]
// the high 4 bits of u[6] will be overwritten by version
copy(u[6:8], customB)

// Copy customC (62 bits from 8 bytes) into u[8:16]
// The high 2 bits of u[8] will be overwritten by variant
copy(u[8:16], customC)

u.SetVersion(V8)
u.SetVariant(VariantRFC9562)

return u, nil
}

// getClockSequence returns the epoch and clock sequence of the provided time,
// used for generating V1,V6 and V7 UUIDs.
//
Expand Down
171 changes: 171 additions & 0 deletions generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ func TestGenerator(t *testing.T) {
t.Run("NewV5", testNewV5)
t.Run("NewV6", testNewV6)
t.Run("NewV7", testNewV7)
t.Run("NewV8", testNewV8)
}

func testNewV1(t *testing.T) {
Expand Down Expand Up @@ -1174,3 +1175,173 @@ func testErrCheck(t *testing.T, name string, errContains string, err error) bool

return true
}

func testNewV8(t *testing.T) {
t.Run("Basic", makeTestNewV8Basic())
t.Run("VersionAndVariant", makeTestNewV8VersionAndVariant())
t.Run("CustomFields", makeTestNewV8CustomFields())
t.Run("InvalidLength", makeTestNewV8InvalidLength())
}

func makeTestNewV8Basic() func(t *testing.T) {
return func(t *testing.T) {
customA := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}
customB := []byte{0x07, 0x08}
customC := []byte{0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}

u, err := NewV8(customA, customB, customC)
if err != nil {
t.Fatal(err)
}
if u == Nil {
t.Error("UUID is nil")
}
}
}

func makeTestNewV8VersionAndVariant() func(t *testing.T) {
return func(t *testing.T) {
customA := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}
customB := []byte{0x07, 0x08}
customC := []byte{0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}

u, err := NewV8(customA, customB, customC)
if err != nil {
t.Fatal(err)
}
if got, want := u.Version(), V8; got != want {
t.Errorf("got version %d, want %d", got, want)
}
if got, want := u.Variant(), VariantRFC9562; got != want {
t.Errorf("got variant %d, want %d", got, want)
}
}
}

func makeTestNewV8CustomFields() func(t *testing.T) {
return func(t *testing.T) {
// Test that custom data is correctly placed in the UUID
// customA: 48 bits = 6 bytes -> u[0:6]
// customB: 12 bits -> lower 12 bits of u[6:8] (high 4 bits are version)
// customC: 62 bits -> lower 62 bits of u[8:16] (high 2 bits are variant)
customA := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
customB := []byte{0x01, 0x23} // 0x0123; only the lower 12 bits (0x123) are used
customC := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}

u, err := NewV8(customA, customB, customC)
if err != nil {
t.Fatal(err)
}

// Check customA bytes
if !bytes.Equal(u[0:6], customA) {
t.Errorf("customA mismatch: got %x, want %x", u[0:6], customA)
}

// Check version is set correctly (high nibble of byte 6)
if u[6]>>4 != V8 {
t.Errorf("version bits incorrect: got %d, want %d", u[6]>>4, V8)
}

// Check customB lower 12 bits (low nibble of u[6] and all of u[7])
bLow := (uint16(u[6]&0x0f) << 8) | uint16(u[7])
if bLow != 0x123 {
t.Errorf("customB bits incorrect: got %x, want %x", bLow, 0x123)
}

// Check variant is set correctly (high 2 bits of byte 8)
if (u[8] >> 6) != 0x02 {
t.Errorf("variant bits incorrect: got %x, want %x", u[8]>>6, 0x02)
}

// Check customC lower 62 bits are preserved (excluding variant bits)
wantC8 := (customC[0] & 0x3f) // variant overwrites top 2 bits
// Actually the implementation masks u[8] before setting variant
// So we expect the lower 6 bits of customC[0] to be in u[8], then variant added
gotC8Lower := u[8] & 0x3f
if gotC8Lower != wantC8 {
t.Errorf("customC[0] lower bits incorrect: got %x, want %x", gotC8Lower, wantC8)
}
if !bytes.Equal(u[9:16], customC[1:8]) {
t.Errorf("customC[1:8] mismatch: got %x, want %x", u[9:16], customC[1:8])
}
}
}

func makeTestNewV8InvalidLength() func(t *testing.T) {
return func(t *testing.T) {
// Test that incorrect lengths return errors
tests := []struct {
name string
customA []byte
customB []byte
customC []byte
errMsg string
}{
{
name: "customA too short",
customA: []byte{0x01, 0x02}, // 2 bytes instead of 6
customB: []byte{0x01, 0x23},
customC: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
errMsg: "customA must be exactly 6 bytes",
},
{
name: "customA too long",
customA: []byte{0xFF, 0xFF, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, // 8 bytes
customB: []byte{0x01, 0x23},
customC: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
errMsg: "customA must be exactly 6 bytes",
},
{
name: "customB too short",
customA: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},
customB: []byte{0x01}, // 1 byte instead of 2
customC: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
errMsg: "customB must be exactly 2 bytes",
},
{
name: "customB too long",
customA: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},
customB: []byte{0xFF, 0x01, 0x23}, // 3 bytes
customC: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
errMsg: "customB must be exactly 2 bytes",
},
{
name: "customC too short",
customA: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},
customB: []byte{0x01, 0x23},
customC: []byte{0x01, 0x02}, // 2 bytes instead of 8
errMsg: "customC must be exactly 8 bytes",
},
{
name: "customC too long",
customA: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},
customB: []byte{0x01, 0x23},
customC: []byte{0xFF, 0xFF, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, // 10 bytes
errMsg: "customC must be exactly 8 bytes",
},
{
name: "all empty",
customA: nil,
customB: nil,
customC: nil,
errMsg: "customA must be exactly 6 bytes",
},
}

for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
_, err := NewV8(tc.customA, tc.customB, tc.customC)
if err == nil {
t.Fatal("expected error, got nil")
}
if !errors.Is(err, ErrV8FieldLength) {
t.Errorf("expected ErrV8FieldLength, got %v", err)
}
if !strings.Contains(err.Error(), tc.errMsg) {
t.Errorf("error message %q should contain %q", err.Error(), tc.errMsg)
}
})
}
}
}
4 changes: 2 additions & 2 deletions uuid.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
// Package uuid provides implementations of the Universally Unique Identifier
// (UUID), as specified in RFC-9562 (formerly RFC-4122).
//
// RFC-9562[1] provides the specification for versions 1, 3, 4, 5, 6 and 7.
// RFC-9562[1] provides the specification for versions 1, 3, 4, 5, 6, 7 and 8.
//
// DCE 1.1[2] provides the specification for version 2, but version 2 support
// was removed from this package in v4 due to some concerns with the
Expand Down Expand Up @@ -60,7 +60,7 @@ const (
V5 // Version 5 (namespace name-based)
V6 // Version 6 (k-sortable timestamp and random data, field-compatible with v1)
V7 // Version 7 (k-sortable timestamp and random data)
_ // Version 8 (k-sortable timestamp, meant for custom implementations) [not implemented]
V8 // Version 8 (custom UUID implementations)
)

// UUID layout variants.
Expand Down
Loading