]> git.giacomolli.uk - onehouse/blob - protocol/identifier.go
Validate typed publications before encoding
[onehouse] / protocol / identifier.go
1 package protocol
2
3 import (
4 "crypto/rand"
5 "fmt"
6 "strings"
7 "time"
8 )
9
10 const idAlphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ"
11
12 // NewID returns a canonical ULID with a millisecond timestamp and 80 random
13 // bits. Times outside the unsigned 48-bit millisecond range are rejected.
14 func NewID(now time.Time) (string, error) {
15 milliseconds := now.UnixMilli()
16 if milliseconds < 0 || milliseconds >= 1<<48 {
17 return "", fmt.Errorf("ID time is outside the ULID range")
18 }
19
20 var data [16]byte
21
22 // Six bytes hold the timestamp and ten bytes hold random data.
23 for index := 5; index >= 0; index-- {
24 data[index] = byte(milliseconds)
25 milliseconds >>= 8
26 }
27
28 _, err := rand.Read(data[6:])
29 if err != nil {
30 return "", fmt.Errorf("read ID randomness: %w", err)
31 }
32
33 var text [26]byte
34
35 for index := range text {
36 var value byte
37
38 for bit := range 5 {
39 position := index*5 + bit - 2
40 value <<= 1
41 if position >= 0 {
42 value |= (data[position/8] >> (7 - position%8)) & 1
43 }
44 }
45
46 text[index] = idAlphabet[value]
47 }
48
49 return string(text[:]), nil
50 }
51
52 // ValidID reports whether value is a canonical uppercase ULID.
53 func ValidID(value string) bool {
54 if len(value) != 26 || value[0] > '7' {
55 return false
56 }
57
58 for _, character := range value {
59 if !strings.ContainsRune(idAlphabet, character) {
60 return false
61 }
62 }
63
64 return true
65 }
66
67 // ValidIdentifier accepts lowercase ASCII letters and digits with single
68 // internal hyphen or period separators. Empty identifiers are invalid.
69 func ValidIdentifier(value string) bool {
70 separator := true
71
72 for _, character := range value {
73 switch {
74 case character >= 'a' && character <= 'z', character >= '0' && character <= '9':
75 separator = false
76 case character == '-' || character == '.':
77 if separator {
78 return false
79 }
80
81 separator = true
82 default:
83 return false
84 }
85 }
86
87 return !separator
88 }