more refactoring
Some checks failed
Build and Publish / BuildAndDeploy (push) Successful in 3m6s
Build and Publish / BuildAndDeployAmd64 (push) Has been cancelled

This commit is contained in:
matst80
2025-10-10 13:22:36 +00:00
parent c30be581cd
commit 159253b8b0
13 changed files with 1475 additions and 295 deletions

327
cart_id.go Normal file
View File

@@ -0,0 +1,327 @@
package main
import (
"crypto/rand"
"encoding/binary"
"errors"
"fmt"
"strings"
)
// cart_id.go
//
// Compact CartID implementation using 64 bits of cryptographic randomness,
// base62 encoded (0-9 A-Z a-z). Typical length is 11 characters (since 62^11 > 2^64).
//
// Motivation:
// * Shorter identifiers for cookies / URLs than legacy padded 16-byte CartId
// * O(1) hashing (raw uint64) for consistent hashing ring integration
// * Extremely low collision probability (birthday bound negligible at scale)
//
// Backward Compatibility Strategy (Phased):
// Phase 1: Introduce CartID helpers while continuing to accept legacy CartId.
// Phase 2: Internally migrate maps to key by uint64 (CartID.Raw()).
// Phase 3: Canonicalize all inbound IDs to short base62; reissue Set-Cart-Id header.
//
// NOTE:
// The legacy type `CartId [16]byte` is still present elsewhere; helper
// UpgradeLegacyCartId bridges that representation to the new form without
// breaking deterministic mapping for existing carts.
//
// Security / Predictability:
// Uses crypto/rand for generation. If ever required, you can layer an
// HMAC-based derivation for additional secrecy. Current approach already
// provides 64 bits of entropy (brute force infeasible for practical risk).
//
// Future Extensions:
// * Time-sortable IDs: prepend a 48-bit timestamp field and encode 80 bits.
// * Add metrics counters for: generated_new, parsed_existing, legacy_fallback.
// * Add a pool of pre-generated IDs for ultra-low-latency hot paths (rarely needed).
//
// Public Surface Summary:
// NewCartID() (CartID, error)
// ParseCartID(string) (CartID, bool)
// FallbackFromString(string) CartID
// UpgradeLegacyCartId(CartId) CartID
// CanonicalizeIncoming(string) (CartID, bool /*wasGenerated*/, error)
//
// Encoding Details:
// encodeBase62 / decodeBase62 maintain a stable alphabet. DO NOT change
// alphabet order once IDs are in circulation, or previously issued IDs
// will change meaning.
//
// Zero Values:
// The zero value CartID{} has raw=0, txt="0". Treat it as valid but
// usually you will call NewCartID instead.
//
// ---------------------------------------------------------------------------
const base62Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
// Precomputed reverse lookup table for decode (255 = 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)
}
}
// CartID is the compact representation of a cart identifier.
// raw: 64-bit entropy (also used directly for consistent hashing).
// txt: cached base62 textual form.
type CartID struct {
raw uint64
txt string
}
// String returns the canonical base62 encoded ID.
func (c CartID) String() string {
if c.txt == "" { // lazily encode if constructed manually
c.txt = encodeBase62(c.raw)
}
return c.txt
}
// Raw returns the 64-bit numeric value (useful for hashing / ring lookup).
func (c CartID) Raw() uint64 {
return c.raw
}
// IsZero reports whether this CartID is the zero value.
func (c CartID) IsZero() bool {
return c.raw == 0
}
// NewCartID generates a new cryptographically random 64-bit ID.
func NewCartID() (CartID, error) {
var b [8]byte
if _, err := rand.Read(b[:]); err != nil {
return CartID{}, fmt.Errorf("NewCartID: %w", err)
}
u := binary.BigEndian.Uint64(b[:])
// Reject zero if you want to avoid ever producing "0" (optional).
if u == 0 {
// Extremely unlikely; recurse once.
return NewCartID()
}
return CartID{raw: u, txt: encodeBase62(u)}, nil
}
// MustNewCartID panics on failure (suitable for tests / initialization).
func MustNewCartID() CartID {
id, err := NewCartID()
if err != nil {
panic(err)
}
return id
}
// ParseCartID attempts to parse a base62 canonical ID.
// Returns (id, true) if fully valid; (zero, false) otherwise.
func ParseCartID(s string) (CartID, bool) {
if len(s) == 0 {
return CartID{}, false
}
// Basic length sanity; allow a bit of headroom for future timestamp variant.
if len(s) > 16 {
return CartID{}, false
}
u, ok := decodeBase62(s)
if !ok {
return CartID{}, false
}
return CartID{raw: u, txt: s}, true
}
// FallbackFromString produces a deterministic CartID from arbitrary input
// using a 64-bit FNV-1a hash. This allows legacy or malformed IDs to map
// consistently into the new scheme (collision probability still low).
func FallbackFromString(s string) CartID {
const (
offset64 = 1469598103934665603
prime64 = 1099511628211
)
h := uint64(offset64)
for i := 0; i < len(s); i++ {
h ^= uint64(s[i])
h *= prime64
}
return CartID{raw: h, txt: encodeBase62(h)}
}
// UpgradeLegacyCartId converts the old 16-byte CartId (padded) to CartID
// by hashing its trimmed string form. Keeps stable mapping across restarts.
func UpgradeLegacyCartId(old CartId) CartID {
return FallbackFromString(old.String())
}
// CanonicalizeIncoming normalizes user-provided ID strings.
// Behavior:
//
// Empty string -> generate new ID (wasGenerated = true)
// Valid base62 -> parse and return (wasGenerated = false)
// Anything else -> fallback deterministic hash (wasGenerated = false)
//
// Errors only occur if crypto/rand fails during generation.
func CanonicalizeIncoming(s string) (CartID, bool, error) {
if s == "" {
id, err := NewCartID()
return id, true, err
}
if cid, ok := ParseCartID(s); ok {
return cid, false, nil
}
// Legacy heuristic: if length == 16 and contains non-base62 chars, treat as legacy padded ID.
if len(s) == 16 && !isAllBase62(s) {
return FallbackFromString(strings.TrimRight(s, "\x00")), false, nil
}
return FallbackFromString(s), false, nil
}
// isAllBase62 returns true if every byte is in the base62 alphabet.
func isAllBase62(s string) bool {
for i := 0; i < len(s); i++ {
if base62Rev[s[i]] == 0xFF {
return false
}
}
return true
}
// encodeBase62 turns a uint64 into base62 text.
// Complexity: O(log_62 n) ~ at most 11 iterations for 64 bits.
func encodeBase62(u uint64) string {
if u == 0 {
return "0"
}
// 62^11 = 743008370688 > 2^39; 62^11 > 2^64? Actually 62^11 ~= 5.18e19 < 2^64 (1.84e19)? 2^64 ≈ 1.84e19.
// 62^11 ≈ 5.18e19 > 2^64? Correction: 2^64 ≈ 1.844e19, so 62^11 > 2^64. Thus 11 chars suffice.
var buf [11]byte
i := len(buf)
for u > 0 {
i--
buf[i] = base62Alphabet[u%62]
u /= 62
}
return string(buf[i:])
}
// decodeBase62 converts a base62 string to uint64.
// Returns (value, false) if any invalid character appears.
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
}
// ErrInvalidCartID can be returned by higher-level validation layers if you decide
// to reject fallback-derived IDs (currently unused here).
var ErrInvalidCartID = errors.New("invalid cart id")
// ---------------------------------------------------------------------------
// Legacy / Compatibility Conversion Helpers
// ---------------------------------------------------------------------------
// CartIDToLegacy converts a CartID (base62) into the legacy fixed-size CartId
// ([16]byte) by copying the textual form (truncated or zero-padded).
// NOTE: If the base62 string is longer than 16 (should not happen with current
// 64-bit space), it will be truncated.
func CartIDToLegacy(c CartID) CartId {
var id CartId
txt := c.String()
copy(id[:], []byte(txt))
return id
}
// LegacyToCartID upgrades a legacy CartId (padded) to a CartID by hashing its
// trimmed string form (deterministic). This preserves stable mapping without
// depending on original randomness.
func LegacyToCartID(old CartId) CartID {
return UpgradeLegacyCartId(old)
}
// CartIDToKey returns the numeric key representation (uint64) for map indexing.
func CartIDToKey(c CartID) uint64 {
return c.Raw()
}
// LegacyToCartKey converts a legacy CartId to the numeric key via deterministic
// fallback hashing. (Uses the same logic as LegacyToCartID then returns raw.)
func LegacyToCartKey(old CartId) uint64 {
return LegacyToCartID(old).Raw()
}
// ---------------------- Optional Helper Utilities ----------------------------
// CartIDOrNew tries to parse s; if empty OR invalid returns a fresh ID.
func CartIDOrNew(s string) (CartID, bool /*wasParsed*/, error) {
if cid, ok := ParseCartID(s); ok {
return cid, true, nil
}
id, err := NewCartID()
return id, false, err
}
// MustParseCartID panics if s is not a valid base62 ID (useful in tests).
func MustParseCartID(s string) CartID {
if cid, ok := ParseCartID(s); ok {
return cid
}
panic(fmt.Sprintf("invalid CartID: %s", s))
}
// DebugString returns a verbose description (for logging / diagnostics).
func (c CartID) DebugString() string {
return fmt.Sprintf("CartID(raw=%d txt=%s)", c.raw, c.String())
}
// Equal compares two CartIDs by raw value.
func (c CartID) Equal(other CartID) bool {
return c.raw == other.raw
}
// CanonicalizeOrLegacy preserves legacy (non-base62) IDs without altering their
// textual form, avoiding the previous behavior where fallback hashing replaced
// the original string with a base62-encoded hash (which broke deterministic
// key derivation across mixed call paths).
//
// Behavior:
// - s == "" -> generate new CartID (generatedNew = true, wasBase62 = true)
// - base62 ok -> return parsed CartID (generatedNew = false, wasBase62 = true)
// - otherwise -> treat as legacy: raw = hash(s), txt = original s
//
// Returns:
//
// cid - CartID (txt preserved for legacy inputs)
// generatedNew - true only when a brand new ID was created due to empty input
// wasBase62 - true if the input was already canonical base62 (or generated)
// err - only set if crypto/rand fails when generating a new ID
func CanonicalizeOrLegacy(s string) (cid CartID, generatedNew bool, wasBase62 bool, err error) {
if s == "" {
id, e := NewCartID()
if e != nil {
return CartID{}, false, false, e
}
return id, true, true, nil
}
if parsed, ok := ParseCartID(s); ok {
return parsed, false, true, nil
}
// Legacy path: keep original text so downstream legacy-to-key hashing
// (which uses the visible string) yields consistent keys across code paths.
hashCID := FallbackFromString(s)
// Preserve original textual form
hashCID.txt = s
return hashCID, false, false, nil
}