github.com/TBD54566975/ftl@v0.219.0/internal/model/keys.go (about) 1 //nolint:revive 2 package model 3 4 import ( 5 "crypto/rand" 6 "database/sql" 7 "database/sql/driver" 8 "encoding" 9 "fmt" 10 "reflect" 11 "strings" 12 13 base36 "github.com/multiformats/go-base36" 14 ) 15 16 // Overridable random source for testing 17 var randRead = rand.Read 18 19 // A constraint that requires itself be a pointer to a T that implements KeyPayload. 20 // 21 // This is necessary so that keyType.Payload can be a value rather than a pointer. 22 type keyPayloadConstraint[T any] interface { 23 *T 24 KeyPayload 25 } 26 27 // KeyPayload is an interface that all key payloads must implement. 28 type KeyPayload interface { 29 Kind() string 30 String() string 31 // Parse the hyphen-separated parts of the payload 32 Parse(parts []string) error 33 // RandomBytes determines the number of random bytes the key should include. 34 RandomBytes() int 35 } 36 37 // KeyType is a helper type to avoid having to write a bunch of boilerplate. 38 type KeyType[T any, TP keyPayloadConstraint[T]] struct { 39 Payload T 40 Suffix []byte 41 } 42 43 var _ interface { 44 sql.Scanner 45 driver.Valuer 46 encoding.TextUnmarshaler 47 encoding.TextMarshaler 48 } = (*KeyType[ControllerPayload, *ControllerPayload])(nil) 49 50 func (d KeyType[T, TP]) IsZero() bool { 51 return d.Equal(KeyType[T, TP]{}) 52 } 53 54 func (d KeyType[T, TP]) Equal(other KeyType[T, TP]) bool { 55 return reflect.DeepEqual(d, other) 56 } 57 58 func (d KeyType[T, TP]) Value() (driver.Value, error) { 59 return d.String(), nil 60 } 61 62 // Scan from DB representation. 63 func (d *KeyType[T, TP]) Scan(src any) error { 64 input, ok := src.(string) 65 if !ok { 66 return fmt.Errorf("expected key to be a string but it's a %T", src) 67 } 68 key, err := parseKey[T, TP](input) 69 if err != nil { 70 return err 71 } 72 *d = key 73 return nil 74 } 75 76 func (d KeyType[T, TP]) Kind() string { 77 var payload TP = &d.Payload 78 return payload.Kind() 79 } 80 81 func (d KeyType[T, TP]) String() string { 82 parts := []string{d.Kind()} 83 var payload TP = &d.Payload 84 if payload := payload.String(); payload != "" { 85 parts = append(parts, payload) 86 } 87 parts = append(parts, base36.EncodeToStringLc(d.Suffix)) 88 return strings.Join(parts, "-") 89 } 90 91 func (d KeyType[T, TP]) MarshalText() ([]byte, error) { return []byte(d.String()), nil } 92 func (d *KeyType[T, TP]) UnmarshalText(bytes []byte) error { 93 id, err := parseKey[T, TP](string(bytes)) 94 if err != nil { 95 return err 96 } 97 *d = id 98 return nil 99 } 100 101 // Generate a new key. 102 // 103 // If the payload specifies a randomness greater than 0, a random suffix will be generated. 104 // The payload will be parsed from payloadComponents, which must be a hyphen-separated string. 105 func newKey[T any, TP keyPayloadConstraint[T]](components ...string) (kt KeyType[T, TP]) { 106 var payload TP = &kt.Payload 107 if err := payload.Parse(components); err != nil { 108 panic(fmt.Errorf("failed to parse payload %q: %w", strings.Join(components, "-"), err)) 109 } 110 if randomness := payload.RandomBytes(); randomness > 0 { 111 kt.Suffix = make([]byte, randomness) 112 if _, err := randRead(kt.Suffix); err != nil { 113 panic(fmt.Errorf("failed to generate random suffix: %w", err)) 114 } 115 } 116 return kt 117 } 118 119 // Parse a key in the form <kind>[-<payload>][-<suffix>] 120 // 121 // Suffix will be parsed if the payload specifies a randomness greater than 0. 122 func parseKey[T any, TP keyPayloadConstraint[T]](key string) (kt KeyType[T, TP], err error) { 123 components := strings.Split(key, "-") 124 if len(components) == 0 { 125 return kt, fmt.Errorf("expected a prefix for key %q", key) 126 } 127 128 // Validate and strip kind. 129 var payload TP = &kt.Payload 130 if components[0] != payload.Kind() { 131 return kt, fmt.Errorf("expected prefix %q for key %q", payload.Kind(), key) 132 } 133 components = components[1:] 134 135 // Optionally parse and strip random suffix. 136 randomness := payload.RandomBytes() 137 if randomness > 0 { 138 if len(components) == 0 { 139 return kt, fmt.Errorf("expected a suffix for key %q", key) 140 } 141 var err error 142 kt.Suffix, err = base36.DecodeString(components[len(components)-1]) 143 if err != nil { 144 return kt, fmt.Errorf("expected a base36 suffix for key %q: %w", key, err) 145 } 146 if len(kt.Suffix) != randomness { 147 return kt, fmt.Errorf("expected a suffix of %d bytes for key %q, not %d", randomness, key, len(kt.Suffix)) 148 } 149 components = components[:len(components)-1] 150 } 151 152 if err := payload.Parse(components); err != nil { 153 return kt, fmt.Errorf("failed to parse payload for key %q: %w", key, err) 154 } 155 156 return kt, nil 157 }