131 lines
3.2 KiB
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
|
|
}
|