commit 9adac9b32a07667538b66f0c961e2c930d115c86 Author: Darko Luketic Date: Thu Aug 3 14:44:03 2023 +0200 initial diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..cbc5070 --- /dev/null +++ b/go.mod @@ -0,0 +1,11 @@ +module code.icod.de/dalu/oid36 + +go 1.20 + +require github.com/stretchr/testify v1.8.4 + +require ( + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect +) diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..8cf6655 --- /dev/null +++ b/go.sum @@ -0,0 +1,9 @@ +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= +github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/internal/base36/base36.go b/internal/base36/base36.go new file mode 100644 index 0000000..8e4f3dc --- /dev/null +++ b/internal/base36/base36.go @@ -0,0 +1,167 @@ +package base36 + +import ( + "math/big" + "strings" +) + +var ( + base36 = []byte{ + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + 'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', + 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', + 'U', 'V', 'W', 'X', 'Y', 'Z'} + + //index = map[byte]int{ + // '0': 0, '1': 1, '2': 2, '3': 3, '4': 4, + // '5': 5, '6': 6, '7': 7, '8': 8, '9': 9, + // 'A': 10, 'B': 11, 'C': 12, 'D': 13, 'E': 14, + // 'F': 15, 'G': 16, 'H': 17, 'I': 18, 'J': 19, + // 'K': 20, 'L': 21, 'M': 22, 'N': 23, 'O': 24, + // 'P': 25, 'Q': 26, 'R': 27, 'S': 28, 'T': 29, + // 'U': 30, 'V': 31, 'W': 32, 'X': 33, 'Y': 34, + // 'Z': 35, + // 'a': 10, 'b': 11, 'c': 12, 'd': 13, 'e': 14, + // 'f': 15, 'g': 16, 'h': 17, 'i': 18, 'j': 19, + // 'k': 20, 'l': 21, 'm': 22, 'n': 23, 'o': 24, + // 'p': 25, 'q': 26, 'r': 27, 's': 28, 't': 29, + // 'u': 30, 'v': 31, 'w': 32, 'x': 33, 'y': 34, + // 'z': 35, + //} + uint8Index = []uint64{ + 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 1, 2, + 3, 4, 5, 6, 7, 8, 9, 0, 0, 0, + 0, 0, 0, 0, 10, 11, 12, 13, 14, + 15, 16, 17, 18, 19, 20, 21, 22, 23, 24, + 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, + 35, 0, 0, 0, 0, 0, 0, 10, 11, 12, 13, + 14, 15, 16, 17, 18, 19, 20, 21, 22, 23, + 24, 25, 26, 27, 28, 29, 30, 31, 32, 33, + 34, 35, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, + 0, 0, 0, 0, 0, // 256 + } + pow36Index = []uint64{ + 1, 36, 1296, 46656, 1679616, 60466176, + 2176782336, 78364164096, 2821109907456, + 101559956668416, 3656158440062976, + 131621703842267136, 4738381338321616896, + 9223372036854775808, + } +) + +// Encode encodes a number to base36. +func Encode(value uint64) string { + var res [16]byte + var i int + for i = len(res) - 1; ; i-- { + res[i] = base36[value%36] + value /= 36 + if value == 0 { + break + } + } + + return string(res[i:]) +} + +// Decode decodes a base36-encoded string. +func Decode(s string) uint64 { + if len(s) > 13 { + s = s[:12] + } + res := uint64(0) + l := len(s) - 1 + for idx := 0; idx < len(s); idx++ { + c := s[l-idx] + res += uint8Index[c] * pow36Index[idx] + } + return res +} + +var bigRadix = big.NewInt(36) +var bigZero = big.NewInt(0) + +// EncodeBytesAsBytes encodes a byte slice to base36. +func EncodeBytesAsBytes(b []byte) []byte { + x := new(big.Int) + x.SetBytes(b) + + answer := make([]byte, 0, len(b)*136/100) + for x.Cmp(bigZero) > 0 { + mod := new(big.Int) + x.DivMod(x, bigRadix, mod) + answer = append(answer, base36[mod.Int64()]) + } + + // leading zero bytes + for _, i := range b { + if i != 0 { + break + } + answer = append(answer, base36[0]) + } + + // reverse + alen := len(answer) + for i := 0; i < alen/2; i++ { + answer[i], answer[alen-1-i] = answer[alen-1-i], answer[i] + } + + return answer +} + +// EncodeBytes encodes a byte slice to base36 string. +func EncodeBytes(b []byte) string { + return string(EncodeBytesAsBytes(b)) +} + +// DecodeToBytes decodes a base36 string to a byte slice, using alphabet. +func DecodeToBytes(b string) []byte { + alphabet := string(base36) + answer := big.NewInt(0) + j := big.NewInt(1) + + for i := len(b) - 1; i >= 0; i-- { + tmp := strings.IndexAny(alphabet, string(b[i])) + if tmp == -1 { + return []byte("") + } + idx := big.NewInt(int64(tmp)) + tmp1 := big.NewInt(0) + tmp1.Mul(j, idx) + + answer.Add(answer, tmp1) + j.Mul(j, bigRadix) + } + + tmpval := answer.Bytes() + + var numZeros int + for numZeros = 0; numZeros < len(b); numZeros++ { + if b[numZeros] != alphabet[0] { + break + } + } + flen := numZeros + len(tmpval) + val := make([]byte, flen, flen) + copy(val[numZeros:], tmpval) + + return val +} diff --git a/internal/base36/base36_test.go b/internal/base36/base36_test.go new file mode 100644 index 0000000..5f1e13e --- /dev/null +++ b/internal/base36/base36_test.go @@ -0,0 +1,51 @@ +package base36 + +import ( + "math" + "strings" + "testing" + + "github.com/stretchr/testify/assert" +) + +var raw = []uint64{0, 50, 100, 999, 1000, 1111, 5959, 99999, + 123456789, 5481594952936519619, math.MaxInt64 / 2048, math.MaxInt64 / 512, + math.MaxInt64, math.MaxUint64} + +var encoded = []string{"0", "1E", "2S", "RR", "RS", "UV", "4LJ", "255R", + "21I3V9", "15N9Z8L3AU4EB", "18CE53UN18F", "4XDKKFEK4XR", + "1Y2P0IJ32E8E7", "3W5E11264SGSF"} + +func TestEncode(t *testing.T) { + for i, v := range raw { + assert.Equal(t, encoded[i], Encode(v)) + } +} + +func TestDecode(t *testing.T) { + for i, v := range encoded { + assert.Equal(t, raw[i], Decode(v)) + assert.Equal(t, raw[i], Decode(strings.ToLower(v))) + } +} + +func BenchmarkEncode(b *testing.B) { + for i := 0; i < b.N; i++ { + Encode(5481594952936519619) + } +} + +func BenchmarkEncodeBytesAsBytes(b *testing.B) { + data := []byte{ + 0x86, 0x4F, 0xD2, 0x6F, 0xB5, 0x59, 0xF7, 0x5B, 0x48, 0x4F, 0x2A, 0x48, 0x4F, 0x2A, + } + for i := 0; i < b.N; i++ { + EncodeBytesAsBytes(data) + } +} + +func BenchmarkDecode(b *testing.B) { + for i := 0; i < b.N; i++ { + Decode("1Y2P0IJ32E8E7") + } +} diff --git a/oid36.go b/oid36.go new file mode 100644 index 0000000..9f6da3c --- /dev/null +++ b/oid36.go @@ -0,0 +1,21 @@ +package oid36 + +import ( + "encoding/hex" + + "code.icod.de/dalu/oid36/internal/base36" +) + +func ObjectIDToBase36(oid string) (string, error) { + b, e := hex.DecodeString(oid) + if e != nil { + return "", e + } + s := base36.EncodeBytes(b) + return s, nil +} + +func Base36ToObjectID(bid string) string { + b := base36.DecodeToBytes(bid) + return hex.EncodeToString(b) +} diff --git a/oid36_test.go b/oid36_test.go new file mode 100644 index 0000000..38eb838 --- /dev/null +++ b/oid36_test.go @@ -0,0 +1,32 @@ +package oid36_test + +import ( + "testing" + + "code.icod.de/dalu/oid36" +) + +func TestObjectIDToBase36(t *testing.T) { + oid := "617fc6a58149e807907dac6f" + exp := "2XBEMITFMZGGAZ8UC1B" + result, e := oid36.ObjectIDToBase36(oid) + if e != nil { + t.Error(e) + t.Fail() + return + } + if result != exp { + t.Fail() + return + } +} + +func TestBase36ToObjectID(t *testing.T) { + bid := "2XBEMITFMZGGAZ8UC1B" + exp := "617fc6a58149e807907dac6f" + result := oid36.Base36ToObjectID(bid) + if result != exp { + t.Fail() + return + } +}