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 | +} | ... | ... |