github.com/openfga/openfga@v1.5.4-rc1/internal/keys/hasher.go (about)

     1  package keys
     2  
     3  import (
     4  	"fmt"
     5  	"sort"
     6  	"strconv"
     7  
     8  	openfgav1 "github.com/openfga/api/proto/openfga/v1"
     9  	"golang.org/x/exp/maps"
    10  	"google.golang.org/protobuf/types/known/structpb"
    11  )
    12  
    13  type hasher interface {
    14  	WriteString(value string) error
    15  }
    16  
    17  type hashableValue interface {
    18  	Append(hasher) error
    19  }
    20  
    21  // strinHasher implements the hashableValue interface for string types.
    22  type stringHasher string
    23  
    24  var _ hashableValue = (*stringHasher)(nil)
    25  
    26  func (s stringHasher) Append(h hasher) error {
    27  	return h.WriteString(string(s))
    28  }
    29  
    30  // NewTupleKeysHasher returns a hasher for an array of *openfgav1.TupleKey.
    31  // It sorts the tuples first to guarantee that two arrays that are identical except for the ordering
    32  // return the same hash.
    33  func NewTupleKeysHasher(tupleKeys ...*openfgav1.TupleKey) *tupleKeysHasher {
    34  	return &tupleKeysHasher{tupleKeys}
    35  }
    36  
    37  // tupleKeysHasher implements the hashableValue interface for TupleKey protobuf types.
    38  type tupleKeysHasher struct {
    39  	tupleKeys []*openfgav1.TupleKey
    40  }
    41  
    42  var _ hashableValue = (*tupleKeysHasher)(nil)
    43  
    44  func (t tupleKeysHasher) Append(h hasher) error {
    45  	sortedTupleKeys := append([]*openfgav1.TupleKey(nil), t.tupleKeys...) // Copy input to avoid mutating it
    46  
    47  	sort.SliceStable(sortedTupleKeys, func(i, j int) bool {
    48  		if sortedTupleKeys[i].GetObject() != sortedTupleKeys[j].GetObject() {
    49  			return sortedTupleKeys[i].GetObject() < sortedTupleKeys[j].GetObject()
    50  		}
    51  
    52  		if sortedTupleKeys[i].GetRelation() != sortedTupleKeys[j].GetRelation() {
    53  			return sortedTupleKeys[i].GetRelation() < sortedTupleKeys[j].GetRelation()
    54  		}
    55  
    56  		if sortedTupleKeys[i].GetUser() != sortedTupleKeys[j].GetUser() {
    57  			return sortedTupleKeys[i].GetUser() < sortedTupleKeys[j].GetUser()
    58  		}
    59  
    60  		return true
    61  	})
    62  
    63  	// prefix to avoid overlap with previous strings written
    64  	if err := h.WriteString("/"); err != nil {
    65  		return err
    66  	}
    67  
    68  	n := 0
    69  	for _, tupleKey := range sortedTupleKeys {
    70  		key := fmt.Sprintf("%s#%s@%s", tupleKey.GetObject(), tupleKey.GetRelation(), tupleKey.GetUser())
    71  
    72  		if err := h.WriteString(key); err != nil {
    73  			return err
    74  		}
    75  
    76  		if n < len(t.tupleKeys)-1 {
    77  			if err := h.WriteString(","); err != nil {
    78  				return err
    79  			}
    80  		}
    81  
    82  		n++
    83  	}
    84  
    85  	return nil
    86  }
    87  
    88  // contextHasher represents a hashable protobuf Struct.
    89  //
    90  // The contextHasher can be used to generate a stable hash of a protobuf Struct. The fields
    91  // of the struct are ordered to produce a stable hash, and the values for each struct key
    92  // are produced using the structValueHasher, which produces a stable hash value for the Struct
    93  // value.
    94  type contextHasher struct {
    95  	*structpb.Struct
    96  }
    97  
    98  // NewContextHasher constructs a contextHasher which can be used to produce
    99  // a stable hash of a protobuf Struct.
   100  func NewContextHasher(s *structpb.Struct) *contextHasher {
   101  	return &contextHasher{s}
   102  }
   103  
   104  var _ hashableValue = (*contextHasher)(nil)
   105  
   106  func (c contextHasher) Append(h hasher) error {
   107  	if c.Struct == nil {
   108  		return nil
   109  	}
   110  
   111  	fields := c.GetFields()
   112  	keys := maps.Keys(fields)
   113  	sort.Strings(keys)
   114  
   115  	for _, key := range keys {
   116  		if err := h.WriteString(fmt.Sprintf("'%s:'", key)); err != nil {
   117  			return err
   118  		}
   119  
   120  		valueHasher := structValueHasher{fields[key]}
   121  		if err := valueHasher.Append(h); err != nil {
   122  			return err
   123  		}
   124  
   125  		if err := h.WriteString(","); err != nil {
   126  			return err
   127  		}
   128  	}
   129  
   130  	return nil
   131  }
   132  
   133  // structValueHasher represents a hashable protobuf Struct value.
   134  //
   135  // The structValueHasher can be used to generate a stable hash of a protobuf Struct value.
   136  type structValueHasher struct {
   137  	*structpb.Value
   138  }
   139  
   140  var _ hashableValue = (*structValueHasher)(nil)
   141  
   142  func (s structValueHasher) Append(h hasher) error {
   143  	switch val := s.Kind.(type) {
   144  	case *structpb.Value_BoolValue:
   145  		return h.WriteString(fmt.Sprintf("%v", val.BoolValue))
   146  	case *structpb.Value_NullValue:
   147  		return h.WriteString("null")
   148  	case *structpb.Value_StringValue:
   149  		return h.WriteString(val.StringValue)
   150  	case *structpb.Value_NumberValue:
   151  		return h.WriteString(strconv.FormatFloat(val.NumberValue, 'f', -1, 64)) // -1 precision ensures we represent the 64-bit value with the maximum precision needed to represent it, see strconv#FormatFloat for more info.
   152  	case *structpb.Value_ListValue:
   153  		n := 0
   154  		values := val.ListValue.GetValues()
   155  
   156  		for _, v := range values {
   157  			valueHasher := structValueHasher{v}
   158  			if err := valueHasher.Append(h); err != nil {
   159  				return err
   160  			}
   161  
   162  			if n < len(values)-1 {
   163  				if err := h.WriteString(","); err != nil {
   164  					return err
   165  				}
   166  			}
   167  
   168  			n++
   169  		}
   170  	case *structpb.Value_StructValue:
   171  		return contextHasher{val.StructValue}.Append(h)
   172  	default:
   173  		panic("unexpected structpb value encountered")
   174  	}
   175  
   176  	return nil
   177  }