From 03e219bb3446dd6635f9df4857688fe9af0d4003 Mon Sep 17 00:00:00 2001 From: Tim Heckman Date: Sat, 28 Aug 2021 00:40:32 -0700 Subject: [PATCH] initial implementation of UUIDv6 and UUIDv7 based on RFC Draft Rev 2 There is currently an RFC draft in progress[1] to add three new UUID formats, versions 6, 7, and 8, that change the internal data format so that the generated UUIDs are k-sortable. Version 8 is a special UUID format designed for future implementations or extensions, and as a result isn't implemented here. Because this implementation is based on a draft RFC, they may be subject to change in later revisions. As such, these new functions/methods include a comment indicating they do not yet fall under the API compatibility guarantee of the project, and changes to meet new revisions to the spec will be done in minor releases. [1] https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-02 --- README.md | 1 + generator.go | 310 ++++++++++++++++++++++++++- generator_test.go | 519 ++++++++++++++++++++++++++++++++++++++++++++++ uuid.go | 37 +++- uuid_test.go | 25 +++ 5 files changed, 887 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 2685a83..4830300 100644 --- a/README.md +++ b/README.md @@ -106,3 +106,4 @@ func main() { * [RFC-4122](https://tools.ietf.org/html/rfc4122) * [DCE 1.1: Authentication and Security Services](http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01) +* [New UUID Formats RFC Draft (Peabody) Rev 02](https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-02) diff --git a/generator.go b/generator.go index 2783d9e..38bf685 100644 --- a/generator.go +++ b/generator.go @@ -26,6 +26,7 @@ import ( "crypto/rand" "crypto/sha1" "encoding/binary" + "errors" "fmt" "hash" "io" @@ -66,12 +67,45 @@ func NewV5(ns UUID, name string) UUID { return DefaultGenerator.NewV5(ns, name) } +// NewV6 returns a k-sortable UUID based on a timestamp and 48 bits of +// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit +// order being adjusted to allow the UUID to be k-sortable. +// +// This is implemented based on revision 02 of the Peabody UUID draft, and may +// be subject to change pending further revisions. Until the final specification +// revision is finished, changes required to implement updates to the spec will +// not be considered a breaking change. They will happen as a minor version +// releases until the spec is final. +func NewV6() (UUID, error) { + return DefaultGenerator.NewV6() +} + +// NewV7 returns a k-sortable UUID based on the current UNIX epoch, with the +// ability to configure the timestamp's precision from millisecond all the way +// to nanosecond. The additional precision is supported by reducing the amount +// of pseudorandom data that makes up the rest of the UUID. +// +// If an unknown Precision argument is passed to this method it will panic. As +// such it's strongly encouraged to use the package-provided constants for this +// value. +// +// This is implemented based on revision 02 of the Peabody UUID draft, and may +// be subject to change pending further revisions. Until the final specification +// revision is finished, changes required to implement updates to the spec will +// not be considered a breaking change. They will happen as a minor version +// releases until the spec is final. +func NewV7(p Precision) (UUID, error) { + return DefaultGenerator.NewV7(p) +} + // Generator provides an interface for generating UUIDs. type Generator interface { NewV1() (UUID, error) NewV3(ns UUID, name string) UUID NewV4() (UUID, error) NewV5(ns UUID, name string) UUID + NewV6() (UUID, error) + NewV7(Precision) (UUID, error) } // Gen is a reference UUID generator based on the specifications laid out in @@ -97,6 +131,10 @@ type Gen struct { lastTime uint64 clockSequence uint16 hardwareAddr [6]byte + + v7LastTime uint64 + v7LastSubsec uint64 + v7ClockSequence uint16 } // interface check -- build will fail if *Gen doesn't satisfy Generator @@ -182,7 +220,39 @@ func (g *Gen) NewV5(ns UUID, name string) UUID { return u } -// getClockSequence returns the epoch and clock sequence. +// NewV6 returns a k-sortable UUID based on a timestamp and 48 bits of +// pseudorandom data. The timestamp in a V6 UUID is the same as V1, with the bit +// order being adjusted to allow the UUID to be k-sortable. +// +// This is implemented based on revision 02 of the Peabody UUID draft, and may +// be subject to change pending further revisions. Until the final specification +// revision is finished, changes required to implement updates to the spec will +// not be considered a breaking change. They will happen as a minor version +// releases until the spec is final. +func (g *Gen) NewV6() (UUID, error) { + var u UUID + + if _, err := io.ReadFull(g.rand, u[10:]); err != nil { + return Nil, err + } + + timeNow, clockSeq, err := g.getClockSequence() + if err != nil { + return Nil, err + } + + binary.BigEndian.PutUint32(u[0:], uint32(timeNow>>28)) // set time_high + binary.BigEndian.PutUint16(u[4:], uint16(timeNow>>12)) // set time_mid + binary.BigEndian.PutUint16(u[6:], uint16(timeNow&0xfff)) // set time_low (minus four version bits) + binary.BigEndian.PutUint16(u[8:], clockSeq&0x3fff) // set clk_seq_hi_res (minus two variant bits) + + u.SetVersion(V6) + u.SetVariant(VariantRFC4122) + + return u, nil +} + +// getClockSequence returns the epoch and clock sequence for V1 and V6 UUIDs. func (g *Gen) getClockSequence() (uint64, uint16, error) { var err error g.clockSequenceOnce.Do(func() { @@ -210,6 +280,244 @@ func (g *Gen) getClockSequence() (uint64, uint16, error) { return timeNow, g.clockSequence, nil } +// Precision is used to configure the V7 generator, to specify how precise the +// timestamp within the UUID should be. +type Precision byte + +const ( + NanosecondPrecision Precision = iota + MicrosecondPrecision + MillisecondPrecision +) + +func (p Precision) String() string { + switch p { + case NanosecondPrecision: + return "nanosecond" + + case MicrosecondPrecision: + return "microsecond" + + case MillisecondPrecision: + return "millisecond" + + default: + return "unknown" + } +} + +// Duration returns the time.Duration for a specific precision. If the Precision +// value is not known, this returns 0. +func (p Precision) Duration() time.Duration { + switch p { + case NanosecondPrecision: + return time.Nanosecond + + case MicrosecondPrecision: + return time.Microsecond + + case MillisecondPrecision: + return time.Millisecond + + default: + return 0 + } +} + +// NewV7 returns a k-sortable UUID based on the current UNIX epoch, with the +// ability to configure the timestamp's precision from millisecond all the way +// to nanosecond. The additional precision is supported by reducing the amount +// of pseudorandom data that makes up the rest of the UUID. +// +// If an unknown Precision argument is passed to this method it will panic. As +// such it's strongly encouraged to use the package-provided constants for this +// value. +// +// This is implemented based on revision 02 of the Peabody UUID draft, and may +// be subject to change pending further revisions. Until the final specification +// revision is finished, changes required to implement updates to the spec will +// not be considered a breaking change. They will happen as a minor version +// releases until the spec is final. +func (g *Gen) NewV7(p Precision) (UUID, error) { + var u UUID + var err error + + switch p { + case NanosecondPrecision: + u, err = g.newV7Nano() + + case MicrosecondPrecision: + u, err = g.newV7Micro() + + case MillisecondPrecision: + u, err = g.newV7Milli() + + default: + panic(fmt.Sprintf("unknown precision value %d", p)) + } + + if err != nil { + return Nil, err + } + + u.SetVersion(V7) + u.SetVariant(VariantRFC4122) + + return u, nil +} + +func (g *Gen) newV7Milli() (UUID, error) { + var u UUID + + if _, err := io.ReadFull(g.rand, u[8:]); err != nil { + return Nil, err + } + + sec, nano, seq, err := g.getV7ClockSequence(MillisecondPrecision) + if err != nil { + return Nil, err + } + + msec := (nano / 1000000) & 0xfff + + d := (sec << 28) // set unixts field + d |= (msec << 16) // set msec field + d |= (uint64(seq) & 0xfff) // set seq field + + binary.BigEndian.PutUint64(u[:], d) + + return u, nil +} + +func (g *Gen) newV7Micro() (UUID, error) { + var u UUID + + if _, err := io.ReadFull(g.rand, u[10:]); err != nil { + return Nil, err + } + + sec, nano, seq, err := g.getV7ClockSequence(MicrosecondPrecision) + if err != nil { + return Nil, err + } + + usec := nano / 1000 + usech := (usec << 4) & 0xfff0000 + usecl := usec & 0xfff + + d := (sec << 28) // set unixts field + d |= usech | usecl // set usec fields + + binary.BigEndian.PutUint64(u[:], d) + binary.BigEndian.PutUint16(u[8:], seq) + + return u, nil +} + +func (g *Gen) newV7Nano() (UUID, error) { + var u UUID + + if _, err := io.ReadFull(g.rand, u[11:]); err != nil { + return Nil, err + } + + sec, nano, seq, err := g.getV7ClockSequence(NanosecondPrecision) + if err != nil { + return Nil, err + } + + nano &= 0x3fffffffff + nanoh := nano >> 26 + nanom := (nano >> 14) & 0xfff + nanol := uint16(nano & 0x3fff) + + d := (sec << 28) // set unixts field + d |= (nanoh << 16) | nanom // set nsec high and med fields + + binary.BigEndian.PutUint64(u[:], d) + binary.BigEndian.PutUint16(u[8:], nanol) // set nsec low field + + u[10] = byte(seq) // set seq field + + return u, nil +} + +const ( + maxSeq14 = (1 << 14) - 1 + maxSeq12 = (1 << 12) - 1 + maxSeq8 = (1 << 8) - 1 +) + +// getV7ClockSequence returns the unix epoch, nanoseconds of current second, and +// the sequence for V7 UUIDs. +func (g *Gen) getV7ClockSequence(p Precision) (epoch uint64, nano uint64, seq uint16, err error) { + g.storageMutex.Lock() + defer g.storageMutex.Unlock() + + tn := g.epochFunc() + unix := uint64(tn.Unix()) + nsec := uint64(tn.Nanosecond()) + + // V7 UUIDs have more precise requirements around how the clock sequence + // value is generated and used. Specifically they require that the sequence + // be zero, unless we've already generated a UUID within this unit of time + // (millisecond, microsecond, or nanosecond) at which point you should + // increment the sequence. Likewise if time has warped backwards for some reason (NTP + // adjustment?), we also increment the clock sequence to reduce the risk of a + // collision. + switch { + case unix < g.v7LastTime: + g.v7ClockSequence++ + + case unix > g.v7LastTime: + g.v7ClockSequence = 0 + + case unix == g.v7LastTime: + switch p { + case NanosecondPrecision: + if nsec <= g.v7LastSubsec { + if g.v7ClockSequence >= maxSeq8 { + return 0, 0, 0, errors.New("generating nanosecond precision UUIDv7s too fast: internal clock sequence would roll over") + } + + g.v7ClockSequence++ + } else { + g.v7ClockSequence = 0 + } + + case MicrosecondPrecision: + if nsec/1000 <= g.v7LastSubsec/1000 { + if g.v7ClockSequence >= maxSeq14 { + return 0, 0, 0, errors.New("generating microsecond precision UUIDv7s too fast: internal clock sequence would roll over") + } + + g.v7ClockSequence++ + } else { + g.v7ClockSequence = 0 + } + + case MillisecondPrecision: + if nsec/1000000 <= g.v7LastSubsec/1000000 { + if g.v7ClockSequence >= maxSeq12 { + return 0, 0, 0, errors.New("generating millisecond precision UUIDv7s too fast: internal clock sequence would roll over") + } + + g.v7ClockSequence++ + } else { + g.v7ClockSequence = 0 + } + + default: + panic(fmt.Sprintf("unknown precision value %d", p)) + } + } + + g.v7LastTime = unix + g.v7LastSubsec = nsec + + return unix, nsec, g.v7ClockSequence, nil +} + // Returns the hardware address. func (g *Gen) getHardwareAddr() ([]byte, error) { var err error diff --git a/generator_test.go b/generator_test.go index 317956f..eaf8203 100644 --- a/generator_test.go +++ b/generator_test.go @@ -24,8 +24,10 @@ package uuid import ( "bytes" "crypto/rand" + "encoding/binary" "fmt" "net" + "strings" "testing" "time" ) @@ -35,6 +37,8 @@ func TestGenerator(t *testing.T) { t.Run("NewV3", testNewV3) t.Run("NewV4", testNewV4) t.Run("NewV5", testNewV5) + t.Run("NewV6", testNewV6) + t.Run("NewV7", testNewV7) } func testNewV1(t *testing.T) { @@ -318,6 +322,492 @@ func testNewV5DifferentNamespaces(t *testing.T) { } } +func testNewV6(t *testing.T) { + t.Run("Basic", testNewV6Basic) + t.Run("DifferentAcrossCalls", testNewV6DifferentAcrossCalls) + t.Run("StaleEpoch", testNewV6StaleEpoch) + t.Run("FaultyRand", testNewV6FaultyRand) + t.Run("ShortRandomRead", testNewV6ShortRandomRead) + t.Run("KSortable", testNewV6KSortable) +} + +func testNewV6Basic(t *testing.T) { + u, err := NewV6() + if err != nil { + t.Fatal(err) + } + if got, want := u.Version(), V6; got != want { + t.Errorf("generated UUID with version %d, want %d", got, want) + } + if got, want := u.Variant(), VariantRFC4122; got != want { + t.Errorf("generated UUID with variant %d, want %d", got, want) + } +} + +func testNewV6DifferentAcrossCalls(t *testing.T) { + u1, err := NewV6() + if err != nil { + t.Fatal(err) + } + u2, err := NewV6() + if err != nil { + t.Fatal(err) + } + if u1 == u2 { + t.Errorf("generated identical UUIDs across calls: %v", u1) + } +} + +func testNewV6StaleEpoch(t *testing.T) { + g := &Gen{ + epochFunc: func() time.Time { + return time.Unix(0, 0) + }, + hwAddrFunc: defaultHWAddrFunc, + rand: rand.Reader, + } + u1, err := g.NewV6() + if err != nil { + t.Fatal(err) + } + u2, err := g.NewV6() + if err != nil { + t.Fatal(err) + } + if u1 == u2 { + t.Errorf("generated identical UUIDs across calls: %v", u1) + } +} + +func testNewV6FaultyRand(t *testing.T) { + t.Run("randomData", func(t *testing.T) { + g := &Gen{ + epochFunc: time.Now, + hwAddrFunc: defaultHWAddrFunc, + rand: &faultyReader{ + readToFail: 0, // fail immediately + }, + } + u, err := g.NewV6() + if err == nil { + t.Fatalf("got %v, want error", u) + } + if u != Nil { + t.Fatalf("got %v on error, want Nil", u) + } + }) + + t.Run("clockSequence", func(t *testing.T) { + g := &Gen{ + epochFunc: time.Now, + hwAddrFunc: defaultHWAddrFunc, + rand: &faultyReader{ + readToFail: 1, // fail immediately + }, + } + u, err := g.NewV6() + if err == nil { + t.Fatalf("got %v, want error", u) + } + if u != Nil { + t.Fatalf("got %v on error, want Nil", u) + } + }) +} + +func testNewV6ShortRandomRead(t *testing.T) { + g := &Gen{ + epochFunc: time.Now, + rand: bytes.NewReader([]byte{42}), + } + u, err := g.NewV6() + if err == nil { + t.Errorf("got %v, nil error", u) + } +} + +func testNewV6KSortable(t *testing.T) { + uuids := make([]UUID, 10) + for i := range uuids { + u, err := NewV6() + testErrCheck(t, "NewV6()", "", err) + + uuids[i] = u + + time.Sleep(time.Microsecond) + } + + for i := 1; i < len(uuids); i++ { + p, n := uuids[i-1], uuids[i] + isLess := p.String() < n.String() + if !isLess { + t.Errorf("uuids[%d] (%s) not less than uuids[%d] (%s)", i-1, p, i, n) + } + } +} + +func testNewV7(t *testing.T) { + t.Run("InvalidPrecision", testNewV7InvalidPrecision) + + for _, p := range []Precision{NanosecondPrecision, MicrosecondPrecision, MillisecondPrecision} { + t.Run(p.String(), func(t *testing.T) { + t.Run("Basic", makeTestNewV7Basic(p)) + t.Run("Basic10000000", makeTestNewV7Basic10000000(p)) + t.Run("DifferentAcrossCalls", makeTestNewV7DifferentAcrossCalls(p)) + t.Run("StaleEpoch", makeTestNewV7StaleEpoch(p)) + t.Run("FaultyRand", makeTestNewV7FaultyRand(p)) + t.Run("ShortRandomRead", makeTestNewV7ShortRandomRead(p)) + t.Run("ClockSequenceBehaviors", makeTestNewV7ClockSequenceBehaviors(p)) + t.Run("KSortable", makeTestNewV7KSortable(p)) + }) + } + + t.Run("ClockSequence", testNewV7ClockSequence) +} + +func testNewV7InvalidPrecision(t *testing.T) { + t.Run("NewV7", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatal("call did not panic") + } + }() + + NewV7(255) + }) + + t.Run("getV7ClockSequence", func(t *testing.T) { + defer func() { + if r := recover(); r == nil { + t.Fatal("expected panic did not occur") + } + }() + + g := NewGen() + g.epochFunc = func() time.Time { + return time.Unix(0, 0) + } + + g.getV7ClockSequence(255) + }) +} + +func makeTestNewV7Basic(p Precision) func(t *testing.T) { + return func(t *testing.T) { + u, err := NewV7(p) + if err != nil { + t.Fatal(err) + } + if got, want := u.Version(), V7; got != want { + t.Errorf("got version %d, want %d", got, want) + } + if got, want := u.Variant(), VariantRFC4122; got != want { + t.Errorf("got variant %d, want %d", got, want) + } + } +} + +func makeTestNewV7Basic10000000(p Precision) func(t *testing.T) { + return func(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + if p == MillisecondPrecision { + t.Skip("skipping test, see: https://github.com/uuid6/uuid6-ietf-draft/issues/40") + } + + g := NewGen() + + for i := 0; i < 10000000; i++ { + u, err := g.NewV7(p) + if err != nil { + t.Fatal(err) + } + if got, want := u.Version(), V7; got != want { + t.Errorf("got version %d, want %d", got, want) + } + if got, want := u.Variant(), VariantRFC4122; got != want { + t.Errorf("got variant %d, want %d", got, want) + } + } + } +} + +func makeTestNewV7DifferentAcrossCalls(p Precision) func(t *testing.T) { + return func(t *testing.T) { + g := NewGen() + + u1, err := g.NewV7(p) + if err != nil { + t.Fatal(err) + } + u2, err := g.NewV7(p) + if err != nil { + t.Fatal(err) + } + if u1 == u2 { + t.Errorf("generated identical UUIDs across calls: %v", u1) + } + } +} + +func makeTestNewV7StaleEpoch(p Precision) func(t *testing.T) { + return func(t *testing.T) { + g := &Gen{ + epochFunc: func() time.Time { + return time.Unix(0, 0) + }, + rand: rand.Reader, + } + u1, err := g.NewV7(p) + if err != nil { + t.Fatal(err) + } + u2, err := g.NewV7(p) + if err != nil { + t.Fatal(err) + } + if u1 == u2 { + t.Errorf("generated identical UUIDs across calls: %v", u1) + } + } +} + +func makeTestNewV7FaultyRand(p Precision) func(t *testing.T) { + return func(t *testing.T) { + g := &Gen{ + epochFunc: time.Now, + rand: &faultyReader{ + readToFail: 0, // fail immediately + }, + } + u, err := g.NewV7(p) + if err == nil { + t.Errorf("got %v, nil error", u) + } + } +} + +func makeTestNewV7ShortRandomRead(p Precision) func(t *testing.T) { + return func(t *testing.T) { + g := &Gen{ + epochFunc: time.Now, + rand: bytes.NewReader([]byte{42}), + } + u, err := g.NewV7(p) + if err == nil { + t.Errorf("got %v, nil error", u) + } + } +} + +func makeTestNewV7KSortable(p Precision) func(t *testing.T) { + return func(t *testing.T) { + uuids := make([]UUID, 10) + for i := range uuids { + u, err := NewV7(p) + testErrCheck(t, "NewV6()", "", err) + + uuids[i] = u + + time.Sleep(p.Duration()) + } + + for i := 1; i < len(uuids); i++ { + p, n := uuids[i-1], uuids[i] + isLess := p.String() < n.String() + if !isLess { + t.Errorf("uuids[%d] (%s) not less than uuids[%d] (%s)", i-1, p, i, n) + } + } + } +} + +// to get 100% code coverage we need to do some glass box testing +func makeTestNewV7ClockSequenceBehaviors(p Precision) func(t *testing.T) { + return func(t *testing.T) { + t.Run("TimeWarp", func(t *testing.T) { + g := NewGen() + tn := time.Now() + unix := uint64(tn.Unix()) + 100 + nsec := uint64(tn.Nanosecond()) + + g.v7LastTime = unix + g.v7LastSubsec = nsec + + _, err := g.NewV7(p) + testErrCheck(t, "g.NewV7()", "", err) + + if g.v7ClockSequence != 1 { + t.Fatalf("g.v7ClockSequence = %d, want 1", g.v7ClockSequence) + } + }) + + t.Run("NominalTime", func(t *testing.T) { + g := NewGen() + g.v7ClockSequence = 100 + + tn := time.Now() + unix := uint64(tn.Unix()) - 100 + nsec := uint64(tn.Nanosecond()) + + g.v7LastTime = unix + g.v7LastSubsec = nsec + + _, err := g.NewV7(p) + testErrCheck(t, "g.NewV7()", "", err) + + if g.v7ClockSequence != 0 { + t.Fatalf("g.v7ClockSequence = %d, want 0", g.v7ClockSequence) + } + }) + + t.Run("Overflow", func(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + wantErrStr := fmt.Sprintf("generating %s precision UUIDv7s too fast: internal clock sequence would roll over", p.String()) + + g := NewGen() + + g.epochFunc = func() time.Time { + return time.Unix(0, 0) + } + + g.v7ClockSequence = maxSeq14 + 1 + g.v7LastTime = uint64(g.epochFunc().Unix()) + g.v7LastSubsec = uint64(g.epochFunc().Nanosecond()) + + _, err := g.NewV7(p) + testErrCheck(t, "g.NewV7()", wantErrStr, err) + }) + } +} + +func testNewV7ClockSequence(t *testing.T) { + if testing.Short() { + t.Skip("skipping test in short mode.") + } + + g := NewGen() + + // hack to try and reduce race conditions based on when the test starts + nsec := time.Now().Nanosecond() + sleepDur := int(time.Second) - nsec + time.Sleep(time.Duration(sleepDur)) + + u1, err := g.NewV7(MillisecondPrecision) + if err != nil { + t.Fatalf("failed to generate V7 UUID #1: %v", err) + } + + u2, err := g.NewV7(MillisecondPrecision) + if err != nil { + t.Fatalf("failed to generate V7 UUID #2: %v", err) + } + + time.Sleep(time.Millisecond) + + u3, err := g.NewV7(MillisecondPrecision) + if err != nil { + t.Fatalf("failed to generate V7 UUID #3: %v", err) + } + + time.Sleep(time.Second) + + u4, err := g.NewV7(MillisecondPrecision) + if err != nil { + t.Fatalf("failed to generate V7 UUID #3: %v", err) + } + + s1 := binary.BigEndian.Uint16(u1[6:8]) & 0xfff + s2 := binary.BigEndian.Uint16(u2[6:8]) & 0xfff + s3 := binary.BigEndian.Uint16(u3[6:8]) & 0xfff + s4 := binary.BigEndian.Uint16(u4[6:8]) & 0xfff + + if s1 != 0 { + t.Errorf("sequence 1 should be zero, was %d", s1) + } + + if s2 != s1+1 { + t.Errorf("sequence 2 expected to be one above sequence 1; seq 1: %d, seq 2: %d", s1, s2) + } + + if s3 != 0 { + t.Errorf("sequence 3 should be zero, was %d", s3) + } + + if s4 != 0 { + t.Errorf("sequence 4 should be zero, was %d", s4) + } +} + +func TestPrecision_String(t *testing.T) { + tests := []struct { + p Precision + want string + }{ + { + p: NanosecondPrecision, + want: "nanosecond", + }, + { + p: MillisecondPrecision, + want: "millisecond", + }, + { + p: MicrosecondPrecision, + want: "microsecond", + }, + { + p: 0xff, + want: "unknown", + }, + } + + for _, tt := range tests { + t.Run(tt.want, func(t *testing.T) { + if got := tt.p.String(); got != tt.want { + t.Errorf("got = %s, want %s", got, tt.want) + } + }) + } +} + +func TestPrecision_Duration(t *testing.T) { + tests := []struct { + p Precision + want time.Duration + }{ + { + p: NanosecondPrecision, + want: time.Nanosecond, + }, + { + p: MillisecondPrecision, + want: time.Millisecond, + }, + { + p: MicrosecondPrecision, + want: time.Microsecond, + }, + { + p: 0xff, + want: 0, + }, + } + + for _, tt := range tests { + t.Run(tt.p.String(), func(t *testing.T) { + if got := tt.p.Duration(); got != tt.want { + t.Errorf("got = %s, want %s", got, tt.want) + } + }) + } +} + func BenchmarkGenerator(b *testing.B) { b.Run("NewV1", func(b *testing.B) { for i := 0; i < b.N; i++ { @@ -353,3 +843,32 @@ func (r *faultyReader) Read(dest []byte) (int, error) { } return rand.Read(dest) } + +// testErrCheck looks to see if errContains is a substring of err.Error(). If +// not, this calls t.Fatal(). It also calls t.Fatal() if there was an error, but +// errContains is empty. Returns true if you should continue running the test, +// or false if you should stop the test. +func testErrCheck(t *testing.T, name string, errContains string, err error) bool { + t.Helper() + + if len(errContains) > 0 { + if err == nil { + t.Fatalf("%s error = , should contain %q", name, errContains) + return false + } + + if errStr := err.Error(); !strings.Contains(errStr, errContains) { + t.Fatalf("%s error = %q, should contain %q", name, errStr, errContains) + return false + } + + return false + } + + if err != nil && len(errContains) == 0 { + t.Fatalf("%s unexpected error: %v", name, err) + return false + } + + return true +} diff --git a/uuid.go b/uuid.go index 78aed6e..54431ab 100644 --- a/uuid.go +++ b/uuid.go @@ -20,11 +20,13 @@ // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. // Package uuid provides implementations of the Universally Unique Identifier -// (UUID), as specified in RFC-4122, +// (UUID), as specified in RFC-4122 and the Peabody RFC Draft (revision 02). // -// RFC-4122[1] provides the specification for versions 1, 3, 4, and 5. +// RFC-4122[1] provides the specification for versions 1, 3, 4, and 5. The +// Peabody UUID RFC Draft[2] provides the specification for the new k-sortable +// UUIDs, versions 6 and 7. // -// DCE 1.1[2] provides the specification for version 2, but version 2 support +// DCE 1.1[3] provides the specification for version 2, but version 2 support // was removed from this package in v4 due to some concerns with the // specification itself. Reading the spec, it seems that it would result in // generating UUIDs that aren't very unique. In having read the spec it seemed @@ -34,7 +36,8 @@ // ensure we were understanding the specification correctly. // // [1] https://tools.ietf.org/html/rfc4122 -// [2] http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01 +// [2] https://datatracker.ietf.org/doc/html/draft-peabody-dispatch-new-uuid-format-02 +// [3] http://pubs.opengroup.org/onlinepubs/9696989899/chap5.htm#tagcjh_08_02_01_01 package uuid import ( @@ -60,6 +63,9 @@ const ( V3 // Version 3 (namespace name-based) V4 // Version 4 (random) V5 // Version 5 (namespace name-based) + V6 // Version 6 (k-sortable timestamp and random data) [peabody draft] + V7 // Version 7 (k-sortable timestamp, with configurable precision, and random data) [peabody draft] + _ // Version 8 (k-sortable timestamp, meant for custom implementations) [peabody draft] [not implemented] ) // UUID layout variants. @@ -88,6 +94,7 @@ const _100nsPerSecond = 10000000 func (t Timestamp) Time() (time.Time, error) { secs := uint64(t) / _100nsPerSecond nsecs := 100 * (uint64(t) % _100nsPerSecond) + return time.Unix(int64(secs)-(epochStart/_100nsPerSecond), int64(nsecs)), nil } @@ -98,12 +105,34 @@ func TimestampFromV1(u UUID) (Timestamp, error) { err := fmt.Errorf("uuid: %s is version %d, not version 1", u, u.Version()) return 0, err } + low := binary.BigEndian.Uint32(u[0:4]) mid := binary.BigEndian.Uint16(u[4:6]) hi := binary.BigEndian.Uint16(u[6:8]) & 0xfff + return Timestamp(uint64(low) + (uint64(mid) << 32) + (uint64(hi) << 48)), nil } +// TimestampFromV6 returns the Timestamp embedded within a V6 UUID. This +// function returns an error if the UUID is any version other than 6. +// +// This is implemented based on revision 01 of the Peabody UUID draft, and may +// be subject to change pending further revisions. Until the final specification +// revision is finished, changes required to implement updates to the spec will +// not be considered a breaking change. They will happen as a minor version +// releases until the spec is final. +func TimestampFromV6(u UUID) (Timestamp, error) { + if u.Version() != 6 { + return 0, fmt.Errorf("uuid: %s is version %d, not version 6", u, u.Version()) + } + + hi := binary.BigEndian.Uint32(u[0:4]) + mid := binary.BigEndian.Uint16(u[4:6]) + low := binary.BigEndian.Uint16(u[6:8]) & 0xfff + + return Timestamp(uint64(low) + (uint64(mid) << 12) + (uint64(hi) << 28)), nil +} + // String parse helpers. var ( urnPrefix = []byte("urn:uuid:") diff --git a/uuid_test.go b/uuid_test.go index a73ecb9..28842c4 100644 --- a/uuid_test.go +++ b/uuid_test.go @@ -219,3 +219,28 @@ func TestTimestampFromV1(t *testing.T) { } } } + +func TestTimestampFromV6(t *testing.T) { + tests := []struct { + u UUID + want Timestamp + wanterr bool + }{ + {u: Must(NewV1()), wanterr: true}, + {u: Must(FromString("00000000-0000-6000-0000-000000000000")), want: 0}, + {u: Must(FromString("1ec06cff-e9b1-621c-8627-ba3fd7e551c9")), want: 138493178941215260}, + {u: Must(FromString("ffffffff-ffff-6fff-ffff-ffffffffffff")), want: Timestamp(1<<60 - 1)}, + } + + for _, tt := range tests { + got, err := TimestampFromV6(tt.u) + + switch { + case tt.wanterr && err == nil: + t.Errorf("TimestampFromV6(%v) want error, got %v", tt.u, got) + + case tt.want != got: + t.Errorf("TimestampFromV6(%v) got %v, want %v", tt.u, got, tt.want) + } + } +}