diff --git a/tools/utils/short-uuid.go b/tools/utils/short-uuid.go new file mode 100644 index 000000000..7e9e8ecba --- /dev/null +++ b/tools/utils/short-uuid.go @@ -0,0 +1,91 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package utils + +import ( + "math" + "math/big" + + "github.com/ALTree/bigfloat" + "github.com/google/uuid" +) + +const ( + ESCAPE_CODE_SAFE_ALPHABET = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789!\"#$%&'()*+,-./:;<=>?@[\\]^_`{|}~ " + HUMAN_ALPHABET = "23456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ" +) + +func num_to_string(number *big.Int, alphabet []rune, alphabet_len *big.Int, pad_to_length int) string { + var zero, digit big.Int + if number.Sign() < 0 { + *number = zero + } + capacity := 64 + if pad_to_length > capacity { + capacity = pad_to_length + } + ans := make([]rune, 0, capacity) + for number.Cmp(&zero) == 1 { + number.DivMod(number, alphabet_len, &digit) + ans = append(ans, alphabet[digit.Uint64()]) + } + al := len(ans) + if pad_to_length > -1 && al < pad_to_length { + ans = ans[:pad_to_length] + for i := al; i < pad_to_length; i++ { + ans[i] = alphabet[0] + } + } + return string(ans) +} + +func get_padding_length(alphabet_len int) int { + bi := big.NewInt(2) + bi = bi.Exp(bi, big.NewInt(128), nil) + bb := new(big.Float).SetPrec(256) + bb.SetInt(bi) + log_al := bigfloat.Log(big.NewFloat(float64(alphabet_len)).SetPrec(256)) + log_b := bigfloat.Log(bb) + res := new(big.Float).SetPrec(256) + res = res.Quo(log_b, log_al) + val, _ := res.Float64() + return int(math.Ceil(val)) +} + +type ShortUUID struct { + alphabet []rune + alphabet_len big.Int + pad_to_length int +} + +func CreateShortUUID(alphabet string) *ShortUUID { + if alphabet == "" { + alphabet = HUMAN_ALPHABET + } + var ans = ShortUUID{ + alphabet: []rune(alphabet), + } + ans.pad_to_length = get_padding_length(len(ans.alphabet)) + ans.alphabet_len.SetUint64(uint64(len(ans.alphabet))) + return &ans +} + +func (self *ShortUUID) Uuid4() (string, error) { + b, err := uuid.NewRandom() + if err != nil { + return "", err + } + bb, err := b.MarshalBinary() + if err != nil { + return "", err + } + var bi big.Int + bi.SetBytes(bb) + return num_to_string(&bi, self.alphabet, &self.alphabet_len, self.pad_to_length), nil +} + +var HumanUUID *ShortUUID + +func init() { + HumanUUID = CreateShortUUID(HUMAN_ALPHABET) +} diff --git a/tools/utils/short-uuid_test.go b/tools/utils/short-uuid_test.go new file mode 100644 index 000000000..3f9506588 --- /dev/null +++ b/tools/utils/short-uuid_test.go @@ -0,0 +1,28 @@ +// License: GPLv3 Copyright: 2022, Kovid Goyal, + +package utils + +import ( + "math/big" + "testing" +) + +func TestShortUUID(t *testing.T) { + if HumanUUID.pad_to_length != 22 { + t.Fatalf("pad length for human UUID is %d not %d", HumanUUID.pad_to_length, 22) + } + u, err := HumanUUID.Uuid4() + if err != nil { + t.Fatal(err) + } + if len(u) != 22 { + t.Fatalf("uuid4 %s has unexpected length: %d", u, len(u)) + } + + b := big.NewInt(int64(1234567890123456789)) + q := num_to_string(b, HumanUUID.alphabet, &HumanUUID.alphabet_len, HumanUUID.pad_to_length) + const expected = "bzT6LtUjw4422222222222" + if q != expected { + t.Fatalf("unexpected short human serialization: %s != %s", q, expected) + } +}