Skip to content

[typeid] Enforce that the first suffix character is in the 0-7 range #75

New issue

Have a question about this project? # for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “#”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? # to your account

Merged
merged 3 commits into from
Jul 6, 2023
Merged
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 7 additions & 2 deletions typeid/typeid-go/testdata/invalid.yml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
# Each example contains an invalid TypeID string. Implementations are expected
# to throw an error when attempting to parse/validate these strings.
#
# Last updated: 2023-06-29
# Last updated: 2023-07-05

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

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

@@ -81,3 +81,8 @@
# This example would be valid if we were using the crockford hyphenation rules
typeid: "prefix_123456789-0123456789-0123456"
description: "The suffix can't ignore hyphens as in the crockford encoding"

- name: suffix-overflow
# This is the first suffix that overflows into 129 bits
typeid: "prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz"
description: "The should encode at most 128-bits"
9 changes: 7 additions & 2 deletions typeid/typeid-go/testdata/valid.yml
Original file line number Diff line number Diff line change
@@ -17,13 +17,13 @@
# decoding and re-encoding the id, the result is the same as the original.
#
# In other words, the following property should always hold:
# random_typeid: encode(decode(random_typeid))
# random_typeid == encode(decode(random_typeid))
#
# Finally, while implementations should be able to decode the values below,
# note that not all of them are UUIDv7s. When *generating* new random typeids,
# implementations should always use UUIDv7s.
#
# Last updated: 2023-06-29
# Last updated: 2023-07-05

- name: nil
typeid: "00000000000000000000000000"
@@ -50,6 +50,11 @@
prefix: ""
uuid: "00000000-0000-0000-0000-000000000020"

- name: max-valid
typeid: "7zzzzzzzzzzzzzzzzzzzzzzzzz"
prefix: ""
uuid: "ffffffff-ffff-ffff-ffff-ffffffffffff"

- name: valid-alphabet
typeid: "prefix_0123456789abcdefghjkmnpqrs"
prefix: "prefix"
12 changes: 7 additions & 5 deletions typeid/typeid-go/typeid.go
Original file line number Diff line number Diff line change
@@ -153,12 +153,14 @@ func validatePrefix(prefix string) error {
}

func validateSuffix(suffix string) error {
// Validate the suffix by decoding it:
// 1. If the suffix is empty, it is valid
// 2. If the suffix is not empty, it must be a valid base32 string
if suffix == "" {
return nil
if len(suffix) != 26 {
return fmt.Errorf("invalid suffix: %s. Suffix length is %d, expected 26", suffix, len(suffix))
}

if suffix[0] > '7' {
return fmt.Errorf("invalid suffix: '%s'. Suffix must start with a 0-7 digit to avoid overflows", suffix)
}
// Validate the suffix by decoding it, it must be a valid base32 string
if _, err := base32.Decode(suffix); err != nil {
return fmt.Errorf("invalid suffix: %w", err)
}
4 changes: 4 additions & 0 deletions typeid/typeid-js/src/typeid.ts
Original file line number Diff line number Diff line change
@@ -36,6 +36,10 @@ export class TypeID {
throw new Error(`Invalid length. Suffix should have 26 characters, got ${suffix.length}`);
}

if (this.suffix[0] > "7") {
throw new Error("Invalid suffix. First character must be in the range [0-7]");
}

// Validate the suffix by decoding it. If it's invalid, an error will be thrown.
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const unused = decode(this.suffix);
5 changes: 5 additions & 0 deletions typeid/typeid-js/test/invalid.ts
Original file line number Diff line number Diff line change
@@ -84,5 +84,10 @@ export default [
"name": "suffix-hyphens-crockford",
"typeid": "prefix_123456789-0123456789-0123456",
"description": "The suffix can't ignore hyphens as in the crockford encoding"
},
{
"name": "suffix-overflow",
"typeid": "prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz",
"description": "The should encode at most 128-bits"
}
]
6 changes: 6 additions & 0 deletions typeid/typeid-js/test/valid.ts
Original file line number Diff line number Diff line change
@@ -31,6 +31,12 @@ export default [
"prefix": "",
"uuid": "00000000-0000-0000-0000-000000000020"
},
{
"name": "max-valid",
"typeid": "7zzzzzzzzzzzzzzzzzzzzzzzzz",
"prefix": "",
"uuid": "ffffffff-ffff-ffff-ffff-ffffffffffff"
},
{
"name": "valid-alphabet",
"typeid": "prefix_0123456789abcdefghjkmnpqrs",
14 changes: 11 additions & 3 deletions typeid/typeid/spec/README.md
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
# TypeID Specification (Version 0.1.0)
# TypeID Specification (Version 0.2.0)

## Overview
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
but in our case the alphabet encoding is strict: always in lowercase, no hyphens allowed,
and we never decode multiple ambiguous characters to the same value.

Technically speaking, 26 characters in base32 can encode 130 bits of data, but UUIDs
are 128 bits. To prevent overflow errors, the maximum possible suffix for a typeid
is `7zzzzzzzzzzzzzzzzzzzzzzzzz`. Implementations should reject any suffix greater than
that value, by checking that the first character is a `7` or less.

#### Compatibility with UUID
When genarating a new TypeID, the generated UUID suffix MUST decode to a valid UUIDv7.

@@ -92,6 +97,9 @@ To assist library authors in validating their implementations, we provide:
+ A reference implementation in [Go](https://github.com/jetpack-io/typeid-go)
with extensive testing.
+ A [valid.yml](valid.yml) file containing a list of valid typeids along
with their corresponding decoded UUIDs.
with their corresponding decoded UUIDs. For convienience, we also provide
a [valid.json](valid.json) file containing the same data in JSON format.
+ An [invalid.yml](invalid.yml) file containing a list of strings that are
invalid typeids and should fail to parse/decode.
invalid typeids and should fail to parse/decode. For convienience, we also
provide a [invalid.json](invalid.json) file containing the same data in
JSON format.
92 changes: 92 additions & 0 deletions typeid/typeid/spec/invalid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
[
{
"name": "prefix-uppercase",
"typeid": "PREFIX_00000000000000000000000000",
"description": "The prefix should be lowercase with no uppercase letters"
},
{
"name": "prefix-numeric",
"typeid": "12345_00000000000000000000000000",
"description": "The prefix can't have numbers, it needs to be alphabetic"
},
{
"name": "prefix-period",
"typeid": "pre.fix_00000000000000000000000000",
"description": "The prefix can't have symbols, it needs to be alphabetic"
},
{
"name": "prefix-underscore",
"typeid": "pre_fix_00000000000000000000000000",
"description": "The prefix can't have symbols, it needs to be alphabetic"
},
{
"name": "prefix-non-ascii",
"typeid": "préfix_00000000000000000000000000",
"description": "The prefix can only have ascii letters"
},
{
"name": "prefix-spaces",
"typeid": " prefix_00000000000000000000000000",
"description": "The prefix can't have any spaces"
},
{
"name": "prefix-64-chars",
"typeid": "abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijkl_00000000000000000000000000",
"description": "The prefix can't be 64 characters, it needs to be 63 characters or less"
},
{
"name": "separator-empty-prefix",
"typeid": "_00000000000000000000000000",
"description": "If the prefix is empty, the separator should not be there"
},
{
"name": "separator-empty",
"typeid": "_",
"description": "A separator by itself should not be treated as the empty string"
},
{
"name": "suffix-short",
"typeid": "prefix_1234567890123456789012345",
"description": "The suffix can't be 25 characters, it needs to be exactly 26 characters"
},
{
"name": "suffix-long",
"typeid": "prefix_123456789012345678901234567",
"description": "The suffix can't be 27 characters, it needs to be exactly 26 characters"
},
{
"name": "suffix-spaces",
"typeid": "prefix_1234567890123456789012345 ",
"description": "The suffix can't have any spaces"
},
{
"name": "suffix-uppercase",
"typeid": "prefix_0123456789ABCDEFGHJKMNPQRS",
"description": "The suffix should be lowercase with no uppercase letters"
},
{
"name": "suffix-hyphens",
"typeid": "prefix_123456789-123456789-123456",
"description": "The suffix should be lowercase with no uppercase letters"
},
{
"name": "suffix-wrong-alphabet",
"typeid": "prefix_ooooooiiiiiiuuuuuuulllllll",
"description": "The suffix should only have letters from the spec's alphabet"
},
{
"name": "suffix-ambiguous-crockford",
"typeid": "prefix_i23456789ol23456789oi23456",
"description": "The suffix should not have any ambiguous characters from the crockford encoding"
},
{
"name": "suffix-hyphens-crockford",
"typeid": "prefix_123456789-0123456789-0123456",
"description": "The suffix can't ignore hyphens as in the crockford encoding"
},
{
"name": "suffix-overflow",
"typeid": "prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz",
"description": "The should encode at most 128-bits"
}
]
9 changes: 7 additions & 2 deletions typeid/typeid/spec/invalid.yml
Original file line number Diff line number Diff line change
@@ -4,7 +4,7 @@
# Each example contains an invalid TypeID string. Implementations are expected
# to throw an error when attempting to parse/validate these strings.
#
# Last updated: 2023-06-29
# Last updated: 2023-07-05

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

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

@@ -81,3 +81,8 @@
# This example would be valid if we were using the crockford hyphenation rules
typeid: "prefix_123456789-0123456789-0123456"
description: "The suffix can't ignore hyphens as in the crockford encoding"

- name: suffix-overflow
# This is the first suffix that overflows into 129 bits
typeid: "prefix_8zzzzzzzzzzzzzzzzzzzzzzzzz"
description: "The should encode at most 128-bits"
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
description: "The should encode at most 128-bits"
description: "The suffix should encode at most 128-bits"

50 changes: 50 additions & 0 deletions typeid/typeid/spec/valid.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
[
{
"name": "nil",
"typeid": "00000000000000000000000000",
"prefix": "",
"uuid": "00000000-0000-0000-0000-000000000000"
},
{
"name": "one",
"typeid": "00000000000000000000000001",
"prefix": "",
"uuid": "00000000-0000-0000-0000-000000000001"
},
{
"name": "ten",
"typeid": "0000000000000000000000000a",
"prefix": "",
"uuid": "00000000-0000-0000-0000-00000000000a"
},
{
"name": "sixteen",
"typeid": "0000000000000000000000000g",
"prefix": "",
"uuid": "00000000-0000-0000-0000-000000000010"
},
{
"name": "thirty-two",
"typeid": "00000000000000000000000010",
"prefix": "",
"uuid": "00000000-0000-0000-0000-000000000020"
},
{
"name": "max-valid",
"typeid": "7zzzzzzzzzzzzzzzzzzzzzzzzz",
"prefix": "",
"uuid": "ffffffff-ffff-ffff-ffff-ffffffffffff"
},
{
"name": "valid-alphabet",
"typeid": "prefix_0123456789abcdefghjkmnpqrs",
"prefix": "prefix",
"uuid": "0110c853-1d09-52d8-d73e-1194e95b5f19"
},
{
"name": "valid-uuidv7",
"typeid": "prefix_01h455vb4pex5vsknk084sn02q",
"prefix": "prefix",
"uuid": "01890a5d-ac96-774b-bcce-b302099a8057"
}
]
7 changes: 6 additions & 1 deletion typeid/typeid/spec/valid.yml
Original file line number Diff line number Diff line change
@@ -23,7 +23,7 @@
# note that not all of them are UUIDv7s. When *generating* new random typeids,
# implementations should always use UUIDv7s.
#
# Last updated: 2023-06-29
# Last updated: 2023-07-05

- name: nil
typeid: "00000000000000000000000000"
@@ -50,6 +50,11 @@
prefix: ""
uuid: "00000000-0000-0000-0000-000000000020"

- name: max-valid
typeid: "7zzzzzzzzzzzzzzzzzzzzzzzzz"
prefix: ""
uuid: "ffffffff-ffff-ffff-ffff-ffffffffffff"

- name: valid-alphabet
typeid: "prefix_0123456789abcdefghjkmnpqrs"
prefix: "prefix"