Skip to content

Commit d6786d1

Browse files
authored
[typeid] Enforce that the first suffix character is in the 0-7 range (#75)
Change spec and implementations to enforce that the first suffix character should be in the `0-7` range. This is because 26 characters in base32 encode 130-bits, but UUIDs are only 128-bits. In order to guarantee that there are no overflow errors and that the `typeid <-> uuid` mapping is a bijective function, the maximum possible suffix is `7zzzzzzzzzzzzzzzzzzzzzzzzz`. This was first reported by @fxlae in jetify-com/typeid#20 This PR: 1. Updates the spec 2. Updates the test data files provided by the spec 3. Updates our `go` and `typescript` implementations
1 parent 31de444 commit d6786d1

File tree

11 files changed

+202
-15
lines changed

11 files changed

+202
-15
lines changed

typeid/typeid-go/testdata/invalid.yml

+7-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Each example contains an invalid TypeID string. Implementations are expected
55
# to throw an error when attempting to parse/validate these strings.
66
#
7-
# Last updated: 2023-06-29
7+
# Last updated: 2023-07-05
88

99
- name: prefix-uppercase
1010
typeid: "PREFIX_00000000000000000000000000"
@@ -31,7 +31,7 @@
3131
description: "The prefix can't have any spaces"
3232

3333
- name: prefix-64-chars
34-
# 123456789 123456789 123456789 123456789 123456789 123456789 1234
34+
# 123456789 123456789 123456789 123456789 123456789 123456789 1234
3535
typeid: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl_00000000000000000000000000"
3636
description: "The prefix can't be 64 characters, it needs to be 63 characters or less"
3737

@@ -81,3 +81,8 @@
8181
# This example would be valid if we were using the crockford hyphenation rules
8282
typeid: "prefix_123456789-0123456789-0123456"
8383
description: "The suffix can't ignore hyphens as in the crockford encoding"
84+
85+
- name: suffix-overflow
86+
# This is the first suffix that overflows into 129 bits
87+
typeid: "prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz"
88+
description: "The should encode at most 128-bits"

typeid/typeid-go/testdata/valid.yml

+7-2
Original file line numberDiff line numberDiff line change
@@ -17,13 +17,13 @@
1717
# decoding and re-encoding the id, the result is the same as the original.
1818
#
1919
# In other words, the following property should always hold:
20-
# random_typeid: encode(decode(random_typeid))
20+
# random_typeid == encode(decode(random_typeid))
2121
#
2222
# Finally, while implementations should be able to decode the values below,
2323
# note that not all of them are UUIDv7s. When *generating* new random typeids,
2424
# implementations should always use UUIDv7s.
2525
#
26-
# Last updated: 2023-06-29
26+
# Last updated: 2023-07-05
2727

2828
- name: nil
2929
typeid: "00000000000000000000000000"
@@ -50,6 +50,11 @@
5050
prefix: ""
5151
uuid: "00000000-0000-0000-0000-000000000020"
5252

53+
- name: max-valid
54+
typeid: "7zzzzzzzzzzzzzzzzzzzzzzzzz"
55+
prefix: ""
56+
uuid: "ffffffff-ffff-ffff-ffff-ffffffffffff"
57+
5358
- name: valid-alphabet
5459
typeid: "prefix_0123456789abcdefghjkmnpqrs"
5560
prefix: "prefix"

typeid/typeid-go/typeid.go

+7-5
Original file line numberDiff line numberDiff line change
@@ -153,12 +153,14 @@ func validatePrefix(prefix string) error {
153153
}
154154

155155
func validateSuffix(suffix string) error {
156-
// Validate the suffix by decoding it:
157-
// 1. If the suffix is empty, it is valid
158-
// 2. If the suffix is not empty, it must be a valid base32 string
159-
if suffix == "" {
160-
return nil
156+
if len(suffix) != 26 {
157+
return fmt.Errorf("invalid suffix: %s. Suffix length is %d, expected 26", suffix, len(suffix))
158+
}
159+
160+
if suffix[0] > '7' {
161+
return fmt.Errorf("invalid suffix: '%s'. Suffix must start with a 0-7 digit to avoid overflows", suffix)
161162
}
163+
// Validate the suffix by decoding it, it must be a valid base32 string
162164
if _, err := base32.Decode(suffix); err != nil {
163165
return fmt.Errorf("invalid suffix: %w", err)
164166
}

typeid/typeid-js/src/typeid.ts

+4
Original file line numberDiff line numberDiff line change
@@ -36,6 +36,10 @@ export class TypeID {
3636
throw new Error(`Invalid length. Suffix should have 26 characters, got ${suffix.length}`);
3737
}
3838

39+
if (this.suffix[0] > "7") {
40+
throw new Error("Invalid suffix. First character must be in the range [0-7]");
41+
}
42+
3943
// Validate the suffix by decoding it. If it's invalid, an error will be thrown.
4044
// eslint-disable-next-line @typescript-eslint/no-unused-vars
4145
const unused = decode(this.suffix);

typeid/typeid-js/test/invalid.ts

+5
Original file line numberDiff line numberDiff line change
@@ -84,5 +84,10 @@ export default [
8484
"name": "suffix-hyphens-crockford",
8585
"typeid": "prefix_123456789-0123456789-0123456",
8686
"description": "The suffix can't ignore hyphens as in the crockford encoding"
87+
},
88+
{
89+
"name": "suffix-overflow",
90+
"typeid": "prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz",
91+
"description": "The should encode at most 128-bits"
8792
}
8893
]

typeid/typeid-js/test/valid.ts

+6
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,12 @@ export default [
3131
"prefix": "",
3232
"uuid": "00000000-0000-0000-0000-000000000020"
3333
},
34+
{
35+
"name": "max-valid",
36+
"typeid": "7zzzzzzzzzzzzzzzzzzzzzzzzz",
37+
"prefix": "",
38+
"uuid": "ffffffff-ffff-ffff-ffff-ffffffffffff"
39+
},
3440
{
3541
"name": "valid-alphabet",
3642
"typeid": "prefix_0123456789abcdefghjkmnpqrs",

typeid/typeid/spec/README.md

+11-3
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
# TypeID Specification (Version 0.1.0)
1+
# TypeID Specification (Version 0.2.0)
22

33
## Overview
44
TypeIDs are a type-safe extension of UUIDv7, they encode UUIDs in base32 and add a type prefix.
@@ -72,6 +72,11 @@ This is the same alphabet used by [Crockford's base32 encoding](https://www.croc
7272
but in our case the alphabet encoding is strict: always in lowercase, no hyphens allowed,
7373
and we never decode multiple ambiguous characters to the same value.
7474

75+
Technically speaking, 26 characters in base32 can encode 130 bits of data, but UUIDs
76+
are 128 bits. To prevent overflow errors, the maximum possible suffix for a typeid
77+
is `7zzzzzzzzzzzzzzzzzzzzzzzzz`. Implementations should reject any suffix greater than
78+
that value, by checking that the first character is a `7` or less.
79+
7580
#### Compatibility with UUID
7681
When genarating a new TypeID, the generated UUID suffix MUST decode to a valid UUIDv7.
7782

@@ -92,6 +97,9 @@ To assist library authors in validating their implementations, we provide:
9297
+ A reference implementation in [Go](https://github.com/jetpack-io/typeid-go)
9398
with extensive testing.
9499
+ A [valid.yml](valid.yml) file containing a list of valid typeids along
95-
with their corresponding decoded UUIDs.
100+
with their corresponding decoded UUIDs. For convienience, we also provide
101+
a [valid.json](valid.json) file containing the same data in JSON format.
96102
+ An [invalid.yml](invalid.yml) file containing a list of strings that are
97-
invalid typeids and should fail to parse/decode.
103+
invalid typeids and should fail to parse/decode. For convienience, we also
104+
provide a [invalid.json](invalid.json) file containing the same data in
105+
JSON format.

typeid/typeid/spec/invalid.json

+92
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
[
2+
{
3+
"name": "prefix-uppercase",
4+
"typeid": "PREFIX_00000000000000000000000000",
5+
"description": "The prefix should be lowercase with no uppercase letters"
6+
},
7+
{
8+
"name": "prefix-numeric",
9+
"typeid": "12345_00000000000000000000000000",
10+
"description": "The prefix can't have numbers, it needs to be alphabetic"
11+
},
12+
{
13+
"name": "prefix-period",
14+
"typeid": "pre.fix_00000000000000000000000000",
15+
"description": "The prefix can't have symbols, it needs to be alphabetic"
16+
},
17+
{
18+
"name": "prefix-underscore",
19+
"typeid": "pre_fix_00000000000000000000000000",
20+
"description": "The prefix can't have symbols, it needs to be alphabetic"
21+
},
22+
{
23+
"name": "prefix-non-ascii",
24+
"typeid": "préfix_00000000000000000000000000",
25+
"description": "The prefix can only have ascii letters"
26+
},
27+
{
28+
"name": "prefix-spaces",
29+
"typeid": " prefix_00000000000000000000000000",
30+
"description": "The prefix can't have any spaces"
31+
},
32+
{
33+
"name": "prefix-64-chars",
34+
"typeid": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl_00000000000000000000000000",
35+
"description": "The prefix can't be 64 characters, it needs to be 63 characters or less"
36+
},
37+
{
38+
"name": "separator-empty-prefix",
39+
"typeid": "_00000000000000000000000000",
40+
"description": "If the prefix is empty, the separator should not be there"
41+
},
42+
{
43+
"name": "separator-empty",
44+
"typeid": "_",
45+
"description": "A separator by itself should not be treated as the empty string"
46+
},
47+
{
48+
"name": "suffix-short",
49+
"typeid": "prefix_1234567890123456789012345",
50+
"description": "The suffix can't be 25 characters, it needs to be exactly 26 characters"
51+
},
52+
{
53+
"name": "suffix-long",
54+
"typeid": "prefix_123456789012345678901234567",
55+
"description": "The suffix can't be 27 characters, it needs to be exactly 26 characters"
56+
},
57+
{
58+
"name": "suffix-spaces",
59+
"typeid": "prefix_1234567890123456789012345 ",
60+
"description": "The suffix can't have any spaces"
61+
},
62+
{
63+
"name": "suffix-uppercase",
64+
"typeid": "prefix_0123456789ABCDEFGHJKMNPQRS",
65+
"description": "The suffix should be lowercase with no uppercase letters"
66+
},
67+
{
68+
"name": "suffix-hyphens",
69+
"typeid": "prefix_123456789-123456789-123456",
70+
"description": "The suffix should be lowercase with no uppercase letters"
71+
},
72+
{
73+
"name": "suffix-wrong-alphabet",
74+
"typeid": "prefix_ooooooiiiiiiuuuuuuulllllll",
75+
"description": "The suffix should only have letters from the spec's alphabet"
76+
},
77+
{
78+
"name": "suffix-ambiguous-crockford",
79+
"typeid": "prefix_i23456789ol23456789oi23456",
80+
"description": "The suffix should not have any ambiguous characters from the crockford encoding"
81+
},
82+
{
83+
"name": "suffix-hyphens-crockford",
84+
"typeid": "prefix_123456789-0123456789-0123456",
85+
"description": "The suffix can't ignore hyphens as in the crockford encoding"
86+
},
87+
{
88+
"name": "suffix-overflow",
89+
"typeid": "prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz",
90+
"description": "The should encode at most 128-bits"
91+
}
92+
]

typeid/typeid/spec/invalid.yml

+7-2
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
# Each example contains an invalid TypeID string. Implementations are expected
55
# to throw an error when attempting to parse/validate these strings.
66
#
7-
# Last updated: 2023-06-29
7+
# Last updated: 2023-07-05
88

99
- name: prefix-uppercase
1010
typeid: "PREFIX_00000000000000000000000000"
@@ -31,7 +31,7 @@
3131
description: "The prefix can't have any spaces"
3232

3333
- name: prefix-64-chars
34-
# 123456789 123456789 123456789 123456789 123456789 123456789 1234
34+
# 123456789 123456789 123456789 123456789 123456789 123456789 1234
3535
typeid: "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl_00000000000000000000000000"
3636
description: "The prefix can't be 64 characters, it needs to be 63 characters or less"
3737

@@ -81,3 +81,8 @@
8181
# This example would be valid if we were using the crockford hyphenation rules
8282
typeid: "prefix_123456789-0123456789-0123456"
8383
description: "The suffix can't ignore hyphens as in the crockford encoding"
84+
85+
- name: suffix-overflow
86+
# This is the first suffix that overflows into 129 bits
87+
typeid: "prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz"
88+
description: "The should encode at most 128-bits"

typeid/typeid/spec/valid.json

+50
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
[
2+
{
3+
"name": "nil",
4+
"typeid": "00000000000000000000000000",
5+
"prefix": "",
6+
"uuid": "00000000-0000-0000-0000-000000000000"
7+
},
8+
{
9+
"name": "one",
10+
"typeid": "00000000000000000000000001",
11+
"prefix": "",
12+
"uuid": "00000000-0000-0000-0000-000000000001"
13+
},
14+
{
15+
"name": "ten",
16+
"typeid": "0000000000000000000000000a",
17+
"prefix": "",
18+
"uuid": "00000000-0000-0000-0000-00000000000a"
19+
},
20+
{
21+
"name": "sixteen",
22+
"typeid": "0000000000000000000000000g",
23+
"prefix": "",
24+
"uuid": "00000000-0000-0000-0000-000000000010"
25+
},
26+
{
27+
"name": "thirty-two",
28+
"typeid": "00000000000000000000000010",
29+
"prefix": "",
30+
"uuid": "00000000-0000-0000-0000-000000000020"
31+
},
32+
{
33+
"name": "max-valid",
34+
"typeid": "7zzzzzzzzzzzzzzzzzzzzzzzzz",
35+
"prefix": "",
36+
"uuid": "ffffffff-ffff-ffff-ffff-ffffffffffff"
37+
},
38+
{
39+
"name": "valid-alphabet",
40+
"typeid": "prefix_0123456789abcdefghjkmnpqrs",
41+
"prefix": "prefix",
42+
"uuid": "0110c853-1d09-52d8-d73e-1194e95b5f19"
43+
},
44+
{
45+
"name": "valid-uuidv7",
46+
"typeid": "prefix_01h455vb4pex5vsknk084sn02q",
47+
"prefix": "prefix",
48+
"uuid": "01890a5d-ac96-774b-bcce-b302099a8057"
49+
}
50+
]

typeid/typeid/spec/valid.yml

+6-1
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
# note that not all of them are UUIDv7s. When *generating* new random typeids,
2424
# implementations should always use UUIDv7s.
2525
#
26-
# Last updated: 2023-06-29
26+
# Last updated: 2023-07-05
2727

2828
- name: nil
2929
typeid: "00000000000000000000000000"
@@ -50,6 +50,11 @@
5050
prefix: ""
5151
uuid: "00000000-0000-0000-0000-000000000020"
5252

53+
- name: max-valid
54+
typeid: "7zzzzzzzzzzzzzzzzzzzzzzzzz"
55+
prefix: ""
56+
uuid: "ffffffff-ffff-ffff-ffff-ffffffffffff"
57+
5358
- name: valid-alphabet
5459
typeid: "prefix_0123456789abcdefghjkmnpqrs"
5560
prefix: "prefix"

0 commit comments

Comments
 (0)