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