Skip to content

Commit 875e708

Browse files
camerackerCopilotCopilot
authored
Feature/uuidv8 (#241)
* added v8 * code cleanup * Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> * docs: add UUID v8 to RFC-9562 version list in package doc comment (#244) * Initial plan * docs: include version 8 in RFC-9562 package doc comment Co-authored-by: cameracker <14242948+cameracker@users.noreply.github.com> --------- Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: cameracker <14242948+cameracker@users.noreply.github.com> * simplify customB data insertion for v8 * respond to review feedback --------- Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> Co-authored-by: Copilot <198982749+Copilot@users.noreply.github.com> Co-authored-by: cameracker <14242948+cameracker@users.noreply.github.com>
1 parent 74d7e0c commit 875e708

File tree

5 files changed

+242
-8
lines changed

5 files changed

+242
-8
lines changed

README.md

Lines changed: 7 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@ and parsing of UUIDs in different formats.
1515

1616
This package supports the following UUID versions:
1717

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

2526
## Project History
2627

error.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,9 @@ const (
3434

3535
// ErrInvalidVersion indicates an unsupported or invalid UUID version.
3636
ErrInvalidVersion = Error("uuid:")
37+
38+
// ErrV8FieldLength indicates a V8 custom field has incorrect length.
39+
ErrV8FieldLength = Error("uuid: V8 field has incorrect length")
3740
)
3841

3942
// Wrapped errors for backward compatibility. These wrap ErrIncorrectFormatInString

generator.go

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ import (
2626
"crypto/rand"
2727
"crypto/sha1"
2828
"encoding/binary"
29+
"fmt"
2930
"io"
3031
"net"
3132
"sync"
@@ -98,6 +99,18 @@ func NewV7AtTime(atTime time.Time) (UUID, error) {
9899
return DefaultGenerator.NewV7AtTime(atTime)
99100
}
100101

102+
// NewV8 returns a custom UUID based on user-provided data as specified in RFC 9562.
103+
// The UUID is constructed from three fields:
104+
// - customA: exactly 6 bytes (48 bits) - occupies bits 0-47
105+
// - customB: exactly 2 bytes (only lower 12 bits used) - occupies bits 52-63
106+
// - customC: exactly 8 bytes (only lower 62 bits used) - occupies bits 66-127
107+
//
108+
// Version (4 bits) and variant (2 bits) are set automatically.
109+
// Returns ErrV8FieldLength if any field is not exactly the required length.
110+
func NewV8(customA []byte, customB []byte, customC []byte) (UUID, error) {
111+
return DefaultGenerator.NewV8(customA, customB, customC)
112+
}
113+
101114
// Generator provides an interface for generating UUIDs.
102115
type Generator interface {
103116
NewV1() (UUID, error)
@@ -109,6 +122,7 @@ type Generator interface {
109122
NewV6AtTime(time.Time) (UUID, error)
110123
NewV7() (UUID, error)
111124
NewV7AtTime(time.Time) (UUID, error)
125+
NewV8([]byte, []byte, []byte) (UUID, error)
112126
}
113127

114128
// 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) {
403417
return u, nil
404418
}
405419

420+
// NewV8 returns a UUID based on user-provided data as specified in RFC 9562.
421+
// See the package-level NewV8 function for documentation.
422+
func (g *Gen) NewV8(customA []byte, customB []byte, customC []byte) (UUID, error) {
423+
var u UUID
424+
/* https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-8
425+
0 1 2 3
426+
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
427+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
428+
| custom_a |
429+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
430+
| custom_a | ver | custom_b |
431+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
432+
|var| custom_c |
433+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
434+
| custom_c |
435+
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */
436+
437+
// Validate input lengths
438+
if len(customA) != 6 {
439+
return Nil, fmt.Errorf("%w: customA must be exactly 6 bytes (48 bits), got %d", ErrV8FieldLength, len(customA))
440+
}
441+
if len(customB) != 2 {
442+
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))
443+
}
444+
if len(customC) != 8 {
445+
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))
446+
}
447+
448+
// Copy customA (48 bits = 6 bytes) into u[0:6]
449+
copy(u[0:6], customA)
450+
451+
// Copy customB (16 bits from 2 bytes) into u[6:8]
452+
// the high 4 bits of u[6] will be overwritten by version
453+
copy(u[6:8], customB)
454+
455+
// Copy customC (62 bits from 8 bytes) into u[8:16]
456+
// The high 2 bits of u[8] will be overwritten by variant
457+
copy(u[8:16], customC)
458+
459+
u.SetVersion(V8)
460+
u.SetVariant(VariantRFC9562)
461+
462+
return u, nil
463+
}
464+
406465
// getClockSequence returns the epoch and clock sequence of the provided time,
407466
// used for generating V1,V6 and V7 UUIDs.
408467
//

generator_test.go

Lines changed: 171 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ func TestGenerator(t *testing.T) {
4040
t.Run("NewV5", testNewV5)
4141
t.Run("NewV6", testNewV6)
4242
t.Run("NewV7", testNewV7)
43+
t.Run("NewV8", testNewV8)
4344
}
4445

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

11751176
return true
11761177
}
1178+
1179+
func testNewV8(t *testing.T) {
1180+
t.Run("Basic", makeTestNewV8Basic())
1181+
t.Run("VersionAndVariant", makeTestNewV8VersionAndVariant())
1182+
t.Run("CustomFields", makeTestNewV8CustomFields())
1183+
t.Run("InvalidLength", makeTestNewV8InvalidLength())
1184+
}
1185+
1186+
func makeTestNewV8Basic() func(t *testing.T) {
1187+
return func(t *testing.T) {
1188+
customA := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}
1189+
customB := []byte{0x07, 0x08}
1190+
customC := []byte{0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}
1191+
1192+
u, err := NewV8(customA, customB, customC)
1193+
if err != nil {
1194+
t.Fatal(err)
1195+
}
1196+
if u == Nil {
1197+
t.Error("UUID is nil")
1198+
}
1199+
}
1200+
}
1201+
1202+
func makeTestNewV8VersionAndVariant() func(t *testing.T) {
1203+
return func(t *testing.T) {
1204+
customA := []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06}
1205+
customB := []byte{0x07, 0x08}
1206+
customC := []byte{0x09, 0x0a, 0x0b, 0x0c, 0x0d, 0x0e, 0x0f, 0x10}
1207+
1208+
u, err := NewV8(customA, customB, customC)
1209+
if err != nil {
1210+
t.Fatal(err)
1211+
}
1212+
if got, want := u.Version(), V8; got != want {
1213+
t.Errorf("got version %d, want %d", got, want)
1214+
}
1215+
if got, want := u.Variant(), VariantRFC9562; got != want {
1216+
t.Errorf("got variant %d, want %d", got, want)
1217+
}
1218+
}
1219+
}
1220+
1221+
func makeTestNewV8CustomFields() func(t *testing.T) {
1222+
return func(t *testing.T) {
1223+
// Test that custom data is correctly placed in the UUID
1224+
// customA: 48 bits = 6 bytes -> u[0:6]
1225+
// customB: 12 bits -> lower 12 bits of u[6:8] (high 4 bits are version)
1226+
// customC: 62 bits -> lower 62 bits of u[8:16] (high 2 bits are variant)
1227+
customA := []byte{0xAA, 0xBB, 0xCC, 0xDD, 0xEE, 0xFF}
1228+
customB := []byte{0x01, 0x23} // 0x0123; only the lower 12 bits (0x123) are used
1229+
customC := []byte{0x11, 0x22, 0x33, 0x44, 0x55, 0x66, 0x77, 0x88}
1230+
1231+
u, err := NewV8(customA, customB, customC)
1232+
if err != nil {
1233+
t.Fatal(err)
1234+
}
1235+
1236+
// Check customA bytes
1237+
if !bytes.Equal(u[0:6], customA) {
1238+
t.Errorf("customA mismatch: got %x, want %x", u[0:6], customA)
1239+
}
1240+
1241+
// Check version is set correctly (high nibble of byte 6)
1242+
if u[6]>>4 != V8 {
1243+
t.Errorf("version bits incorrect: got %d, want %d", u[6]>>4, V8)
1244+
}
1245+
1246+
// Check customB lower 12 bits (low nibble of u[6] and all of u[7])
1247+
bLow := (uint16(u[6]&0x0f) << 8) | uint16(u[7])
1248+
if bLow != 0x123 {
1249+
t.Errorf("customB bits incorrect: got %x, want %x", bLow, 0x123)
1250+
}
1251+
1252+
// Check variant is set correctly (high 2 bits of byte 8)
1253+
if (u[8] >> 6) != 0x02 {
1254+
t.Errorf("variant bits incorrect: got %x, want %x", u[8]>>6, 0x02)
1255+
}
1256+
1257+
// Check customC lower 62 bits are preserved (excluding variant bits)
1258+
wantC8 := (customC[0] & 0x3f) // variant overwrites top 2 bits
1259+
// Actually the implementation masks u[8] before setting variant
1260+
// So we expect the lower 6 bits of customC[0] to be in u[8], then variant added
1261+
gotC8Lower := u[8] & 0x3f
1262+
if gotC8Lower != wantC8 {
1263+
t.Errorf("customC[0] lower bits incorrect: got %x, want %x", gotC8Lower, wantC8)
1264+
}
1265+
if !bytes.Equal(u[9:16], customC[1:8]) {
1266+
t.Errorf("customC[1:8] mismatch: got %x, want %x", u[9:16], customC[1:8])
1267+
}
1268+
}
1269+
}
1270+
1271+
func makeTestNewV8InvalidLength() func(t *testing.T) {
1272+
return func(t *testing.T) {
1273+
// Test that incorrect lengths return errors
1274+
tests := []struct {
1275+
name string
1276+
customA []byte
1277+
customB []byte
1278+
customC []byte
1279+
errMsg string
1280+
}{
1281+
{
1282+
name: "customA too short",
1283+
customA: []byte{0x01, 0x02}, // 2 bytes instead of 6
1284+
customB: []byte{0x01, 0x23},
1285+
customC: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
1286+
errMsg: "customA must be exactly 6 bytes",
1287+
},
1288+
{
1289+
name: "customA too long",
1290+
customA: []byte{0xFF, 0xFF, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06}, // 8 bytes
1291+
customB: []byte{0x01, 0x23},
1292+
customC: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
1293+
errMsg: "customA must be exactly 6 bytes",
1294+
},
1295+
{
1296+
name: "customB too short",
1297+
customA: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},
1298+
customB: []byte{0x01}, // 1 byte instead of 2
1299+
customC: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
1300+
errMsg: "customB must be exactly 2 bytes",
1301+
},
1302+
{
1303+
name: "customB too long",
1304+
customA: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},
1305+
customB: []byte{0xFF, 0x01, 0x23}, // 3 bytes
1306+
customC: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08},
1307+
errMsg: "customB must be exactly 2 bytes",
1308+
},
1309+
{
1310+
name: "customC too short",
1311+
customA: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},
1312+
customB: []byte{0x01, 0x23},
1313+
customC: []byte{0x01, 0x02}, // 2 bytes instead of 8
1314+
errMsg: "customC must be exactly 8 bytes",
1315+
},
1316+
{
1317+
name: "customC too long",
1318+
customA: []byte{0x01, 0x02, 0x03, 0x04, 0x05, 0x06},
1319+
customB: []byte{0x01, 0x23},
1320+
customC: []byte{0xFF, 0xFF, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08}, // 10 bytes
1321+
errMsg: "customC must be exactly 8 bytes",
1322+
},
1323+
{
1324+
name: "all empty",
1325+
customA: nil,
1326+
customB: nil,
1327+
customC: nil,
1328+
errMsg: "customA must be exactly 6 bytes",
1329+
},
1330+
}
1331+
1332+
for _, tc := range tests {
1333+
t.Run(tc.name, func(t *testing.T) {
1334+
_, err := NewV8(tc.customA, tc.customB, tc.customC)
1335+
if err == nil {
1336+
t.Fatal("expected error, got nil")
1337+
}
1338+
if !errors.Is(err, ErrV8FieldLength) {
1339+
t.Errorf("expected ErrV8FieldLength, got %v", err)
1340+
}
1341+
if !strings.Contains(err.Error(), tc.errMsg) {
1342+
t.Errorf("error message %q should contain %q", err.Error(), tc.errMsg)
1343+
}
1344+
})
1345+
}
1346+
}
1347+
}

uuid.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
// Package uuid provides implementations of the Universally Unique Identifier
2323
// (UUID), as specified in RFC-9562 (formerly RFC-4122).
2424
//
25-
// RFC-9562[1] provides the specification for versions 1, 3, 4, 5, 6 and 7.
25+
// RFC-9562[1] provides the specification for versions 1, 3, 4, 5, 6, 7 and 8.
2626
//
2727
// DCE 1.1[2] provides the specification for version 2, but version 2 support
2828
// was removed from this package in v4 due to some concerns with the
@@ -60,7 +60,7 @@ const (
6060
V5 // Version 5 (namespace name-based)
6161
V6 // Version 6 (k-sortable timestamp and random data, field-compatible with v1)
6262
V7 // Version 7 (k-sortable timestamp and random data)
63-
_ // Version 8 (k-sortable timestamp, meant for custom implementations) [not implemented]
63+
V8 // Version 8 (custom UUID implementations)
6464
)
6565

6666
// UUID layout variants.

0 commit comments

Comments
 (0)