package protocol import ( "crypto/rand" "fmt" "strings" "time" ) const idAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" // NewID returns a canonical ULID with a millisecond timestamp and 80 random // bits. Times outside the unsigned 48-bit millisecond range are rejected. func NewID(now time.Time) (string, error) { milliseconds := now.UnixMilli() if milliseconds < 0 || milliseconds >= 1<<48 { return "", fmt.Errorf("ID time is outside the ULID range") } var data [16]byte // Six bytes hold the timestamp and ten bytes hold random data. for index := 5; index >= 0; index-- { data[index] = byte(milliseconds) milliseconds >>= 8 } _, err := rand.Read(data[6:]) if err != nil { return "", fmt.Errorf("read ID randomness: %w", err) } var text [26]byte for index := range text { var value byte for bit := range 5 { position := index*5 + bit - 2 value <<= 1 if position >= 0 { value |= (data[position/8] >> (7 - position%8)) & 1 } } text[index] = idAlphabet[value] } return string(text[:]), nil } // ValidID reports whether value is a canonical uppercase ULID. func ValidID(value string) bool { if len(value) != 26 || value[0] > '7' { return false } for _, character := range value { if !strings.ContainsRune(idAlphabet, character) { return false } } return true } // ValidIdentifier accepts lowercase ASCII letters and digits with single // internal hyphen or period separators. Empty identifiers are invalid. func ValidIdentifier(value string) bool { separator := true for _, character := range value { switch { case character >= 'a' && character <= 'z', character >= '0' && character <= '9': separator = false case character == '-' || character == '.': if separator { return false } separator = true default: return false } } return !separator }