Skip to content

Commit

Permalink
Impl new uid types
Browse files Browse the repository at this point in the history
  • Loading branch information
xorkevin committed Aug 23, 2023
1 parent 2d422f6 commit 9d1a8b3
Show file tree
Hide file tree
Showing 2 changed files with 95 additions and 87 deletions.
153 changes: 75 additions & 78 deletions util/uid/uid.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,119 +2,116 @@ package uid

import (
"crypto/rand"
"encoding/base32"
"encoding/base64"
"encoding/binary"
"strings"
"time"

"xorkevin.dev/kerrors"
)

type (
// UID is an identifier that can be initialized with a custom length composed of a user specified time, hash, and random bits
UID struct {
u []byte
}
)

var (
// ErrRand is returned when failing to read random bytes
ErrRand errRand
// ErrInvalidUID is returned on an invalid uid
ErrInvalidUID errInvalidUID
)
// ErrRand is returned when failing to read random bytes
var ErrRand errRand

type (
errRand struct{}
errInvalidUID struct{}
errRand struct{}
)

func (e errRand) Error() string {
return "Error reading rand"
}

func (e errInvalidUID) Error() string {
return "Invalid uid"
}
var base64HexEncoding = base64.NewEncoding("-0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz").WithPadding(base64.NoPadding)

// New creates a new UID
func New(size int) (*UID, error) {
u := make([]byte, size)
_, err := rand.Read(u)
type (
// UID is a time orderable universally unique identifier
UID struct {
u [16]byte
}
)

// New creates a new [UID]
func New() (*UID, error) {
u := &UID{}
_, err := rand.Read(u.u[6:])
if err != nil {
return nil, kerrors.WithKind(err, ErrRand, "Failed reading crypto/rand")
}

return &UID{
u: u,
}, nil
now := uint64(time.Now().Round(0).UnixMilli())
u.u[0] = byte(now)
u.u[1] = byte(now >> 8)
u.u[2] = byte(now >> 16)
u.u[3] = byte(now >> 24)
u.u[4] = byte(now >> 32)
u.u[5] = byte(now >> 40)
return u, nil
}

// Bytes returns the full raw bytes of an UID
func (u *UID) Bytes() []byte {
return u.u
// Bytes returns the full raw bytes
func (u UID) Bytes() []byte {
return u.u[:]
}

// Base64 returns the full raw bytes of an UID encoded in unpadded base64url
func (u *UID) Base64() string {
return base64.RawURLEncoding.EncodeToString(u.u)
// Base64 returns the full raw bytes encoded in unpadded base64hex
func (u UID) Base64() string {
return base64HexEncoding.EncodeToString(u.u[:])
}

type (
// Snowflake is a uid approximately sortable by time
Snowflake struct {
u []byte
}
)

const (
timeSize = 8
// Snowflake is a short, time orderable unique identifier
Snowflake uint64
)

// NewSnowflake creates a new snowflake uid
func NewSnowflake(randsize int) (*Snowflake, error) {
u := make([]byte, timeSize+randsize)
// NewSnowflake returns a new [Snowflake] with a provided seq number
func NewSnowflake(seq uint32) Snowflake {
now := uint64(time.Now().Round(0).UnixMilli())
binary.BigEndian.PutUint64(u[:timeSize], now)
_, err := rand.Read(u[timeSize:])
if err != nil {
return nil, kerrors.WithKind(err, ErrRand, "Failed reading crypto/rand")
}
return &Snowflake{
u: u,
}, nil
now = now << 24
now |= (uint64(seq) & 0xffffff)
return Snowflake(now)
}

// Bytes returns the full raw bytes of a snowflake
func (s *Snowflake) Bytes() []byte {
return s.u
// Base64 returns the full raw bytes encoded in unpadded base64hex
func (s Snowflake) Base64() string {
var u [8]byte
binary.BigEndian.PutUint64(u[:], uint64(s))
return base64HexEncoding.EncodeToString(u[:])
}

var base32RawHexEncoding = base32.HexEncoding.WithPadding(base32.NoPadding)

// Base32 returns the full raw bytes of a snowflake in unpadded base32hex
func (s *Snowflake) Base32() string {
return strings.ToLower(base32RawHexEncoding.EncodeToString(s.u))
// NewRandSnowflake returns a new [Snowflake] with random bytes for the seq
func NewRandSnowflake() (Snowflake, error) {
var u [3]byte
_, err := rand.Read(u[:])
if err != nil {
return 0, kerrors.WithKind(err, ErrRand, "Failed reading crypto/rand")
}
k := uint32(u[0])
k |= uint32(u[1]) << 8
k |= uint32(u[2]) << 16
return NewSnowflake(k), nil
}

const (
reqIDUnusedTimeSize = 3
reqIDTimeSize = 5
reqIDTotalTimeSize = reqIDUnusedTimeSize + reqIDTimeSize
reqIDCounterSize = 3
reqIDUnusedCounterSize = 1
reqIDTotalCounterSize = reqIDCounterSize + reqIDUnusedCounterSize
reqIDSize = reqIDTimeSize + reqIDCounterSize
reqIDCounterShift = 8 * reqIDUnusedCounterSize
type (
// Key is a secret key
Key struct {
u [32]byte
}
)

func ReqID(count uint32) string {
// id looks like:
// reqIDUnusedTimeSize | reqIDTimeSize | reqIDCounterSize | reqIDUnusedCounterSize
b := [reqIDTotalTimeSize + reqIDTotalCounterSize]byte{}
now := uint64(time.Now().Round(0).UnixMilli())
binary.BigEndian.PutUint64(b[:reqIDTotalTimeSize], now)
binary.BigEndian.PutUint32(b[reqIDTotalTimeSize:], count<<reqIDCounterShift)
return strings.ToLower(base32RawHexEncoding.EncodeToString(b[reqIDUnusedTimeSize : reqIDUnusedTimeSize+reqIDSize]))
// NewKey creates a new Key
func NewKey() (*Key, error) {
u := &Key{}
_, err := rand.Read(u.u[:])
if err != nil {
return nil, kerrors.WithKind(err, ErrRand, "Failed reading crypto/rand")
}
return u, nil
}

// Bytes returns the full raw bytes
func (u Key) Bytes() []byte {
return u.u[:]
}

// Base64 returns the full raw bytes encoded in unpadded base64hex
func (u Key) Base64() string {
return base64HexEncoding.EncodeToString(u.u[:])
}
29 changes: 20 additions & 9 deletions util/uid/uid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,30 +6,41 @@ import (
"github.com/stretchr/testify/require"
)

func TestNew(t *testing.T) {
func TestUID(t *testing.T) {
t.Parallel()

assert := require.New(t)

{
u, err := New(8)
u, err := New()
assert.NoError(err, "New uid should not error")
assert.NotNil(u, "Uid should not be nil")
assert.Len(u.Bytes(), 8, "Uid bytes should have the correct length")
assert.Len(u.Base64(), 11, "Uid base64 should have the correct length")
assert.Len(u.Bytes(), 16, "Uid bytes should have the correct length")
assert.Len(u.Base64(), 22, "Uid base64 should have the correct length")
}
}

func TestNewSnowflake(t *testing.T) {
func TestSnowflake(t *testing.T) {
t.Parallel()

assert := require.New(t)

{
u, err := NewSnowflake(8)
u, err := NewRandSnowflake()
assert.NoError(err, "New snowflake should not error")
assert.NotNil(u, "Snowflake should not be nil")
assert.Len(u.Bytes(), 8+timeSize, "Snowflake bytes should have the correct length")
assert.Len(u.Base32(), 26, "Snowflake base32 should have the correct length")
assert.Len(u.Base64(), 11, "Snowflake base64 should have the correct length")
}
}

func TestKey(t *testing.T) {
t.Parallel()

assert := require.New(t)

{
u, err := NewKey()
assert.NoError(err, "New key should not error")
assert.Len(u.Bytes(), 32, "Key bytes should have the correct length")
assert.Len(u.Base64(), 43, "Key base64 should have the correct length")
}
}

0 comments on commit 9d1a8b3

Please sign in to comment.