From b35aa6a595277504b1ec94c520d4091ec050b9d5 Mon Sep 17 00:00:00 2001 From: MikeWang Date: Fri, 24 Nov 2023 07:50:18 +0800 Subject: [PATCH] add uuid version 6 and 7 (#139) * add uuid version 6 and 7 * fix comment --- time.go | 21 +++++++--- uuid_test.go | 114 +++++++++++++++++++++++++++++++++++++++++++++++++++ version6.go | 56 +++++++++++++++++++++++++ version7.go | 75 +++++++++++++++++++++++++++++++++ 4 files changed, 261 insertions(+), 5 deletions(-) create mode 100644 version6.go create mode 100644 version7.go diff --git a/time.go b/time.go index e6ef06c..c351129 100644 --- a/time.go +++ b/time.go @@ -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, 6 and 7 UUIDs. 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. diff --git a/uuid_test.go b/uuid_test.go index 6f22e8a..fc97f2d 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -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 { @@ -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) + } +} diff --git a/version6.go b/version6.go new file mode 100644 index 0000000..339a959 --- /dev/null +++ b/version6.go @@ -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 +} diff --git a/version7.go b/version7.go new file mode 100644 index 0000000..ba9dd5e --- /dev/null +++ b/version7.go @@ -0,0 +1,75 @@ +// 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. +// 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. +// 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 +}