Skip to content

Commit

Permalink
fix: make iid fit into 63 bits (#10) (#11)
Browse files Browse the repository at this point in the history
* fix: shift timestamp 1 bit to right

* test: make RandomReader and Timestamp mockable
  • Loading branch information
robojones authored Jul 9, 2019
1 parent 1493aef commit c63fa88
Show file tree
Hide file tree
Showing 5 changed files with 76 additions and 16 deletions.
12 changes: 8 additions & 4 deletions iid.go
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,23 @@ const (

var enc = base64.NewEncoding(encoder)

var RandReader = rand.Reader

var Timestamp = func() uint32 {
return uint32(time.Now().Unix())
}

// New generates a completely new Iid containing the current timestamp and four cryptographically secure random bytes.
func New() Iid {
b := make([]byte, iidLen)

// Set the last four bytes to random values.
if _, err := rand.Read(b[offset:]); err != nil {
if _, err := RandReader.Read(b[offset:]); err != nil {
panic(errors.Wrap(err, "generate new iid"))
}

// Write current UNIX time in ms to the first four bytes.
now := time.Now()
s := uint32(now.Unix())
binary.BigEndian.PutUint32(b, s)
writeTime(b, Timestamp())

return Iid(b)
}
Expand Down
31 changes: 23 additions & 8 deletions iid_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,28 @@ import (
"fmt"
"github.com/stretchr/testify/assert"
"log"
"os"
"testing"
"time"
)

const fixedTime uint32 = 1562658708

type mockReader struct {}
func (*mockReader) Read(b []byte) (n int, e error) {
return len(b), nil
}

func TestMain(m *testing.M) {
// Mock all inputs.
RandReader = &mockReader{}
Timestamp = func () uint32 {
return fixedTime
}

os.Exit(m.Run())
}


func TestEncoding(t *testing.T) {
// Verify that the encoding is sortable.
for i := 1; i < len(encoder); i++ {
Expand All @@ -17,19 +35,16 @@ func TestEncoding(t *testing.T) {
}

func TestNew(t *testing.T) {
before := time.Now().Unix()
id := New()
after := time.Now().Unix()

idTime := int64(binary.BigEndian.Uint32(id))

assert.True(t, idTime >= before)
assert.True(t, idTime <= after)
idTime := binary.BigEndian.Uint32(id)
assert.True(t, idTime == (fixedTime >> 1))
}

func ExampleNew() {
id := New()
fmt.Println(id)

// Output: Ad7YmV-----
}

func TestFromString(t *testing.T) {
Expand Down
10 changes: 6 additions & 4 deletions readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,14 +8,16 @@ Small, globally unique IDs implemented in Go.

- Can be sorted by creation time
- Globally unique
- Small size (can be stored in a 64 bit unsigned integer or an 11 byte string)
- Small size (can be stored in a 64 bit integer or an 11 byte string)
- base64url encoded string format: the ids can be used in URLs

## ID Format

| 4 Byte | 4 Byte |
| -------------------- | ------------------------------------- |
| Timestamp in seconds | Cryptographically secure random bytes |
| 1 Bit | 32 Bit | 31 Bit |
| -------- | -------------------- | ------------------------------------- |
| Not used | Timestamp in seconds | Cryptographically secure random bytes |

The first bit is not used so the ID fits into 64 bit signed integers.

## Usage Example

Expand Down
21 changes: 21 additions & 0 deletions write.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package iid

import (
"github.com/pkg/errors"
)

// Writes the provided t into the first 33 bit of b
func writeTime(b []byte, t uint32) {
if len(b) < 5 {
panic(errors.New("length of b is less than 5"))
}

b[0] = byte(t >> 25)
b[1] = byte(t >> 17)
b[2] = byte(t >> 9)
b[3] = byte(t >> 1)
// unset the first bit
b[4] = b[4] << 1 >> 1
// set the first bit to the value of the last bit of t
b[4] = b[4] | byte(t << 7)
}
18 changes: 18 additions & 0 deletions write_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package iid

import (
"github.com/stretchr/testify/assert"
"testing"
)

func TestWrite(t *testing.T) {
var b uint8 = 0xFF
buf := make([]byte, 5)
writeTime(buf, 0xFFFFFFFF)

assert.Equal(t, b >> 1, buf[0])
assert.Equal(t, b, buf[1])
assert.Equal(t, b, buf[2])
assert.Equal(t, b, buf[3])
assert.Equal(t, b << 7, buf[4])
}

0 comments on commit c63fa88

Please sign in to comment.