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  }