-
Notifications
You must be signed in to change notification settings - Fork 5
/
Copy pathkey.go
371 lines (318 loc) · 9.96 KB
/
key.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
// Package uuidkey encodes UUIDs to a readable Key format via the Base32-Crockford codec.
package uuidkey
import (
"errors"
"fmt"
"strconv"
"strings"
"github.com/richardlehane/crock32"
)
// Key validation constraint constants
const (
// KeyLengthWithHyphens is the total length of a valid UUID Key, including hyphens.
KeyLengthWithHyphens = 31 // 7 + 1 + 7 + 1 + 7 + 1 + 7 = 31 characters
// KeyLengthWithoutHyphens is the total length of a valid UUID Key, excluding hyphens.
KeyLengthWithoutHyphens = 28 // 7 + 7 + 7 + 7 = 28 characters
// KeyPartLength is the length of each part in a UUID Key.
// A UUID Key consists of 4 parts separated by hyphens.
KeyPartLength = 7
// KeyHyphenCount is the number of hyphens in a valid UUID Key.
KeyHyphenCount = 3
// KeyPartsCount is the number of parts in a valid UUID Key.
KeyPartsCount = KeyHyphenCount + 1
// UUIDLength is the standard length of a UUID string, including hyphens.
// Reference: RFC 4122 (https://tools.ietf.org/html/rfc4122)
UUIDLength = 36
)
// Key is a UUID Key string.
type Key string
// String will convert your Key into a string.
func (k Key) String() string {
return string(k)
}
// config is a struct that contains configuration settings
type config struct {
hyphens bool
entropySize numOfCrock32Chars
}
// defaultConfig is the default configuration for the uuidkey package
var defaultConfig = config{
hyphens: true,
entropySize: EntropyBits160,
}
// Option is a function that configures options
type Option func(c *config)
// apply will apply the options to the default options
func apply(opts ...Option) config {
c := defaultConfig
for _, opt := range opts {
opt(&c)
}
return c
}
// WithoutHyphens expects no hyphens in the Key
var WithoutHyphens Option = func(c *config) {
c.hyphens = false
}
// Parse converts a Key formatted string into a Key type.
func Parse(key string) (Key, error) {
k := Key(key)
if !k.IsValid() {
return "", errors.New("invalid UUID Key")
}
return k, nil
}
// IsValid verifies if a given Key follows the correct format.
// The format should be:
// - 31 characters long (with hyphens) or 28 characters (without hyphens)
// - Uppercase
// - Contains only alphanumeric characters
// - Contains 3 hyphens (if hyphenated)
// - Each part is 7 characters long
// - Each part contains only valid crockford base32 characters (I, L, O, U are not allowed)
func (k Key) IsValid() bool {
length := len(k)
if length == KeyLengthWithHyphens {
// Check hyphens first (faster than character validation)
if k[7] != '-' || k[15] != '-' || k[23] != '-' {
return false
}
// Use direct string indexing instead of slicing
return isValidPart(string(k[0:7])) && isValidPart(string(k[8:15])) &&
isValidPart(string(k[16:23])) && isValidPart(string(k[24:31]))
}
if length == KeyLengthWithoutHyphens {
// Use direct string indexing instead of slicing
return isValidPart(string(k[0:7])) && isValidPart(string(k[7:14])) &&
isValidPart(string(k[14:21])) && isValidPart(string(k[21:28]))
}
return false
}
// isValidPart checks if a 7-character part of the key is valid:
// - Must be exactly 7 characters
// - Must be uppercase alphanumeric
// - Must not contain I, L, O, U (invalid in crockford base32)
func isValidPart(part string) bool {
if len(part) != KeyPartLength {
return false
}
for i := 0; i < KeyPartLength; i++ {
c := part[i]
// Combine conditions to reduce branching
if c > 'Z' || (c < '0' || (c > '9' && c < 'A')) ||
c == 'I' || c == 'L' || c == 'O' || c == 'U' {
return false
}
}
return true
}
// UUID will validate and convert a given Key into a UUID string.
func (k Key) UUID() (string, error) {
if !k.IsValid() {
return "", errors.New("invalid UUID key")
}
return k.Decode()
}
// decode will convert your given string into original UUID part string
func decode(s string) string {
i, _ := crock32.Decode(s)
var builder strings.Builder
builder.Grow(8) // We know the result will be 8 chars
decoded := strconv.FormatUint(i, 16)
padding := 8 - len(decoded)
for i := 0; i < padding; i++ {
builder.WriteByte('0')
}
builder.WriteString(decoded)
return builder.String()
}
// Encode will encode a given UUID string into a Key.
// It pre-allocates the exact string capacity needed for better performance.
func Encode(uuid string, opts ...Option) (Key, error) {
options := apply(opts...)
if len(uuid) != UUIDLength {
return "", fmt.Errorf("invalid UUID length: expected %d characters, got %d", UUIDLength, len(uuid))
}
// Pre-allocate a strings.Builder with exact capacity
var builder strings.Builder
if options.hyphens {
builder.Grow(KeyLengthWithHyphens)
} else {
builder.Grow(KeyLengthWithoutHyphens)
}
// Process each part
processAndWritePart(&builder, uuid[0:8])
if options.hyphens {
builder.WriteByte('-')
}
processAndWritePart(&builder, uuid[9:13]+uuid[14:18])
if options.hyphens {
builder.WriteByte('-')
}
processAndWritePart(&builder, uuid[19:23]+uuid[24:28])
if options.hyphens {
builder.WriteByte('-')
}
processAndWritePart(&builder, uuid[28:36])
return Key(builder.String()), nil
}
func processAndWritePart(builder *strings.Builder, src string) {
n, _ := strconv.ParseUint(src, 16, 64)
encoded := crock32.Encode(n)
padding := 7 - len(encoded)
// Write padding zeros
for i := 0; i < padding; i++ {
builder.WriteByte('0')
}
// Write encoded part in uppercase
for i := 0; i < len(encoded); i++ {
builder.WriteByte(toUpper(encoded[i]))
}
}
// toUpper converts a single byte to uppercase if it's a lowercase letter
func toUpper(c byte) byte {
if c >= 'a' && c <= 'z' {
return c - 32
}
return c
}
// EncodeBytes encodes a [16]byte UUID into a Key.
func EncodeBytes(uuid [16]byte, opts ...Option) (Key, error) {
options := apply(opts...)
var builder strings.Builder
if options.hyphens {
builder.Grow(KeyLengthWithHyphens)
} else {
builder.Grow(KeyLengthWithoutHyphens)
}
// Process each 4-byte group
writeEncodedPart(&builder, uint64(uuid[0])<<24|uint64(uuid[1])<<16|uint64(uuid[2])<<8|uint64(uuid[3]))
if options.hyphens {
builder.WriteByte('-')
}
writeEncodedPart(&builder, uint64(uuid[4])<<24|uint64(uuid[5])<<16|uint64(uuid[6])<<8|uint64(uuid[7]))
if options.hyphens {
builder.WriteByte('-')
}
writeEncodedPart(&builder, uint64(uuid[8])<<24|uint64(uuid[9])<<16|uint64(uuid[10])<<8|uint64(uuid[11]))
if options.hyphens {
builder.WriteByte('-')
}
writeEncodedPart(&builder, uint64(uuid[12])<<24|uint64(uuid[13])<<16|uint64(uuid[14])<<8|uint64(uuid[15]))
return Key(builder.String()), nil
}
func writeEncodedPart(builder *strings.Builder, n uint64) {
encoded := crock32.Encode(n)
padding := 7 - len(encoded)
// Write padding zeros
for i := 0; i < padding; i++ {
builder.WriteByte('0')
}
// Write encoded part in uppercase
for i := 0; i < len(encoded); i++ {
builder.WriteByte(toUpper(encoded[i]))
}
}
// Decode will decode a given Key into a UUID string with basic length validation.
func (k Key) Decode() (string, error) {
// determine if we should expect hyphens given the length of the key
hyphens := false
length := len(k)
if length != KeyLengthWithoutHyphens {
if length != KeyLengthWithHyphens {
return "", fmt.Errorf("invalid Key length: expected %d or %d characters, got %d",
KeyLengthWithoutHyphens, KeyLengthWithHyphens, length)
}
hyphens = true
}
var builder strings.Builder
builder.Grow(UUIDLength) // Pre-allocate exact size needed: 36 bytes
// select the 4 parts of the key string
var s1, s2, s3, s4 string
s := string(k)
if hyphens {
s1 = s[0:7] // [38QARV0]-1ET0G6Z-2CJD9VA-2ZZAR0X
s2 = s[8:15] // 38QARV0-[1ET0G6Z]-2CJD9VA-2ZZAR0X
s3 = s[16:23] // 38QARV0-1ET0G6Z-[2CJD9VA]-2ZZAR0X
s4 = s[24:31] // 38QARV0-1ET0G6Z-2CJD9VA-[2ZZAR0X]
} else {
s1 = s[0:7] // [38QARV0]1ET0G6Z2CJD9VA2ZZAR0X
s2 = s[7:14] // 38QARV0[1ET0G6Z]2CJD9VA2ZZAR0X
s3 = s[14:21] // 38QARV01ET0G6Z[2CJD9VA]2ZZAR0X
s4 = s[21:28] // 38QARV01ET0G6Z2CJD9VA[2ZZAR0X]
}
// decode each string part into original UUID part string
n1 := decode(s1)
n2 := decode(s2)
n3 := decode(s3)
n4 := decode(s4)
// select the 4 parts of the decoded parts
n2a := n2[0:4]
n2b := n2[4:8]
n3a := n3[0:4]
n3b := n3[4:8]
// Write parts with proper formatting
builder.WriteString(n1)
builder.WriteByte('-')
builder.WriteString(n2a)
builder.WriteByte('-')
builder.WriteString(n2b)
builder.WriteByte('-')
builder.WriteString(n3a)
builder.WriteByte('-')
builder.WriteString(n3b)
builder.WriteString(n4)
return builder.String(), nil
}
// Bytes converts a Key to a [16]byte UUID.
func (k Key) Bytes() ([16]byte, error) {
length := len(k)
if length != KeyLengthWithoutHyphens && length != KeyLengthWithHyphens {
return [16]byte{}, fmt.Errorf("invalid Key length: expected %d or %d characters, got %d",
KeyLengthWithoutHyphens, KeyLengthWithHyphens, length)
}
hyphens := length == KeyLengthWithHyphens
var uuid [16]byte
var err error
// Avoid string conversion by working directly with the Key type
s := string(k)
if hyphens {
if err = processByteGroup(s[0:7], &uuid, 0); err != nil {
return [16]byte{}, err
}
if err = processByteGroup(s[8:15], &uuid, 4); err != nil {
return [16]byte{}, err
}
if err = processByteGroup(s[16:23], &uuid, 8); err != nil {
return [16]byte{}, err
}
if err = processByteGroup(s[24:31], &uuid, 12); err != nil {
return [16]byte{}, err
}
} else {
if err = processByteGroup(s[0:7], &uuid, 0); err != nil {
return [16]byte{}, err
}
if err = processByteGroup(s[7:14], &uuid, 4); err != nil {
return [16]byte{}, err
}
if err = processByteGroup(s[14:21], &uuid, 8); err != nil {
return [16]byte{}, err
}
if err = processByteGroup(s[21:28], &uuid, 12); err != nil {
return [16]byte{}, err
}
}
return uuid, nil
}
func processByteGroup(part string, uuid *[16]byte, offset int) error {
n, err := crock32.Decode(strings.ToLower(part))
if err != nil {
return fmt.Errorf("failed to decode Key part: %v", err)
}
uuid[offset] = byte(n >> 24)
uuid[offset+1] = byte(n >> 16)
uuid[offset+2] = byte(n >> 8)
uuid[offset+3] = byte(n)
return nil
}