160 lines
3.9 KiB
Go
160 lines
3.9 KiB
Go
package main
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/json"
|
|
"fmt"
|
|
)
|
|
|
|
// cart_id.go
|
|
//
|
|
// Breaking change:
|
|
// Unified cart identifier as a raw 64-bit unsigned integer (type CartId uint64).
|
|
// External textual representation: base62 (0-9 A-Z a-z), shortest possible
|
|
// encoding for 64 bits (max 11 characters, since 62^11 > 2^64).
|
|
//
|
|
// Rationale:
|
|
// - Replaces legacy fixed [16]byte padded string and transitional CartID wrapper.
|
|
// - Provides compact, URL/cookie-friendly identifiers.
|
|
// - O(1) hashing and minimal memory footprint.
|
|
// - 64 bits of crypto randomness => negligible collision probability at realistic scale.
|
|
//
|
|
// Public API:
|
|
// type CartId uint64
|
|
// func NewCartId() (CartId, error)
|
|
// func MustNewCartId() CartId
|
|
// func ParseCartId(string) (CartId, bool)
|
|
// func MustParseCartId(string) CartId
|
|
// (CartId).String() string
|
|
// (CartId).MarshalJSON() / UnmarshalJSON()
|
|
//
|
|
// NOTE:
|
|
// All legacy helpers (UpgradeLegacyCartId, Fallback hashing, Canonicalize variants,
|
|
// CartIDToLegacy, LegacyToCartID) have been removed as part of the breaking change.
|
|
//
|
|
// ---------------------------------------------------------------------------
|
|
|
|
type CartId uint64
|
|
|
|
const base62Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
|
|
|
|
// Reverse lookup (0xFF marks invalid)
|
|
var base62Rev [256]byte
|
|
|
|
func init() {
|
|
for i := range base62Rev {
|
|
base62Rev[i] = 0xFF
|
|
}
|
|
for i := 0; i < len(base62Alphabet); i++ {
|
|
base62Rev[base62Alphabet[i]] = byte(i)
|
|
}
|
|
}
|
|
|
|
// String returns the canonical base62 encoding of the 64-bit id.
|
|
func (id CartId) String() string {
|
|
return encodeBase62(uint64(id))
|
|
}
|
|
|
|
// MarshalJSON encodes the cart id as a JSON string.
|
|
func (id CartId) MarshalJSON() ([]byte, error) {
|
|
return json.Marshal(id.String())
|
|
}
|
|
|
|
// UnmarshalJSON decodes a cart id from a JSON string containing base62 text.
|
|
func (id *CartId) UnmarshalJSON(data []byte) error {
|
|
var s string
|
|
if err := json.Unmarshal(data, &s); err != nil {
|
|
return err
|
|
}
|
|
parsed, ok := ParseCartId(s)
|
|
if !ok {
|
|
return fmt.Errorf("invalid cart id: %q", s)
|
|
}
|
|
*id = parsed
|
|
return nil
|
|
}
|
|
|
|
// NewCartId generates a new cryptographically random non-zero 64-bit id.
|
|
func NewCartId() (CartId, error) {
|
|
var b [8]byte
|
|
if _, err := rand.Read(b[:]); err != nil {
|
|
return 0, fmt.Errorf("NewCartId: %w", err)
|
|
}
|
|
u := (uint64(b[0]) << 56) |
|
|
(uint64(b[1]) << 48) |
|
|
(uint64(b[2]) << 40) |
|
|
(uint64(b[3]) << 32) |
|
|
(uint64(b[4]) << 24) |
|
|
(uint64(b[5]) << 16) |
|
|
(uint64(b[6]) << 8) |
|
|
uint64(b[7])
|
|
if u == 0 {
|
|
// Extremely unlikely; regenerate once to avoid "0" identifier if desired.
|
|
return NewCartId()
|
|
}
|
|
return CartId(u), nil
|
|
}
|
|
|
|
// MustNewCartId panics if generation fails.
|
|
func MustNewCartId() CartId {
|
|
id, err := NewCartId()
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
return id
|
|
}
|
|
|
|
// ParseCartId parses a base62 string into a CartId.
|
|
// Returns (0,false) for invalid input.
|
|
func ParseCartId(s string) (CartId, bool) {
|
|
// Accept length 1..11 (11 sufficient for 64 bits). Reject >11 immediately.
|
|
// Provide a slightly looser upper bound (<=16) only if you anticipate future
|
|
// extensions; here we stay strict.
|
|
if len(s) == 0 || len(s) > 11 {
|
|
return 0, false
|
|
}
|
|
u, ok := decodeBase62(s)
|
|
if !ok {
|
|
return 0, false
|
|
}
|
|
return CartId(u), true
|
|
}
|
|
|
|
// MustParseCartId panics on invalid base62 input.
|
|
func MustParseCartId(s string) CartId {
|
|
id, ok := ParseCartId(s)
|
|
if !ok {
|
|
panic(fmt.Sprintf("invalid cart id: %q", s))
|
|
}
|
|
return id
|
|
}
|
|
|
|
// encodeBase62 converts a uint64 to base62 (shortest form).
|
|
func encodeBase62(u uint64) string {
|
|
if u == 0 {
|
|
return "0"
|
|
}
|
|
var buf [11]byte
|
|
i := len(buf)
|
|
for u > 0 {
|
|
i--
|
|
buf[i] = base62Alphabet[u%62]
|
|
u /= 62
|
|
}
|
|
return string(buf[i:])
|
|
}
|
|
|
|
// decodeBase62 converts base62 text to uint64.
|
|
func decodeBase62(s string) (uint64, bool) {
|
|
var v uint64
|
|
for i := 0; i < len(s); i++ {
|
|
c := s[i]
|
|
d := base62Rev[c]
|
|
if d == 0xFF {
|
|
return 0, false
|
|
}
|
|
v = v*62 + uint64(d)
|
|
}
|
|
return v, true
|
|
}
|