package cart 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 }