202 lines
4.3 KiB
Go
202 lines
4.3 KiB
Go
package uuidv7
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"net/url"
|
|
"strings"
|
|
"time"
|
|
)
|
|
|
|
// New returns a UUIDv7 (RFC 9562) as a 16-byte array.
|
|
func New() ([16]byte, error) {
|
|
var out [16]byte
|
|
|
|
var r [10]byte
|
|
if _, err := rand.Read(r[:]); err != nil {
|
|
return out, err
|
|
}
|
|
|
|
// 48-bit Unix epoch milliseconds
|
|
ms := uint64(time.Now().UnixNano() / int64(time.Millisecond))
|
|
out[0] = byte(ms >> 40)
|
|
out[1] = byte(ms >> 32)
|
|
out[2] = byte(ms >> 24)
|
|
out[3] = byte(ms >> 16)
|
|
out[4] = byte(ms >> 8)
|
|
out[5] = byte(ms)
|
|
|
|
// Version (0111) in high 4 bits of byte 6
|
|
out[6] = (r[0] & 0x0f) | 0x70
|
|
out[7] = r[1]
|
|
|
|
// Variant (10) in high 2 bits of byte 8
|
|
out[8] = (r[2] & 0x3f) | 0x80
|
|
out[9] = r[3]
|
|
out[10] = r[4]
|
|
out[11] = r[5]
|
|
out[12] = r[6]
|
|
out[13] = r[7]
|
|
out[14] = r[8]
|
|
out[15] = r[9]
|
|
|
|
return out, nil
|
|
}
|
|
|
|
// MustNew panics if UUID generation fails.
|
|
func MustNew() [16]byte {
|
|
u, err := New()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return u
|
|
}
|
|
|
|
// Bytes returns a new UUIDv7 as []byte for BINARY(16) columns.
|
|
func Bytes() ([]byte, error) {
|
|
u, err := New()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
b := make([]byte, 16)
|
|
copy(b, u[:])
|
|
return b, nil
|
|
}
|
|
|
|
// MustBytes panics if UUID generation fails.
|
|
func MustBytes() []byte {
|
|
u := MustNew()
|
|
b := make([]byte, 16)
|
|
copy(b, u[:])
|
|
return b
|
|
}
|
|
|
|
// String formats a UUID as a canonical string (8-4-4-4-12).
|
|
func String(u [16]byte) (string, error) {
|
|
if len(u) != 16 {
|
|
return "", errors.New("invalid uuid length")
|
|
}
|
|
dst := make([]byte, 36)
|
|
hex.Encode(dst[0:8], u[0:4])
|
|
dst[8] = '-'
|
|
hex.Encode(dst[9:13], u[4:6])
|
|
dst[13] = '-'
|
|
hex.Encode(dst[14:18], u[6:8])
|
|
dst[18] = '-'
|
|
hex.Encode(dst[19:23], u[8:10])
|
|
dst[23] = '-'
|
|
hex.Encode(dst[24:36], u[10:16])
|
|
return string(dst), nil
|
|
}
|
|
|
|
// BytesToString formats a UUID stored in []byte as a canonical string.
|
|
func BytesToString(b []byte) (string, error) {
|
|
if len(b) != 16 {
|
|
return "", errors.New("invalid uuid length")
|
|
}
|
|
var u [16]byte
|
|
copy(u[:], b)
|
|
return String(u)
|
|
}
|
|
|
|
// ParseString parses a canonical UUID string into []byte (16 bytes).
|
|
func ParseString(s string) ([]byte, error) {
|
|
if len(s) != 36 {
|
|
return nil, errors.New("invalid uuid string length")
|
|
}
|
|
buf := make([]byte, 16)
|
|
_, err := hex.Decode(buf[0:4], []byte(s[0:8]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = hex.Decode(buf[4:6], []byte(s[9:13]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = hex.Decode(buf[6:8], []byte(s[14:18]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = hex.Decode(buf[8:10], []byte(s[19:23]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
_, err = hex.Decode(buf[10:16], []byte(s[24:36]))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return buf, nil
|
|
}
|
|
|
|
// ParsePathLikeString parses a UUID from a path-like value. This is useful for
|
|
// URL params that may arrive escaped, wrapped in a full URL, or with a trailing slash.
|
|
func ParsePathLikeString(raw string) ([]byte, error) {
|
|
for _, candidate := range candidateUUIDStrings(raw) {
|
|
if parsed, err := ParseString(candidate); err == nil {
|
|
return parsed, nil
|
|
}
|
|
}
|
|
return nil, errors.New("invalid uuid string")
|
|
}
|
|
|
|
func candidateUUIDStrings(raw string) []string {
|
|
raw = strings.TrimSpace(raw)
|
|
if raw == "" {
|
|
return nil
|
|
}
|
|
|
|
candidates := make([]string, 0, 6)
|
|
seen := make(map[string]struct{}, 6)
|
|
push := func(value string) {
|
|
value = strings.TrimSpace(value)
|
|
if value == "" {
|
|
return
|
|
}
|
|
if _, ok := seen[value]; ok {
|
|
return
|
|
}
|
|
seen[value] = struct{}{}
|
|
candidates = append(candidates, value)
|
|
}
|
|
|
|
push(raw)
|
|
if decoded, err := url.PathUnescape(raw); err == nil && decoded != raw {
|
|
push(decoded)
|
|
}
|
|
if decoded, err := url.QueryUnescape(raw); err == nil && decoded != raw {
|
|
push(decoded)
|
|
}
|
|
|
|
for i := 0; i < len(candidates); i++ {
|
|
value := candidates[i]
|
|
value = strings.TrimSuffix(value, "/")
|
|
push(value)
|
|
|
|
if idx := strings.IndexAny(value, "?#"); idx >= 0 {
|
|
push(value[:idx])
|
|
}
|
|
if idx := strings.LastIndex(value, "/"); idx >= 0 && idx+1 < len(value) {
|
|
push(value[idx+1:])
|
|
}
|
|
if idx := strings.LastIndex(value, "="); idx >= 0 && idx+1 < len(value) {
|
|
push(value[idx+1:])
|
|
}
|
|
for _, part := range strings.Split(value, "/") {
|
|
push(part)
|
|
}
|
|
for _, part := range strings.FieldsFunc(value, func(r rune) bool {
|
|
switch r {
|
|
case '/', '?', '#', '&', '=':
|
|
return true
|
|
default:
|
|
return false
|
|
}
|
|
}) {
|
|
push(part)
|
|
}
|
|
}
|
|
|
|
return candidates
|
|
}
|