package core import ( "crypto/rand" "database/sql/driver" "errors" "sync" "time" ) type ID struct { str string // 26-character base32 encoded string } /* The ID type takes some of the ideas of UUIDv7 and ULID, but tweaks some details for simple implementation. IDs are sortable with millisecond resolution only. IDs are not guaranteed to be monotonic. No attempt is made to keep IDs monotonic within the same millisecond. Data that needs to be precisely sorted by time more should store a timestamp with appropriate resolution, and of course needs to take into account all the complexity of synchronizing clocks in distributed systems. The encoding of ULID is attractive: no hyphens in the string form helps with situations where hyphens are not supported (such as ltree in postgres), and the encoding is case insensitive which avoids tricky issues (such as collation). IDs are based on 16 bytes, the first 6 bytes are a unix timestamp in big-endian order, the rest are randomly generated data. Data is generated using crypto/rand. Random data is buffered, so that each ID doesn't need to generate random data. Failure to generate random data will panic, but crypto/rand documents that errors are never returned: https://pkg.go.dev/crypto/rand#Read IDs stored as encoded strings, because IDs are very often stored, transmitted, and otherwise used as strings in other systems, such as databases, so this library trades storage size efficiency for less work repeatedly encoding/decoding the bytes. */ func NewID() ID { var data [16]byte t := time.Now().UnixMilli() data[0] = byte(t >> 40) data[1] = byte(t >> 32) data[2] = byte(t >> 24) data[3] = byte(t >> 16) data[4] = byte(t >> 8) data[5] = byte(t) poolMtx.Lock() if poolPos == poolSize { // https://pkg.go.dev/crypto/rand#Read // Read fills b with cryptographically secure random bytes. // It never returns an error, and always fills b entirely. // Read calls io.ReadFull on Reader and crashes the program irrecoverably // if an error is returned. The default Reader uses operating system APIs // that are documented to never return an error on all but legacy Linux systems. _, err := rand.Read(pool[:]) if err != nil { poolMtx.Unlock() panic(err) } poolPos = 0 } copy(data[6:], pool[poolPos:poolPos+10]) poolPos += 10 poolMtx.Unlock() return ID{str: encodeBase32(data)} } // ParseID parses a string into an ID func ParseID(s string) (ID, error) { if len(s) == 0 { return ID{}, nil } // Validate by decoding _, err := decodeBase32(s) if err != nil { return ID{}, err } return ID{str: s}, nil } // IsZero returns true if the ID is the zero value func (id ID) IsZero() bool { return id.str == "" } // crypto/rand data is not necessarily cheap to generate, // so keep a buffer of generated data available that can // be used for multiple IDs. Each ID uses 10 bytes of data. // pool size should be a multiple of 10 so that the pool position // aligns exactly with the pool size when the pool is empty. const poolSize = 100 var ( poolMtx sync.Mutex poolPos = 0 pool [poolSize * 10]byte ) // Crockford base32 alphabet: https://www.crockford.com/base32.html const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" // decodeMap maps ASCII characters to their base32 values (0xFF = invalid) var decodeMap [256]byte func init() { rand.Read(pool[:]) // Initialize decode map with invalid marker for i := range decodeMap { decodeMap[i] = 0xFF } for i, c := range alphabet { decodeMap[c] = byte(i) } } // encodeBase32 encodes 16 bytes to 26 characters using Crockford base32. // 16 bytes = 128 bits. Base32 uses 5 bits per character. // 128 / 5 = 25.6, so we need 26 characters (130 bits, 2 padding bits). func encodeBase32(data [16]byte) string { result := make([]byte, 26) // Process 5 bytes (40 bits) at a time, which encodes to 8 characters for i := 0; i < 3; i++ { offset := i * 5 encode5Bytes(data[offset:offset+5], result[i*8:(i+1)*8]) } // Last byte: 1 byte = 8 bits = 2 characters (10 bits with 2 padding bits) result[24] = alphabet[data[15]>>3] result[25] = alphabet[(data[15]&0x07)<<2] return string(result) } // encode5Bytes encodes 5 bytes (40 bits) to 8 base32 characters func encode5Bytes(src []byte, dst []byte) { // byte 0: bits 7-3 dst[0] = alphabet[src[0]>>3] // byte 0: bits 2-0, byte 1: bits 7-6 dst[1] = alphabet[((src[0]&0x07)<<2)|(src[1]>>6)] // byte 1: bits 5-1 dst[2] = alphabet[(src[1]>>1)&0x1F] // byte 1: bit 0, byte 2: bits 7-4 dst[3] = alphabet[((src[1]&0x01)<<4)|(src[2]>>4)] // byte 2: bits 3-0, byte 3: bit 7 dst[4] = alphabet[((src[2]&0x0F)<<1)|(src[3]>>7)] // byte 3: bits 6-2 dst[5] = alphabet[(src[3]>>2)&0x1F] // byte 3: bits 1-0, byte 4: bits 7-5 dst[6] = alphabet[((src[3]&0x03)<<3)|(src[4]>>5)] // byte 4: bits 4-0 dst[7] = alphabet[src[4]&0x1F] } // decodeBase32 decodes 26 characters to 16 bytes func decodeBase32(s string) ([16]byte, error) { var result [16]byte if len(s) != 26 { return result, errors.New("invalid ID length: expected 26 characters") } // Decode 8 characters at a time (5 bytes) for i := 0; i < 3; i++ { offset := i * 5 chars := s[i*8 : (i+1)*8] if err := decode5Bytes(chars, result[offset:offset+5]); err != nil { return result, err } } // Last 2 characters decode to 1 byte (with 2 padding bits ignored) v0 := decodeMap[s[24]] v1 := decodeMap[s[25]] if v0 == 0xFF || v1 == 0xFF { return result, errors.New("invalid character in ID") } result[15] = (v0 << 3) | (v1 >> 2) return result, nil } // decode5Bytes decodes 8 base32 characters to 5 bytes func decode5Bytes(s string, dst []byte) error { // Decode characters var v [8]byte for i := 0; i < 8; i++ { v[i] = decodeMap[s[i]] if v[i] == 0xFF { return errors.New("invalid character in ID") } } // Reconstruct bytes dst[0] = (v[0] << 3) | (v[1] >> 2) dst[1] = (v[1] << 6) | (v[2] << 1) | (v[3] >> 4) dst[2] = (v[3] << 4) | (v[4] >> 1) dst[3] = (v[4] << 7) | (v[5] << 2) | (v[6] >> 3) dst[4] = (v[6] << 5) | v[7] return nil } func (id ID) String() string { return id.str } // Equal returns true if this ID equals the other ID func (id ID) Equal(other ID) bool { return id.str == other.str } // Compare returns -1 if id < other, 0 if id == other, 1 if id > other. // IDs are compared lexicographically, which matches byte-order comparison // because the encoding preserves sort order. func (id ID) Compare(other ID) int { if id.str < other.str { return -1 } if id.str > other.str { return 1 } return 0 } func (id ID) MarshalBinary() ([]byte, error) { data, err := decodeBase32(id.str) if err != nil { return nil, err } b := make([]byte, 16) copy(b, data[:]) return b, nil } func (id ID) MarshalText() ([]byte, error) { return []byte(id.str), nil } func (id *ID) UnmarshalBinary(data []byte) error { if len(data) != 16 { return errors.New("invalid ID binary length: expected 16 bytes") } var arr [16]byte copy(arr[:], data) id.str = encodeBase32(arr) return nil } func (id *ID) UnmarshalText(data []byte) error { parsed, err := ParseID(string(data)) if err != nil { return err } *id = parsed return nil } // Value implements driver.Valuer for SQL storage (stores as text) func (id ID) Value() (driver.Value, error) { return id.String(), nil } // Scan implements sql.Scanner for SQL retrieval (reads from text) func (id *ID) Scan(src any) error { switch v := src.(type) { case string: // Handle empty string as zero ID (matches Value() behavior) if v == "" { *id = ID{} return nil } return id.UnmarshalText([]byte(v)) case []byte: // Handle empty byte slice as zero ID (matches Value() behavior) if len(v) == 0 { *id = ID{} return nil } return id.UnmarshalText(v) case nil: return errors.New("cannot scan nil into ID") default: return errors.New("cannot scan type into ID") } }