Files
2026-06-15 21:25:57 +07:00

125 lines
2.9 KiB
Go

package service
import (
"crypto/rand"
"math/big"
"regexp"
"strings"
"github.com/snip/backend/internal/domain"
)
const base62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
// ReservedCodes can never be claimed as custom aliases, because these top-level
// paths are owned by the frontend / api commands.
var ReservedCodes = map[string]struct{}{
"": {},
"api": {},
"login": {},
"dashboard": {},
"assets": {},
"static": {},
"healthz": {},
"favicon.svg": {},
"favicon.ico": {},
"robots.txt": {},
"unlock": {},
}
// IsReserved reports whether a code collides with an app-owned path.
func IsReserved(code string) bool {
_, ok := ReservedCodes[strings.ToLower(code)]
return ok
}
func encodeBase62(n int64) string {
if n == 0 {
return string(base62[0])
}
var b strings.Builder
for n > 0 {
b.WriteByte(base62[n%62])
n /= 62
}
// reverse
s := []byte(b.String())
for i, j := 0, len(s)-1; i < j; i, j = i+1, j-1 {
s[i], s[j] = s[j], s[i]
}
return string(s)
}
// scramble spreads sequential ids so codes don't look enumerable, while staying
// short. Reversible multiplicative hash over a 31-bit space.
func scramble(id int64) int64 {
const prime = 2654435761
const mod = 0x7fffffff
return ((id + 1) * prime) % mod
}
// CodeFromSequence turns a DB sequence value into the shortest unique base62
// code (no collision checks needed — the sequence guarantees uniqueness).
func CodeFromSequence(seq int64) string {
return encodeBase62(scramble(seq + 100000))
}
var aliasRe = regexp.MustCompile(`^[a-zA-Z0-9_-]{3,32}$`)
// ValidateAlias checks a user-supplied custom code.
func ValidateAlias(alias string) error {
if !aliasRe.MatchString(alias) {
return domain.ErrInvalidAlias
}
if IsReserved(alias) {
return domain.ErrReserved
}
return nil
}
var urlRe = regexp.MustCompile(`(?i)^https?://[^\s.]+\.[^\s]{2,}$`)
// NormalizeURL ensures a scheme is present.
func NormalizeURL(raw string) string {
s := strings.TrimSpace(raw)
if s == "" {
return ""
}
low := strings.ToLower(s)
if strings.HasPrefix(low, "http://") || strings.HasPrefix(low, "https://") {
return s
}
return "https://" + s
}
// ValidURL validates a normalized destination.
func ValidURL(raw string) bool {
return urlRe.MatchString(raw)
}
var pinRe = regexp.MustCompile(`^\d{6}$`)
// ValidPin reports whether a string is exactly 6 digits.
func ValidPin(pin string) bool { return pinRe.MatchString(pin) }
func pick(arr []string) string {
n, _ := rand.Int(rand.Reader, big.NewInt(int64(len(arr))))
return arr[n.Int64()]
}
// MemorableSlug builds an easy-to-say "adjective-noun-verb" code.
func MemorableSlug() string {
return pick(adjectives) + "-" + pick(nouns) + "-" + pick(verbs)
}
// newID mints an opaque link id.
func newID() string {
const n = 10
b := make([]byte, n)
for i := range b {
idx, _ := rand.Int(rand.Reader, big.NewInt(62))
b[i] = base62[idx.Int64()]
}
return "l_" + string(b)
}