commit 0c255850f20754f08e2f327b984d3cdf52be07d6 Author: Wisellama Date: Sat Feb 17 20:21:36 2024 -0800 It works diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..b25c15b --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +*~ diff --git a/crockford_test.go b/crockford_test.go new file mode 100644 index 0000000..f2b4e87 --- /dev/null +++ b/crockford_test.go @@ -0,0 +1,15 @@ +package main + +import ( + "testing" +) + +func TestCrockfordEncode(t *testing.T) { + input := []byte{1, 141, 178, 57, 150, 88, 1, 148, 253, 194, 250, 47, 252, 192, 65, 211} + + expected := "01HPS3K5JR06AFVGQT5ZYC0GEK" + output := CrockfordEncode(input) + if expected != output { + t.Errorf("expected %v, got %v", expected, output) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..ebaaa7f --- /dev/null +++ b/go.mod @@ -0,0 +1,5 @@ +module example.com/ulid + +go 1.21.5 + +require github.com/oklog/ulid v1.3.1 diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..0c38094 --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/oklog/ulid v1.3.1 h1:EGfNDEx6MqHz8B3uNV6QAib1UR2Lm97sHi3ocA6ESJ4= +github.com/oklog/ulid v1.3.1/go.mod h1:CirwcVhetQ6Lv90oh/F+FBtV6XMibvdAFo93nm5qn4U= diff --git a/main.go b/main.go new file mode 100644 index 0000000..50a1ae3 --- /dev/null +++ b/main.go @@ -0,0 +1,194 @@ +package main + +import ( + "encoding/binary" + "log" + "math/rand" + "time" + + "github.com/oklog/ulid" +) + +// ULID spec is mirrored here: +// https://git.wisellama.rocks/Mirrors/ulid-spec + +func main() { + t := time.Date(2024, 02, 16, 14, 02, 15, 17, time.UTC) + ms := uint64(t.UnixMilli()) + msBytes := GetMSBytes(t) + + log.Printf("%X%X%X%X%X%X", msBytes[0], msBytes[1], msBytes[2], msBytes[3], msBytes[4], msBytes[5]) + + seed := int64(0) + entropy := rand.New(rand.NewSource(seed)) + + u, err := ulid.New(ms, entropy) + if err != nil { + log.Fatal(err) + } + + log.Printf("%v", u) + log.Printf("%016X", u.Time()) + + log.Printf("%016X", ms) + + log.Printf("%016X", ms) + + // ULID is a 128-bit (16-byte) value similar to a UUID. + // The first 48-bits (6 bytes) are based on a timestamp. + // The remaining 80-bits (10 bytes) are random. + // I'm not implementing the monotonicity part of the ULID spec + // because I don't need it. + + entropy = rand.New(rand.NewSource(seed)) + randomBytes := make([]byte, 10) + _, err = entropy.Read(randomBytes) + if err != nil { + log.Fatalf("failed to read bytes from entropy source") + } + + if len(msBytes) != 6 { + log.Fatalf("timestamp bytes are wrong") + } + if len(randomBytes) != 10 { + log.Fatalf("random bytes are wrong") + } + + ulidBytes := make([]byte, 0, 16) + for _, b := range msBytes { + ulidBytes = append(ulidBytes, b) + } + for _, b := range randomBytes { + ulidBytes = append(ulidBytes, b) + } + + ulidString := CrockfordEncode(ulidBytes) + log.Printf("ULID string: %v", ulidString) + +} + +// GetMSBytes returns the given Unix time in milliseconds as a 6-byte +// array. It truncates the 64-bit Unix epoch time down to 48-bits (6 +// bytes) and returns that 6 byte array. According to the ULID spec, +// 48-bits is enough room that we won't run out of space until 10889 +// AD. +func GetMSBytes(t time.Time) []byte { + ms := uint64(t.UnixMilli()) + + // Put the 64-bit int into a byte array + bytes := make([]byte, 8) + binary.BigEndian.PutUint64(bytes, ms) + + // Chop off the first 2 bytes (16 bits) to get the 6 byte (48-bit) + // output. + return bytes[2:] +} + +var ( + crockfordEncodeMap = map[uint64]rune{ + 0: '0', + 1: '1', + 2: '2', + 3: '3', + 4: '4', + 5: '5', + 6: '6', + 7: '7', + 8: '8', + 9: '9', + 10: 'A', + 11: 'B', + 12: 'C', + 13: 'D', + 14: 'E', + 15: 'F', + 16: 'G', + 17: 'H', + 18: 'J', + 19: 'K', + 20: 'M', + 21: 'N', + 22: 'P', + 23: 'Q', + 24: 'R', + 25: 'S', + 26: 'T', + 27: 'V', + 28: 'W', + 29: 'X', + 30: 'Y', + 31: 'Z', + } + + crockfordDecodeMap = map[rune]uint64{ + '0': 0, + 'O': 0, + 'o': 0, + '1': 1, + 'I': 1, + 'i': 1, + } +) + +// CrockfordEncode takes a byte array and encodes every 5-bits as a +// character string according to Crockford's base 32 encoding. +// +// https://www.crockford.com/base32.html +func CrockfordEncode(bytes []byte) string { + // Crockford is a base 32 encoding. + // 2^5 = 32, so every 5 bits will give us a character. + // Each byte is 8 bits, so we'll have to smoosh a few bytes together. + // For ULIDs, we have 128 bits which doesn't evenly divide by 5. + // + // Technically we'll be encoding 130 bits of information + // (divisible by 5), but the timestamp will should always start + // with zeros. + if len(bytes) < 16 { + log.Printf("failed to encode, expected a 16 byte ULID") + return "" + } + + log.Printf("bytes: %v", bytes) + + // Split our bytes up into groups 40 bits each = 120 out of our + // 130 bits. Put these into byte arrays that are 8 bytes long so + // that we can convert them into uint64s. + last := append([]byte{0, 0, 0}, bytes[11:]...) // 11 12 13 14 15 + log.Printf("last: %b", last) + third := append([]byte{0, 0, 0}, bytes[6:11]...) // 6 7 8 9 10 + second := append([]byte{0, 0, 0}, bytes[1:7]...) // 1 2 3 4 5 + // Plus the last 8 bits and 2 padding zeros to give us the remaining 10. + first := append([]byte{0, 0, 0, 0, 0, 0, 0}, bytes[0:1]...) // 0 + + // Convert each of those into integers so we have all the bits in one place. + lastInt := binary.BigEndian.Uint64(last) + thirdInt := binary.BigEndian.Uint64(third) + secondInt := binary.BigEndian.Uint64(second) + firstInt := binary.BigEndian.Uint64(first) + + // Encode those ints into strings 5-bits at a time. + output := make([]rune, 0, 26) + shiftedInt := uint64(0) + for i := 1; i >= 0; i-- { + shiftedInt = firstInt >> (i * 5) + lookup := shiftedInt & 0b11111 + output = append(output, crockfordEncodeMap[lookup]) + } + for i := 7; i >= 0; i-- { + shiftedInt = secondInt >> (i * 5) + lookup := shiftedInt & 0b11111 + output = append(output, crockfordEncodeMap[lookup]) + } + for i := 7; i >= 0; i-- { + shiftedInt = thirdInt >> (i * 5) + lookup := shiftedInt & 0b11111 + output = append(output, crockfordEncodeMap[lookup]) + } + for i := 7; i >= 0; i-- { + shiftedInt = lastInt >> (i * 5) + lookup := shiftedInt & 0b11111 + output = append(output, crockfordEncodeMap[lookup]) + } + + return string(output) +}