2024-02-02 00:44:01 +00:00
|
|
|
|
package tind
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/rand"
|
|
|
|
|
"errors"
|
|
|
|
|
"log"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// Config is a configuration for generating and working with TinD id's of
|
|
|
|
|
// possibly varying sizes and runesets
|
|
|
|
|
type Config struct {
|
|
|
|
|
size int
|
|
|
|
|
runes []rune
|
|
|
|
|
runesize int
|
|
|
|
|
mod byte
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const (
|
|
|
|
|
defaultRunes = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ"
|
|
|
|
|
defaultSize = 4
|
|
|
|
|
)
|
|
|
|
|
|
2024-02-24 20:16:04 +00:00
|
|
|
|
// New starts off a factory pattern to build a TinD
|
2024-02-02 00:44:01 +00:00
|
|
|
|
// with either the default values, or setting custom values
|
|
|
|
|
// using the `With` functions
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// Get a default TinD id
|
2024-02-24 04:47:26 +00:00
|
|
|
|
// `id := New().Gen()`
|
2024-02-02 00:44:01 +00:00
|
|
|
|
//
|
|
|
|
|
// Get a TinD with 8 bytes but the default runeset
|
2024-02-24 04:47:26 +00:00
|
|
|
|
// `id := New().WithSize(8).Gen()`
|
2024-02-02 00:44:01 +00:00
|
|
|
|
//
|
|
|
|
|
// Get a TinD of default size with a special runeset
|
2024-02-24 04:47:26 +00:00
|
|
|
|
// `id := New().WithRuneset("😃😡🕰️🧰BGD").Gen()`
|
2024-02-02 00:44:01 +00:00
|
|
|
|
//
|
|
|
|
|
// You can always define the configuration and create new TinD's
|
|
|
|
|
// using it.
|
|
|
|
|
// Example:
|
|
|
|
|
//
|
|
|
|
|
// myNumberOnlyConfig := New().WithSize(8).WithRuneset("0123456789")
|
2024-02-24 20:16:04 +00:00
|
|
|
|
func New() *Config {
|
|
|
|
|
c := &Config{
|
2024-02-02 00:44:01 +00:00
|
|
|
|
size: defaultSize,
|
|
|
|
|
runes: []rune(defaultRunes),
|
|
|
|
|
runesize: len(defaultRunes),
|
|
|
|
|
}
|
2024-02-24 20:16:04 +00:00
|
|
|
|
c.calcMod()
|
|
|
|
|
return c
|
2024-02-02 00:44:01 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-02-24 20:16:04 +00:00
|
|
|
|
// NewFrom will build a configuration from a byte slice. Because this is a
|
2024-02-24 04:47:26 +00:00
|
|
|
|
// byte slice, the provided input doesn't matter, it's more about the length,
|
2024-02-24 20:16:04 +00:00
|
|
|
|
func NewFrom(in []byte) *Config {
|
|
|
|
|
c := New()
|
|
|
|
|
c.size = len(in)
|
|
|
|
|
c.calcMod()
|
|
|
|
|
return c
|
2024-02-24 04:47:26 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-02-02 00:44:01 +00:00
|
|
|
|
// WithRuneset requires a string as input, where each character must be unique.
|
|
|
|
|
// The string can be comprised of any unique unicode character.
|
2024-02-24 04:47:26 +00:00
|
|
|
|
// Will filter out duplicate runes before storing it, meaning if you provide a
|
|
|
|
|
// runeset with duplicates, the size of the runeset will be reduced to ensure
|
|
|
|
|
// consistency. Dont' rely on len(runeset) of your custom runeset to be
|
|
|
|
|
// accurate.
|
2024-02-02 00:44:01 +00:00
|
|
|
|
//
|
|
|
|
|
// This does slow down initialization, but the final product still runs fast to
|
|
|
|
|
// generate ids
|
2024-02-24 04:47:26 +00:00
|
|
|
|
// When updating a runeset, the values of runesize and mod will be updated
|
2024-02-02 08:04:48 +00:00
|
|
|
|
func (c *Config) WithRuneset(r []rune) *Config {
|
2024-02-02 00:44:01 +00:00
|
|
|
|
c.runes = ensureUniqueRunes(r)
|
|
|
|
|
c.runesize = len(c.runes)
|
2024-02-24 20:16:04 +00:00
|
|
|
|
c.calcMod()
|
2024-02-02 00:44:01 +00:00
|
|
|
|
return c
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-24 20:16:04 +00:00
|
|
|
|
func (c *Config) calcMod() {
|
|
|
|
|
c.mod = byte(c.runesize - 1)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// ensureUniqueRunes loops through the provided runeset to make sure only unique
|
|
|
|
|
// runes are included. This ensures that, given the same runeset in the same
|
|
|
|
|
// order, a TinD can be decoded from a string.
|
2024-02-02 08:04:48 +00:00
|
|
|
|
func ensureUniqueRunes(r []rune) []rune {
|
2024-02-02 00:44:01 +00:00
|
|
|
|
unq := make(map[rune]int)
|
|
|
|
|
var index int
|
|
|
|
|
// loop through the runes to find duplicates
|
2024-02-02 08:04:48 +00:00
|
|
|
|
for _, v := range r {
|
2024-02-02 00:44:01 +00:00
|
|
|
|
// if the current rune hasn't been seen yet, assign it
|
|
|
|
|
if _, ok := unq[v]; !ok {
|
|
|
|
|
unq[v] = index
|
|
|
|
|
index++
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
rr := make([]rune, len(unq))
|
|
|
|
|
for k, n := range unq {
|
|
|
|
|
rr[n] = k
|
|
|
|
|
}
|
|
|
|
|
return rr
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// WithSize sets the number of bytes in the TinD ID
|
|
|
|
|
func (c *Config) WithSize(s int) *Config {
|
|
|
|
|
c.size = s
|
|
|
|
|
return c
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Zero returns a zero value TinD ID of the configured size
|
|
|
|
|
func (c *Config) Zero() TinD {
|
|
|
|
|
t := TinD{
|
|
|
|
|
bytes: make([]byte, c.size),
|
|
|
|
|
config: c,
|
|
|
|
|
}
|
|
|
|
|
return t
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Gen generates a TinD with the given configuration
|
|
|
|
|
func (c *Config) Gen() TinD {
|
2024-02-02 08:04:48 +00:00
|
|
|
|
bytes := make([]byte, c.size)
|
|
|
|
|
runes := make([]rune, c.size)
|
|
|
|
|
_, err := rand.Read(bytes)
|
2024-02-02 00:44:01 +00:00
|
|
|
|
if err != nil {
|
|
|
|
|
log.Fatalln(err)
|
|
|
|
|
}
|
|
|
|
|
// Make sure each byte fits the rune so it can be encoded and decoded
|
|
|
|
|
for i := 0; i < c.size; i++ {
|
2024-02-02 08:04:48 +00:00
|
|
|
|
bytes[i] = bytes[i] & c.mod
|
|
|
|
|
runes[i] = c.runes[bytes[i]]
|
|
|
|
|
}
|
|
|
|
|
t := TinD{
|
|
|
|
|
bytes: bytes,
|
2024-02-23 04:18:46 +00:00
|
|
|
|
runes: runes,
|
2024-02-02 08:04:48 +00:00
|
|
|
|
config: c,
|
2024-02-02 00:44:01 +00:00
|
|
|
|
}
|
|
|
|
|
return t
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// FromString takes a string representation of a TinD id and returns
|
|
|
|
|
// the actual TinD as bytes
|
|
|
|
|
func (c *Config) FromString(in string) (TinD, error) {
|
|
|
|
|
// Make sure the number of characters matches what's needed
|
|
|
|
|
if len(in) != c.size {
|
|
|
|
|
return c.Zero(), errors.New("given id isn't the same size as the configuration")
|
|
|
|
|
}
|
2024-02-02 08:04:48 +00:00
|
|
|
|
t := TinD{
|
2024-02-23 04:18:46 +00:00
|
|
|
|
bytes: make([]byte, c.size),
|
2024-02-02 08:04:48 +00:00
|
|
|
|
config: c,
|
|
|
|
|
}
|
2024-02-02 00:44:01 +00:00
|
|
|
|
r := []rune(in)
|
2024-02-02 08:04:48 +00:00
|
|
|
|
for i := 0; i < c.size; i++ {
|
2024-02-02 00:44:01 +00:00
|
|
|
|
for j := 0; j < c.runesize; j++ {
|
2024-02-02 08:04:48 +00:00
|
|
|
|
if r[i] == c.runes[j] {
|
|
|
|
|
t.bytes[i] = byte(j)
|
2024-02-02 00:44:01 +00:00
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return TinD(t), nil
|
|
|
|
|
}
|
2024-02-23 04:18:46 +00:00
|
|
|
|
|
2024-02-24 04:47:26 +00:00
|
|
|
|
// Load imports a TinD byte string, or really any byte string, that fits within
|
|
|
|
|
// the constraints of the configuration. You can't load an 8byte slice into a
|
|
|
|
|
// 4byte TinD configuration.
|
|
|
|
|
// To generate a configuration from a given byte slice, use NewConfigFrom.
|
2024-02-23 04:18:46 +00:00
|
|
|
|
func (c *Config) Load(in []byte) (TinD, error) {
|
|
|
|
|
if len(in) != c.size {
|
|
|
|
|
return c.Zero(), errors.New("given id isn't the same size as the configuration")
|
|
|
|
|
}
|
|
|
|
|
t := TinD{
|
|
|
|
|
bytes: in,
|
|
|
|
|
config: c,
|
|
|
|
|
}
|
|
|
|
|
return t, nil
|
|
|
|
|
}
|