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

Update to RFC 9562 #117

Merged
merged 11 commits into from May 12, 2024
24 changes: 9 additions & 15 deletions README.md
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
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
Copy link
Member

Choose a reason for hiding this comment

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

Nit: maybe include the source link in the godoc comment so that someone doesn't have to dive into the code to find it.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@dylan-bourque I thought about that, but I think it's too much information that most users won't care about. The link is only there to show the origin of the bit layout below it, which is not something that needs to be in the godoc. The README has links to the full RFC, and that shows up in the godoc already.

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,
kohenkatz marked this conversation as resolved.
Show resolved Hide resolved
//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
Copy link
Member

Choose a reason for hiding this comment

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

Same nit here

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
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