mirror of
https://github.com/ThisTine/Snip.git
synced 2026-08-18 23:18:47 +07:00
125 lines
2.9 KiB
Go
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)
|
|
}
|