Skip to content

Commit

Permalink
Update to RFC 9562 (#117)
Browse files Browse the repository at this point in the history
  • Loading branch information
kohenkatz authored May 12, 2024
1 parent 7930207 commit 0bd0b33
Show file tree
Hide file tree
Showing 5 changed files with 115 additions and 139 deletions.
24 changes: 9 additions & 15 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,17 @@
[![Go Report Card](https://goreportcard.com/badge/github.com/gofrs/uuid)](https://goreportcard.com/report/github.com/gofrs/uuid)

Package uuid provides a pure Go implementation of Universally Unique Identifiers
(UUID) variant as defined in RFC-4122. This package supports both the creation
(UUID) variant as defined in RFC-9562. This package supports both the creation
and parsing of UUIDs in different formats.

This package supports the following UUID versions:
* Version 1, based on timestamp and MAC address (RFC-4122)
* Version 3, based on MD5 hashing of a named value (RFC-4122)
* Version 4, based on random numbers (RFC-4122)
* Version 5, based on SHA-1 hashing of a named value (RFC-4122)

This package also supports experimental Universally Unique Identifier implementations based on a
[draft RFC](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html) that updates RFC-4122
* Version 6, a k-sortable id based on timestamp, and field-compatible with v1 (draft-peabody-dispatch-new-uuid-format, RFC-4122)
* Version 7, a k-sortable id based on timestamp (draft-peabody-dispatch-new-uuid-format, RFC-4122)

The v6 and v7 IDs are **not** considered a part of the stable API, and may be subject to behavior or API changes as part of minor releases
to this package. They will be updated as the draft RFC changes, and will become stable if and when the draft RFC is accepted.
* 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

## Project History

Expand Down Expand Up @@ -50,7 +45,7 @@ deficiencies.

## Requirements

This package requires Go 1.17 or later
This package requires Go 1.19 or later

## Usage

Expand Down Expand Up @@ -90,6 +85,5 @@ func main() {

## References

* [RFC-4122](https://tools.ietf.org/html/rfc4122)
* [RFC-9562](https://tools.ietf.org/html/rfc9563) (replaces RFC-4122)
* [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 04](https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#)
150 changes: 72 additions & 78 deletions generator.go
Original file line number Diff line number Diff line change
Expand Up @@ -70,24 +70,12 @@ func NewV5(ns UUID, name string) UUID {
// 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 03 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 millisecond precision
// UNIX epoch and 74 bits of pseudorandom data. It supports single-node batch generation (multiple UUIDs in the same timestamp) with a Monotonic Random counter.
//
// This is implemented based on revision 04 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() (UUID, error) {
return DefaultGenerator.NewV7()
}
Expand All @@ -103,7 +91,7 @@ type Generator interface {
}

// Gen is a reference UUID generator based on the specifications laid out in
// RFC-4122 and DCE 1.1: Authentication and Security Services. This type
// RFC-9562 and DCE 1.1: Authentication and Security Services. This type
// satisfies the Generator interface as defined in this package.
//
// For consumers who are generating V1 UUIDs, but don't want to expose the MAC
Expand Down Expand Up @@ -242,7 +230,7 @@ func (g *Gen) NewV1() (UUID, error) {
copy(u[10:], hardwareAddr)

u.SetVersion(V1)
u.SetVariant(VariantRFC4122)
u.SetVariant(VariantRFC9562)

return u, nil
}
Expand All @@ -251,7 +239,7 @@ func (g *Gen) NewV1() (UUID, error) {
func (g *Gen) NewV3(ns UUID, name string) UUID {
u := newFromHash(md5.New(), ns, name)
u.SetVersion(V3)
u.SetVariant(VariantRFC4122)
u.SetVariant(VariantRFC9562)

return u
}
Expand All @@ -263,7 +251,7 @@ func (g *Gen) NewV4() (UUID, error) {
return Nil, err
}
u.SetVersion(V4)
u.SetVariant(VariantRFC4122)
u.SetVariant(VariantRFC9562)

return u, nil
}
Expand All @@ -272,92 +260,59 @@ func (g *Gen) NewV4() (UUID, error) {
func (g *Gen) NewV5(ns UUID, name string) UUID {
u := newFromHash(sha1.New(), ns, name)
u.SetVersion(V5)
u.SetVariant(VariantRFC4122)
u.SetVariant(VariantRFC9562)

return u
}

// 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 03 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) {
/* https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-6
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 | ver | time_low |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|var| clock_seq | node |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| node |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+ */
var u UUID

if _, err := io.ReadFull(g.rand, u[10:]); err != nil {
return Nil, err
}

timeNow, clockSeq, err := g.getClockSequence(false)
timeNow, _, err := g.getClockSequence(false)
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,V6 and V7 UUIDs.
//
// When useUnixTSMs is false, it uses the Coordinated Universal Time (UTC) as a count of 100-
//
// nanosecond intervals since 00:00:00.00, 15 October 1582 (the date of Gregorian reform to the Christian calendar).
func (g *Gen) getClockSequence(useUnixTSMs bool) (uint64, uint16, error) {
var err error
g.clockSequenceOnce.Do(func() {
buf := make([]byte, 2)
if _, err = io.ReadFull(g.rand, buf); err != nil {
return
}
g.clockSequence = binary.BigEndian.Uint16(buf)
})
if err != nil {
return 0, 0, err
// Based on the RFC 9562 recommendation that this data be fully random and not a monotonic counter,
//we do NOT support batching version 6 UUIDs.
//set clock_seq (14 bits) and node (48 bits) pseudo-random bits (first 2 bits will be overridden)
if _, err = io.ReadFull(g.rand, u[8:]); err != nil {
return Nil, err
}

g.storageMutex.Lock()
defer g.storageMutex.Unlock()
u.SetVersion(V6)

var timeNow uint64
if useUnixTSMs {
timeNow = uint64(g.epochFunc().UnixMilli())
} else {
timeNow = g.getEpoch()
}
// Clock didn't change since last UUID generation.
// Should increase clock sequence.
if timeNow <= g.lastTime {
g.clockSequence++
}
g.lastTime = timeNow
//overwrite first 2 bits of byte[8] for the variant
u.SetVariant(VariantRFC9562)

return timeNow, g.clockSequence, nil
return u, nil
}

// NewV7 returns a k-sortable UUID based on the current millisecond precision
// UNIX epoch and 74 bits of pseudorandom data.
//
// This is implemented based on revision 04 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() (UUID, error) {
var u UUID
/* https://www.ietf.org/archive/id/draft-peabody-dispatch-new-uuid-format-04.html#name-uuid-version-7
0 1 2 3
/* https://datatracker.ietf.org/doc/html/rfc9562#name-uuid-version-7
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 |
Expand All @@ -381,9 +336,11 @@ func (g *Gen) NewV7() (UUID, error) {
u[4] = byte(ms >> 8)
u[5] = byte(ms)

//support batching by using a monotonic pseudo-random sequence
//Support batching by using a monotonic pseudo-random sequence,
//as described in RFC 9562 section 6.2, Method 1.
//The 6th byte contains the version and partially rand_a data.
//We will lose the most significant bites from the clockSeq (with SetVersion), but it is ok, we need the least significant that contains the counter to ensure the monotonic property
//We will lose the most significant bites from the clockSeq (with SetVersion), but it is ok,
//we need the least significant that contains the counter to ensure the monotonic property
binary.BigEndian.PutUint16(u[6:8], clockSeq) // set rand_a with clock seq which is random and monotonic

//override first 4bits of u[6].
Expand All @@ -394,11 +351,48 @@ func (g *Gen) NewV7() (UUID, error) {
return Nil, err
}
//override first 2 bits of byte[8] for the variant
u.SetVariant(VariantRFC4122)
u.SetVariant(VariantRFC9562)

return u, nil
}

// getClockSequence returns the epoch and clock sequence for V1,V6 and V7 UUIDs.
//
// When useUnixTSMs is false, it uses the Coordinated Universal Time (UTC) as a count of 100-
//
// nanosecond intervals since 00:00:00.00, 15 October 1582 (the date of Gregorian reform to the Christian calendar).
func (g *Gen) getClockSequence(useUnixTSMs bool) (uint64, uint16, error) {
var err error
g.clockSequenceOnce.Do(func() {
buf := make([]byte, 2)
if _, err = io.ReadFull(g.rand, buf); err != nil {
return
}
g.clockSequence = binary.BigEndian.Uint16(buf)
})
if err != nil {
return 0, 0, err
}

g.storageMutex.Lock()
defer g.storageMutex.Unlock()

var timeNow uint64
if useUnixTSMs {
timeNow = uint64(g.epochFunc().UnixMilli())
} else {
timeNow = g.getEpoch()
}
// Clock didn't change since last UUID generation.
// Should increase clock sequence.
if timeNow <= g.lastTime {
g.clockSequence++
}
g.lastTime = timeNow

return timeNow, g.clockSequence, nil
}

// Returns the hardware address.
func (g *Gen) getHardwareAddr() ([]byte, error) {
var err error
Expand All @@ -414,7 +408,7 @@ func (g *Gen) getHardwareAddr() ([]byte, error) {
if _, err = io.ReadFull(g.rand, g.hardwareAddr[:]); err != nil {
return
}
// Set multicast bit as recommended by RFC-4122
// Set multicast bit as recommended by RFC-9562
g.hardwareAddr[0] |= 0x01
})
if err != nil {
Expand Down
18 changes: 9 additions & 9 deletions generator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,7 @@ func testNewV1Basic(t *testing.T) {
if got, want := u.Version(), V1; got != want {
t.Errorf("generated UUID with version %d, want %d", got, want)
}
if got, want := u.Variant(), VariantRFC4122; got != want {
if got, want := u.Variant(), VariantRFC9562; got != want {
t.Errorf("generated UUID with variant %d, want %d", got, want)
}
}
Expand All @@ -110,7 +110,7 @@ func testNewV1BasicWithOptions(t *testing.T) {
if got, want := u.Version(), V1; got != want {
t.Errorf("generated UUID with version %d, want %d", got, want)
}
if got, want := u.Variant(), VariantRFC4122; got != want {
if got, want := u.Variant(), VariantRFC9562; got != want {
t.Errorf("generated UUID with variant %d, want %d", got, want)
}
}
Expand Down Expand Up @@ -249,7 +249,7 @@ func testNewV3Basic(t *testing.T) {
if got, want := u.Version(), V3; got != want {
t.Errorf("NewV3(%v, %q): got version %d, want %d", ns, name, got, want)
}
if got, want := u.Variant(), VariantRFC4122; got != want {
if got, want := u.Variant(), VariantRFC9562; got != want {
t.Errorf("NewV3(%v, %q): got variant %d, want %d", ns, name, got, want)
}
want := "5df41881-3aed-3515-88a7-2f4a814cf09e"
Expand Down Expand Up @@ -296,7 +296,7 @@ func testNewV4Basic(t *testing.T) {
if got, want := u.Version(), V4; got != want {
t.Errorf("got version %d, want %d", got, want)
}
if got, want := u.Variant(), VariantRFC4122; got != want {
if got, want := u.Variant(), VariantRFC9562; got != want {
t.Errorf("got variant %d, want %d", got, want)
}
}
Expand Down Expand Up @@ -383,7 +383,7 @@ func testNewV5Basic(t *testing.T) {
if got, want := u.Version(), V5; got != want {
t.Errorf("NewV5(%v, %q): got version %d, want %d", ns, name, got, want)
}
if got, want := u.Variant(), VariantRFC4122; got != want {
if got, want := u.Variant(), VariantRFC9562; got != want {
t.Errorf("NewV5(%v, %q): got variant %d, want %d", ns, name, got, want)
}
want := "2ed6657d-e927-568b-95e1-2665a8aea6a2"
Expand Down Expand Up @@ -433,7 +433,7 @@ func testNewV6Basic(t *testing.T) {
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 {
if got, want := u.Variant(), VariantRFC9562; got != want {
t.Errorf("generated UUID with variant %d, want %d", got, want)
}
}
Expand Down Expand Up @@ -624,7 +624,7 @@ func makeTestNewV7Basic() func(t *testing.T) {
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 {
if got, want := u.Variant(), VariantRFC9562; got != want {
t.Errorf("got variant %d, want %d", got, want)
}
}
Expand Down Expand Up @@ -652,7 +652,7 @@ func makeTestNewV7TestVector() func(t *testing.T) {
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 {
if got, want := u.Variant(), VariantRFC9562; got != want {
t.Errorf("got variant %d, want %d", got, want)
}
if got, want := u.String()[:15], "017f22e2-79b0-7"; got != want {
Expand All @@ -677,7 +677,7 @@ func makeTestNewV7Basic10000000() func(t *testing.T) {
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 {
if got, want := u.Variant(), VariantRFC9562; got != want {
t.Errorf("got variant %d, want %d", got, want)
}
}
Expand Down
Loading

0 comments on commit 0bd0b33

Please sign in to comment.