diff --git a/README.md b/README.md index 29bfcdf..581040d 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/error.go b/error.go index 3bde262..c406615 100644 --- a/error.go +++ b/error.go @@ -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 diff --git a/generator.go b/generator.go index 4facd3a..d742967 100644 --- a/generator.go +++ b/generator.go @@ -26,6 +26,7 @@ import ( "crypto/rand" "crypto/sha1" "encoding/binary" + "fmt" "io" "net" "sync" @@ -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) @@ -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 @@ -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. // diff --git a/generator_test.go b/generator_test.go index 9cbbd08..7949950 100644 --- a/generator_test.go +++ b/generator_test.go @@ -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) { @@ -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) + } + }) + } + } +} diff --git a/uuid.go b/uuid.go index bd59d48..c012b4f 100644 --- a/uuid.go +++ b/uuid.go @@ -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 @@ -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.