ulid/ulid.go

131 lines
3.2 KiB
Go

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
}