125 lines
3.2 KiB
Go
125 lines
3.2 KiB
Go
// Package domain contains transport- and storage-independent RVBox rules.
|
|
package domain
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"errors"
|
|
"fmt"
|
|
"io"
|
|
"sync"
|
|
"time"
|
|
|
|
googleuuid "github.com/google/uuid"
|
|
)
|
|
|
|
var (
|
|
ErrInvalidUUIDv7 = errors.New("invalid canonical UUIDv7")
|
|
ErrUUIDTimeRange = errors.New("UUIDv7 time is outside its 48-bit range")
|
|
)
|
|
|
|
const maxUUIDv7UnixMilli = uint64(1<<48 - 1)
|
|
|
|
// UUID is the canonical binary form used by RVBox domain and storage code.
|
|
type UUID [16]byte
|
|
|
|
// String returns the lowercase RFC 9562 canonical representation.
|
|
func (u UUID) String() string {
|
|
return googleuuid.UUID(u).String()
|
|
}
|
|
|
|
// ParseUUIDv7 accepts only the lowercase, hyphenated RFC 9562 form.
|
|
func ParseUUIDv7(value string) (UUID, error) {
|
|
if len(value) != 36 {
|
|
return UUID{}, fmt.Errorf("%w: expected 36 characters", ErrInvalidUUIDv7)
|
|
}
|
|
|
|
parsed, err := googleuuid.Parse(value)
|
|
if err != nil || parsed.String() != value {
|
|
return UUID{}, fmt.Errorf("%w: non-canonical encoding", ErrInvalidUUIDv7)
|
|
}
|
|
if parsed.Version() != 7 || parsed.Variant() != googleuuid.RFC4122 {
|
|
return UUID{}, fmt.Errorf("%w: expected RFC variant and version 7", ErrInvalidUUIDv7)
|
|
}
|
|
|
|
return UUID(parsed), nil
|
|
}
|
|
|
|
// UUIDv7Generator emits lexically increasing UUIDv7 values even when the wall
|
|
// clock stalls or moves backwards. Calls are safe from multiple goroutines.
|
|
type UUIDv7Generator struct {
|
|
mu sync.Mutex
|
|
now func() time.Time
|
|
entropy io.Reader
|
|
started bool
|
|
lastMS uint64
|
|
sequence uint16
|
|
}
|
|
|
|
// NewUUIDv7Generator creates a generator. Nil arguments select the system clock
|
|
// and crypto/rand.Reader. Supplying dependencies keeps boundary tests hermetic.
|
|
func NewUUIDv7Generator(now func() time.Time, entropy io.Reader) *UUIDv7Generator {
|
|
if now == nil {
|
|
now = time.Now
|
|
}
|
|
if entropy == nil {
|
|
entropy = rand.Reader
|
|
}
|
|
return &UUIDv7Generator{now: now, entropy: entropy}
|
|
}
|
|
|
|
var defaultUUIDv7Generator = NewUUIDv7Generator(nil, nil)
|
|
|
|
// NewUUIDv7 uses the process-wide generator.
|
|
func NewUUIDv7() (UUID, error) {
|
|
return defaultUUIDv7Generator.New()
|
|
}
|
|
|
|
// New returns a canonical RFC 9562 UUIDv7.
|
|
func (g *UUIDv7Generator) New() (UUID, error) {
|
|
g.mu.Lock()
|
|
defer g.mu.Unlock()
|
|
|
|
instant := g.now()
|
|
millis := instant.UnixMilli()
|
|
if millis < 0 || uint64(millis) > maxUUIDv7UnixMilli {
|
|
return UUID{}, ErrUUIDTimeRange
|
|
}
|
|
|
|
var random [10]byte
|
|
if _, err := io.ReadFull(g.entropy, random[:]); err != nil {
|
|
return UUID{}, fmt.Errorf("read UUIDv7 entropy: %w", err)
|
|
}
|
|
|
|
wallMS := uint64(millis)
|
|
logicalMS := wallMS
|
|
sequence := uint16(random[0]&0x0f)<<8 | uint16(random[1])
|
|
if g.started && wallMS <= g.lastMS {
|
|
logicalMS = g.lastMS
|
|
sequence = g.sequence + 1
|
|
if sequence > 0x0fff {
|
|
if logicalMS == maxUUIDv7UnixMilli {
|
|
return UUID{}, ErrUUIDTimeRange
|
|
}
|
|
logicalMS++
|
|
sequence = 0
|
|
}
|
|
}
|
|
|
|
var result UUID
|
|
result[0] = byte(logicalMS >> 40)
|
|
result[1] = byte(logicalMS >> 32)
|
|
result[2] = byte(logicalMS >> 24)
|
|
result[3] = byte(logicalMS >> 16)
|
|
result[4] = byte(logicalMS >> 8)
|
|
result[5] = byte(logicalMS)
|
|
result[6] = 0x70 | byte(sequence>>8)
|
|
result[7] = byte(sequence)
|
|
result[8] = 0x80 | random[2]&0x3f
|
|
copy(result[9:], random[3:])
|
|
|
|
g.started = true
|
|
g.lastMS = logicalMS
|
|
g.sequence = sequence
|
|
return result, nil
|
|
}
|