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 }