internal/store/ulid.go
1
package store
3
import (
4
"crypto/rand"
5
"encoding/binary"
6
"fmt"
7
"strings"
8
"time"
9
)
11
const (
12
crockfordAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
14
ulidLength = 26
15
bitsPerChar = 5
16
timestampBytes = 6
17
entropyBytes = 10
19
paddingBits = ulidLength*bitsPerChar - 8*(timestampBytes+entropyBytes)
20
)
22
// NewID returns a ULID: a 48-bit millisecond timestamp followed by 80 random
23
// bits, rendered as 26 Crockford base32 characters that sort by creation time.
24
func NewID(at time.Time) (string, error) {
25
milliseconds := at.UnixMilli()
26
if milliseconds < 0 || milliseconds>>(8*timestampBytes) != 0 {
27
return "", fmt.Errorf("timestamp %s does not fit in a ULID", at)
28
}
30
var id [timestampBytes + entropyBytes]byte
31
var wide [8]byte
32
binary.BigEndian.PutUint64(wide[:], uint64(milliseconds))
33
copy(id[:timestampBytes], wide[8-timestampBytes:])
35
if _, err := rand.Read(id[timestampBytes:]); err != nil {
36
return "", fmt.Errorf("reading ULID entropy: %w", err)
37
}
38
return encodeCrockford(id), nil
39
}
41
func ValidID(id string) bool {
42
if len(id) != ulidLength || id[0] > '7' {
43
return false
44
}
45
for _, character := range id {
46
if !strings.ContainsRune(crockfordAlphabet, character) {
47
return false
48
}
49
}
50
return true
51
}
53
func encodeCrockford(id [timestampBytes + entropyBytes]byte) string {
54
out := make([]byte, ulidLength)
55
for i := range out {
56
out[i] = crockfordAlphabet[fiveBitsAt(id, i*bitsPerChar)]
57
}
58
return string(out)
59
}
61
func fiveBitsAt(id [timestampBytes + entropyBytes]byte, offset int) byte {
62
var value byte
63
for i := range bitsPerChar {
64
value <<= 1
65
if bitSet(id, offset+i-paddingBits) {
66
value |= 1
67
}
68
}
69
return value
70
}
72
func bitSet(id [timestampBytes + entropyBytes]byte, n int) bool {
73
if n < 0 {
74
return false
75
}
76
return id[n/8]&(1<<(7-n%8)) != 0
77
}