Commit 0fc94993f38fc854d2cb608d540701f934f0c35d
1 parent
d5aaa06d
Exists in
master
增加生成uuid
Showing
18 changed files
with
2695 additions
and
1 deletions
Show diff stats
src/HttpServer/main/main.go
| ... | ... | @@ -0,0 +1,12 @@ |
| 1 | +version: 2 | |
| 2 | +jobs: | |
| 3 | + build: | |
| 4 | + working_directory: /go/src/github.com/segmentio/ksuid | |
| 5 | + docker: | |
| 6 | + - image: circleci/golang | |
| 7 | + steps: | |
| 8 | + - checkout | |
| 9 | + - setup_remote_docker: { reusable: true, docker_layer_caching: true } | |
| 10 | + - run: go get -v -t ./... | |
| 11 | + - run: go vet ./... | |
| 12 | + - run: go test -v -race ./... | ... | ... |
| ... | ... | @@ -0,0 +1,31 @@ |
| 1 | +# Compiled Object files, Static and Dynamic libs (Shared Objects) | |
| 2 | +*.o | |
| 3 | +*.a | |
| 4 | +*.so | |
| 5 | + | |
| 6 | +# Folders | |
| 7 | +_obj | |
| 8 | +_test | |
| 9 | + | |
| 10 | +# Architecture specific extensions/prefixes | |
| 11 | +*.[568vq] | |
| 12 | +[568vq].out | |
| 13 | + | |
| 14 | +*.cgo1.go | |
| 15 | +*.cgo2.c | |
| 16 | +_cgo_defun.c | |
| 17 | +_cgo_gotypes.go | |
| 18 | +_cgo_export.* | |
| 19 | + | |
| 20 | +_testmain.go | |
| 21 | + | |
| 22 | +*.exe | |
| 23 | +*.test | |
| 24 | +*.prof | |
| 25 | +/ksuid | |
| 26 | + | |
| 27 | +# Emacs | |
| 28 | +*~ | |
| 29 | + | |
| 30 | +# govendor | |
| 31 | +/vendor/*/ | ... | ... |
| ... | ... | @@ -0,0 +1,21 @@ |
| 1 | +MIT License | |
| 2 | + | |
| 3 | +Copyright (c) 2017 Segment.io | |
| 4 | + | |
| 5 | +Permission is hereby granted, free of charge, to any person obtaining a copy | |
| 6 | +of this software and associated documentation files (the "Software"), to deal | |
| 7 | +in the Software without restriction, including without limitation the rights | |
| 8 | +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell | |
| 9 | +copies of the Software, and to permit persons to whom the Software is | |
| 10 | +furnished to do so, subject to the following conditions: | |
| 11 | + | |
| 12 | +The above copyright notice and this permission notice shall be included in all | |
| 13 | +copies or substantial portions of the Software. | |
| 14 | + | |
| 15 | +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR | |
| 16 | +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, | |
| 17 | +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE | |
| 18 | +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER | |
| 19 | +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, | |
| 20 | +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE | |
| 21 | +SOFTWARE. | ... | ... |
| ... | ... | @@ -0,0 +1,146 @@ |
| 1 | +# ksuid [](https://goreportcard.com/report/github.com/segmentio/ksuid) [](https://godoc.org/github.com/segmentio/ksuid) [](https://circleci.com/gh/segmentio/ksuid.svg?style=shield) | |
| 2 | + | |
| 3 | +ksuid is a Go library that can generate and parse KSUIDs. | |
| 4 | + | |
| 5 | +# Install | |
| 6 | +```bash | |
| 7 | +go get -u github.com/segmentio/ksuid | |
| 8 | +``` | |
| 9 | + | |
| 10 | +# What is a KSUID? | |
| 11 | + | |
| 12 | +KSUID is for K-Sortable Unique IDentifier. It's a way to generate globally | |
| 13 | +unique IDs similar to RFC 4122 UUIDs, but contain a time component so they | |
| 14 | +can be "roughly" sorted by time of creation. The remainder of the KSUID is | |
| 15 | +randomly generated bytes. | |
| 16 | + | |
| 17 | +# Why use KSUIDs? | |
| 18 | + | |
| 19 | +Distributed systems often require unique IDs. There are numerous solutions | |
| 20 | +out there for doing this, so why KSUID? | |
| 21 | + | |
| 22 | +## 1. Sortable by Timestamp | |
| 23 | + | |
| 24 | +Unlike the more common choice of UUIDv4, KSUIDs contain a timestamp component | |
| 25 | +that allows them to be roughly sorted by generation time. This is obviously not | |
| 26 | +a strong guarantee as it depends on wall clocks, but is still incredibly useful | |
| 27 | +in practice. | |
| 28 | + | |
| 29 | +## 2. No Coordination Required | |
| 30 | + | |
| 31 | +Snowflake IDs[1] and derivatives require coordination, which significantly | |
| 32 | +increases the complexity of implementation and creates operations overhead. | |
| 33 | +While RFC 4122 UUIDv1s do have a time component, there aren't enough bytes of | |
| 34 | +randomness to provide strong protections against duplicate ID generation. | |
| 35 | + | |
| 36 | +KSUIDs use 128-bits of pseudorandom data, which provides a 64-times larger | |
| 37 | +number space than the 122-bits in the well-accepted RFC 4122 UUIDv4 standard. | |
| 38 | +The additional timestamp component drives down the extremely rare chance of | |
| 39 | +duplication to the point of near physical infeasibility, even assuming extreme | |
| 40 | +clock skew (> 24-hours) that would cause other severe anomalies. | |
| 41 | + | |
| 42 | +1. https://blog.twitter.com/2010/announcing-snowflake | |
| 43 | + | |
| 44 | +## 3. Lexographically Sortable, Portable Representations | |
| 45 | + | |
| 46 | +The binary and string representations are lexicographically sortable, which | |
| 47 | +allows them to be dropped into systems which do not natively support KSUIDs | |
| 48 | +and retain their k-sortable characteristics. | |
| 49 | + | |
| 50 | +The string representation is that it is base62-encoded, so that they can "fit" | |
| 51 | +anywhere alphanumeric strings are accepted. | |
| 52 | + | |
| 53 | +# How do they work? | |
| 54 | + | |
| 55 | +KSUIDs are 20-bytes: a 32-bit unsigned integer UTC timestamp and a 128-bit | |
| 56 | +randomly generated payload. The timestamp uses big-endian encoding, to allow | |
| 57 | +lexicographic sorting. The timestamp epoch is adjusted to March 5th, 2014, | |
| 58 | +providing over 100 years of useful life starting at UNIX epoch + 14e8. The | |
| 59 | +payload uses a cryptographically-strong pseudorandom number generator. | |
| 60 | + | |
| 61 | +The string representation is fixed at 27-characters encoded using a base62 | |
| 62 | +encoding that also sorts lexicographically. | |
| 63 | + | |
| 64 | +# Command Line Tool | |
| 65 | + | |
| 66 | +This package comes with a simple command-line tool `ksuid`. This tool can | |
| 67 | +generate KSUIDs as well as inspect the internal components for debugging | |
| 68 | +purposes. | |
| 69 | + | |
| 70 | +## Usage examples | |
| 71 | + | |
| 72 | +### Generate 4 KSUID | |
| 73 | + | |
| 74 | +```sh | |
| 75 | +$ ./ksuid -n 4 | |
| 76 | +0ujsszwN8NRY24YaXiTIE2VWDTS | |
| 77 | +0ujsswThIGTUYm2K8FjOOfXtY1K | |
| 78 | +0ujssxh0cECutqzMgbtXSGnjorm | |
| 79 | +0ujsszgFvbiEr7CDgE3z8MAUPFt | |
| 80 | +``` | |
| 81 | + | |
| 82 | +### Inspect the components of a KSUID | |
| 83 | + | |
| 84 | +Using the inspect formatting on just 1 ksuid: | |
| 85 | + | |
| 86 | +```sh | |
| 87 | +$ ./ksuid -f inspect $(./ksuid) | |
| 88 | + | |
| 89 | +REPRESENTATION: | |
| 90 | + | |
| 91 | + String: 0ujtsYcgvSTl8PAuAdqWYSMnLOv | |
| 92 | + Raw: 0669F7EFB5A1CD34B5F99D1154FB6853345C9735 | |
| 93 | + | |
| 94 | +COMPONENTS: | |
| 95 | + | |
| 96 | + Time: 2017-10-09 21:00:47 -0700 PDT | |
| 97 | + Timestamp: 107608047 | |
| 98 | + Payload: B5A1CD34B5F99D1154FB6853345C9735 | |
| 99 | +``` | |
| 100 | + | |
| 101 | +Using the template formatting on 4 ksuid: | |
| 102 | + | |
| 103 | +```sh | |
| 104 | +$ ./ksuid -f template -t '{{ .Time }}: {{ .Payload }}' $(./ksuid -n 4) | |
| 105 | +2017-10-09 21:05:37 -0700 PDT: 304102BC687E087CC3A811F21D113CCF | |
| 106 | +2017-10-09 21:05:37 -0700 PDT: EAF0B240A9BFA55E079D887120D962F0 | |
| 107 | +2017-10-09 21:05:37 -0700 PDT: DF0761769909ABB0C7BB9D66F79FC041 | |
| 108 | +2017-10-09 21:05:37 -0700 PDT: 1A8F0E3D0BDEB84A5FAD702876F46543 | |
| 109 | +``` | |
| 110 | + | |
| 111 | +### Generate detailed versions of new KSUID | |
| 112 | + | |
| 113 | +Generate a new KSUID with the corresponding time using the time formatting: | |
| 114 | + | |
| 115 | +```sh | |
| 116 | +$ go run cmd/ksuid/main.go -f time -v | |
| 117 | +0uk0ava2lavfJwMceJOOEFXEDxl: 2017-10-09 21:56:00 -0700 PDT | |
| 118 | +``` | |
| 119 | + | |
| 120 | +Generate 4 new KSUID with details using template formatting: | |
| 121 | + | |
| 122 | +```sh | |
| 123 | +$ ./ksuid -f template -t '{ "timestamp": "{{ .Timestamp }}", "payload": "{{ .Payload }}", "ksuid": "{{.String}}"}' -n 4 | |
| 124 | +{ "timestamp": "107611700", "payload": "9850EEEC191BF4FF26F99315CE43B0C8", "ksuid": "0uk1Hbc9dQ9pxyTqJ93IUrfhdGq"} | |
| 125 | +{ "timestamp": "107611700", "payload": "CC55072555316F45B8CA2D2979D3ED0A", "ksuid": "0uk1HdCJ6hUZKDgcxhpJwUl5ZEI"} | |
| 126 | +{ "timestamp": "107611700", "payload": "BA1C205D6177F0992D15EE606AE32238", "ksuid": "0uk1HcdvF0p8C20KtTfdRSB9XIm"} | |
| 127 | +{ "timestamp": "107611700", "payload": "67517BA309EA62AE7991B27BB6F2FCAC", "ksuid": "0uk1Ha7hGJ1Q9Xbnkt0yZgNwg3g"} | |
| 128 | +``` | |
| 129 | + | |
| 130 | +Display the detailed version of a new KSUID: | |
| 131 | + | |
| 132 | +```sh | |
| 133 | +$ ./ksuid -f inspect | |
| 134 | + | |
| 135 | +REPRESENTATION: | |
| 136 | + | |
| 137 | + String: 0ujzPyRiIAffKhBux4PvQdDqMHY | |
| 138 | + Raw: 066A029C73FC1AA3B2446246D6E89FCD909E8FE8 | |
| 139 | + | |
| 140 | +COMPONENTS: | |
| 141 | + | |
| 142 | + Time: 2017-10-09 21:46:20 -0700 PDT | |
| 143 | + Timestamp: 107610780 | |
| 144 | + Payload: 73FC1AA3B2446246D6E89FCD909E8FE8 | |
| 145 | + | |
| 146 | +``` | ... | ... |
| ... | ... | @@ -0,0 +1,211 @@ |
| 1 | +package ksuid | |
| 2 | + | |
| 3 | +import "errors" | |
| 4 | + | |
| 5 | +const ( | |
| 6 | + // lexographic ordering (based on Unicode table) is 0-9A-Za-z | |
| 7 | + base62Characters = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" | |
| 8 | + zeroString = "000000000000000000000000000" | |
| 9 | + offsetUppercase = 10 | |
| 10 | + offsetLowercase = 36 | |
| 11 | +) | |
| 12 | + | |
| 13 | +var ( | |
| 14 | + errShortBuffer = errors.New("the output buffer is too small to hold to decoded value") | |
| 15 | +) | |
| 16 | + | |
| 17 | +// Converts a base 62 byte into the number value that it represents. | |
| 18 | +func base62Value(digit byte) byte { | |
| 19 | + switch { | |
| 20 | + case digit >= '0' && digit <= '9': | |
| 21 | + return digit - '0' | |
| 22 | + case digit >= 'A' && digit <= 'Z': | |
| 23 | + return offsetUppercase + (digit - 'A') | |
| 24 | + default: | |
| 25 | + return offsetLowercase + (digit - 'a') | |
| 26 | + } | |
| 27 | +} | |
| 28 | + | |
| 29 | +// This function encodes the base 62 representation of the src KSUID in binary | |
| 30 | +// form into dst. | |
| 31 | +// | |
| 32 | +// In order to support a couple of optimizations the function assumes that src | |
| 33 | +// is 20 bytes long and dst is 27 bytes long. | |
| 34 | +// | |
| 35 | +// Any unused bytes in dst will be set to the padding '0' byte. | |
| 36 | +func fastEncodeBase62(dst []byte, src []byte) { | |
| 37 | + const srcBase = 4294967296 | |
| 38 | + const dstBase = 62 | |
| 39 | + | |
| 40 | + // Split src into 5 4-byte words, this is where most of the efficiency comes | |
| 41 | + // from because this is a O(N^2) algorithm, and we make N = N / 4 by working | |
| 42 | + // on 32 bits at a time. | |
| 43 | + parts := [5]uint32{ | |
| 44 | + /* | |
| 45 | + These is an inlined version of: | |
| 46 | + | |
| 47 | + binary.BigEndian.Uint32(src[0:4]), | |
| 48 | + binary.BigEndian.Uint32(src[4:8]), | |
| 49 | + binary.BigEndian.Uint32(src[8:12]), | |
| 50 | + binary.BigEndian.Uint32(src[12:16]), | |
| 51 | + binary.BigEndian.Uint32(src[16:20]), | |
| 52 | + | |
| 53 | + For some reason it gave better performance, may be caused by the | |
| 54 | + bound check that the Uint32 function does. | |
| 55 | + */ | |
| 56 | + uint32(src[0])<<24 | uint32(src[1])<<16 | uint32(src[2])<<8 | uint32(src[3]), | |
| 57 | + uint32(src[4])<<24 | uint32(src[5])<<16 | uint32(src[6])<<8 | uint32(src[7]), | |
| 58 | + uint32(src[8])<<24 | uint32(src[9])<<16 | uint32(src[10])<<8 | uint32(src[11]), | |
| 59 | + uint32(src[12])<<24 | uint32(src[13])<<16 | uint32(src[14])<<8 | uint32(src[15]), | |
| 60 | + uint32(src[16])<<24 | uint32(src[17])<<16 | uint32(src[18])<<8 | uint32(src[19]), | |
| 61 | + } | |
| 62 | + | |
| 63 | + n := len(dst) | |
| 64 | + bp := parts[:] | |
| 65 | + bq := [5]uint32{} | |
| 66 | + | |
| 67 | + for len(bp) != 0 { | |
| 68 | + quotient := bq[:0] | |
| 69 | + remainder := uint64(0) | |
| 70 | + | |
| 71 | + for _, c := range bp { | |
| 72 | + value := uint64(c) + uint64(remainder)*srcBase | |
| 73 | + digit := value / dstBase | |
| 74 | + remainder = value % dstBase | |
| 75 | + | |
| 76 | + if len(quotient) != 0 || digit != 0 { | |
| 77 | + quotient = append(quotient, uint32(digit)) | |
| 78 | + } | |
| 79 | + } | |
| 80 | + | |
| 81 | + // Writes at the end of the destination buffer because we computed the | |
| 82 | + // lowest bits first. | |
| 83 | + n-- | |
| 84 | + dst[n] = base62Characters[remainder] | |
| 85 | + bp = quotient | |
| 86 | + } | |
| 87 | + | |
| 88 | + // Add padding at the head of the destination buffer for all bytes that were | |
| 89 | + // not set. | |
| 90 | + copy(dst[:n], zeroString) | |
| 91 | +} | |
| 92 | + | |
| 93 | +// This function appends the base 62 representation of the KSUID in src to dst, | |
| 94 | +// and returns the extended byte slice. | |
| 95 | +// The result is left-padded with '0' bytes to always append 27 bytes to the | |
| 96 | +// destination buffer. | |
| 97 | +func fastAppendEncodeBase62(dst []byte, src []byte) []byte { | |
| 98 | + dst = reserve(dst, stringEncodedLength) | |
| 99 | + n := len(dst) | |
| 100 | + fastEncodeBase62(dst[n:n+stringEncodedLength], src) | |
| 101 | + return dst[:n+stringEncodedLength] | |
| 102 | +} | |
| 103 | + | |
| 104 | +// This function decodes the base 62 representation of the src KSUID to the | |
| 105 | +// binary form into dst. | |
| 106 | +// | |
| 107 | +// In order to support a couple of optimizations the function assumes that src | |
| 108 | +// is 27 bytes long and dst is 20 bytes long. | |
| 109 | +// | |
| 110 | +// Any unused bytes in dst will be set to zero. | |
| 111 | +func fastDecodeBase62(dst []byte, src []byte) error { | |
| 112 | + const srcBase = 62 | |
| 113 | + const dstBase = 4294967296 | |
| 114 | + | |
| 115 | + // This line helps BCE (Bounds Check Elimination). | |
| 116 | + // It may be safely removed. | |
| 117 | + _ = src[26] | |
| 118 | + | |
| 119 | + parts := [27]byte{ | |
| 120 | + base62Value(src[0]), | |
| 121 | + base62Value(src[1]), | |
| 122 | + base62Value(src[2]), | |
| 123 | + base62Value(src[3]), | |
| 124 | + base62Value(src[4]), | |
| 125 | + base62Value(src[5]), | |
| 126 | + base62Value(src[6]), | |
| 127 | + base62Value(src[7]), | |
| 128 | + base62Value(src[8]), | |
| 129 | + base62Value(src[9]), | |
| 130 | + | |
| 131 | + base62Value(src[10]), | |
| 132 | + base62Value(src[11]), | |
| 133 | + base62Value(src[12]), | |
| 134 | + base62Value(src[13]), | |
| 135 | + base62Value(src[14]), | |
| 136 | + base62Value(src[15]), | |
| 137 | + base62Value(src[16]), | |
| 138 | + base62Value(src[17]), | |
| 139 | + base62Value(src[18]), | |
| 140 | + base62Value(src[19]), | |
| 141 | + | |
| 142 | + base62Value(src[20]), | |
| 143 | + base62Value(src[21]), | |
| 144 | + base62Value(src[22]), | |
| 145 | + base62Value(src[23]), | |
| 146 | + base62Value(src[24]), | |
| 147 | + base62Value(src[25]), | |
| 148 | + base62Value(src[26]), | |
| 149 | + } | |
| 150 | + | |
| 151 | + n := len(dst) | |
| 152 | + bp := parts[:] | |
| 153 | + bq := [stringEncodedLength]byte{} | |
| 154 | + | |
| 155 | + for len(bp) > 0 { | |
| 156 | + quotient := bq[:0] | |
| 157 | + remainder := uint64(0) | |
| 158 | + | |
| 159 | + for _, c := range bp { | |
| 160 | + value := uint64(c) + uint64(remainder)*srcBase | |
| 161 | + digit := value / dstBase | |
| 162 | + remainder = value % dstBase | |
| 163 | + | |
| 164 | + if len(quotient) != 0 || digit != 0 { | |
| 165 | + quotient = append(quotient, byte(digit)) | |
| 166 | + } | |
| 167 | + } | |
| 168 | + | |
| 169 | + if n < 4 { | |
| 170 | + return errShortBuffer | |
| 171 | + } | |
| 172 | + | |
| 173 | + dst[n-4] = byte(remainder >> 24) | |
| 174 | + dst[n-3] = byte(remainder >> 16) | |
| 175 | + dst[n-2] = byte(remainder >> 8) | |
| 176 | + dst[n-1] = byte(remainder) | |
| 177 | + n -= 4 | |
| 178 | + bp = quotient | |
| 179 | + } | |
| 180 | + | |
| 181 | + var zero [20]byte | |
| 182 | + copy(dst[:n], zero[:]) | |
| 183 | + return nil | |
| 184 | +} | |
| 185 | + | |
| 186 | +// This function appends the base 62 decoded version of src into dst. | |
| 187 | +func fastAppendDecodeBase62(dst []byte, src []byte) []byte { | |
| 188 | + dst = reserve(dst, byteLength) | |
| 189 | + n := len(dst) | |
| 190 | + fastDecodeBase62(dst[n:n+byteLength], src) | |
| 191 | + return dst[:n+byteLength] | |
| 192 | +} | |
| 193 | + | |
| 194 | +// Ensures that at least nbytes are available in the remaining capacity of the | |
| 195 | +// destination slice, if not, a new copy is made and returned by the function. | |
| 196 | +func reserve(dst []byte, nbytes int) []byte { | |
| 197 | + c := cap(dst) | |
| 198 | + n := len(dst) | |
| 199 | + | |
| 200 | + if avail := c - n; avail < nbytes { | |
| 201 | + c *= 2 | |
| 202 | + if (c - n) < nbytes { | |
| 203 | + c = n + nbytes | |
| 204 | + } | |
| 205 | + b := make([]byte, n, c) | |
| 206 | + copy(b, dst) | |
| 207 | + dst = b | |
| 208 | + } | |
| 209 | + | |
| 210 | + return dst | |
| 211 | +} | ... | ... |
| ... | ... | @@ -0,0 +1,245 @@ |
| 1 | +package ksuid | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "bytes" | |
| 5 | + "sort" | |
| 6 | + "strings" | |
| 7 | + "testing" | |
| 8 | +) | |
| 9 | + | |
| 10 | +func TestBase10ToBase62AndBack(t *testing.T) { | |
| 11 | + number := []byte{1, 2, 3, 4} | |
| 12 | + encoded := base2base(number, 10, 62) | |
| 13 | + decoded := base2base(encoded, 62, 10) | |
| 14 | + | |
| 15 | + if bytes.Compare(number, decoded) != 0 { | |
| 16 | + t.Fatal(number, " != ", decoded) | |
| 17 | + } | |
| 18 | +} | |
| 19 | + | |
| 20 | +func TestBase256ToBase62AndBack(t *testing.T) { | |
| 21 | + number := []byte{255, 254, 253, 251} | |
| 22 | + encoded := base2base(number, 256, 62) | |
| 23 | + decoded := base2base(encoded, 62, 256) | |
| 24 | + | |
| 25 | + if bytes.Compare(number, decoded) != 0 { | |
| 26 | + t.Fatal(number, " != ", decoded) | |
| 27 | + } | |
| 28 | +} | |
| 29 | + | |
| 30 | +func TestEncodeAndDecodeBase62(t *testing.T) { | |
| 31 | + helloWorld := []byte("hello world") | |
| 32 | + encoded := encodeBase62(helloWorld) | |
| 33 | + decoded := decodeBase62(encoded) | |
| 34 | + | |
| 35 | + if len(encoded) < len(helloWorld) { | |
| 36 | + t.Fatal("length of encoded base62 string", encoded, "should be >= than raw bytes!") | |
| 37 | + | |
| 38 | + } | |
| 39 | + | |
| 40 | + if bytes.Compare(helloWorld, decoded) != 0 { | |
| 41 | + t.Fatal(decoded, " != ", helloWorld) | |
| 42 | + } | |
| 43 | +} | |
| 44 | + | |
| 45 | +func TestLexographicOrdering(t *testing.T) { | |
| 46 | + unsortedStrings := make([]string, 256) | |
| 47 | + for i := 0; i < 256; i++ { | |
| 48 | + s := string(encodeBase62([]byte{0, byte(i)})) | |
| 49 | + unsortedStrings[i] = strings.Repeat("0", 2-len(s)) + s | |
| 50 | + } | |
| 51 | + | |
| 52 | + if !sort.StringsAreSorted(unsortedStrings) { | |
| 53 | + sortedStrings := make([]string, len(unsortedStrings)) | |
| 54 | + for i, s := range unsortedStrings { | |
| 55 | + sortedStrings[i] = s | |
| 56 | + } | |
| 57 | + sort.Strings(sortedStrings) | |
| 58 | + | |
| 59 | + t.Fatal("base62 encoder does not produce lexographically sorted output.", | |
| 60 | + "expected:", sortedStrings, | |
| 61 | + "actual:", unsortedStrings) | |
| 62 | + } | |
| 63 | +} | |
| 64 | + | |
| 65 | +func TestBase62Value(t *testing.T) { | |
| 66 | + s := base62Characters | |
| 67 | + | |
| 68 | + for i := range s { | |
| 69 | + v := int(base62Value(s[i])) | |
| 70 | + | |
| 71 | + if v != i { | |
| 72 | + t.Error("bad value:") | |
| 73 | + t.Log("<<<", i) | |
| 74 | + t.Log(">>>", v) | |
| 75 | + } | |
| 76 | + } | |
| 77 | +} | |
| 78 | + | |
| 79 | +func TestFastAppendEncodeBase62(t *testing.T) { | |
| 80 | + for i := 0; i != 1000; i++ { | |
| 81 | + id := New() | |
| 82 | + | |
| 83 | + b0 := id[:] | |
| 84 | + b1 := appendEncodeBase62(nil, b0) | |
| 85 | + b2 := fastAppendEncodeBase62(nil, b0) | |
| 86 | + | |
| 87 | + s1 := string(leftpad(b1, '0', stringEncodedLength)) | |
| 88 | + s2 := string(b2) | |
| 89 | + | |
| 90 | + if s1 != s2 { | |
| 91 | + t.Error("bad base62 representation of", id) | |
| 92 | + t.Log("<<<", s1, len(s1)) | |
| 93 | + t.Log(">>>", s2, len(s2)) | |
| 94 | + } | |
| 95 | + } | |
| 96 | +} | |
| 97 | + | |
| 98 | +func TestFastAppendDecodeBase62(t *testing.T) { | |
| 99 | + for i := 0; i != 1000; i++ { | |
| 100 | + id := New() | |
| 101 | + b0 := leftpad(encodeBase62(id[:]), '0', stringEncodedLength) | |
| 102 | + | |
| 103 | + b1 := appendDecodeBase62(nil, []byte(string(b0))) // because it modifies the input buffer | |
| 104 | + b2 := fastAppendDecodeBase62(nil, b0) | |
| 105 | + | |
| 106 | + if !bytes.Equal(leftpad(b1, 0, byteLength), b2) { | |
| 107 | + t.Error("bad binary representation of", string(b0)) | |
| 108 | + t.Log("<<<", b1) | |
| 109 | + t.Log(">>>", b2) | |
| 110 | + } | |
| 111 | + } | |
| 112 | +} | |
| 113 | + | |
| 114 | +func BenchmarkAppendEncodeBase62(b *testing.B) { | |
| 115 | + a := [stringEncodedLength]byte{} | |
| 116 | + id := New() | |
| 117 | + | |
| 118 | + for i := 0; i != b.N; i++ { | |
| 119 | + appendEncodeBase62(a[:0], id[:]) | |
| 120 | + } | |
| 121 | +} | |
| 122 | + | |
| 123 | +func BenchmarkAppendFastEncodeBase62(b *testing.B) { | |
| 124 | + a := [stringEncodedLength]byte{} | |
| 125 | + id := New() | |
| 126 | + | |
| 127 | + for i := 0; i != b.N; i++ { | |
| 128 | + fastAppendEncodeBase62(a[:0], id[:]) | |
| 129 | + } | |
| 130 | +} | |
| 131 | + | |
| 132 | +func BenchmarkAppendDecodeBase62(b *testing.B) { | |
| 133 | + a := [byteLength]byte{} | |
| 134 | + id := []byte(New().String()) | |
| 135 | + | |
| 136 | + for i := 0; i != b.N; i++ { | |
| 137 | + b := [stringEncodedLength]byte{} | |
| 138 | + copy(b[:], id) | |
| 139 | + appendDecodeBase62(a[:0], b[:]) | |
| 140 | + } | |
| 141 | +} | |
| 142 | + | |
| 143 | +func BenchmarkAppendFastDecodeBase62(b *testing.B) { | |
| 144 | + a := [byteLength]byte{} | |
| 145 | + id := []byte(New().String()) | |
| 146 | + | |
| 147 | + for i := 0; i != b.N; i++ { | |
| 148 | + fastAppendDecodeBase62(a[:0], id) | |
| 149 | + } | |
| 150 | +} | |
| 151 | + | |
| 152 | +// The functions bellow were the initial implementation of the base conversion | |
| 153 | +// algorithms, they were replaced by optimized versions later on. We keep them | |
| 154 | +// in the test files as a reference to ensure compatibility between the generic | |
| 155 | +// and optimized implementations. | |
| 156 | + | |
| 157 | +func appendBase2Base(dst []byte, src []byte, inBase int, outBase int) []byte { | |
| 158 | + off := len(dst) | |
| 159 | + bs := src[:] | |
| 160 | + bq := [stringEncodedLength]byte{} | |
| 161 | + | |
| 162 | + for len(bs) > 0 { | |
| 163 | + length := len(bs) | |
| 164 | + quotient := bq[:0] | |
| 165 | + remainder := 0 | |
| 166 | + | |
| 167 | + for i := 0; i != length; i++ { | |
| 168 | + acc := int(bs[i]) + remainder*inBase | |
| 169 | + d := acc/outBase | 0 | |
| 170 | + remainder = acc % outBase | |
| 171 | + | |
| 172 | + if len(quotient) > 0 || d > 0 { | |
| 173 | + quotient = append(quotient, byte(d)) | |
| 174 | + } | |
| 175 | + } | |
| 176 | + | |
| 177 | + // Appends in reverse order, the byte slice gets reversed before it's | |
| 178 | + // returned by the function. | |
| 179 | + dst = append(dst, byte(remainder)) | |
| 180 | + bs = quotient | |
| 181 | + } | |
| 182 | + | |
| 183 | + reverse(dst[off:]) | |
| 184 | + return dst | |
| 185 | +} | |
| 186 | + | |
| 187 | +func base2base(src []byte, inBase int, outBase int) []byte { | |
| 188 | + return appendBase2Base(nil, src, inBase, outBase) | |
| 189 | +} | |
| 190 | + | |
| 191 | +func appendEncodeBase62(dst []byte, src []byte) []byte { | |
| 192 | + off := len(dst) | |
| 193 | + dst = appendBase2Base(dst, src, 256, 62) | |
| 194 | + for i, c := range dst[off:] { | |
| 195 | + dst[off+i] = base62Characters[c] | |
| 196 | + } | |
| 197 | + return dst | |
| 198 | +} | |
| 199 | + | |
| 200 | +func encodeBase62(in []byte) []byte { | |
| 201 | + return appendEncodeBase62(nil, in) | |
| 202 | +} | |
| 203 | + | |
| 204 | +func appendDecodeBase62(dst []byte, src []byte) []byte { | |
| 205 | + // Kind of intrusive, we modify the input buffer... it's OK here, it saves | |
| 206 | + // a memory allocation in Parse. | |
| 207 | + for i, b := range src { | |
| 208 | + // O(1)... technically. Has better real-world perf than a map | |
| 209 | + src[i] = byte(strings.IndexByte(base62Characters, b)) | |
| 210 | + } | |
| 211 | + return appendBase2Base(dst, src, 62, 256) | |
| 212 | +} | |
| 213 | + | |
| 214 | +func decodeBase62(src []byte) []byte { | |
| 215 | + return appendDecodeBase62( | |
| 216 | + make([]byte, 0, len(src)*2), | |
| 217 | + append(make([]byte, 0, len(src)), src...), | |
| 218 | + ) | |
| 219 | +} | |
| 220 | + | |
| 221 | +func reverse(b []byte) { | |
| 222 | + i := 0 | |
| 223 | + j := len(b) - 1 | |
| 224 | + | |
| 225 | + for i < j { | |
| 226 | + b[i], b[j] = b[j], b[i] | |
| 227 | + i++ | |
| 228 | + j-- | |
| 229 | + } | |
| 230 | +} | |
| 231 | + | |
| 232 | +func leftpad(b []byte, c byte, n int) []byte { | |
| 233 | + if n -= len(b); n > 0 { | |
| 234 | + for i := 0; i != n; i++ { | |
| 235 | + b = append(b, c) | |
| 236 | + } | |
| 237 | + | |
| 238 | + copy(b[n:], b) | |
| 239 | + | |
| 240 | + for i := 0; i != n; i++ { | |
| 241 | + b[i] = c | |
| 242 | + } | |
| 243 | + } | |
| 244 | + return b | |
| 245 | +} | ... | ... |
| ... | ... | @@ -0,0 +1,142 @@ |
| 1 | +package main | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "bytes" | |
| 5 | + "encoding/hex" | |
| 6 | + "flag" | |
| 7 | + "fmt" | |
| 8 | + "io" | |
| 9 | + "os" | |
| 10 | + "strings" | |
| 11 | + "text/template" | |
| 12 | + "time" | |
| 13 | + | |
| 14 | + "github.com/segmentio/ksuid" | |
| 15 | +) | |
| 16 | + | |
| 17 | +var ( | |
| 18 | + count int | |
| 19 | + format string | |
| 20 | + tpltxt string | |
| 21 | + verbose bool | |
| 22 | +) | |
| 23 | + | |
| 24 | +func init() { | |
| 25 | + flag.IntVar(&count, "n", 1, "Number of KSUIDs to generate when called with no other arguments.") | |
| 26 | + flag.StringVar(&format, "f", "string", "One of string, inspect, time, timestamp, payload, raw, or template.") | |
| 27 | + flag.StringVar(&tpltxt, "t", "", "The Go template used to format the output.") | |
| 28 | + flag.BoolVar(&verbose, "v", false, "Turn on verbose mode.") | |
| 29 | +} | |
| 30 | + | |
| 31 | +func main() { | |
| 32 | + flag.Parse() | |
| 33 | + args := flag.Args() | |
| 34 | + | |
| 35 | + var print func(ksuid.KSUID) | |
| 36 | + switch format { | |
| 37 | + case "string": | |
| 38 | + print = printString | |
| 39 | + case "inspect": | |
| 40 | + print = printInspect | |
| 41 | + case "time": | |
| 42 | + print = printTime | |
| 43 | + case "timestamp": | |
| 44 | + print = printTimestamp | |
| 45 | + case "payload": | |
| 46 | + print = printPayload | |
| 47 | + case "raw": | |
| 48 | + print = printRaw | |
| 49 | + case "template": | |
| 50 | + print = printTemplate | |
| 51 | + default: | |
| 52 | + fmt.Println("Bad formatting function:", format) | |
| 53 | + os.Exit(1) | |
| 54 | + } | |
| 55 | + | |
| 56 | + if len(args) == 0 { | |
| 57 | + for i := 0; i < count; i++ { | |
| 58 | + args = append(args, ksuid.New().String()) | |
| 59 | + } | |
| 60 | + } | |
| 61 | + | |
| 62 | + var ids []ksuid.KSUID | |
| 63 | + for _, arg := range args { | |
| 64 | + id, err := ksuid.Parse(arg) | |
| 65 | + if err != nil { | |
| 66 | + fmt.Printf("Error when parsing %q: %s\n\n", arg, err) | |
| 67 | + flag.PrintDefaults() | |
| 68 | + os.Exit(1) | |
| 69 | + } | |
| 70 | + ids = append(ids, id) | |
| 71 | + } | |
| 72 | + | |
| 73 | + for _, id := range ids { | |
| 74 | + if verbose { | |
| 75 | + fmt.Printf("%s: ", id) | |
| 76 | + } | |
| 77 | + print(id) | |
| 78 | + } | |
| 79 | +} | |
| 80 | + | |
| 81 | +func printString(id ksuid.KSUID) { | |
| 82 | + fmt.Println(id.String()) | |
| 83 | +} | |
| 84 | + | |
| 85 | +func printInspect(id ksuid.KSUID) { | |
| 86 | + const inspectFormat = ` | |
| 87 | +REPRESENTATION: | |
| 88 | + | |
| 89 | + String: %v | |
| 90 | + Raw: %v | |
| 91 | + | |
| 92 | +COMPONENTS: | |
| 93 | + | |
| 94 | + Time: %v | |
| 95 | + Timestamp: %v | |
| 96 | + Payload: %v | |
| 97 | + | |
| 98 | +` | |
| 99 | + fmt.Printf(inspectFormat, | |
| 100 | + id.String(), | |
| 101 | + strings.ToUpper(hex.EncodeToString(id.Bytes())), | |
| 102 | + id.Time(), | |
| 103 | + id.Timestamp(), | |
| 104 | + strings.ToUpper(hex.EncodeToString(id.Payload())), | |
| 105 | + ) | |
| 106 | +} | |
| 107 | + | |
| 108 | +func printTime(id ksuid.KSUID) { | |
| 109 | + fmt.Println(id.Time()) | |
| 110 | +} | |
| 111 | + | |
| 112 | +func printTimestamp(id ksuid.KSUID) { | |
| 113 | + fmt.Println(id.Timestamp()) | |
| 114 | +} | |
| 115 | + | |
| 116 | +func printPayload(id ksuid.KSUID) { | |
| 117 | + os.Stdout.Write(id.Payload()) | |
| 118 | +} | |
| 119 | + | |
| 120 | +func printRaw(id ksuid.KSUID) { | |
| 121 | + os.Stdout.Write(id.Bytes()) | |
| 122 | +} | |
| 123 | + | |
| 124 | +func printTemplate(id ksuid.KSUID) { | |
| 125 | + b := &bytes.Buffer{} | |
| 126 | + t := template.Must(template.New("").Parse(tpltxt)) | |
| 127 | + t.Execute(b, struct { | |
| 128 | + String string | |
| 129 | + Raw string | |
| 130 | + Time time.Time | |
| 131 | + Timestamp uint32 | |
| 132 | + Payload string | |
| 133 | + }{ | |
| 134 | + String: id.String(), | |
| 135 | + Raw: strings.ToUpper(hex.EncodeToString(id.Bytes())), | |
| 136 | + Time: id.Time(), | |
| 137 | + Timestamp: id.Timestamp(), | |
| 138 | + Payload: strings.ToUpper(hex.EncodeToString(id.Payload())), | |
| 139 | + }) | |
| 140 | + b.WriteByte('\n') | |
| 141 | + io.Copy(os.Stdout, b) | |
| 142 | +} | ... | ... |
| ... | ... | @@ -0,0 +1,352 @@ |
| 1 | +package ksuid | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "bytes" | |
| 5 | + "crypto/rand" | |
| 6 | + "database/sql/driver" | |
| 7 | + "encoding/binary" | |
| 8 | + "fmt" | |
| 9 | + "io" | |
| 10 | + "math" | |
| 11 | + "sync" | |
| 12 | + "time" | |
| 13 | +) | |
| 14 | + | |
| 15 | +const ( | |
| 16 | + // KSUID's epoch starts more recently so that the 32-bit number space gives a | |
| 17 | + // significantly higher useful lifetime of around 136 years from March 2017. | |
| 18 | + // This number (14e8) was picked to be easy to remember. | |
| 19 | + epochStamp int64 = 1400000000 | |
| 20 | + | |
| 21 | + // Timestamp is a uint32 | |
| 22 | + timestampLengthInBytes = 4 | |
| 23 | + | |
| 24 | + // Payload is 16-bytes | |
| 25 | + payloadLengthInBytes = 16 | |
| 26 | + | |
| 27 | + // KSUIDs are 20 bytes when binary encoded | |
| 28 | + byteLength = timestampLengthInBytes + payloadLengthInBytes | |
| 29 | + | |
| 30 | + // The length of a KSUID when string (base62) encoded | |
| 31 | + stringEncodedLength = 27 | |
| 32 | + | |
| 33 | + // A string-encoded minimum value for a KSUID | |
| 34 | + minStringEncoded = "000000000000000000000000000" | |
| 35 | + | |
| 36 | + // A string-encoded maximum value for a KSUID | |
| 37 | + maxStringEncoded = "aWgEPTl1tmebfsQzFP4bxwgy80V" | |
| 38 | +) | |
| 39 | + | |
| 40 | +// KSUIDs are 20 bytes: | |
| 41 | +// 00-03 byte: uint32 BE UTC timestamp with custom epoch | |
| 42 | +// 04-19 byte: random "payload" | |
| 43 | +type KSUID [byteLength]byte | |
| 44 | + | |
| 45 | +var ( | |
| 46 | + rander = rand.Reader | |
| 47 | + randMutex = sync.Mutex{} | |
| 48 | + randBuffer = [payloadLengthInBytes]byte{} | |
| 49 | + | |
| 50 | + errSize = fmt.Errorf("Valid KSUIDs are %v bytes", byteLength) | |
| 51 | + errStrSize = fmt.Errorf("Valid encoded KSUIDs are %v characters", stringEncodedLength) | |
| 52 | + errStrValue = fmt.Errorf("Valid encoded KSUIDs are bounded by %s and %s", minStringEncoded, maxStringEncoded) | |
| 53 | + errPayloadSize = fmt.Errorf("Valid KSUID payloads are %v bytes", payloadLengthInBytes) | |
| 54 | + | |
| 55 | + // Represents a completely empty (invalid) KSUID | |
| 56 | + Nil KSUID | |
| 57 | + // Represents the highest value a KSUID can have | |
| 58 | + Max = KSUID{255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255, 255} | |
| 59 | +) | |
| 60 | + | |
| 61 | +// Append appends the string representation of i to b, returning a slice to a | |
| 62 | +// potentially larger memory area. | |
| 63 | +func (i KSUID) Append(b []byte) []byte { | |
| 64 | + return fastAppendEncodeBase62(b, i[:]) | |
| 65 | +} | |
| 66 | + | |
| 67 | +// The timestamp portion of the ID as a Time object | |
| 68 | +func (i KSUID) Time() time.Time { | |
| 69 | + return correctedUTCTimestampToTime(i.Timestamp()) | |
| 70 | +} | |
| 71 | + | |
| 72 | +// The timestamp portion of the ID as a bare integer which is uncorrected | |
| 73 | +// for KSUID's special epoch. | |
| 74 | +func (i KSUID) Timestamp() uint32 { | |
| 75 | + return binary.BigEndian.Uint32(i[:timestampLengthInBytes]) | |
| 76 | +} | |
| 77 | + | |
| 78 | +// The 16-byte random payload without the timestamp | |
| 79 | +func (i KSUID) Payload() []byte { | |
| 80 | + return i[timestampLengthInBytes:] | |
| 81 | +} | |
| 82 | + | |
| 83 | +// String-encoded representation that can be passed through Parse() | |
| 84 | +func (i KSUID) String() string { | |
| 85 | + return string(i.Append(make([]byte, 0, stringEncodedLength))) | |
| 86 | +} | |
| 87 | + | |
| 88 | +// Raw byte representation of KSUID | |
| 89 | +func (i KSUID) Bytes() []byte { | |
| 90 | + // Safe because this is by-value | |
| 91 | + return i[:] | |
| 92 | +} | |
| 93 | + | |
| 94 | +// IsNil returns true if this is a "nil" KSUID | |
| 95 | +func (i KSUID) IsNil() bool { | |
| 96 | + return i == Nil | |
| 97 | +} | |
| 98 | + | |
| 99 | +// Get satisfies the flag.Getter interface, making it possible to use KSUIDs as | |
| 100 | +// part of of the command line options of a program. | |
| 101 | +func (i KSUID) Get() interface{} { | |
| 102 | + return i | |
| 103 | +} | |
| 104 | + | |
| 105 | +// Set satisfies the flag.Value interface, making it possible to use KSUIDs as | |
| 106 | +// part of of the command line options of a program. | |
| 107 | +func (i *KSUID) Set(s string) error { | |
| 108 | + return i.UnmarshalText([]byte(s)) | |
| 109 | +} | |
| 110 | + | |
| 111 | +func (i KSUID) MarshalText() ([]byte, error) { | |
| 112 | + return []byte(i.String()), nil | |
| 113 | +} | |
| 114 | + | |
| 115 | +func (i KSUID) MarshalBinary() ([]byte, error) { | |
| 116 | + return i.Bytes(), nil | |
| 117 | +} | |
| 118 | + | |
| 119 | +func (i *KSUID) UnmarshalText(b []byte) error { | |
| 120 | + id, err := Parse(string(b)) | |
| 121 | + if err != nil { | |
| 122 | + return err | |
| 123 | + } | |
| 124 | + *i = id | |
| 125 | + return nil | |
| 126 | +} | |
| 127 | + | |
| 128 | +func (i *KSUID) UnmarshalBinary(b []byte) error { | |
| 129 | + id, err := FromBytes(b) | |
| 130 | + if err != nil { | |
| 131 | + return err | |
| 132 | + } | |
| 133 | + *i = id | |
| 134 | + return nil | |
| 135 | +} | |
| 136 | + | |
| 137 | +// Value converts the KSUID into a SQL driver value which can be used to | |
| 138 | +// directly use the KSUID as parameter to a SQL query. | |
| 139 | +func (i KSUID) Value() (driver.Value, error) { | |
| 140 | + if i.IsNil() { | |
| 141 | + return nil, nil | |
| 142 | + } | |
| 143 | + return i.String(), nil | |
| 144 | +} | |
| 145 | + | |
| 146 | +// Scan implements the sql.Scanner interface. It supports converting from | |
| 147 | +// string, []byte, or nil into a KSUID value. Attempting to convert from | |
| 148 | +// another type will return an error. | |
| 149 | +func (i *KSUID) Scan(src interface{}) error { | |
| 150 | + switch v := src.(type) { | |
| 151 | + case nil: | |
| 152 | + return i.scan(nil) | |
| 153 | + case []byte: | |
| 154 | + return i.scan(v) | |
| 155 | + case string: | |
| 156 | + return i.scan([]byte(v)) | |
| 157 | + default: | |
| 158 | + return fmt.Errorf("Scan: unable to scan type %T into KSUID", v) | |
| 159 | + } | |
| 160 | +} | |
| 161 | + | |
| 162 | +func (i *KSUID) scan(b []byte) error { | |
| 163 | + switch len(b) { | |
| 164 | + case 0: | |
| 165 | + *i = Nil | |
| 166 | + return nil | |
| 167 | + case byteLength: | |
| 168 | + return i.UnmarshalBinary(b) | |
| 169 | + case stringEncodedLength: | |
| 170 | + return i.UnmarshalText(b) | |
| 171 | + default: | |
| 172 | + return errSize | |
| 173 | + } | |
| 174 | +} | |
| 175 | + | |
| 176 | +// Parse decodes a string-encoded representation of a KSUID object | |
| 177 | +func Parse(s string) (KSUID, error) { | |
| 178 | + if len(s) != stringEncodedLength { | |
| 179 | + return Nil, errStrSize | |
| 180 | + } | |
| 181 | + | |
| 182 | + src := [stringEncodedLength]byte{} | |
| 183 | + dst := [byteLength]byte{} | |
| 184 | + | |
| 185 | + copy(src[:], s[:]) | |
| 186 | + | |
| 187 | + if err := fastDecodeBase62(dst[:], src[:]); err != nil { | |
| 188 | + return Nil, errStrValue | |
| 189 | + } | |
| 190 | + | |
| 191 | + return FromBytes(dst[:]) | |
| 192 | +} | |
| 193 | + | |
| 194 | +func timeToCorrectedUTCTimestamp(t time.Time) uint32 { | |
| 195 | + return uint32(t.Unix() - epochStamp) | |
| 196 | +} | |
| 197 | + | |
| 198 | +func correctedUTCTimestampToTime(ts uint32) time.Time { | |
| 199 | + return time.Unix(int64(ts)+epochStamp, 0) | |
| 200 | +} | |
| 201 | + | |
| 202 | +// Generates a new KSUID. In the strange case that random bytes | |
| 203 | +// can't be read, it will panic. | |
| 204 | +func New() KSUID { | |
| 205 | + ksuid, err := NewRandom() | |
| 206 | + if err != nil { | |
| 207 | + panic(fmt.Sprintf("Couldn't generate KSUID, inconceivable! error: %v", err)) | |
| 208 | + } | |
| 209 | + return ksuid | |
| 210 | +} | |
| 211 | + | |
| 212 | +// Generates a new KSUID | |
| 213 | +func NewRandom() (ksuid KSUID, err error) { | |
| 214 | + return NewRandomWithTime(time.Now()) | |
| 215 | +} | |
| 216 | + | |
| 217 | +func NewRandomWithTime(t time.Time) (ksuid KSUID, err error) { | |
| 218 | + // Go's default random number generators are not safe for concurrent use by | |
| 219 | + // multiple goroutines, the use of the rander and randBuffer are explicitly | |
| 220 | + // synchronized here. | |
| 221 | + randMutex.Lock() | |
| 222 | + | |
| 223 | + _, err = io.ReadAtLeast(rander, randBuffer[:], len(randBuffer)) | |
| 224 | + copy(ksuid[timestampLengthInBytes:], randBuffer[:]) | |
| 225 | + | |
| 226 | + randMutex.Unlock() | |
| 227 | + | |
| 228 | + if err != nil { | |
| 229 | + ksuid = Nil // don't leak random bytes on error | |
| 230 | + return | |
| 231 | + } | |
| 232 | + | |
| 233 | + ts := timeToCorrectedUTCTimestamp(t) | |
| 234 | + binary.BigEndian.PutUint32(ksuid[:timestampLengthInBytes], ts) | |
| 235 | + return | |
| 236 | +} | |
| 237 | + | |
| 238 | +// Constructs a KSUID from constituent parts | |
| 239 | +func FromParts(t time.Time, payload []byte) (KSUID, error) { | |
| 240 | + if len(payload) != payloadLengthInBytes { | |
| 241 | + return Nil, errPayloadSize | |
| 242 | + } | |
| 243 | + | |
| 244 | + var ksuid KSUID | |
| 245 | + | |
| 246 | + ts := timeToCorrectedUTCTimestamp(t) | |
| 247 | + binary.BigEndian.PutUint32(ksuid[:timestampLengthInBytes], ts) | |
| 248 | + | |
| 249 | + copy(ksuid[timestampLengthInBytes:], payload) | |
| 250 | + | |
| 251 | + return ksuid, nil | |
| 252 | +} | |
| 253 | + | |
| 254 | +// Constructs a KSUID from a 20-byte binary representation | |
| 255 | +func FromBytes(b []byte) (KSUID, error) { | |
| 256 | + var ksuid KSUID | |
| 257 | + | |
| 258 | + if len(b) != byteLength { | |
| 259 | + return Nil, errSize | |
| 260 | + } | |
| 261 | + | |
| 262 | + copy(ksuid[:], b) | |
| 263 | + return ksuid, nil | |
| 264 | +} | |
| 265 | + | |
| 266 | +// Sets the global source of random bytes for KSUID generation. This | |
| 267 | +// should probably only be set once globally. While this is technically | |
| 268 | +// thread-safe as in it won't cause corruption, there's no guarantee | |
| 269 | +// on ordering. | |
| 270 | +func SetRand(r io.Reader) { | |
| 271 | + if r == nil { | |
| 272 | + rander = rand.Reader | |
| 273 | + return | |
| 274 | + } | |
| 275 | + rander = r | |
| 276 | +} | |
| 277 | + | |
| 278 | +// Implements comparison for KSUID type | |
| 279 | +func Compare(a, b KSUID) int { | |
| 280 | + return bytes.Compare(a[:], b[:]) | |
| 281 | +} | |
| 282 | + | |
| 283 | +// Sorts the given slice of KSUIDs | |
| 284 | +func Sort(ids []KSUID) { | |
| 285 | + quickSort(ids, 0, len(ids)-1) | |
| 286 | +} | |
| 287 | + | |
| 288 | +// IsSorted checks whether a slice of KSUIDs is sorted | |
| 289 | +func IsSorted(ids []KSUID) bool { | |
| 290 | + if len(ids) != 0 { | |
| 291 | + min := ids[0] | |
| 292 | + for _, id := range ids[1:] { | |
| 293 | + if bytes.Compare(min[:], id[:]) > 0 { | |
| 294 | + return false | |
| 295 | + } | |
| 296 | + min = id | |
| 297 | + } | |
| 298 | + } | |
| 299 | + return true | |
| 300 | +} | |
| 301 | + | |
| 302 | +func quickSort(a []KSUID, lo int, hi int) { | |
| 303 | + if lo < hi { | |
| 304 | + pivot := a[hi] | |
| 305 | + i := lo - 1 | |
| 306 | + | |
| 307 | + for j, n := lo, hi; j != n; j++ { | |
| 308 | + if bytes.Compare(a[j][:], pivot[:]) < 0 { | |
| 309 | + i++ | |
| 310 | + a[i], a[j] = a[j], a[i] | |
| 311 | + } | |
| 312 | + } | |
| 313 | + | |
| 314 | + i++ | |
| 315 | + if bytes.Compare(a[hi][:], a[i][:]) < 0 { | |
| 316 | + a[i], a[hi] = a[hi], a[i] | |
| 317 | + } | |
| 318 | + | |
| 319 | + quickSort(a, lo, i-1) | |
| 320 | + quickSort(a, i+1, hi) | |
| 321 | + } | |
| 322 | +} | |
| 323 | + | |
| 324 | +// Next returns the next KSUID after id. | |
| 325 | +func (id KSUID) Next() KSUID { | |
| 326 | + zero := makeUint128(0, 0) | |
| 327 | + | |
| 328 | + t := id.Timestamp() | |
| 329 | + u := uint128Payload(id) | |
| 330 | + v := add128(u, makeUint128(0, 1)) | |
| 331 | + | |
| 332 | + if v == zero { // overflow | |
| 333 | + t++ | |
| 334 | + } | |
| 335 | + | |
| 336 | + return v.ksuid(t) | |
| 337 | +} | |
| 338 | + | |
| 339 | +// Prev returns the previoud KSUID before id. | |
| 340 | +func (id KSUID) Prev() KSUID { | |
| 341 | + max := makeUint128(math.MaxUint64, math.MaxUint64) | |
| 342 | + | |
| 343 | + t := id.Timestamp() | |
| 344 | + u := uint128Payload(id) | |
| 345 | + v := sub128(u, makeUint128(0, 1)) | |
| 346 | + | |
| 347 | + if v == max { // overflow | |
| 348 | + t-- | |
| 349 | + } | |
| 350 | + | |
| 351 | + return v.ksuid(t) | |
| 352 | +} | ... | ... |
| ... | ... | @@ -0,0 +1,389 @@ |
| 1 | +package ksuid | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "bytes" | |
| 5 | + "encoding/json" | |
| 6 | + "flag" | |
| 7 | + "fmt" | |
| 8 | + "sort" | |
| 9 | + "strings" | |
| 10 | + "testing" | |
| 11 | + "time" | |
| 12 | +) | |
| 13 | + | |
| 14 | +func TestConstructionTimestamp(t *testing.T) { | |
| 15 | + x := New() | |
| 16 | + nowTime := time.Now().Round(1 * time.Minute) | |
| 17 | + xTime := x.Time().Round(1 * time.Minute) | |
| 18 | + | |
| 19 | + if xTime != nowTime { | |
| 20 | + t.Fatal(xTime, "!=", nowTime) | |
| 21 | + } | |
| 22 | +} | |
| 23 | + | |
| 24 | +func TestNil(t *testing.T) { | |
| 25 | + if !Nil.IsNil() { | |
| 26 | + t.Fatal("Nil should be Nil!") | |
| 27 | + } | |
| 28 | + | |
| 29 | + x, _ := FromBytes(make([]byte, byteLength)) | |
| 30 | + if !x.IsNil() { | |
| 31 | + t.Fatal("Zero-byte array should be Nil!") | |
| 32 | + } | |
| 33 | +} | |
| 34 | + | |
| 35 | +func TestEncoding(t *testing.T) { | |
| 36 | + x, _ := FromBytes(make([]byte, byteLength)) | |
| 37 | + if !x.IsNil() { | |
| 38 | + t.Fatal("Zero-byte array should be Nil!") | |
| 39 | + } | |
| 40 | + | |
| 41 | + encoded := x.String() | |
| 42 | + expected := strings.Repeat("0", stringEncodedLength) | |
| 43 | + | |
| 44 | + if encoded != expected { | |
| 45 | + t.Fatal("expected", expected, "encoded", encoded) | |
| 46 | + } | |
| 47 | +} | |
| 48 | + | |
| 49 | +func TestPadding(t *testing.T) { | |
| 50 | + b := make([]byte, byteLength) | |
| 51 | + for i := 0; i < byteLength; i++ { | |
| 52 | + b[i] = 255 | |
| 53 | + } | |
| 54 | + | |
| 55 | + x, _ := FromBytes(b) | |
| 56 | + xEncoded := x.String() | |
| 57 | + nilEncoded := Nil.String() | |
| 58 | + | |
| 59 | + if len(xEncoded) != len(nilEncoded) { | |
| 60 | + t.Fatal("Encoding should produce equal-length strings for zero and max case") | |
| 61 | + } | |
| 62 | +} | |
| 63 | + | |
| 64 | +func TestParse(t *testing.T) { | |
| 65 | + _, err := Parse("123") | |
| 66 | + if err != errStrSize { | |
| 67 | + t.Fatal("Expected Parsing a 3-char string to return an error") | |
| 68 | + } | |
| 69 | + | |
| 70 | + parsed, err := Parse(strings.Repeat("0", stringEncodedLength)) | |
| 71 | + if err != nil { | |
| 72 | + t.Fatal("Unexpected error", err) | |
| 73 | + } | |
| 74 | + | |
| 75 | + if Compare(parsed, Nil) != 0 { | |
| 76 | + t.Fatal("Parsing all-zeroes string should equal Nil value", | |
| 77 | + "expected:", Nil, | |
| 78 | + "actual:", parsed) | |
| 79 | + } | |
| 80 | + | |
| 81 | + maxBytes := make([]byte, byteLength) | |
| 82 | + for i := 0; i < byteLength; i++ { | |
| 83 | + maxBytes[i] = 255 | |
| 84 | + } | |
| 85 | + maxBytesKSUID, err := FromBytes(maxBytes) | |
| 86 | + if err != nil { | |
| 87 | + t.Fatal("Unexpected error", err) | |
| 88 | + } | |
| 89 | + | |
| 90 | + maxParseKSUID, err := Parse(maxStringEncoded) | |
| 91 | + if err != nil { | |
| 92 | + t.Fatal("Unexpected error", err) | |
| 93 | + } | |
| 94 | + | |
| 95 | + if Compare(maxBytesKSUID, maxParseKSUID) != 0 { | |
| 96 | + t.Fatal("String decoder broke for max string") | |
| 97 | + } | |
| 98 | +} | |
| 99 | + | |
| 100 | +func TestIssue25(t *testing.T) { | |
| 101 | + // https://github.com/segmentio/ksuid/issues/25 | |
| 102 | + for _, s := range []string{ | |
| 103 | + "aaaaaaaaaaaaaaaaaaaaaaaaaaa", | |
| 104 | + "aWgEPTl1tmebfsQzFP4bxwgy80!", | |
| 105 | + } { | |
| 106 | + _, err := Parse(s) | |
| 107 | + if err != errStrValue { | |
| 108 | + t.Error("invalid KSUID representations cannot be successfully parsed, got err =", err) | |
| 109 | + } | |
| 110 | + } | |
| 111 | +} | |
| 112 | + | |
| 113 | +func TestEncodeAndDecode(t *testing.T) { | |
| 114 | + x := New() | |
| 115 | + builtFromEncodedString, err := Parse(x.String()) | |
| 116 | + if err != nil { | |
| 117 | + t.Fatal("Unexpected error", err) | |
| 118 | + } | |
| 119 | + | |
| 120 | + if Compare(x, builtFromEncodedString) != 0 { | |
| 121 | + t.Fatal("Parse(X).String() != X") | |
| 122 | + } | |
| 123 | +} | |
| 124 | + | |
| 125 | +func TestMarshalText(t *testing.T) { | |
| 126 | + var id1 = New() | |
| 127 | + var id2 KSUID | |
| 128 | + | |
| 129 | + if err := id2.UnmarshalText([]byte(id1.String())); err != nil { | |
| 130 | + t.Fatal(err) | |
| 131 | + } | |
| 132 | + | |
| 133 | + if id1 != id2 { | |
| 134 | + t.Fatal(id1, "!=", id2) | |
| 135 | + } | |
| 136 | + | |
| 137 | + if b, err := id2.MarshalText(); err != nil { | |
| 138 | + t.Fatal(err) | |
| 139 | + } else if s := string(b); s != id1.String() { | |
| 140 | + t.Fatal(s) | |
| 141 | + } | |
| 142 | +} | |
| 143 | + | |
| 144 | +func TestMarshalBinary(t *testing.T) { | |
| 145 | + var id1 = New() | |
| 146 | + var id2 KSUID | |
| 147 | + | |
| 148 | + if err := id2.UnmarshalBinary(id1.Bytes()); err != nil { | |
| 149 | + t.Fatal(err) | |
| 150 | + } | |
| 151 | + | |
| 152 | + if id1 != id2 { | |
| 153 | + t.Fatal(id1, "!=", id2) | |
| 154 | + } | |
| 155 | + | |
| 156 | + if b, err := id2.MarshalBinary(); err != nil { | |
| 157 | + t.Fatal(err) | |
| 158 | + } else if bytes.Compare(b, id1.Bytes()) != 0 { | |
| 159 | + t.Fatal("bad binary form:", id2) | |
| 160 | + } | |
| 161 | +} | |
| 162 | + | |
| 163 | +func TestMashalJSON(t *testing.T) { | |
| 164 | + var id1 = New() | |
| 165 | + var id2 KSUID | |
| 166 | + | |
| 167 | + if b, err := json.Marshal(id1); err != nil { | |
| 168 | + t.Fatal(err) | |
| 169 | + } else if err := json.Unmarshal(b, &id2); err != nil { | |
| 170 | + t.Fatal(err) | |
| 171 | + } else if id1 != id2 { | |
| 172 | + t.Error(id1, "!=", id2) | |
| 173 | + } | |
| 174 | +} | |
| 175 | + | |
| 176 | +func TestFlag(t *testing.T) { | |
| 177 | + var id1 = New() | |
| 178 | + var id2 KSUID | |
| 179 | + | |
| 180 | + fset := flag.NewFlagSet("test", flag.ContinueOnError) | |
| 181 | + fset.Var(&id2, "id", "the KSUID") | |
| 182 | + | |
| 183 | + if err := fset.Parse([]string{"-id", id1.String()}); err != nil { | |
| 184 | + t.Fatal(err) | |
| 185 | + } | |
| 186 | + | |
| 187 | + if id1 != id2 { | |
| 188 | + t.Error(id1, "!=", id2) | |
| 189 | + } | |
| 190 | +} | |
| 191 | + | |
| 192 | +func TestSqlValuer(t *testing.T) { | |
| 193 | + id, _ := Parse(maxStringEncoded) | |
| 194 | + | |
| 195 | + if v, err := id.Value(); err != nil { | |
| 196 | + t.Error(err) | |
| 197 | + } else if s, ok := v.(string); !ok { | |
| 198 | + t.Error("not a string value") | |
| 199 | + } else if s != maxStringEncoded { | |
| 200 | + t.Error("bad string value::", s) | |
| 201 | + } | |
| 202 | +} | |
| 203 | + | |
| 204 | +func TestSqlValuerNilValue(t *testing.T) { | |
| 205 | + if v, err := Nil.Value(); err != nil { | |
| 206 | + t.Error(err) | |
| 207 | + } else if v != nil { | |
| 208 | + t.Errorf("bad nil value: %v", v) | |
| 209 | + } | |
| 210 | +} | |
| 211 | + | |
| 212 | +func TestSqlScanner(t *testing.T) { | |
| 213 | + id1 := New() | |
| 214 | + id2 := New() | |
| 215 | + | |
| 216 | + tests := []struct { | |
| 217 | + ksuid KSUID | |
| 218 | + value interface{} | |
| 219 | + }{ | |
| 220 | + {Nil, nil}, | |
| 221 | + {id1, id1.String()}, | |
| 222 | + {id2, id2.Bytes()}, | |
| 223 | + } | |
| 224 | + | |
| 225 | + for _, test := range tests { | |
| 226 | + t.Run(fmt.Sprintf("%T", test.value), func(t *testing.T) { | |
| 227 | + var id KSUID | |
| 228 | + | |
| 229 | + if err := id.Scan(test.value); err != nil { | |
| 230 | + t.Error(err) | |
| 231 | + } | |
| 232 | + | |
| 233 | + if id != test.ksuid { | |
| 234 | + t.Error("bad KSUID:") | |
| 235 | + t.Logf("expected %v", test.ksuid) | |
| 236 | + t.Logf("found %v", id) | |
| 237 | + } | |
| 238 | + }) | |
| 239 | + } | |
| 240 | +} | |
| 241 | + | |
| 242 | +func TestAppend(t *testing.T) { | |
| 243 | + for _, repr := range []string{"0pN1Own7255s7jwpwy495bAZeEa", "aWgEPTl1tmebfsQzFP4bxwgy80V"} { | |
| 244 | + k, _ := Parse(repr) | |
| 245 | + a := make([]byte, 0, stringEncodedLength) | |
| 246 | + | |
| 247 | + a = append(a, "?: "...) | |
| 248 | + a = k.Append(a) | |
| 249 | + | |
| 250 | + if s := string(a); s != "?: "+repr { | |
| 251 | + t.Error(s) | |
| 252 | + } | |
| 253 | + } | |
| 254 | +} | |
| 255 | + | |
| 256 | +func TestSort(t *testing.T) { | |
| 257 | + ids1 := [11]KSUID{} | |
| 258 | + ids2 := [11]KSUID{} | |
| 259 | + | |
| 260 | + for i := range ids1 { | |
| 261 | + ids1[i] = New() | |
| 262 | + } | |
| 263 | + | |
| 264 | + ids2 = ids1 | |
| 265 | + sort.Slice(ids2[:], func(i, j int) bool { | |
| 266 | + return Compare(ids2[i], ids2[j]) < 0 | |
| 267 | + }) | |
| 268 | + | |
| 269 | + Sort(ids1[:]) | |
| 270 | + | |
| 271 | + if !IsSorted(ids1[:]) { | |
| 272 | + t.Error("not sorted") | |
| 273 | + } | |
| 274 | + | |
| 275 | + if ids1 != ids2 { | |
| 276 | + t.Error("bad order:") | |
| 277 | + t.Log(ids1) | |
| 278 | + t.Log(ids2) | |
| 279 | + } | |
| 280 | +} | |
| 281 | + | |
| 282 | +func TestPrevNext(t *testing.T) { | |
| 283 | + tests := []struct { | |
| 284 | + id KSUID | |
| 285 | + prev KSUID | |
| 286 | + next KSUID | |
| 287 | + }{ | |
| 288 | + { | |
| 289 | + id: Nil, | |
| 290 | + prev: Max, | |
| 291 | + next: KSUID{0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 1}, | |
| 292 | + }, | |
| 293 | + { | |
| 294 | + id: Max, | |
| 295 | + prev: KSUID{0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xfe}, | |
| 296 | + next: Nil, | |
| 297 | + }, | |
| 298 | + } | |
| 299 | + | |
| 300 | + for _, test := range tests { | |
| 301 | + t.Run(test.id.String(), func(t *testing.T) { | |
| 302 | + testPrevNext(t, test.id, test.prev, test.next) | |
| 303 | + }) | |
| 304 | + } | |
| 305 | +} | |
| 306 | + | |
| 307 | +func TestGetTimestamp(t *testing.T) { | |
| 308 | + nowTime := time.Now() | |
| 309 | + x, _ := NewRandomWithTime(nowTime) | |
| 310 | + xTime := int64(x.Timestamp()) | |
| 311 | + unix := nowTime.Unix() | |
| 312 | + if xTime != unix - epochStamp { | |
| 313 | + t.Fatal(xTime, "!=", unix) | |
| 314 | + } | |
| 315 | +} | |
| 316 | + | |
| 317 | +func testPrevNext(t *testing.T, id, prev, next KSUID) { | |
| 318 | + id1 := id.Prev() | |
| 319 | + id2 := id.Next() | |
| 320 | + | |
| 321 | + if id1 != prev { | |
| 322 | + t.Error("previous id of the nil KSUID is wrong:", id1, "!=", prev) | |
| 323 | + } | |
| 324 | + | |
| 325 | + if id2 != next { | |
| 326 | + t.Error("next id of the nil KSUID is wrong:", id2, "!=", next) | |
| 327 | + } | |
| 328 | +} | |
| 329 | + | |
| 330 | +func BenchmarkAppend(b *testing.B) { | |
| 331 | + a := make([]byte, 0, stringEncodedLength) | |
| 332 | + k := New() | |
| 333 | + | |
| 334 | + for i := 0; i != b.N; i++ { | |
| 335 | + k.Append(a) | |
| 336 | + } | |
| 337 | +} | |
| 338 | + | |
| 339 | +func BenchmarkString(b *testing.B) { | |
| 340 | + k := New() | |
| 341 | + | |
| 342 | + for i := 0; i != b.N; i++ { | |
| 343 | + _ = k.String() | |
| 344 | + } | |
| 345 | +} | |
| 346 | + | |
| 347 | +func BenchmarkParse(b *testing.B) { | |
| 348 | + for i := 0; i != b.N; i++ { | |
| 349 | + Parse(maxStringEncoded) | |
| 350 | + } | |
| 351 | +} | |
| 352 | + | |
| 353 | +func BenchmarkCompare(b *testing.B) { | |
| 354 | + k1 := New() | |
| 355 | + k2 := New() | |
| 356 | + | |
| 357 | + for i := 0; i != b.N; i++ { | |
| 358 | + Compare(k1, k2) | |
| 359 | + } | |
| 360 | +} | |
| 361 | + | |
| 362 | +func BenchmarkSort(b *testing.B) { | |
| 363 | + ids1 := [101]KSUID{} | |
| 364 | + ids2 := [101]KSUID{} | |
| 365 | + | |
| 366 | + for i := range ids1 { | |
| 367 | + ids1[i] = New() | |
| 368 | + } | |
| 369 | + | |
| 370 | + for i := 0; i != b.N; i++ { | |
| 371 | + ids2 = ids1 | |
| 372 | + Sort(ids2[:]) | |
| 373 | + } | |
| 374 | +} | |
| 375 | + | |
| 376 | +func BenchmarkNew(b *testing.B) { | |
| 377 | + b.Run("with crypto rand", func(b *testing.B) { | |
| 378 | + SetRand(nil) | |
| 379 | + for i := 0; i != b.N; i++ { | |
| 380 | + New() | |
| 381 | + } | |
| 382 | + }) | |
| 383 | + b.Run("with math rand", func(b *testing.B) { | |
| 384 | + SetRand(FastRander) | |
| 385 | + for i := 0; i != b.N; i++ { | |
| 386 | + New() | |
| 387 | + } | |
| 388 | + }) | |
| 389 | +} | ... | ... |
| ... | ... | @@ -0,0 +1,55 @@ |
| 1 | +package ksuid | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + cryptoRand "crypto/rand" | |
| 5 | + "encoding/binary" | |
| 6 | + "io" | |
| 7 | + "math/rand" | |
| 8 | +) | |
| 9 | + | |
| 10 | +// FastRander is an io.Reader that uses math/rand and is optimized for | |
| 11 | +// generating 16 bytes KSUID payloads. It is intended to be used as a | |
| 12 | +// performance improvements for programs that have no need for | |
| 13 | +// cryptographically secure KSUIDs and are generating a lot of them. | |
| 14 | +var FastRander = newRBG() | |
| 15 | + | |
| 16 | +func newRBG() io.Reader { | |
| 17 | + r, err := newRandomBitsGenerator() | |
| 18 | + if err != nil { | |
| 19 | + panic(err) | |
| 20 | + } | |
| 21 | + return r | |
| 22 | +} | |
| 23 | + | |
| 24 | +func newRandomBitsGenerator() (r io.Reader, err error) { | |
| 25 | + var seed int64 | |
| 26 | + | |
| 27 | + if seed, err = readCryptoRandomSeed(); err != nil { | |
| 28 | + return | |
| 29 | + } | |
| 30 | + | |
| 31 | + r = &randSourceReader{source: rand.NewSource(seed).(rand.Source64)} | |
| 32 | + return | |
| 33 | +} | |
| 34 | + | |
| 35 | +func readCryptoRandomSeed() (seed int64, err error) { | |
| 36 | + var b [8]byte | |
| 37 | + | |
| 38 | + if _, err = io.ReadFull(cryptoRand.Reader, b[:]); err != nil { | |
| 39 | + return | |
| 40 | + } | |
| 41 | + | |
| 42 | + seed = int64(binary.LittleEndian.Uint64(b[:])) | |
| 43 | + return | |
| 44 | +} | |
| 45 | + | |
| 46 | +type randSourceReader struct { | |
| 47 | + source rand.Source64 | |
| 48 | +} | |
| 49 | + | |
| 50 | +func (r *randSourceReader) Read(b []byte) (int, error) { | |
| 51 | + // optimized for generating 16 bytes payloads | |
| 52 | + binary.LittleEndian.PutUint64(b[:8], r.source.Uint64()) | |
| 53 | + binary.LittleEndian.PutUint64(b[8:], r.source.Uint64()) | |
| 54 | + return 16, nil | |
| 55 | +} | ... | ... |
| ... | ... | @@ -0,0 +1,55 @@ |
| 1 | +package ksuid | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "encoding/binary" | |
| 5 | + "errors" | |
| 6 | + "math" | |
| 7 | +) | |
| 8 | + | |
| 9 | +// Sequence is a KSUID generator which produces a sequence of ordered KSUIDs | |
| 10 | +// from a seed. | |
| 11 | +// | |
| 12 | +// Up to 65536 KSUIDs can be generated by for a single seed. | |
| 13 | +// | |
| 14 | +// A typical usage of a Sequence looks like this: | |
| 15 | +// | |
| 16 | +// seq := ksuid.Sequence{ | |
| 17 | +// Seed: ksuid.New(), | |
| 18 | +// } | |
| 19 | +// id, err := seq.Next() | |
| 20 | +// | |
| 21 | +// Sequence values are not safe to use concurrently from multiple goroutines. | |
| 22 | +type Sequence struct { | |
| 23 | + // The seed is used as base for the KSUID generator, all generated KSUIDs | |
| 24 | + // share the same leading 18 bytes of the seed. | |
| 25 | + Seed KSUID | |
| 26 | + count uint32 // uint32 for overflow, only 2 bytes are used | |
| 27 | +} | |
| 28 | + | |
| 29 | +// Next produces the next KSUID in the sequence, or returns an error if the | |
| 30 | +// sequence has been exhausted. | |
| 31 | +func (seq *Sequence) Next() (KSUID, error) { | |
| 32 | + id := seq.Seed // copy | |
| 33 | + count := seq.count | |
| 34 | + if count > math.MaxUint16 { | |
| 35 | + return Nil, errors.New("too many IDs were generated") | |
| 36 | + } | |
| 37 | + seq.count++ | |
| 38 | + return withSequenceNumber(id, uint16(count)), nil | |
| 39 | +} | |
| 40 | + | |
| 41 | +// Bounds returns the inclusive min and max bounds of the KSUIDs that may be | |
| 42 | +// generated by the sequence. If all ids have been generated already then the | |
| 43 | +// returned min value is equal to the max. | |
| 44 | +func (seq *Sequence) Bounds() (min KSUID, max KSUID) { | |
| 45 | + count := seq.count | |
| 46 | + if count > math.MaxUint16 { | |
| 47 | + count = math.MaxUint16 | |
| 48 | + } | |
| 49 | + return withSequenceNumber(seq.Seed, uint16(count)), withSequenceNumber(seq.Seed, math.MaxUint16) | |
| 50 | +} | |
| 51 | + | |
| 52 | +func withSequenceNumber(id KSUID, n uint16) KSUID { | |
| 53 | + binary.BigEndian.PutUint16(id[len(id)-2:], n) | |
| 54 | + return id | |
| 55 | +} | ... | ... |
| ... | ... | @@ -0,0 +1,33 @@ |
| 1 | +package ksuid | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "encoding/binary" | |
| 5 | + "math" | |
| 6 | + "testing" | |
| 7 | +) | |
| 8 | + | |
| 9 | +func TestSequence(t *testing.T) { | |
| 10 | + seq := Sequence{Seed: New()} | |
| 11 | + | |
| 12 | + if min, max := seq.Bounds(); min == max { | |
| 13 | + t.Error("min and max of KSUID range must differ when no ids have been generated") | |
| 14 | + } | |
| 15 | + | |
| 16 | + for i := 0; i <= math.MaxUint16; i++ { | |
| 17 | + id, err := seq.Next() | |
| 18 | + if err != nil { | |
| 19 | + t.Fatal(err) | |
| 20 | + } | |
| 21 | + if j := int(binary.BigEndian.Uint16(id[len(id)-2:])); j != i { | |
| 22 | + t.Fatalf("expected %d but got %d in %s", i, j, id) | |
| 23 | + } | |
| 24 | + } | |
| 25 | + | |
| 26 | + if _, err := seq.Next(); err == nil { | |
| 27 | + t.Fatal("no error returned after exhausting the id generator") | |
| 28 | + } | |
| 29 | + | |
| 30 | + if min, max := seq.Bounds(); min != max { | |
| 31 | + t.Error("after all KSUIDs were generated the min and max must be equal") | |
| 32 | + } | |
| 33 | +} | ... | ... |
| ... | ... | @@ -0,0 +1,343 @@ |
| 1 | +package ksuid | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "bytes" | |
| 5 | + "encoding/binary" | |
| 6 | +) | |
| 7 | + | |
| 8 | +// CompressedSet is an immutable data type which stores a set of KSUIDs. | |
| 9 | +type CompressedSet []byte | |
| 10 | + | |
| 11 | +// Iter returns an iterator that produces all KSUIDs in the set. | |
| 12 | +func (set CompressedSet) Iter() CompressedSetIter { | |
| 13 | + return CompressedSetIter{ | |
| 14 | + content: []byte(set), | |
| 15 | + } | |
| 16 | +} | |
| 17 | + | |
| 18 | +// String satisfies the fmt.Stringer interface, returns a human-readable string | |
| 19 | +// representation of the set. | |
| 20 | +func (set CompressedSet) String() string { | |
| 21 | + b := bytes.Buffer{} | |
| 22 | + b.WriteByte('[') | |
| 23 | + set.writeTo(&b) | |
| 24 | + b.WriteByte(']') | |
| 25 | + return b.String() | |
| 26 | +} | |
| 27 | + | |
| 28 | +// String satisfies the fmt.GoStringer interface, returns a Go representation of | |
| 29 | +// the set. | |
| 30 | +func (set CompressedSet) GoString() string { | |
| 31 | + b := bytes.Buffer{} | |
| 32 | + b.WriteString("ksuid.CompressedSet{") | |
| 33 | + set.writeTo(&b) | |
| 34 | + b.WriteByte('}') | |
| 35 | + return b.String() | |
| 36 | +} | |
| 37 | + | |
| 38 | +func (set CompressedSet) writeTo(b *bytes.Buffer) { | |
| 39 | + a := [27]byte{} | |
| 40 | + | |
| 41 | + for i, it := 0, set.Iter(); it.Next(); i++ { | |
| 42 | + if i != 0 { | |
| 43 | + b.WriteString(", ") | |
| 44 | + } | |
| 45 | + b.WriteByte('"') | |
| 46 | + it.KSUID.Append(a[:0]) | |
| 47 | + b.Write(a[:]) | |
| 48 | + b.WriteByte('"') | |
| 49 | + } | |
| 50 | +} | |
| 51 | + | |
| 52 | +// Compress creates and returns a compressed set of KSUIDs from the list given | |
| 53 | +// as arguments. | |
| 54 | +func Compress(ids ...KSUID) CompressedSet { | |
| 55 | + c := 1 + byteLength + (len(ids) / 5) | |
| 56 | + b := make([]byte, 0, c) | |
| 57 | + return AppendCompressed(b, ids...) | |
| 58 | +} | |
| 59 | + | |
| 60 | +// AppendCompressed uses the given byte slice as pre-allocated storage space to | |
| 61 | +// build a KSUID set. | |
| 62 | +// | |
| 63 | +// Note that the set uses a compression technique to store the KSUIDs, so the | |
| 64 | +// resuling length is not 20 x len(ids). The rule of thumb here is for the given | |
| 65 | +// byte slice to reserve the amount of memory that the application would be OK | |
| 66 | +// to waste. | |
| 67 | +func AppendCompressed(set []byte, ids ...KSUID) CompressedSet { | |
| 68 | + if len(ids) != 0 { | |
| 69 | + if !IsSorted(ids) { | |
| 70 | + Sort(ids) | |
| 71 | + } | |
| 72 | + one := makeUint128(0, 1) | |
| 73 | + | |
| 74 | + // The first KSUID is always written to the set, this is the starting | |
| 75 | + // point for all deltas. | |
| 76 | + set = append(set, byte(rawKSUID)) | |
| 77 | + set = append(set, ids[0][:]...) | |
| 78 | + | |
| 79 | + timestamp := ids[0].Timestamp() | |
| 80 | + lastKSUID := ids[0] | |
| 81 | + lastValue := uint128Payload(ids[0]) | |
| 82 | + | |
| 83 | + for i := 1; i != len(ids); i++ { | |
| 84 | + id := ids[i] | |
| 85 | + | |
| 86 | + if id == lastKSUID { | |
| 87 | + continue | |
| 88 | + } | |
| 89 | + | |
| 90 | + t := id.Timestamp() | |
| 91 | + v := uint128Payload(id) | |
| 92 | + | |
| 93 | + if t != timestamp { | |
| 94 | + d := t - timestamp | |
| 95 | + n := varintLength32(d) | |
| 96 | + | |
| 97 | + set = append(set, timeDelta|byte(n)) | |
| 98 | + set = appendVarint32(set, d, n) | |
| 99 | + set = append(set, id[timestampLengthInBytes:]...) | |
| 100 | + | |
| 101 | + timestamp = t | |
| 102 | + } else { | |
| 103 | + d := sub128(v, lastValue) | |
| 104 | + | |
| 105 | + if d != one { | |
| 106 | + n := varintLength128(d) | |
| 107 | + | |
| 108 | + set = append(set, payloadDelta|byte(n)) | |
| 109 | + set = appendVarint128(set, d, n) | |
| 110 | + } else { | |
| 111 | + l, c := rangeLength(ids[i+1:], t, id, v) | |
| 112 | + m := uint64(l + 1) | |
| 113 | + n := varintLength64(m) | |
| 114 | + | |
| 115 | + set = append(set, payloadRange|byte(n)) | |
| 116 | + set = appendVarint64(set, m, n) | |
| 117 | + | |
| 118 | + i += c | |
| 119 | + id = ids[i] | |
| 120 | + v = uint128Payload(id) | |
| 121 | + } | |
| 122 | + } | |
| 123 | + | |
| 124 | + lastKSUID = id | |
| 125 | + lastValue = v | |
| 126 | + } | |
| 127 | + } | |
| 128 | + return CompressedSet(set) | |
| 129 | +} | |
| 130 | + | |
| 131 | +func rangeLength(ids []KSUID, timestamp uint32, lastKSUID KSUID, lastValue uint128) (length int, count int) { | |
| 132 | + one := makeUint128(0, 1) | |
| 133 | + | |
| 134 | + for i := range ids { | |
| 135 | + id := ids[i] | |
| 136 | + | |
| 137 | + if id == lastKSUID { | |
| 138 | + continue | |
| 139 | + } | |
| 140 | + | |
| 141 | + if id.Timestamp() != timestamp { | |
| 142 | + count = i | |
| 143 | + return | |
| 144 | + } | |
| 145 | + | |
| 146 | + v := uint128Payload(id) | |
| 147 | + | |
| 148 | + if sub128(v, lastValue) != one { | |
| 149 | + count = i | |
| 150 | + return | |
| 151 | + } | |
| 152 | + | |
| 153 | + lastKSUID = id | |
| 154 | + lastValue = v | |
| 155 | + length++ | |
| 156 | + } | |
| 157 | + | |
| 158 | + count = len(ids) | |
| 159 | + return | |
| 160 | +} | |
| 161 | + | |
| 162 | +func appendVarint128(b []byte, v uint128, n int) []byte { | |
| 163 | + c := v.bytes() | |
| 164 | + return append(b, c[len(c)-n:]...) | |
| 165 | +} | |
| 166 | + | |
| 167 | +func appendVarint64(b []byte, v uint64, n int) []byte { | |
| 168 | + c := [8]byte{} | |
| 169 | + binary.BigEndian.PutUint64(c[:], v) | |
| 170 | + return append(b, c[len(c)-n:]...) | |
| 171 | +} | |
| 172 | + | |
| 173 | +func appendVarint32(b []byte, v uint32, n int) []byte { | |
| 174 | + c := [4]byte{} | |
| 175 | + binary.BigEndian.PutUint32(c[:], v) | |
| 176 | + return append(b, c[len(c)-n:]...) | |
| 177 | +} | |
| 178 | + | |
| 179 | +func varint128(b []byte) uint128 { | |
| 180 | + a := [16]byte{} | |
| 181 | + copy(a[16-len(b):], b) | |
| 182 | + return makeUint128FromPayload(a[:]) | |
| 183 | +} | |
| 184 | + | |
| 185 | +func varint64(b []byte) uint64 { | |
| 186 | + a := [8]byte{} | |
| 187 | + copy(a[8-len(b):], b) | |
| 188 | + return binary.BigEndian.Uint64(a[:]) | |
| 189 | +} | |
| 190 | + | |
| 191 | +func varint32(b []byte) uint32 { | |
| 192 | + a := [4]byte{} | |
| 193 | + copy(a[4-len(b):], b) | |
| 194 | + return binary.BigEndian.Uint32(a[:]) | |
| 195 | +} | |
| 196 | + | |
| 197 | +func varintLength128(v uint128) int { | |
| 198 | + if v[1] != 0 { | |
| 199 | + return 8 + varintLength64(v[1]) | |
| 200 | + } | |
| 201 | + return varintLength64(v[0]) | |
| 202 | +} | |
| 203 | + | |
| 204 | +func varintLength64(v uint64) int { | |
| 205 | + switch { | |
| 206 | + case (v & 0xFFFFFFFFFFFFFF00) == 0: | |
| 207 | + return 1 | |
| 208 | + case (v & 0xFFFFFFFFFFFF0000) == 0: | |
| 209 | + return 2 | |
| 210 | + case (v & 0xFFFFFFFFFF000000) == 0: | |
| 211 | + return 3 | |
| 212 | + case (v & 0xFFFFFFFF00000000) == 0: | |
| 213 | + return 4 | |
| 214 | + case (v & 0xFFFFFF0000000000) == 0: | |
| 215 | + return 5 | |
| 216 | + case (v & 0xFFFF000000000000) == 0: | |
| 217 | + return 6 | |
| 218 | + case (v & 0xFF00000000000000) == 0: | |
| 219 | + return 7 | |
| 220 | + default: | |
| 221 | + return 8 | |
| 222 | + } | |
| 223 | +} | |
| 224 | + | |
| 225 | +func varintLength32(v uint32) int { | |
| 226 | + switch { | |
| 227 | + case (v & 0xFFFFFF00) == 0: | |
| 228 | + return 1 | |
| 229 | + case (v & 0xFFFF0000) == 0: | |
| 230 | + return 2 | |
| 231 | + case (v & 0xFF000000) == 0: | |
| 232 | + return 3 | |
| 233 | + default: | |
| 234 | + return 4 | |
| 235 | + } | |
| 236 | +} | |
| 237 | + | |
| 238 | +const ( | |
| 239 | + rawKSUID = 0 | |
| 240 | + timeDelta = (1 << 6) | |
| 241 | + payloadDelta = (1 << 7) | |
| 242 | + payloadRange = (1 << 6) | (1 << 7) | |
| 243 | +) | |
| 244 | + | |
| 245 | +// CompressedSetIter is an iterator type returned by Set.Iter to produce the | |
| 246 | +// list of KSUIDs stored in a set. | |
| 247 | +// | |
| 248 | +// Here's is how the iterator type is commonly used: | |
| 249 | +// | |
| 250 | +// for it := set.Iter(); it.Next(); { | |
| 251 | +// id := it.KSUID | |
| 252 | +// // ... | |
| 253 | +// } | |
| 254 | +// | |
| 255 | +// CompressedSetIter values are not safe to use concurrently from multiple | |
| 256 | +// goroutines. | |
| 257 | +type CompressedSetIter struct { | |
| 258 | + // KSUID is modified by calls to the Next method to hold the KSUID loaded | |
| 259 | + // by the iterator. | |
| 260 | + KSUID KSUID | |
| 261 | + | |
| 262 | + content []byte | |
| 263 | + offset int | |
| 264 | + | |
| 265 | + seqlength uint64 | |
| 266 | + timestamp uint32 | |
| 267 | + lastValue uint128 | |
| 268 | +} | |
| 269 | + | |
| 270 | +// Next moves the iterator forward, returning true if there a KSUID was found, | |
| 271 | +// or false if the iterator as reached the end of the set it was created from. | |
| 272 | +func (it *CompressedSetIter) Next() bool { | |
| 273 | + if it.seqlength != 0 { | |
| 274 | + value := incr128(it.lastValue) | |
| 275 | + it.KSUID = value.ksuid(it.timestamp) | |
| 276 | + it.seqlength-- | |
| 277 | + it.lastValue = value | |
| 278 | + return true | |
| 279 | + } | |
| 280 | + | |
| 281 | + if it.offset == len(it.content) { | |
| 282 | + return false | |
| 283 | + } | |
| 284 | + | |
| 285 | + b := it.content[it.offset] | |
| 286 | + it.offset++ | |
| 287 | + | |
| 288 | + const mask = rawKSUID | timeDelta | payloadDelta | payloadRange | |
| 289 | + tag := int(b) & mask | |
| 290 | + cnt := int(b) & ^mask | |
| 291 | + | |
| 292 | + switch tag { | |
| 293 | + case rawKSUID: | |
| 294 | + off0 := it.offset | |
| 295 | + off1 := off0 + byteLength | |
| 296 | + | |
| 297 | + copy(it.KSUID[:], it.content[off0:off1]) | |
| 298 | + | |
| 299 | + it.offset = off1 | |
| 300 | + it.timestamp = it.KSUID.Timestamp() | |
| 301 | + it.lastValue = uint128Payload(it.KSUID) | |
| 302 | + | |
| 303 | + case timeDelta: | |
| 304 | + off0 := it.offset | |
| 305 | + off1 := off0 + cnt | |
| 306 | + off2 := off1 + payloadLengthInBytes | |
| 307 | + | |
| 308 | + it.timestamp += varint32(it.content[off0:off1]) | |
| 309 | + | |
| 310 | + binary.BigEndian.PutUint32(it.KSUID[:timestampLengthInBytes], it.timestamp) | |
| 311 | + copy(it.KSUID[timestampLengthInBytes:], it.content[off1:off2]) | |
| 312 | + | |
| 313 | + it.offset = off2 | |
| 314 | + it.lastValue = uint128Payload(it.KSUID) | |
| 315 | + | |
| 316 | + case payloadDelta: | |
| 317 | + off0 := it.offset | |
| 318 | + off1 := off0 + cnt | |
| 319 | + | |
| 320 | + delta := varint128(it.content[off0:off1]) | |
| 321 | + value := add128(it.lastValue, delta) | |
| 322 | + | |
| 323 | + it.KSUID = value.ksuid(it.timestamp) | |
| 324 | + it.offset = off1 | |
| 325 | + it.lastValue = value | |
| 326 | + | |
| 327 | + case payloadRange: | |
| 328 | + off0 := it.offset | |
| 329 | + off1 := off0 + cnt | |
| 330 | + | |
| 331 | + value := incr128(it.lastValue) | |
| 332 | + it.KSUID = value.ksuid(it.timestamp) | |
| 333 | + it.seqlength = varint64(it.content[off0:off1]) | |
| 334 | + it.offset = off1 | |
| 335 | + it.seqlength-- | |
| 336 | + it.lastValue = value | |
| 337 | + | |
| 338 | + default: | |
| 339 | + panic("KSUID set iterator is reading malformed data") | |
| 340 | + } | |
| 341 | + | |
| 342 | + return true | |
| 343 | +} | ... | ... |
| ... | ... | @@ -0,0 +1,332 @@ |
| 1 | +package ksuid | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "testing" | |
| 5 | + "time" | |
| 6 | +) | |
| 7 | + | |
| 8 | +func TestCompressedSet(t *testing.T) { | |
| 9 | + tests := []struct { | |
| 10 | + scenario string | |
| 11 | + function func(*testing.T) | |
| 12 | + }{ | |
| 13 | + { | |
| 14 | + scenario: "String", | |
| 15 | + function: testCompressedSetString, | |
| 16 | + }, | |
| 17 | + { | |
| 18 | + scenario: "GoString", | |
| 19 | + function: testCompressedSetGoString, | |
| 20 | + }, | |
| 21 | + { | |
| 22 | + scenario: "sparse", | |
| 23 | + function: testCompressedSetSparse, | |
| 24 | + }, | |
| 25 | + { | |
| 26 | + scenario: "packed", | |
| 27 | + function: testCompressedSetPacked, | |
| 28 | + }, | |
| 29 | + { | |
| 30 | + scenario: "mixed", | |
| 31 | + function: testCompressedSetMixed, | |
| 32 | + }, | |
| 33 | + { | |
| 34 | + scenario: "iterating over a nil compressed set returns no ids", | |
| 35 | + function: testCompressedSetNil, | |
| 36 | + }, | |
| 37 | + { | |
| 38 | + scenario: "concatenating multiple compressed sets is supported", | |
| 39 | + function: testCompressedSetConcat, | |
| 40 | + }, | |
| 41 | + { | |
| 42 | + scenario: "duplicate ids are appear only once in the compressed set", | |
| 43 | + function: testCompressedSetDuplicates, | |
| 44 | + }, | |
| 45 | + { | |
| 46 | + scenario: "building a compressed set with a single id repeated multiple times produces the id only once", | |
| 47 | + function: testCompressedSetSingle, | |
| 48 | + }, | |
| 49 | + { | |
| 50 | + scenario: "iterating over a compressed sequence returns the full sequence", | |
| 51 | + function: testCompressedSetSequence, | |
| 52 | + }, | |
| 53 | + } | |
| 54 | + | |
| 55 | + for _, test := range tests { | |
| 56 | + t.Run(test.scenario, test.function) | |
| 57 | + } | |
| 58 | +} | |
| 59 | + | |
| 60 | +func testCompressedSetString(t *testing.T) { | |
| 61 | + id1, _ := Parse("0uHjRkQoL2JKAQIULPdqqb5fOkk") | |
| 62 | + id2, _ := Parse("0uHjRvkOG5CbtoXW5oCEp3L2xBu") | |
| 63 | + id3, _ := Parse("0uHjSJ4Pe5606kT2XWixK6dirlo") | |
| 64 | + | |
| 65 | + set := Compress(id1, id2, id3) | |
| 66 | + | |
| 67 | + if s := set.String(); s != `["0uHjRkQoL2JKAQIULPdqqb5fOkk", "0uHjRvkOG5CbtoXW5oCEp3L2xBu", "0uHjSJ4Pe5606kT2XWixK6dirlo"]` { | |
| 68 | + t.Error(s) | |
| 69 | + } | |
| 70 | +} | |
| 71 | + | |
| 72 | +func testCompressedSetGoString(t *testing.T) { | |
| 73 | + id1, _ := Parse("0uHjRkQoL2JKAQIULPdqqb5fOkk") | |
| 74 | + id2, _ := Parse("0uHjRvkOG5CbtoXW5oCEp3L2xBu") | |
| 75 | + id3, _ := Parse("0uHjSJ4Pe5606kT2XWixK6dirlo") | |
| 76 | + | |
| 77 | + set := Compress(id1, id2, id3) | |
| 78 | + | |
| 79 | + if s := set.GoString(); s != `ksuid.CompressedSet{"0uHjRkQoL2JKAQIULPdqqb5fOkk", "0uHjRvkOG5CbtoXW5oCEp3L2xBu", "0uHjSJ4Pe5606kT2XWixK6dirlo"}` { | |
| 80 | + t.Error(s) | |
| 81 | + } | |
| 82 | +} | |
| 83 | + | |
| 84 | +func testCompressedSetSparse(t *testing.T) { | |
| 85 | + now := time.Now() | |
| 86 | + | |
| 87 | + times := [100]time.Time{} | |
| 88 | + for i := range times { | |
| 89 | + times[i] = now.Add(time.Duration(i) * 2 * time.Second) | |
| 90 | + } | |
| 91 | + | |
| 92 | + ksuids := [1000]KSUID{} | |
| 93 | + for i := range ksuids { | |
| 94 | + ksuids[i], _ = NewRandomWithTime(times[i%len(times)]) | |
| 95 | + } | |
| 96 | + | |
| 97 | + set := Compress(ksuids[:]...) | |
| 98 | + | |
| 99 | + for i, it := 0, set.Iter(); it.Next(); { | |
| 100 | + if i >= len(ksuids) { | |
| 101 | + t.Error("too many KSUIDs were produced by the set iterator") | |
| 102 | + break | |
| 103 | + } | |
| 104 | + if ksuids[i] != it.KSUID { | |
| 105 | + t.Errorf("bad KSUID at index %d: expected %s but found %s", i, ksuids[i], it.KSUID) | |
| 106 | + } | |
| 107 | + i++ | |
| 108 | + } | |
| 109 | + | |
| 110 | + reportCompressionRatio(t, ksuids[:], set) | |
| 111 | +} | |
| 112 | + | |
| 113 | +func testCompressedSetPacked(t *testing.T) { | |
| 114 | + sequences := [10]Sequence{} | |
| 115 | + for i := range sequences { | |
| 116 | + sequences[i] = Sequence{Seed: New()} | |
| 117 | + } | |
| 118 | + | |
| 119 | + ksuids := [1000]KSUID{} | |
| 120 | + for i := range ksuids { | |
| 121 | + ksuids[i], _ = sequences[i%len(sequences)].Next() | |
| 122 | + } | |
| 123 | + | |
| 124 | + set := Compress(ksuids[:]...) | |
| 125 | + | |
| 126 | + for i, it := 0, set.Iter(); it.Next(); { | |
| 127 | + if i >= len(ksuids) { | |
| 128 | + t.Error("too many KSUIDs were produced by the set iterator") | |
| 129 | + break | |
| 130 | + } | |
| 131 | + if ksuids[i] != it.KSUID { | |
| 132 | + t.Errorf("bad KSUID at index %d: expected %s but found %s", i, ksuids[i], it.KSUID) | |
| 133 | + } | |
| 134 | + i++ | |
| 135 | + } | |
| 136 | + | |
| 137 | + reportCompressionRatio(t, ksuids[:], set) | |
| 138 | +} | |
| 139 | + | |
| 140 | +func testCompressedSetMixed(t *testing.T) { | |
| 141 | + now := time.Now() | |
| 142 | + | |
| 143 | + times := [20]time.Time{} | |
| 144 | + for i := range times { | |
| 145 | + times[i] = now.Add(time.Duration(i) * 2 * time.Second) | |
| 146 | + } | |
| 147 | + | |
| 148 | + sequences := [200]Sequence{} | |
| 149 | + for i := range sequences { | |
| 150 | + seed, _ := NewRandomWithTime(times[i%len(times)]) | |
| 151 | + sequences[i] = Sequence{Seed: seed} | |
| 152 | + } | |
| 153 | + | |
| 154 | + ksuids := [1000]KSUID{} | |
| 155 | + for i := range ksuids { | |
| 156 | + ksuids[i], _ = sequences[i%len(sequences)].Next() | |
| 157 | + } | |
| 158 | + | |
| 159 | + set := Compress(ksuids[:]...) | |
| 160 | + | |
| 161 | + for i, it := 0, set.Iter(); it.Next(); { | |
| 162 | + if i >= len(ksuids) { | |
| 163 | + t.Error("too many KSUIDs were produced by the set iterator") | |
| 164 | + break | |
| 165 | + } | |
| 166 | + if ksuids[i] != it.KSUID { | |
| 167 | + t.Errorf("bad KSUID at index %d: expected %s but found %s", i, ksuids[i], it.KSUID) | |
| 168 | + } | |
| 169 | + i++ | |
| 170 | + } | |
| 171 | + | |
| 172 | + reportCompressionRatio(t, ksuids[:], set) | |
| 173 | +} | |
| 174 | + | |
| 175 | +func testCompressedSetDuplicates(t *testing.T) { | |
| 176 | + sequence := Sequence{Seed: New()} | |
| 177 | + | |
| 178 | + ksuids := [1000]KSUID{} | |
| 179 | + for i := range ksuids[:10] { | |
| 180 | + ksuids[i], _ = sequence.Next() // exercise dedupe on the id range code path | |
| 181 | + } | |
| 182 | + for i := range ksuids[10:] { | |
| 183 | + ksuids[i+10] = New() | |
| 184 | + } | |
| 185 | + for i := 1; i < len(ksuids); i += 4 { | |
| 186 | + ksuids[i] = ksuids[i-1] // generate many dupes | |
| 187 | + } | |
| 188 | + | |
| 189 | + miss := make(map[KSUID]struct{}) | |
| 190 | + uniq := make(map[KSUID]struct{}) | |
| 191 | + | |
| 192 | + for _, id := range ksuids { | |
| 193 | + miss[id] = struct{}{} | |
| 194 | + } | |
| 195 | + | |
| 196 | + set := Compress(ksuids[:]...) | |
| 197 | + | |
| 198 | + for it := set.Iter(); it.Next(); { | |
| 199 | + if _, dupe := uniq[it.KSUID]; dupe { | |
| 200 | + t.Errorf("duplicate id found in compressed set: %s", it.KSUID) | |
| 201 | + } | |
| 202 | + uniq[it.KSUID] = struct{}{} | |
| 203 | + delete(miss, it.KSUID) | |
| 204 | + } | |
| 205 | + | |
| 206 | + if len(miss) != 0 { | |
| 207 | + t.Error("some ids were not found in the compressed set:") | |
| 208 | + for id := range miss { | |
| 209 | + t.Log(id) | |
| 210 | + } | |
| 211 | + } | |
| 212 | +} | |
| 213 | + | |
| 214 | +func testCompressedSetSingle(t *testing.T) { | |
| 215 | + id := New() | |
| 216 | + | |
| 217 | + set := Compress( | |
| 218 | + id, id, id, id, id, id, id, id, id, id, | |
| 219 | + id, id, id, id, id, id, id, id, id, id, | |
| 220 | + id, id, id, id, id, id, id, id, id, id, | |
| 221 | + id, id, id, id, id, id, id, id, id, id, | |
| 222 | + ) | |
| 223 | + | |
| 224 | + n := 0 | |
| 225 | + | |
| 226 | + for it := set.Iter(); it.Next(); { | |
| 227 | + if n != 0 { | |
| 228 | + t.Errorf("too many ids found in the compressed set: %s", it.KSUID) | |
| 229 | + } else if id != it.KSUID { | |
| 230 | + t.Errorf("invalid id found in the compressed set: %s != %s", it.KSUID, id) | |
| 231 | + } | |
| 232 | + n++ | |
| 233 | + } | |
| 234 | + | |
| 235 | + if n == 0 { | |
| 236 | + t.Error("no ids were produced by the compressed set") | |
| 237 | + } | |
| 238 | +} | |
| 239 | + | |
| 240 | +func testCompressedSetSequence(t *testing.T) { | |
| 241 | + seq := Sequence{Seed: New()} | |
| 242 | + | |
| 243 | + ids := make([]KSUID, 5) | |
| 244 | + | |
| 245 | + for i := 0; i < 5; i++ { | |
| 246 | + ids[i], _ = seq.Next() | |
| 247 | + } | |
| 248 | + | |
| 249 | + iter := Compress(ids...).Iter() | |
| 250 | + | |
| 251 | + index := 0 | |
| 252 | + for iter.Next() { | |
| 253 | + if iter.KSUID != ids[index] { | |
| 254 | + t.Errorf("mismatched id at index %d: %s != %s", index, iter.KSUID, ids[index]) | |
| 255 | + } | |
| 256 | + index++ | |
| 257 | + } | |
| 258 | + | |
| 259 | + if index != 5 { | |
| 260 | + t.Errorf("Expected 5 ids, got %d", index) | |
| 261 | + } | |
| 262 | +} | |
| 263 | + | |
| 264 | +func testCompressedSetNil(t *testing.T) { | |
| 265 | + set := CompressedSet(nil) | |
| 266 | + | |
| 267 | + for it := set.Iter(); it.Next(); { | |
| 268 | + t.Errorf("too many ids returned by the iterator of a nil compressed set: %s", it.KSUID) | |
| 269 | + } | |
| 270 | +} | |
| 271 | + | |
| 272 | +func testCompressedSetConcat(t *testing.T) { | |
| 273 | + ksuids := [100]KSUID{} | |
| 274 | + | |
| 275 | + for i := range ksuids { | |
| 276 | + ksuids[i] = New() | |
| 277 | + } | |
| 278 | + | |
| 279 | + set := CompressedSet(nil) | |
| 280 | + set = AppendCompressed(set, ksuids[:42]...) | |
| 281 | + set = AppendCompressed(set, ksuids[42:64]...) | |
| 282 | + set = AppendCompressed(set, ksuids[64:]...) | |
| 283 | + | |
| 284 | + for i, it := 0, set.Iter(); it.Next(); i++ { | |
| 285 | + if ksuids[i] != it.KSUID { | |
| 286 | + t.Errorf("invalid ID at index %d: %s != %s", i, ksuids[i], it.KSUID) | |
| 287 | + } | |
| 288 | + } | |
| 289 | +} | |
| 290 | + | |
| 291 | +func reportCompressionRatio(t *testing.T, ksuids []KSUID, set CompressedSet) { | |
| 292 | + len1 := byteLength * len(ksuids) | |
| 293 | + len2 := len(set) | |
| 294 | + t.Logf("original %d B, compressed %d B (%.4g%%)", len1, len2, 100*(1-(float64(len2)/float64(len1)))) | |
| 295 | +} | |
| 296 | + | |
| 297 | +func BenchmarkCompressedSet(b *testing.B) { | |
| 298 | + ksuids1 := [1000]KSUID{} | |
| 299 | + ksuids2 := [1000]KSUID{} | |
| 300 | + | |
| 301 | + for i := range ksuids1 { | |
| 302 | + ksuids1[i] = New() | |
| 303 | + } | |
| 304 | + | |
| 305 | + ksuids2 = ksuids1 | |
| 306 | + buf := make([]byte, 0, 1024) | |
| 307 | + set := Compress(ksuids2[:]...) | |
| 308 | + | |
| 309 | + b.Run("write", func(b *testing.B) { | |
| 310 | + n := 0 | |
| 311 | + for i := 0; i != b.N; i++ { | |
| 312 | + ksuids2 = ksuids1 | |
| 313 | + buf = AppendCompressed(buf[:0], ksuids2[:]...) | |
| 314 | + n = len(buf) | |
| 315 | + } | |
| 316 | + b.SetBytes(int64(n + len(ksuids2))) | |
| 317 | + }) | |
| 318 | + | |
| 319 | + b.Run("read", func(b *testing.B) { | |
| 320 | + n := 0 | |
| 321 | + for i := 0; i != b.N; i++ { | |
| 322 | + n = 0 | |
| 323 | + for it := set.Iter(); true; { | |
| 324 | + if !it.Next() { | |
| 325 | + n++ | |
| 326 | + break | |
| 327 | + } | |
| 328 | + } | |
| 329 | + } | |
| 330 | + b.SetBytes(int64((n * byteLength) + len(set))) | |
| 331 | + }) | |
| 332 | +} | ... | ... |
| ... | ... | @@ -0,0 +1,141 @@ |
| 1 | +package ksuid | |
| 2 | + | |
| 3 | +import "fmt" | |
| 4 | + | |
| 5 | +// uint128 represents an unsigned 128 bits little endian integer. | |
| 6 | +type uint128 [2]uint64 | |
| 7 | + | |
| 8 | +func uint128Payload(ksuid KSUID) uint128 { | |
| 9 | + return makeUint128FromPayload(ksuid[timestampLengthInBytes:]) | |
| 10 | +} | |
| 11 | + | |
| 12 | +func makeUint128(high uint64, low uint64) uint128 { | |
| 13 | + return uint128{low, high} | |
| 14 | +} | |
| 15 | + | |
| 16 | +func makeUint128FromPayload(payload []byte) uint128 { | |
| 17 | + return uint128{ | |
| 18 | + // low | |
| 19 | + uint64(payload[8])<<56 | | |
| 20 | + uint64(payload[9])<<48 | | |
| 21 | + uint64(payload[10])<<40 | | |
| 22 | + uint64(payload[11])<<32 | | |
| 23 | + uint64(payload[12])<<24 | | |
| 24 | + uint64(payload[13])<<16 | | |
| 25 | + uint64(payload[14])<<8 | | |
| 26 | + uint64(payload[15]), | |
| 27 | + // high | |
| 28 | + uint64(payload[0])<<56 | | |
| 29 | + uint64(payload[1])<<48 | | |
| 30 | + uint64(payload[2])<<40 | | |
| 31 | + uint64(payload[3])<<32 | | |
| 32 | + uint64(payload[4])<<24 | | |
| 33 | + uint64(payload[5])<<16 | | |
| 34 | + uint64(payload[6])<<8 | | |
| 35 | + uint64(payload[7]), | |
| 36 | + } | |
| 37 | +} | |
| 38 | + | |
| 39 | +func (v uint128) ksuid(timestamp uint32) KSUID { | |
| 40 | + return KSUID{ | |
| 41 | + // time | |
| 42 | + byte(timestamp >> 24), | |
| 43 | + byte(timestamp >> 16), | |
| 44 | + byte(timestamp >> 8), | |
| 45 | + byte(timestamp), | |
| 46 | + | |
| 47 | + // high | |
| 48 | + byte(v[1] >> 56), | |
| 49 | + byte(v[1] >> 48), | |
| 50 | + byte(v[1] >> 40), | |
| 51 | + byte(v[1] >> 32), | |
| 52 | + byte(v[1] >> 24), | |
| 53 | + byte(v[1] >> 16), | |
| 54 | + byte(v[1] >> 8), | |
| 55 | + byte(v[1]), | |
| 56 | + | |
| 57 | + // low | |
| 58 | + byte(v[0] >> 56), | |
| 59 | + byte(v[0] >> 48), | |
| 60 | + byte(v[0] >> 40), | |
| 61 | + byte(v[0] >> 32), | |
| 62 | + byte(v[0] >> 24), | |
| 63 | + byte(v[0] >> 16), | |
| 64 | + byte(v[0] >> 8), | |
| 65 | + byte(v[0]), | |
| 66 | + } | |
| 67 | +} | |
| 68 | + | |
| 69 | +func (v uint128) bytes() [16]byte { | |
| 70 | + return [16]byte{ | |
| 71 | + // high | |
| 72 | + byte(v[1] >> 56), | |
| 73 | + byte(v[1] >> 48), | |
| 74 | + byte(v[1] >> 40), | |
| 75 | + byte(v[1] >> 32), | |
| 76 | + byte(v[1] >> 24), | |
| 77 | + byte(v[1] >> 16), | |
| 78 | + byte(v[1] >> 8), | |
| 79 | + byte(v[1]), | |
| 80 | + | |
| 81 | + // low | |
| 82 | + byte(v[0] >> 56), | |
| 83 | + byte(v[0] >> 48), | |
| 84 | + byte(v[0] >> 40), | |
| 85 | + byte(v[0] >> 32), | |
| 86 | + byte(v[0] >> 24), | |
| 87 | + byte(v[0] >> 16), | |
| 88 | + byte(v[0] >> 8), | |
| 89 | + byte(v[0]), | |
| 90 | + } | |
| 91 | +} | |
| 92 | + | |
| 93 | +func (v uint128) String() string { | |
| 94 | + return fmt.Sprintf("0x%016X%016X", v[0], v[1]) | |
| 95 | +} | |
| 96 | + | |
| 97 | +const wordBitSize = 64 | |
| 98 | + | |
| 99 | +func cmp128(x, y uint128) int { | |
| 100 | + if x[1] < y[1] { | |
| 101 | + return -1 | |
| 102 | + } | |
| 103 | + if x[1] > y[1] { | |
| 104 | + return 1 | |
| 105 | + } | |
| 106 | + if x[0] < y[0] { | |
| 107 | + return -1 | |
| 108 | + } | |
| 109 | + if x[0] > y[0] { | |
| 110 | + return 1 | |
| 111 | + } | |
| 112 | + return 0 | |
| 113 | +} | |
| 114 | + | |
| 115 | +func add128(x, y uint128) (z uint128) { | |
| 116 | + x0 := x[0] | |
| 117 | + y0 := y[0] | |
| 118 | + z0 := x0 + y0 | |
| 119 | + z[0] = z0 | |
| 120 | + | |
| 121 | + c := (x0&y0 | (x0|y0)&^z0) >> (wordBitSize - 1) | |
| 122 | + | |
| 123 | + z[1] = x[1] + y[1] + c | |
| 124 | + return | |
| 125 | +} | |
| 126 | + | |
| 127 | +func sub128(x, y uint128) (z uint128) { | |
| 128 | + x0 := x[0] | |
| 129 | + y0 := y[0] | |
| 130 | + z0 := x0 - y0 | |
| 131 | + z[0] = z0 | |
| 132 | + | |
| 133 | + c := (y0&^x0 | (y0|^x0)&z0) >> (wordBitSize - 1) | |
| 134 | + | |
| 135 | + z[1] = x[1] - y[1] - c | |
| 136 | + return | |
| 137 | +} | |
| 138 | + | |
| 139 | +func incr128(x uint128) uint128 { | |
| 140 | + return add128(x, uint128{1, 0}) | |
| 141 | +} | ... | ... |
| ... | ... | @@ -0,0 +1,169 @@ |
| 1 | +package ksuid | |
| 2 | + | |
| 3 | +import ( | |
| 4 | + "fmt" | |
| 5 | + "testing" | |
| 6 | +) | |
| 7 | + | |
| 8 | +func TestCmp128(t *testing.T) { | |
| 9 | + tests := []struct { | |
| 10 | + x uint128 | |
| 11 | + y uint128 | |
| 12 | + k int | |
| 13 | + }{ | |
| 14 | + { | |
| 15 | + x: makeUint128(0, 0), | |
| 16 | + y: makeUint128(0, 0), | |
| 17 | + k: 0, | |
| 18 | + }, | |
| 19 | + { | |
| 20 | + x: makeUint128(0, 1), | |
| 21 | + y: makeUint128(0, 0), | |
| 22 | + k: +1, | |
| 23 | + }, | |
| 24 | + { | |
| 25 | + x: makeUint128(0, 0), | |
| 26 | + y: makeUint128(0, 1), | |
| 27 | + k: -1, | |
| 28 | + }, | |
| 29 | + { | |
| 30 | + x: makeUint128(1, 0), | |
| 31 | + y: makeUint128(0, 1), | |
| 32 | + k: +1, | |
| 33 | + }, | |
| 34 | + { | |
| 35 | + x: makeUint128(0, 1), | |
| 36 | + y: makeUint128(1, 0), | |
| 37 | + k: -1, | |
| 38 | + }, | |
| 39 | + } | |
| 40 | + | |
| 41 | + for _, test := range tests { | |
| 42 | + t.Run(fmt.Sprintf("cmp128(%s,%s)", test.x, test.y), func(t *testing.T) { | |
| 43 | + if k := cmp128(test.x, test.y); k != test.k { | |
| 44 | + t.Error(k, "!=", test.k) | |
| 45 | + } | |
| 46 | + }) | |
| 47 | + } | |
| 48 | +} | |
| 49 | + | |
| 50 | +func TestAdd128(t *testing.T) { | |
| 51 | + tests := []struct { | |
| 52 | + x uint128 | |
| 53 | + y uint128 | |
| 54 | + z uint128 | |
| 55 | + }{ | |
| 56 | + { | |
| 57 | + x: makeUint128(0, 0), | |
| 58 | + y: makeUint128(0, 0), | |
| 59 | + z: makeUint128(0, 0), | |
| 60 | + }, | |
| 61 | + { | |
| 62 | + x: makeUint128(0, 1), | |
| 63 | + y: makeUint128(0, 0), | |
| 64 | + z: makeUint128(0, 1), | |
| 65 | + }, | |
| 66 | + { | |
| 67 | + x: makeUint128(0, 0), | |
| 68 | + y: makeUint128(0, 1), | |
| 69 | + z: makeUint128(0, 1), | |
| 70 | + }, | |
| 71 | + { | |
| 72 | + x: makeUint128(1, 0), | |
| 73 | + y: makeUint128(0, 1), | |
| 74 | + z: makeUint128(1, 1), | |
| 75 | + }, | |
| 76 | + { | |
| 77 | + x: makeUint128(0, 1), | |
| 78 | + y: makeUint128(1, 0), | |
| 79 | + z: makeUint128(1, 1), | |
| 80 | + }, | |
| 81 | + { | |
| 82 | + x: makeUint128(0, 0xFFFFFFFFFFFFFFFF), | |
| 83 | + y: makeUint128(0, 1), | |
| 84 | + z: makeUint128(1, 0), | |
| 85 | + }, | |
| 86 | + } | |
| 87 | + | |
| 88 | + for _, test := range tests { | |
| 89 | + t.Run(fmt.Sprintf("add128(%s,%s)", test.x, test.y), func(t *testing.T) { | |
| 90 | + if z := add128(test.x, test.y); z != test.z { | |
| 91 | + t.Error(z, "!=", test.z) | |
| 92 | + } | |
| 93 | + }) | |
| 94 | + } | |
| 95 | +} | |
| 96 | + | |
| 97 | +func TestSub128(t *testing.T) { | |
| 98 | + tests := []struct { | |
| 99 | + x uint128 | |
| 100 | + y uint128 | |
| 101 | + z uint128 | |
| 102 | + }{ | |
| 103 | + { | |
| 104 | + x: makeUint128(0, 0), | |
| 105 | + y: makeUint128(0, 0), | |
| 106 | + z: makeUint128(0, 0), | |
| 107 | + }, | |
| 108 | + { | |
| 109 | + x: makeUint128(0, 1), | |
| 110 | + y: makeUint128(0, 0), | |
| 111 | + z: makeUint128(0, 1), | |
| 112 | + }, | |
| 113 | + { | |
| 114 | + x: makeUint128(0, 0), | |
| 115 | + y: makeUint128(0, 1), | |
| 116 | + z: makeUint128(0xFFFFFFFFFFFFFFFF, 0xFFFFFFFFFFFFFFFF), | |
| 117 | + }, | |
| 118 | + { | |
| 119 | + x: makeUint128(1, 0), | |
| 120 | + y: makeUint128(0, 1), | |
| 121 | + z: makeUint128(0, 0xFFFFFFFFFFFFFFFF), | |
| 122 | + }, | |
| 123 | + { | |
| 124 | + x: makeUint128(0, 1), | |
| 125 | + y: makeUint128(1, 0), | |
| 126 | + z: makeUint128(0xFFFFFFFFFFFFFFFF, 1), | |
| 127 | + }, | |
| 128 | + { | |
| 129 | + x: makeUint128(0, 0xFFFFFFFFFFFFFFFF), | |
| 130 | + y: makeUint128(0, 1), | |
| 131 | + z: makeUint128(0, 0xFFFFFFFFFFFFFFFE), | |
| 132 | + }, | |
| 133 | + } | |
| 134 | + | |
| 135 | + for _, test := range tests { | |
| 136 | + t.Run(fmt.Sprintf("sub128(%s,%s)", test.x, test.y), func(t *testing.T) { | |
| 137 | + if z := sub128(test.x, test.y); z != test.z { | |
| 138 | + t.Error(z, "!=", test.z) | |
| 139 | + } | |
| 140 | + }) | |
| 141 | + } | |
| 142 | +} | |
| 143 | + | |
| 144 | +func BenchmarkCmp128(b *testing.B) { | |
| 145 | + x := makeUint128(0, 0) | |
| 146 | + y := makeUint128(0, 0) | |
| 147 | + | |
| 148 | + for i := 0; i != b.N; i++ { | |
| 149 | + cmp128(x, y) | |
| 150 | + } | |
| 151 | +} | |
| 152 | + | |
| 153 | +func BenchmarkAdd128(b *testing.B) { | |
| 154 | + x := makeUint128(0, 0) | |
| 155 | + y := makeUint128(0, 0) | |
| 156 | + | |
| 157 | + for i := 0; i != b.N; i++ { | |
| 158 | + add128(x, y) | |
| 159 | + } | |
| 160 | +} | |
| 161 | + | |
| 162 | +func BenchmarkSub128(b *testing.B) { | |
| 163 | + x := makeUint128(0, 0) | |
| 164 | + y := makeUint128(0, 0) | |
| 165 | + | |
| 166 | + for i := 0; i != b.N; i++ { | |
| 167 | + sub128(x, y) | |
| 168 | + } | |
| 169 | +} | ... | ... |