package ulid import ( "encoding/binary" "errors" "fmt" "io" "time" "git.wisellama.rocks/Wisellama/ulid/crockford32" ) // The ULID spec is mirrored here: // https://git.wisellama.rocks/Mirrors/ulid-spec // NewULIDString create a new ULID and returns its encoded string. // See NewULID for more details. func NewULIDString(t time.Time, entropy io.Reader) (string, error) { bytes, err := NewULID(t, entropy) if err != nil { return "", err } s := crockford32.Encode(bytes) return s, nil } // NewULID creates a new ULID. // // A ULID is a 128-bit (16-byte) value similar to a UUID (and // compatible with UUIDs because of this). 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. Any ULIDs created during the same // millisecond will just receive random values with no ordering // guarantee. func NewULID(t time.Time, entropy io.Reader) ([]byte, error) { if entropy == nil { return nil, errors.New("entropy was nil") } randomBytes := make([]byte, 10) _, err := entropy.Read(randomBytes) if err != nil { return nil, fmt.Errorf("failed to read bytes from entropy source: %w", err) } msBytes, err := TimeMSBytes(t) if err != nil { return nil, err } if len(msBytes) != 6 { return nil, errors.New("timestamp bytes are wrong") } if len(randomBytes) != 10 { return nil, errors.New("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) } return ulidBytes, nil } // ParseULID expects a Crockford base-32 encoded string, and it will // parse out the ULID bytes from the string. func ParseULID(s string) ([]byte, error) { b, err := crockford32.Decode(s) if err != nil { return nil, fmt.Errorf("error decoding string: %w", err) } // Validate time _, err = GetTime(b) if err != nil { return nil, err } return b, nil } // TimeMSBytes 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 TimeMSBytes(t time.Time) ([]byte, error) { ms := uint64(t.UnixMilli()) // Put the 64-bit int into a byte array bytes := make([]byte, 8) binary.BigEndian.PutUint64(bytes, ms) if bytes[0] != 0 || bytes[1] != 0 { return nil, errors.New("time overflow") } // Chop off the first 2 bytes (16 bits) to get the 6 byte (48-bit) // output. return bytes[2:], nil } // GetTime parses the first 6 bytes (48-bits) of the given ULID bytes // into a time.Time value. It fails if the time value was too large to // be properly encoded. func GetTime(u []byte) (time.Time, error) { zeroTime := time.Time{} if len(u) != 16 { return zeroTime, errors.New("invalid ULID bytes") } // Zero pad to get 8 bytes timeBytes := []byte{0, 0} timeBytes = append(timeBytes, u[:6]...) epoch := binary.BigEndian.Uint64(timeBytes) maxTime := uint64(2<<48) - 1 if epoch > maxTime { return zeroTime, errors.New("time value was too large") } return time.UnixMilli(int64(epoch)), nil }