mirror of
https://github.com/ThisTine/Snip.git
synced 2026-08-18 23:18:47 +07:00
feat: first commit
This commit is contained in:
@@ -0,0 +1,124 @@
|
||||
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)
|
||||
}
|
||||
Reference in New Issue
Block a user