Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

add uuid version 6 and 7 #139

Merged
merged 2 commits into from
Nov 23, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
21 changes: 16 additions & 5 deletions time.go
Original file line number Diff line number Diff line change
Expand Up @@ -108,12 +108,23 @@ func setClockSequence(seq int) {
}

// Time returns the time in 100s of nanoseconds since 15 Oct 1582 encoded in
// uuid. The time is only defined for version 1 and 2 UUIDs.
// uuid. The time is only defined for version 1, 2 and 6, 7 UUIDs.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

1, 2, 6 and 7.

func (uuid UUID) Time() Time {
time := int64(binary.BigEndian.Uint32(uuid[0:4]))
time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32
time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48
return Time(time)
var t Time
switch uuid.Version() {
case 6:
time := binary.BigEndian.Uint64(uuid[:8]) // Ignore uuid[6] version b0110
t = Time(time)
case 7:
time := binary.BigEndian.Uint64(uuid[:8])
t = Time((time>>16)*10000 + g1582ns100)
default: // forward compatible
time := int64(binary.BigEndian.Uint32(uuid[0:4]))
time |= int64(binary.BigEndian.Uint16(uuid[4:6])) << 32
time |= int64(binary.BigEndian.Uint16(uuid[6:8])&0xfff) << 48
t = Time(time)
}
return t
}

// ClockSequence returns the clock sequence encoded in uuid.
Expand Down
114 changes: 114 additions & 0 deletions uuid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,9 @@ var tests = []test{
{"f47ac10b58cc037285670e02b2c3d479", 0, RFC4122, true},
{"f47ac10b58cc037285670e02b2c3d4790", 0, Invalid, false},
{"f47ac10b58cc037285670e02b2c3d47", 0, Invalid, false},

{"01ee836c-e7c9-619d-929a-525400475911", 6, RFC4122, true},
{"018bd12c-58b0-7683-8a5b-8752d0e86651", 7, RFC4122, true},
}

var constants = []struct {
Expand Down Expand Up @@ -748,3 +751,114 @@ func BenchmarkUUIDs_Strings(b *testing.B) {
uuids.Strings()
}
}

func TestVersion6(t *testing.T) {
uuid1, err := NewV6()
if err != nil {
t.Fatalf("could not create UUID: %v", err)
}
uuid2, err := NewV6()
if err != nil {
t.Fatalf("could not create UUID: %v", err)
}

if uuid1 == uuid2 {
t.Errorf("%s:duplicate uuid", uuid1)
}
if v := uuid1.Version(); v != 6 {
t.Errorf("%s: version %s expected 6", uuid1, v)
}
if v := uuid2.Version(); v != 6 {
t.Errorf("%s: version %s expected 6", uuid2, v)
}
n1 := uuid1.NodeID()
n2 := uuid2.NodeID()
if !bytes.Equal(n1, n2) {
t.Errorf("Different nodes %x != %x", n1, n2)
}
t1 := uuid1.Time()
t2 := uuid2.Time()
q1 := uuid1.ClockSequence()
q2 := uuid2.ClockSequence()

switch {
case t1 == t2 && q1 == q2:
t.Error("time stopped")
case t1 > t2 && q1 == q2:
t.Error("time reversed")
case t1 < t2 && q1 != q2:
t.Error("clock sequence changed unexpectedly")
}
}

// uuid v7 time is only unix milliseconds, so
// uuid1.Time() == uuid2.Time() is right, but uuid1 must != uuid2
func TestVersion7(t *testing.T) {
SetRand(nil)
m := make(map[string]bool)
for x := 1; x < 32; x++ {
uuid, err := NewV7()
if err != nil {
t.Fatalf("could not create UUID: %v", err)
}
s := uuid.String()
if m[s] {
t.Errorf("NewV7 returned duplicated UUID %s", s)
}
m[s] = true
if v := uuid.Version(); v != 7 {
t.Errorf("UUID of version %s", v)
}
if uuid.Variant() != RFC4122 {
t.Errorf("UUID is variant %d", uuid.Variant())
}
}
}

// uuid v7 time is only unix milliseconds, so
// uuid1.Time() == uuid2.Time() is right, but uuid1 must != uuid2
func TestVersion7_pooled(t *testing.T) {
SetRand(nil)
EnableRandPool()
defer DisableRandPool()

m := make(map[string]bool)
for x := 1; x < 128; x++ {
uuid, err := NewV7()
if err != nil {
t.Fatalf("could not create UUID: %v", err)
}
s := uuid.String()
if m[s] {
t.Errorf("NewV7 returned duplicated UUID %s", s)
}
m[s] = true
if v := uuid.Version(); v != 7 {
t.Errorf("UUID of version %s", v)
}
if uuid.Variant() != RFC4122 {
t.Errorf("UUID is variant %d", uuid.Variant())
}
}
}

func TestVersion7FromReader(t *testing.T) {
myString := "8059ddhdle77cb52"
r := bytes.NewReader([]byte(myString))
r2 := bytes.NewReader([]byte(myString))
uuid1, err := NewV7FromReader(r)
if err != nil {
t.Errorf("failed generating UUID from a reader")
}
_, err = NewV7FromReader(r)
if err == nil {
t.Errorf("expecting an error as reader has no more bytes. Got uuid. NewV7FromReader may not be using the provided reader")
}
uuid3, err := NewV7FromReader(r2)
if err != nil {
t.Errorf("failed generating UUID from a reader")
}
if uuid1 != uuid3 {
t.Errorf("expected duplicates, got %q and %q", uuid1, uuid3)
}
}
56 changes: 56 additions & 0 deletions version6.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
// Copyright 2023 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"

// UUID version 6 is a field-compatible version of UUIDv1, reordered for improved DB locality.
// It is expected that UUIDv6 will primarily be used in contexts where there are existing v1 UUIDs.
// Systems that do not involve legacy UUIDv1 SHOULD consider using UUIDv7 instead.
//
// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#uuidv6
//
// NewV6 returns a Version 6 UUID based on the current NodeID and clock
// sequence, and the current time. If the NodeID has not been set by SetNodeID
// or SetNodeInterface then it will be set automatically. If the NodeID cannot
// be set NewV6 set NodeID is random bits automatically . If clock sequence has not been set by
// SetClockSequence then it will be set automatically. If GetTime fails to
// return the current NewV6 returns Nil and an error.
func NewV6() (UUID, error) {
var uuid UUID
now, seq, err := GetTime()
if err != nil {
return uuid, err
}

/*
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time_high |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| time_mid | time_low_and_version |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|clk_seq_hi_res | clk_seq_low | node (0-1) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| node (2-5) |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/

binary.BigEndian.PutUint64(uuid[0:], uint64(now))
binary.BigEndian.PutUint16(uuid[8:], seq)

uuid[6] = 0x60 | (uuid[6] & 0x0F)
uuid[8] = 0x80 | (uuid[8] & 0x3F)

nodeMu.Lock()
if nodeID == zeroID {
setNodeInterface("")
}
copy(uuid[10:], nodeID[:])
nodeMu.Unlock()

return uuid, nil
}
74 changes: 74 additions & 0 deletions version7.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
// Copyright 2023 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 (
"io"
)

// UUID version 7 features a time-ordered value field derived from the widely
// implemented and well known Unix Epoch timestamp source,
// the number of milliseconds seconds since midnight 1 Jan 1970 UTC, leap seconds excluded.
// As well as improved entropy characteristics over versions 1 or 6.
//
// see https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-03#name-uuid-version-7
//
// Implementations SHOULD utilize UUID version 7 over UUID version 1 and 6 if possible.
//
// NewV7 returns a Version 7 UUID based on the current time(Unix Epoch).
// Uses the randomness pool if it was enabled with EnableRandPool.
// NewV7 use NewRandom fill random bits, if error, NewV7 returns Nil and an error
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is no need to specify the internal implementation. Line 22 should be:

// On error, NewV7 returns Nil and an error.

func NewV7() (UUID, error) {
uuid, err := NewRandom()
if err != nil {
return uuid, err
}
makeV7(uuid[:])
return uuid, nil
}

// NewV7FromReader returns a Version 7 UUID based on the current time(Unix Epoch).
// it use NewRandomFromReader fill random bits, if error, NewV7FromReader returns Nil and an error
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

// On error, NewV7FromReader returns Nil and an error.

func NewV7FromReader(r io.Reader) (UUID, error) {
uuid, err := NewRandomFromReader(r)
if err != nil {
return uuid, err
}

makeV7(uuid[:])
return uuid, nil
}

// makeV7 fill 48 bits time (uuid[0] - uuid[5]), set version b0111 (uuid[6])
// uuid[8] already has the right version number (Variant is 10)
// see function NewV7 and NewV7FromReader
func makeV7(uuid []byte) {
/*
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| unix_ts_ms | ver | rand_a |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| rand_b |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
*/
_ = uuid[15] // bounds check

t := timeNow().UnixMilli()

uuid[0] = byte(t >> 40)
uuid[1] = byte(t >> 32)
uuid[2] = byte(t >> 24)
uuid[3] = byte(t >> 16)
uuid[4] = byte(t >> 8)
uuid[5] = byte(t)

uuid[6] = 0x70 | (uuid[6] & 0x0F)
// uuid[8] has already has right version
}