forked from wrenn/wrenn
Switch API ID format from UUID to base36 for compact, E2B-style IDs
DB stays native UUID; the format/parse layer now encodes 16 UUID bytes as 25-char lowercase alphanumeric (base36) strings instead of the standard 36-char hex-with-dashes format. e.g. sb-2e5glxi4g3qnhwci95qev0cg0
This commit is contained in:
@ -4,12 +4,20 @@ import (
|
||||
"crypto/rand"
|
||||
"encoding/hex"
|
||||
"fmt"
|
||||
"math/big"
|
||||
"strings"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
const (
|
||||
base36Alphabet = "0123456789abcdefghijklmnopqrstuvwxyz"
|
||||
base36IDLen = 25 // ceil(128 * log2 / log36) = 25 chars for a full UUID
|
||||
)
|
||||
|
||||
var base36Base = big.NewInt(36)
|
||||
|
||||
// --- Generation ---
|
||||
|
||||
// newUUID returns a new random (v4) UUID wrapped in pgtype.UUID for direct DB use.
|
||||
@ -68,8 +76,41 @@ const (
|
||||
PrefixAdminPermission = "perm-"
|
||||
)
|
||||
|
||||
// uuidToBase36 encodes 16 UUID bytes as a 25-char base36 string (0-9a-z).
|
||||
func uuidToBase36(b [16]byte) string {
|
||||
n := new(big.Int).SetBytes(b[:])
|
||||
buf := make([]byte, base36IDLen)
|
||||
mod := new(big.Int)
|
||||
for i := base36IDLen - 1; i >= 0; i-- {
|
||||
n.DivMod(n, base36Base, mod)
|
||||
buf[i] = base36Alphabet[mod.Int64()]
|
||||
}
|
||||
return string(buf)
|
||||
}
|
||||
|
||||
// base36ToUUID decodes a 25-char base36 string back to 16 UUID bytes.
|
||||
func base36ToUUID(s string) ([16]byte, error) {
|
||||
if len(s) != base36IDLen {
|
||||
return [16]byte{}, fmt.Errorf("expected %d-char base36 ID, got %d", base36IDLen, len(s))
|
||||
}
|
||||
n := new(big.Int)
|
||||
for _, c := range s {
|
||||
idx := strings.IndexRune(base36Alphabet, c)
|
||||
if idx < 0 {
|
||||
return [16]byte{}, fmt.Errorf("invalid base36 character: %c", c)
|
||||
}
|
||||
n.Mul(n, base36Base)
|
||||
n.Add(n, big.NewInt(int64(idx)))
|
||||
}
|
||||
b := n.Bytes()
|
||||
var out [16]byte
|
||||
// big.Int.Bytes() strips leading zeros; right-align into 16-byte array.
|
||||
copy(out[16-len(b):], b)
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func formatUUID(prefix string, id pgtype.UUID) string {
|
||||
return prefix + uuid.UUID(id.Bytes).String()
|
||||
return prefix + uuidToBase36(id.Bytes)
|
||||
}
|
||||
|
||||
func FormatSandboxID(id pgtype.UUID) string { return formatUUID(PrefixSandbox, id) }
|
||||
@ -88,11 +129,11 @@ func parseUUID(prefix, s string) (pgtype.UUID, error) {
|
||||
if !strings.HasPrefix(s, prefix) {
|
||||
return pgtype.UUID{}, fmt.Errorf("invalid ID: expected %q prefix, got %q", prefix, s)
|
||||
}
|
||||
u, err := uuid.Parse(strings.TrimPrefix(s, prefix))
|
||||
b, err := base36ToUUID(strings.TrimPrefix(s, prefix))
|
||||
if err != nil {
|
||||
return pgtype.UUID{}, fmt.Errorf("invalid ID %q: %w", s, err)
|
||||
}
|
||||
return pgtype.UUID{Bytes: u, Valid: true}, nil
|
||||
return pgtype.UUID{Bytes: b, Valid: true}, nil
|
||||
}
|
||||
|
||||
func ParseSandboxID(s string) (pgtype.UUID, error) { return parseUUID(PrefixSandbox, s) }
|
||||
|
||||
118
internal/id/id_test.go
Normal file
118
internal/id/id_test.go
Normal file
@ -0,0 +1,118 @@
|
||||
package id
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/jackc/pgx/v5/pgtype"
|
||||
)
|
||||
|
||||
func TestBase36RoundTrip(t *testing.T) {
|
||||
for i := 0; i < 1000; i++ {
|
||||
orig := uuid.New()
|
||||
encoded := uuidToBase36(orig)
|
||||
|
||||
if len(encoded) != base36IDLen {
|
||||
t.Fatalf("expected %d chars, got %d: %s", base36IDLen, len(encoded), encoded)
|
||||
}
|
||||
|
||||
decoded, err := base36ToUUID(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("decode failed: %v", err)
|
||||
}
|
||||
|
||||
if decoded != orig {
|
||||
t.Fatalf("round-trip failed: %v → %s → %v", orig, encoded, decoded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBase36ZeroUUID(t *testing.T) {
|
||||
var zero [16]byte
|
||||
encoded := uuidToBase36(zero)
|
||||
if encoded != "0000000000000000000000000" {
|
||||
t.Fatalf("zero UUID should encode to all zeros, got %s", encoded)
|
||||
}
|
||||
decoded, err := base36ToUUID(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("decode failed: %v", err)
|
||||
}
|
||||
if decoded != zero {
|
||||
t.Fatalf("round-trip failed for zero UUID")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFormatParseRoundTrip(t *testing.T) {
|
||||
id := NewSandboxID()
|
||||
formatted := FormatSandboxID(id)
|
||||
|
||||
if formatted[:3] != "sb-" {
|
||||
t.Fatalf("expected sb- prefix, got %s", formatted)
|
||||
}
|
||||
if len(formatted) != 3+base36IDLen {
|
||||
t.Fatalf("expected %d chars total, got %d: %s", 3+base36IDLen, len(formatted), formatted)
|
||||
}
|
||||
|
||||
parsed, err := ParseSandboxID(formatted)
|
||||
if err != nil {
|
||||
t.Fatalf("parse failed: %v", err)
|
||||
}
|
||||
if parsed != id {
|
||||
t.Fatalf("round-trip failed: %v → %s → %v", id, formatted, parsed)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBase36InvalidInput(t *testing.T) {
|
||||
// Wrong length.
|
||||
if _, err := base36ToUUID("abc"); err == nil {
|
||||
t.Fatal("expected error for short input")
|
||||
}
|
||||
// Invalid character.
|
||||
if _, err := base36ToUUID("000000000000000000000000!"); err == nil {
|
||||
t.Fatal("expected error for invalid character")
|
||||
}
|
||||
}
|
||||
|
||||
func TestPlatformTeamIDFormats(t *testing.T) {
|
||||
formatted := FormatTeamID(PlatformTeamID)
|
||||
parsed, err := ParseTeamID(formatted)
|
||||
if err != nil {
|
||||
t.Fatalf("parse failed: %v", err)
|
||||
}
|
||||
if parsed != PlatformTeamID {
|
||||
t.Fatalf("platform team ID round-trip failed")
|
||||
}
|
||||
}
|
||||
|
||||
func TestMaxUUID(t *testing.T) {
|
||||
max := [16]byte{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff,
|
||||
0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff}
|
||||
encoded := uuidToBase36(max)
|
||||
if len(encoded) != base36IDLen {
|
||||
t.Fatalf("max UUID encoding wrong length: %d", len(encoded))
|
||||
}
|
||||
decoded, err := base36ToUUID(encoded)
|
||||
if err != nil {
|
||||
t.Fatalf("decode failed: %v", err)
|
||||
}
|
||||
if decoded != max {
|
||||
t.Fatalf("round-trip failed for max UUID")
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkFormatSandboxID(b *testing.B) {
|
||||
id := pgtype.UUID{Bytes: uuid.New(), Valid: true}
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
FormatSandboxID(id)
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkParseSandboxID(b *testing.B) {
|
||||
id := pgtype.UUID{Bytes: uuid.New(), Valid: true}
|
||||
s := FormatSandboxID(id)
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
ParseSandboxID(s)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user