contented/vendor/github.com/speps/go-hashids/hashids.go

347 lines
9.3 KiB
Go
Raw Normal View History

2018-06-09 05:54:49 +00:00
// Go implementation of http://www.hashids.org under MIT license
// Generates hashes from an array of integers, eg. for YouTube like hashes
// Setup: go get github.com/speps/go-hashids
// Original implementations by Ivan Akimov at https://github.com/ivanakimov
// Thanks to Rémy Oudompheng and Peter Hellberg for code review and fixes
package hashids
import (
"errors"
"math"
)
const (
// Version is the version number of the library
Version string = "1.0.0"
// DefaultAlphabet is the default alphabet used by go-hashids
DefaultAlphabet string = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890"
minAlphabetLength int = 16
sepDiv float64 = 3.5
guardDiv float64 = 12.0
)
var sepsOriginal = []rune("cfhistuCFHISTU")
// HashID contains everything needed to encode/decode hashids
type HashID struct {
alphabet []rune
minLength int
salt []rune
seps []rune
guards []rune
}
// HashIDData contains the information needed to generate hashids
type HashIDData struct {
// Alphabet is the alphabet used to generate new ids
Alphabet string
// MinLength is the minimum length of a generated id
MinLength int
// Salt is the secret used to make the generated id harder to guess
Salt string
}
// NewData creates a new HashIDData with the DefaultAlphabet already set.
func NewData() *HashIDData {
return &HashIDData{Alphabet: DefaultAlphabet}
}
// New creates a new HashID
func New() *HashID {
return NewWithData(NewData())
}
// NewWithData creates a new HashID with the provided HashIDData
func NewWithData(data *HashIDData) *HashID {
if len(data.Alphabet) < minAlphabetLength {
panic(errors.New("alphabet must contain at least 16 characters"))
}
// Check if all characters are unique in Alphabet
uniqueCheck := make(map[rune]bool, len(data.Alphabet))
for _, a := range data.Alphabet {
if _, found := uniqueCheck[a]; found {
panic(errors.New("duplicate character in alphabet"))
}
uniqueCheck[a] = true
}
alphabet := []rune(data.Alphabet)
salt := []rune(data.Salt)
seps := make([]rune, len(sepsOriginal))
copy(seps, sepsOriginal)
// seps should contain only characters present in alphabet; alphabet should not contains seps
for i := 0; i < len(seps); i++ {
foundIndex := -1
for j, a := range alphabet {
if a == seps[i] {
foundIndex = j
break
}
}
if foundIndex == -1 {
seps = append(seps[:i], seps[i+1:]...)
i--
} else {
alphabet = append(alphabet[:foundIndex], alphabet[foundIndex+1:]...)
}
}
seps = consistentShuffle(seps, salt)
if len(seps) == 0 || float64(len(alphabet))/float64(len(seps)) > sepDiv {
sepsLength := int(math.Ceil(float64(len(alphabet)) / sepDiv))
if sepsLength == 1 {
sepsLength++
}
if sepsLength > len(seps) {
diff := sepsLength - len(seps)
seps = append(seps, alphabet[:diff]...)
alphabet = alphabet[diff:]
} else {
seps = seps[:sepsLength]
}
}
alphabet = consistentShuffle(alphabet, salt)
guardCount := int(math.Ceil(float64(len(alphabet)) / guardDiv))
var guards []rune
if len(alphabet) < 3 {
guards = seps[:guardCount]
seps = seps[guardCount:]
} else {
guards = alphabet[:guardCount]
alphabet = alphabet[guardCount:]
}
return &HashID{
alphabet: alphabet,
minLength: data.MinLength,
salt: salt,
seps: seps,
guards: guards,
}
}
// Encode hashes an array of int to a string containing at least MinLength characters taken from the Alphabet.
// Use Decode using the same Alphabet and Salt to get back the array of int.
func (h *HashID) Encode(numbers []int) (string, error) {
numbers64 := make([]int64, 0, len(numbers))
for _, id := range numbers {
numbers64 = append(numbers64, int64(id))
}
return h.EncodeInt64(numbers64)
}
// EncodeInt64 hashes an array of int64 to a string containing at least MinLength characters taken from the Alphabet.
// Use DecodeInt64 using the same Alphabet and Salt to get back the array of int64.
func (h *HashID) EncodeInt64(numbers []int64) (string, error) {
if len(numbers) == 0 {
return "", errors.New("encoding empty array of numbers makes no sense")
}
for _, n := range numbers {
if n < 0 {
return "", errors.New("negative number not supported")
}
}
alphabet := make([]rune, len(h.alphabet))
copy(alphabet, h.alphabet)
numbersHash := int64(0)
for i, n := range numbers {
numbersHash += (n % int64(i+100))
}
result := make([]rune, 0, h.minLength)
lottery := alphabet[numbersHash%int64(len(alphabet))]
result = append(result, lottery)
for i, n := range numbers {
buffer := append([]rune{lottery}, append(h.salt, alphabet...)...)
alphabet = consistentShuffle(alphabet, buffer[:len(alphabet)])
hash := hash(n, alphabet)
result = append(result, hash...)
if i+1 < len(numbers) {
n %= int64(hash[0]) + int64(i)
result = append(result, h.seps[n%int64(len(h.seps))])
}
}
if len(result) < h.minLength {
guardIndex := (numbersHash + int64(result[0])) % int64(len(h.guards))
result = append([]rune{h.guards[guardIndex]}, result...)
if len(result) < h.minLength {
guardIndex = (numbersHash + int64(result[2])) % int64(len(h.guards))
result = append(result, h.guards[guardIndex])
}
}
halfLength := len(alphabet) / 2
for len(result) < h.minLength {
alphabet = consistentShuffle(alphabet, alphabet)
result = append(alphabet[halfLength:], append(result, alphabet[:halfLength]...)...)
excess := len(result) - h.minLength
if excess > 0 {
result = result[excess/2 : excess/2+h.minLength]
}
}
return string(result), nil
}
// DEPRECATED: Use DecryptWithError instead
// Decode unhashes the string passed to an array of int.
// It is symmetric with Encode if the Alphabet and Salt are the same ones which were used to hash.
// MinLength has no effect on Decode.
func (h *HashID) Decode(hash string) []int {
result, err := h.DecodeWithError(hash)
if err != nil {
panic(err)
}
return result
}
// Decode unhashes the string passed to an array of int.
// It is symmetric with Encode if the Alphabet and Salt are the same ones which were used to hash.
// MinLength has no effect on Decode.
func (h *HashID) DecodeWithError(hash string) ([]int, error) {
result64, err := h.DecodeInt64WithError(hash)
if err != nil {
return nil, err
}
result := make([]int, 0, len(result64))
for _, id := range result64 {
result = append(result, int(id))
}
return result, nil
}
// DEPRECATED: Use DecryptInt64WithError instead
// DecodeInt64 unhashes the string passed to an array of int64.
// It is symmetric with EncodeInt64 if the Alphabet and Salt are the same ones which were used to hash.
// MinLength has no effect on DecodeInt64.
func (h *HashID) DecodeInt64(hash string) []int64 {
result, err := h.DecodeInt64WithError(hash)
if err != nil {
panic(err)
}
return result
}
// DecodeInt64 unhashes the string passed to an array of int64.
// It is symmetric with EncodeInt64 if the Alphabet and Salt are the same ones which were used to hash.
// MinLength has no effect on DecodeInt64.
func (h *HashID) DecodeInt64WithError(hash string) ([]int64, error) {
hashes := splitRunes([]rune(hash), h.guards)
hashIndex := 0
if len(hashes) == 2 || len(hashes) == 3 {
hashIndex = 1
}
result := make([]int64, 0)
hashBreakdown := hashes[hashIndex]
if len(hashBreakdown) > 0 {
lottery := hashBreakdown[0]
hashBreakdown = hashBreakdown[1:]
hashes = splitRunes(hashBreakdown, h.seps)
alphabet := []rune(h.alphabet)
for _, subHash := range hashes {
buffer := append([]rune{lottery}, append(h.salt, alphabet...)...)
alphabet = consistentShuffle(alphabet, buffer[:len(alphabet)])
number, err := unhash(subHash, alphabet)
if err != nil {
return nil, err
}
result = append(result, number)
}
}
return result, nil
}
func splitRunes(input, seps []rune) [][]rune {
splitIndices := make([]int, 0)
for i, inputRune := range input {
for _, sepsRune := range seps {
if inputRune == sepsRune {
splitIndices = append(splitIndices, i)
}
}
}
result := make([][]rune, 0, len(splitIndices)+1)
inputLeft := input[:]
for _, splitIndex := range splitIndices {
splitIndex -= len(input) - len(inputLeft)
subInput := make([]rune, splitIndex)
copy(subInput, inputLeft[:splitIndex])
result = append(result, subInput)
inputLeft = inputLeft[splitIndex+1:]
}
result = append(result, inputLeft)
return result
}
func hash(input int64, alphabet []rune) []rune {
result := make([]rune, 0)
for {
r := alphabet[input%int64(len(alphabet))]
result = append(result, r)
input /= int64(len(alphabet))
if input == 0 {
break
}
}
reversed := make([]rune, len(result))
for i, r := range result {
reversed[len(result)-i-1] = r
}
return reversed
}
func unhash(input, alphabet []rune) (int64, error) {
result := int64(0)
for i, inputRune := range input {
alphabetPos := -1
for pos, alphabetRune := range alphabet {
if inputRune == alphabetRune {
alphabetPos = pos
break
}
}
if alphabetPos == -1 {
return 0, errors.New("alphabet used for hash was different")
}
result += int64(alphabetPos) * int64(math.Pow(float64(len(alphabet)), float64(len(input)-i-1)))
}
return result, nil
}
func consistentShuffle(alphabet, salt []rune) []rune {
if len(salt) == 0 {
return alphabet
}
result := make([]rune, len(alphabet))
copy(result, alphabet)
for i, v, p := len(result)-1, 0, 0; i > 0; i-- {
p += int(salt[v])
j := (int(salt[v]) + v + p) % i
result[i], result[j] = result[j], result[i]
v = (v + 1) % len(salt)
}
return result
}