|
|
@ -23,11 +23,13 @@ import (
|
|
|
|
"crypto/sha256"
|
|
|
|
"crypto/sha256"
|
|
|
|
"encoding/binary"
|
|
|
|
"encoding/binary"
|
|
|
|
"encoding/hex"
|
|
|
|
"encoding/hex"
|
|
|
|
|
|
|
|
"fmt"
|
|
|
|
"reflect"
|
|
|
|
"reflect"
|
|
|
|
"sync"
|
|
|
|
"sync"
|
|
|
|
"time"
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
|
|
"tailscale.com/util/hashx"
|
|
|
|
"tailscale.com/util/hashx"
|
|
|
|
|
|
|
|
"tailscale.com/util/set"
|
|
|
|
)
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// There is much overlap between the theory of serialization and hashing.
|
|
|
|
// There is much overlap between the theory of serialization and hashing.
|
|
|
@ -152,12 +154,90 @@ func Hash[T any](v *T) Sum {
|
|
|
|
return h.sum()
|
|
|
|
return h.sum()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Option is an optional argument to HasherForType.
|
|
|
|
|
|
|
|
type Option interface {
|
|
|
|
|
|
|
|
isOption()
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
type fieldFilterOpt struct {
|
|
|
|
|
|
|
|
t reflect.Type
|
|
|
|
|
|
|
|
fields set.Set[string]
|
|
|
|
|
|
|
|
includeOnMatch bool // true to include fields, false to exclude them
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (fieldFilterOpt) isOption() {}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func (f fieldFilterOpt) filterStructField(sf reflect.StructField) (include bool) {
|
|
|
|
|
|
|
|
if f.fields.Contains(sf.Name) {
|
|
|
|
|
|
|
|
return f.includeOnMatch
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return !f.includeOnMatch
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// IncludeFields returns an option that modifies the hashing for T to only
|
|
|
|
|
|
|
|
// include the named struct fields.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// T must be a struct type, and must match the type of the value passed to
|
|
|
|
|
|
|
|
// HasherForType.
|
|
|
|
|
|
|
|
func IncludeFields[T any](fields ...string) Option {
|
|
|
|
|
|
|
|
return newFieldFilter[T](true, fields)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// ExcludeFields returns an option that modifies the hashing for T to include
|
|
|
|
|
|
|
|
// all struct fields of T except those provided in fields.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// T must be a struct type, and must match the type of the value passed to
|
|
|
|
|
|
|
|
// HasherForType.
|
|
|
|
|
|
|
|
func ExcludeFields[T any](fields ...string) Option {
|
|
|
|
|
|
|
|
return newFieldFilter[T](false, fields)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func newFieldFilter[T any](include bool, fields []string) Option {
|
|
|
|
|
|
|
|
var zero T
|
|
|
|
|
|
|
|
t := reflect.TypeOf(&zero).Elem()
|
|
|
|
|
|
|
|
fieldSet := set.Set[string]{}
|
|
|
|
|
|
|
|
for _, f := range fields {
|
|
|
|
|
|
|
|
if _, ok := t.FieldByName(f); !ok {
|
|
|
|
|
|
|
|
panic(fmt.Sprintf("unknown field %q for type %v", f, t))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
fieldSet.Add(f)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
return fieldFilterOpt{t, fieldSet, include}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// HasherForType returns a hash that is specialized for the provided type.
|
|
|
|
// HasherForType returns a hash that is specialized for the provided type.
|
|
|
|
func HasherForType[T any]() func(*T) Sum {
|
|
|
|
//
|
|
|
|
|
|
|
|
// HasherForType panics if the opts are invalid for the provided type.
|
|
|
|
|
|
|
|
//
|
|
|
|
|
|
|
|
// Currently, at most one option can be provided (IncludeFields or
|
|
|
|
|
|
|
|
// ExcludeFields) and its type must match the type of T. Those restrictions may
|
|
|
|
|
|
|
|
// be removed in the future, along with documentation about their precedence
|
|
|
|
|
|
|
|
// when combined.
|
|
|
|
|
|
|
|
func HasherForType[T any](opts ...Option) func(*T) Sum {
|
|
|
|
var v *T
|
|
|
|
var v *T
|
|
|
|
seedOnce.Do(initSeed)
|
|
|
|
seedOnce.Do(initSeed)
|
|
|
|
|
|
|
|
if len(opts) > 1 {
|
|
|
|
|
|
|
|
panic("HasherForType only accepts one optional argument") // for now
|
|
|
|
|
|
|
|
}
|
|
|
|
t := reflect.TypeOf(v).Elem()
|
|
|
|
t := reflect.TypeOf(v).Elem()
|
|
|
|
hash := lookupTypeHasher(t)
|
|
|
|
var hash typeHasherFunc
|
|
|
|
|
|
|
|
for _, o := range opts {
|
|
|
|
|
|
|
|
switch o := o.(type) {
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
|
|
|
panic(fmt.Sprintf("unknown HasherOpt %T", o))
|
|
|
|
|
|
|
|
case fieldFilterOpt:
|
|
|
|
|
|
|
|
if t.Kind() != reflect.Struct {
|
|
|
|
|
|
|
|
panic("HasherForStructTypeWithFieldFilter requires T of kind struct")
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if t != o.t {
|
|
|
|
|
|
|
|
panic(fmt.Sprintf("field filter for type %v does not match HasherForType type %v", o.t, t))
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
hash = makeStructHasher(t, o.filterStructField)
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
if hash == nil {
|
|
|
|
|
|
|
|
hash = lookupTypeHasher(t)
|
|
|
|
|
|
|
|
}
|
|
|
|
return func(v *T) (s Sum) {
|
|
|
|
return func(v *T) (s Sum) {
|
|
|
|
// This logic is identical to Hash, but pull out a few statements.
|
|
|
|
// This logic is identical to Hash, but pull out a few statements.
|
|
|
|
h := hasherPool.Get().(*hasher)
|
|
|
|
h := hasherPool.Get().(*hasher)
|
|
|
@ -225,7 +305,7 @@ func makeTypeHasher(t reflect.Type) typeHasherFunc {
|
|
|
|
case reflect.Slice:
|
|
|
|
case reflect.Slice:
|
|
|
|
return makeSliceHasher(t)
|
|
|
|
return makeSliceHasher(t)
|
|
|
|
case reflect.Struct:
|
|
|
|
case reflect.Struct:
|
|
|
|
return makeStructHasher(t)
|
|
|
|
return makeStructHasher(t, keepAllStructFields)
|
|
|
|
case reflect.Map:
|
|
|
|
case reflect.Map:
|
|
|
|
return makeMapHasher(t)
|
|
|
|
return makeMapHasher(t)
|
|
|
|
case reflect.Pointer:
|
|
|
|
case reflect.Pointer:
|
|
|
@ -353,9 +433,12 @@ func makeSliceHasher(t reflect.Type) typeHasherFunc {
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
func makeStructHasher(t reflect.Type) typeHasherFunc {
|
|
|
|
func keepAllStructFields(keepField reflect.StructField) bool { return true }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
func makeStructHasher(t reflect.Type, keepField func(reflect.StructField) bool) typeHasherFunc {
|
|
|
|
type fieldHasher struct {
|
|
|
|
type fieldHasher struct {
|
|
|
|
idx int // index of field for reflect.Type.Field(n); negative if memory is directly hashable
|
|
|
|
idx int // index of field for reflect.Type.Field(n); negative if memory is directly hashable
|
|
|
|
|
|
|
|
keep bool
|
|
|
|
hash typeHasherFunc // only valid if idx is not negative
|
|
|
|
hash typeHasherFunc // only valid if idx is not negative
|
|
|
|
offset uintptr
|
|
|
|
offset uintptr
|
|
|
|
size uintptr
|
|
|
|
size uintptr
|
|
|
@ -365,8 +448,8 @@ func makeStructHasher(t reflect.Type) typeHasherFunc {
|
|
|
|
init := func() {
|
|
|
|
init := func() {
|
|
|
|
for i, numField := 0, t.NumField(); i < numField; i++ {
|
|
|
|
for i, numField := 0, t.NumField(); i < numField; i++ {
|
|
|
|
sf := t.Field(i)
|
|
|
|
sf := t.Field(i)
|
|
|
|
f := fieldHasher{i, nil, sf.Offset, sf.Type.Size()}
|
|
|
|
f := fieldHasher{i, keepField(sf), nil, sf.Offset, sf.Type.Size()}
|
|
|
|
if typeIsMemHashable(sf.Type) {
|
|
|
|
if f.keep && typeIsMemHashable(sf.Type) {
|
|
|
|
f.idx = -1
|
|
|
|
f.idx = -1
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
@ -390,6 +473,9 @@ func makeStructHasher(t reflect.Type) typeHasherFunc {
|
|
|
|
return func(h *hasher, p pointer) {
|
|
|
|
return func(h *hasher, p pointer) {
|
|
|
|
once.Do(init)
|
|
|
|
once.Do(init)
|
|
|
|
for _, field := range fields {
|
|
|
|
for _, field := range fields {
|
|
|
|
|
|
|
|
if !field.keep {
|
|
|
|
|
|
|
|
continue
|
|
|
|
|
|
|
|
}
|
|
|
|
pf := p.structField(field.idx, field.offset, field.size)
|
|
|
|
pf := p.structField(field.idx, field.offset, field.size)
|
|
|
|
if field.idx < 0 {
|
|
|
|
if field.idx < 0 {
|
|
|
|
h.HashBytes(pf.asMemory(field.size))
|
|
|
|
h.HashBytes(pf.asMemory(field.size))
|
|
|
|